import {
  PROVIDER_IN_FILTER_KEY,
  PROVIDER_TYPE_IN_FILTER_KEY,
} from "@/lib/provider-filters";
import {
  buildProviderScheduleSummary,
  describeScheduleCadence,
  getNextScheduledRunInTimezone,
  getScheduleCadenceParts,
  isScheduleConfigured,
} from "@/lib/schedules";
import {
  DEFAULT_SCAN_JOBS_TAB,
  type MetaDataProps,
  type ProviderProps,
  SCAN_JOBS_TAB,
  SCAN_STATE,
  SCAN_TRIGGER,
  type ScanAttributes,
  type ScanFindingsSummary,
  type ScanJobsTab,
  type ScanProps,
  type ScanState,
  type ScanTrigger,
  type ScheduleAttributes,
  type ScheduleProps,
  type SearchParamsProps,
} from "@/types";

export const SCAN_STATE_FILTER_KEYS = [
  "filter[state]",
  "filter[state__in]",
] as const;

const ALL_VALUE = "all";

const SCAN_JOBS_TAB_STATES: Record<ScanJobsTab, ScanState[]> = {
  [SCAN_JOBS_TAB.ACTIVE]: [SCAN_STATE.AVAILABLE, SCAN_STATE.EXECUTING],
  [SCAN_JOBS_TAB.COMPLETED]: [
    SCAN_STATE.COMPLETED,
    SCAN_STATE.FAILED,
    SCAN_STATE.CANCELLED,
  ],
  [SCAN_JOBS_TAB.SCHEDULED]: [SCAN_STATE.SCHEDULED],
};

const toStateFilter = (states: ScanState[]): Record<string, string> => ({
  "filter[state__in]": states.join(","),
});

const SCAN_JOBS_TAB_FILTERS = Object.fromEntries(
  Object.entries(SCAN_JOBS_TAB_STATES).map(([tab, states]) => [
    tab,
    toStateFilter(states),
  ]),
) as Record<ScanJobsTab, Record<string, string>>;

export interface ScanTriggerFilterOption {
  value: typeof ALL_VALUE | ScanTrigger;
  label: string;
}

export interface ScanStatusFilterOption {
  value: typeof ALL_VALUE | ScanState;
  label: string;
}

export function getScanTriggerFilterOptions(
  isCloudEnvironment: boolean,
): ScanTriggerFilterOption[] {
  const options: ScanTriggerFilterOption[] = [
    { value: ALL_VALUE, label: "All Types" },
    { value: SCAN_TRIGGER.MANUAL, label: "Manual" },
    { value: SCAN_TRIGGER.SCHEDULED, label: "Scheduled" },
  ];

  if (isCloudEnvironment) {
    options.push({ value: SCAN_TRIGGER.IMPORTED, label: "Imported" });
  }

  return options;
}

export function isScanStateFilterKey(key: string): boolean {
  return SCAN_STATE_FILTER_KEYS.some((filterKey) => filterKey === key);
}

function isSearchParamValue(value: unknown): value is string | string[] {
  return typeof value === "string" || Array.isArray(value);
}

export function getScanJobsUserFilters(
  searchParams: SearchParamsProps,
): Record<string, string | string[]> {
  const tab = getScanJobsTab(searchParams.tab);

  return Object.entries(searchParams).reduce<Record<string, string | string[]>>(
    (filters, [key, value]) => {
      if (tab === SCAN_JOBS_TAB.SCHEDULED && key === "filter[trigger]") {
        return filters;
      }

      if (
        key.startsWith("filter[") &&
        !isScanStateFilterKey(key) &&
        isSearchParamValue(value)
      ) {
        filters[key] = value;
      }

      return filters;
    },
    {},
  );
}

function parseStateFilter(value?: string | string[]): ScanState[] {
  const rawValue = Array.isArray(value) ? value.join(",") : value;
  if (!rawValue || rawValue === ALL_VALUE) return [];

  return rawValue
    .split(",")
    .filter((item): item is ScanState =>
      Object.values(SCAN_STATE).includes(item as ScanState),
    );
}

export function getScanJobsTab(value?: string | string[]): ScanJobsTab {
  const rawValue = Array.isArray(value) ? value[0] : value;
  const tabs = Object.values(SCAN_JOBS_TAB);

  return tabs.includes(rawValue as ScanJobsTab)
    ? (rawValue as ScanJobsTab)
    : DEFAULT_SCAN_JOBS_TAB;
}

export function getScanJobsTabFilters(
  tab: ScanJobsTab,
  stateFilter?: string | string[],
): Record<string, string> {
  const selectedStates = parseStateFilter(stateFilter);
  const allowedStates = SCAN_JOBS_TAB_STATES[tab];
  const matchingStates = selectedStates.filter((state) =>
    allowedStates.includes(state),
  );

  if (matchingStates.length === 0) return { ...SCAN_JOBS_TAB_FILTERS[tab] };

  return { "filter[state__in]": matchingStates.join(",") };
}

