import type { DatabaseAdapter } from "../db.js";
import { ValidationError, assertAllowedKeys } from "../errors.js";
import { ALL_SOURCES, type AnalyticsSource } from "../sources.js";

type WarningSeverity = "info" | "warning" | "error";

interface ScanWarning {
  code: string;
  message_code: string;
  severity: WarningSeverity;
  source_file_id?: string;
  source?: AnalyticsSource;
  file_outcome?: "recovered" | "failed";
  details?: { count?: unknown };
}

interface ScanRunRow {
  id: string;
  started_at: string;
  completed_at: string | null;
  status: string;
  sources_json: string;
  files_seen: number;
  files_imported: number;
  files_skipped: number;
  files_failed: number;
  warnings_json: string | null;
}

function parseInput(args: unknown): { scan_run_id?: string } {
  if (!args || typeof args !== "object" || Array.isArray(args)) throw new ValidationError("Input must be an object.");
  const obj = args as Record<string, unknown>;
  assertAllowedKeys(obj, ["scan_run_id"]);
  if (obj.scan_run_id !== undefined && (typeof obj.scan_run_id !== "string" || obj.scan_run_id.length === 0 || obj.scan_run_id.length > 128)) {
    throw new ValidationError("scan_run_id must be a non-empty string.");
  }
  return { scan_run_id: obj.scan_run_id as string | undefined };
}

function parseWarnings(raw: string | null): ScanWarning[] {
  if (!raw) return [];
  try {
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [];
    return parsed.flatMap((value): ScanWarning[] => {
      if (!value || typeof value !== "object" || Array.isArray(value)) return [];
      const warning = value as Record<string, unknown>;
      const severity = warning.severity;
      if (typeof warning.code !== "string" || typeof warning.message_code !== "string" || !["info", "warning", "error"].includes(String(severity))) return [];
      return [{
        code: warning.code,
        message_code: warning.message_code,
        severity: severity as WarningSeverity,
        ...(typeof warning.source_file_id === "string" ? { source_file_id: warning.source_file_id } : {})
        , ...(typeof warning.source === "string" && ALL_SOURCES.includes(warning.source as AnalyticsSource) ? { source: warning.source as AnalyticsSource } : {})
        , ...(warning.file_outcome === "recovered" || warning.file_outcome === "failed" ? { file_outcome: warning.file_outcome } : {})
        , ...(warning.details && typeof warning.details === "object" && !Array.isArray(warning.details) ? { details: warning.details as { count?: unknown } } : {})
      }];
    });
  } catch {
    return [];
  }
}

function parseSources(raw: string): AnalyticsSource[] {
  try {
    const values = JSON.parse(raw);
    return Array.isArray(values) ? values.filter((value): value is AnalyticsSource => typeof value === "string" && ALL_SOURCES.includes(value as AnalyticsSource)) : [];
  } catch {
    return [];
  }
}

export async function queryScanDetails(db: DatabaseAdapter, args: unknown): Promise<Record<string, unknown>> {
  const input = parseInput(args);
  const run = input.scan_run_id
    ? await db.get<ScanRunRow>("SELECT id, started_at, completed_at, status, sources_json, files_seen, files_imported, files_skipped, files_failed, warnings_json FROM scan_runs WHERE id = ?", [input.scan_run_id])
    : await db.get<ScanRunRow>("SELECT id, started_at, completed_at, status, sources_json, files_seen, files_imported, files_skipped, files_failed, warnings_json FROM scan_runs WHERE completed_at IS NOT NULL ORDER BY completed_at DESC, id DESC LIMIT 1");
  if (!run) return { run: null, details: [] };

  const sourceByFileId = new Map<string, AnalyticsSource>();
  for (const warning of parseWarnings(run.warnings_json)) {
    if (warning.source && warning.source_file_id) sourceByFileId.set(warning.source_file_id, warning.source);
  }
  const missingSourceIds = [...new Set(parseWarnings(run.warnings_json).flatMap((warning) => warning.source_file_id && !warning.source ? [warning.source_file_id] : []))];
  if (missingSourceIds.length) {
    const placeholders = missingSourceIds.map(() => "?").join(",");
    const rows = await db.all<{ id: string; source: AnalyticsSource }>(`SELECT id, source FROM source_files WHERE id IN (${placeholders})`, missingSourceIds);
    for (const row of rows) sourceByFileId.set(row.id, row.source);
  }
  const grouped = new Map<string, { source: AnalyticsSource | "unknown"; file_outcome: "recovered" | "failed" | "unknown"; severity: WarningSeverity; code: string; message_code: string; occurrences: number; source_file_ids: Set<string> }>();
  for (const warning of parseWarnings(run.warnings_json)) {
    const source = warning.source ?? (warning.source_file_id ? sourceByFileId.get(warning.source_file_id) : undefined) ?? "unknown";
    const file_outcome = warning.file_outcome ?? "unknown";
    const key = `${source}:${file_outcome}:${warning.severity}:${warning.code}`;
    const detail = grouped.get(key) ?? { source, file_outcome, severity: warning.severity, code: warning.code, message_code: warning.message_code, occurrences: 0, source_file_ids: new Set<string>() };
    const count = warning.details?.count;
    detail.occurrences += typeof count === "number" && Number.isFinite(count) && count > 0 ? count : 1;
    if (warning.source_file_id) detail.source_file_ids.add(warning.source_file_id);
    grouped.set(key, detail);
  }
  const details = [...grouped.values()]
    .sort((a, b) => a.source.localeCompare(b.source) || a.file_outcome.localeCompare(b.file_outcome) || a.severity.localeCompare(b.severity) || a.code.localeCompare(b.code))
    .map((detail) => ({ ...detail, source_file_ids: [...detail.source_file_ids].sort() }));
  return {
    run: {
      id: run.id,
      started_at: run.started_at,
      completed_at: run.completed_at,
      status: run.status,
      sources: parseSources(run.sources_json),
      files_seen: run.files_seen,
      files_imported: run.files_imported,
      files_skipped: run.files_skipped,
      files_failed: run.files_failed
    },
    details
  };
}