export function getScanAlias(scan: ScanProps): string {
  if (scan.attributes.trigger === SCAN_TRIGGER.SCHEDULED)
    return "Scheduled Scan";
  return scan.attributes.name?.trim() || "-";
}

export function formatScanDuration(duration?: number | null): string {
  if (duration === null || duration === undefined || duration < 0) return "-";

  const totalSeconds = Math.round(duration);
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  const hours = Math.floor(minutes / 60);
  const remainingMinutes = minutes % 60;

  if (hours > 0) return `${hours}h ${remainingMinutes}m ${seconds}s`;
  if (minutes > 0) return `${minutes} min ${seconds} sec`;
  return `${seconds} sec`;
}

export function getScanScheduleLabel(trigger?: ScanTrigger | string): string {
  if (trigger === "scheduled") return "Scheduled";
  if (trigger === "manual") return "Manual";
  if (trigger === "imported") return "Imported";
  return "-";
}

export function getScanStatusLabel(state?: ScanState | string): string {
  if (state === "available") return "Queued";
  if (!state) return "-";
  return state.charAt(0).toUpperCase() + state.slice(1);
}

export function getScanStatusFilterOptions(
  tab: ScanJobsTab,
): ScanStatusFilterOption[] {
  return [
    { value: ALL_VALUE, label: "All Statuses" },
    ...SCAN_JOBS_TAB_STATES[tab].map((state) => ({
      value: state,
      label: getScanStatusLabel(state),
    })),
  ];
}

export interface BuildPendingScheduleRowsParams {
  providers: ProviderProps[];
  schedulesByProviderId: Record<string, ScheduleAttributes>;
  /** Providers that already have a real `state=scheduled` Scan row. */
  coveredProviderIds: Set<string>;
  now: Date;
}

interface AppendPendingScheduleRowsToPageParams
  extends BuildPendingScheduleRowsParams {
  scans: ScanProps[];
  meta?: MetaDataProps;
  page: number;
  pageSize: number;
}

/**
 * Synthesizes Scheduled-tab rows for configured schedules without a Scan row
 * yet (the backend only creates one after each run).
 */
export function buildPendingScheduleRows({
  providers,
  schedulesByProviderId,
  coveredProviderIds,
  now,
}: BuildPendingScheduleRowsParams): ScanProps[] {
  return providers.flatMap((provider) => {
    if (coveredProviderIds.has(provider.id)) return [];

    const schedule = schedulesByProviderId[provider.id];
    if (!schedule || !isScheduleConfigured(schedule) || !schedule.scan_enabled)
      return [];

    // Prefer the server-computed next fire time; fall back to a client estimate.
    const nextScanAt =
      schedule.next_scan_at ??
      getNextScheduledRunInTimezone(schedule, now)?.toISOString() ??
      null;

    return [
      {
        type: "scans" as const,
        id: `pending-schedule-${provider.id}`,
        attributes: {
          name: "",
          trigger: SCAN_TRIGGER.SCHEDULED,
          state: SCAN_STATE.SCHEDULED,
          unique_resource_count: 0,
          progress: 0,
          scanner_args: null,
          duration: null,
          started_at: null,
          inserted_at: now.toISOString(),
          completed_at: null,
          scheduled_at: nextScanAt,
          next_scan_at: null,
        },
        relationships: {
          provider: { data: { type: "providers", id: provider.id } },
          // No Celery task behind synthetic rows.
          task: { data: { type: "tasks", id: "" } },
        },
        providerInfo: {
          provider: provider.attributes.provider,
          uid: provider.attributes.uid,
          alias: provider.attributes.alias,
        },
        pendingSchedule: {
          summary: describeScheduleCadence(schedule),
          cadence: getScheduleCadenceParts(schedule).cadence,
          nextScanAt,
          lastScanAt: schedule.last_scan_at ?? null,
        },
      },
    ];
  });
}

export function getProviderIdsFromScans(scans: ScanProps[]): Set<string> {
  return new Set(
    scans
      .map((scan) => scan.relationships?.provider?.data?.id)
      .filter((id): id is string => Boolean(id)),
  );
}

export function appendPendingScheduleRowsToPage({
  scans,
  meta,
  page,
  pageSize,
  providers,
  schedulesByProviderId,
  coveredProviderIds,
  now,
}: AppendPendingScheduleRowsToPageParams): {
  data: ScanProps[];
  meta?: MetaDataProps;
} {
  // The API paginates real scheduled scans, while pending rows come from
  // configured schedules without a scan yet. Reconcile both sources here so the
  // rendered rows and pagination metadata describe the same combined list.
  const pendingRows = buildPendingScheduleRows({
    providers,
    schedulesByProviderId,
    coveredProviderIds,
    now,
  });
  const safePageSize = Math.max(1, pageSize);
  const realCount = meta?.pagination.count ?? scans.length;
  const pendingStart = Math.max(0, (page - 1) * safePageSize - realCount);
  const pendingSlots = Math.max(0, safePageSize - scans.length);
  const pagePendingRows = pendingRows.slice(
    pendingStart,
    pendingStart + pendingSlots,
  );
  const combinedCount = realCount + pendingRows.length;
  const combinedPages =
    combinedCount === 0 ? 0 : Math.ceil(combinedCount / safePageSize);

  return {
    data: [...scans, ...pagePendingRows],
    meta: meta
      ? {
          ...meta,
          pagination: {
            ...meta.pagination,
            page,
            count: combinedCount,
            pages: combinedPages,
          },
        }
      : undefined,
  };
}

// Provider filters the `/schedules` endpoint accepts (by id / type). The scans
// filter-bar's uid filter is not supported there, so it is intentionally dropped.
const SCHEDULE_FORWARDED_FILTER_KEYS = [
  PROVIDER_IN_FILTER_KEY,
  PROVIDER_TYPE_IN_FILTER_KEY,
] as const;

/** Provider filters forwarded to `/schedules` so the backend applies them and pagination stays native. */
export function pickScheduleProviderFilters(
  searchParams: SearchParamsProps,
): Record<string, string | string[]> {
  const filters: Record<string, string | string[]> = {};
  for (const key of SCHEDULE_FORWARDED_FILTER_KEYS) {
    const value = searchParams[key];
    if (typeof value === "string" || Array.isArray(value)) {
      filters[key] = value;
    }
  }
  return filters;
}

interface SchedulesPageResult {
  data?: ScheduleProps[] | null;
  included?: { type: string; id: string }[];
  meta?: MetaDataProps;
  error?: unknown;
}

/** Builds Scheduled-tab rows + meta from a `getSchedulesPage` result (advanced/Cloud path). */
export function buildScheduledTabRows(
  result: SchedulesPageResult | null | undefined,
  now: Date,
): { data: ScanProps[]; meta?: MetaDataProps } {
  if (!result || result.error) return { data: [] };

  const providerById = new Map(
    ((result.included ?? []) as ProviderProps[])
      .filter((resource) => resource.type === "providers")
      .map((provider) => [provider.id, provider]),
  );

  const data = (result.data ?? []).map((schedule) =>
    mapScheduleToScanRow(schedule, providerById.get(schedule.id), now),
  );

  return { data, meta: result.meta };
}

/** Maps a `/schedules` resource (1:1 with a provider) to the Scheduled-tab row shape. */
export function mapScheduleToScanRow(
  schedule: ScheduleProps,
  provider: ProviderProps | undefined,
  now: Date,
): ScanProps {
  // Reuse the canonical cadence + next/last-run summary used across the app.
  const summary = buildProviderScheduleSummary(schedule.attributes, now);

  return {
    type: "scans",
    id: `schedule-${schedule.id}`,
    attributes: {
      name: "",
      trigger: SCAN_TRIGGER.SCHEDULED,
      state: SCAN_STATE.SCHEDULED,
      unique_resource_count: 0,
      progress: 0,
      scanner_args: null,
      duration: null,
      started_at: null,
      inserted_at: now.toISOString(),
      completed_at: null,
      scheduled_at: summary.nextScanAt ?? null,
      next_scan_at: null,
    },
    relationships: {
      // The schedule resource id IS the provider id.
      provider: { data: { type: "providers", id: schedule.id } },
      // No Celery task behind a schedule row.
      task: { data: { type: "tasks", id: "" } },
    },
    providerInfo: provider
      ? {
          provider: provider.attributes.provider,
          uid: provider.attributes.uid,
          alias: provider.attributes.alias,
        }
      : undefined,
    pendingSchedule: summary,
  };
}

function getNumericValue(
  source: Record<string, unknown>,
  keys: string[],
): number | undefined {
  for (const key of keys) {
    const value = source[key];
    if (typeof value === "number" && Number.isFinite(value)) return value;
  }

  return undefined;
}

export function getScanFindingsSummary(
  attributes: ScanAttributes,
): ScanFindingsSummary | null {
  const root = attributes as unknown as Record<string, unknown>;
  const nested =
    typeof root.findings === "object" && root.findings !== null
      ? (root.findings as Record<string, unknown>)
      : {};
  const source = { ...root, ...nested };

  const fail = getNumericValue(source, [
    "fail",
    "failed",
    "failed_findings",
    "fail_findings",
  ]);
  const pass = getNumericValue(source, [
    "pass",
    "passed",
    "passed_findings",
    "pass_findings",
  ]);

  if (fail === undefined || pass === undefined) return null;

  return {
    fail,
    pass,
    failNew: getNumericValue(source, ["fail_new", "new_failed_findings"]),
    passNew: getNumericValue(source, ["pass_new", "new_passed_findings"]),
  };
}
