import { createHash, randomUUID } from "node:crypto";
import type { DatabaseAdapter } from "../db.js";
import { ValidationError, assertAllowedKeys } from "../errors.js";
import { ALL_SOURCES, type AnalyticsSource } from "../sources.js";

type DeleteScope = "all" | "sessions" | "runtime_events" | "source_files";
type PeriodType = "day" | "month" | "year";

interface DeleteInput {
  sources: AnalyticsSource[];
  client_surfaces?: Array<"cli" | "desktop" | "vscode" | "vscode_insiders" | "code" | "unknown">;
  session_kinds?: Array<"main" | "subagent" | "task" | "unknown">;
  date_from?: string;
  date_to?: string;
  period_type?: PeriodType;
  period_value?: string;
  delete_scope: DeleteScope;
  dry_run: boolean;
  confirm_delete: boolean;
  confirm_plan_id?: string;
}

interface MatchCounts {
  sessions: number;
  message_metrics: number;
  runtime_events: number;
  source_files: number;
}

function nowIso(): string {
  return new Date().toISOString();
}

function parseIso(value: unknown): string | undefined {
  if (value === undefined || value === null) return undefined;
  if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) throw new ValidationError("invalid date.");
  return new Date(value).toISOString();
}

function rangeFromPeriod(periodType: PeriodType, periodValue: string): { from: string; to: string } {
  if (periodType === "day") {
    if (!/^\d{4}-\d{2}-\d{2}$/u.test(periodValue)) throw new ValidationError("invalid day period_value.");
    const from = new Date(`${periodValue}T00:00:00.000Z`);
    const to = new Date(from.getTime() + 24 * 60 * 60 * 1000);
    return { from: from.toISOString(), to: to.toISOString() };
  }
  if (periodType === "month") {
    if (!/^\d{4}-\d{2}$/u.test(periodValue)) throw new ValidationError("invalid month period_value.");
    const [y, m] = periodValue.split("-").map((v) => Number.parseInt(v, 10));
    const from = new Date(Date.UTC(y, m - 1, 1, 0, 0, 0, 0));
    const to = new Date(Date.UTC(y, m, 1, 0, 0, 0, 0));
    return { from: from.toISOString(), to: to.toISOString() };
  }
  if (!/^\d{4}$/u.test(periodValue)) throw new ValidationError("invalid year period_value.");
  const y = Number.parseInt(periodValue, 10);
  const from = new Date(Date.UTC(y, 0, 1, 0, 0, 0, 0));
  const to = new Date(Date.UTC(y + 1, 0, 1, 0, 0, 0, 0));
  return { from: from.toISOString(), to: to.toISOString() };
}

function parseInput(args: unknown): DeleteInput {
  const obj = (args && typeof args === "object" && !Array.isArray(args) ? args : {}) as Record<string, unknown>;
  assertAllowedKeys(obj, [
    "sources",
    "client_surfaces",
    "session_kinds",
    "date_from",
    "date_to",
    "period_type",
    "period_value",
    "delete_scope",
    "dry_run",
    "confirm_delete",
    "confirm_plan_id"
  ]);

  let sources: AnalyticsSource[] = [...ALL_SOURCES];
  if (Array.isArray(obj.sources)) {
    const parsed: AnalyticsSource[] = [];
    for (const s of obj.sources) {
      if (typeof s !== "string" || !ALL_SOURCES.includes(s as AnalyticsSource)) throw new ValidationError("invalid sources.");
      if (!parsed.includes(s as AnalyticsSource)) parsed.push(s as AnalyticsSource);
    }
    sources = parsed;
  } else if (obj.sources !== undefined) {
    throw new ValidationError("sources must be an array.");
  }

  let client_surfaces: DeleteInput["client_surfaces"];
  if (obj.client_surfaces === null || obj.client_surfaces === undefined) {
    client_surfaces = undefined;
  } else if (Array.isArray(obj.client_surfaces)) {
    const allowed = new Set(["cli", "desktop", "vscode", "vscode_insiders", "code", "unknown"]);
    client_surfaces = [];
    for (const s of obj.client_surfaces) {
      if (typeof s !== "string" || !allowed.has(s)) throw new ValidationError("invalid client_surfaces.");
      if (!client_surfaces.includes(s as "cli" | "desktop" | "vscode" | "vscode_insiders" | "code" | "unknown")) {
        client_surfaces.push(s as "cli" | "desktop" | "vscode" | "vscode_insiders" | "code" | "unknown");
      }
    }
  } else {
    throw new ValidationError("client_surfaces must be an array.");
  }

  let session_kinds: DeleteInput["session_kinds"];
  if (obj.session_kinds === null || obj.session_kinds === undefined) {
    session_kinds = undefined;
  } else if (Array.isArray(obj.session_kinds)) {
    const allowed = new Set(["main", "subagent", "task", "unknown"]);
    session_kinds = [];
    for (const s of obj.session_kinds) {
      if (typeof s !== "string" || !allowed.has(s)) throw new ValidationError("invalid session_kinds.");
      if (!session_kinds.includes(s as "main" | "subagent" | "task" | "unknown")) {
        session_kinds.push(s as "main" | "subagent" | "task" | "unknown");
      }
    }
  } else {
    throw new ValidationError("session_kinds must be an array.");
  }

  const date_from = parseIso(obj.date_from);
  const date_to = parseIso(obj.date_to);
  const period_type = obj.period_type as PeriodType | undefined;
  const period_value = typeof obj.period_value === "string" ? obj.period_value : undefined;
  if ((date_from || date_to) && (period_type || period_value)) {
    throw new ValidationError("period filters and date filters are mutually exclusive.");
  }
  let rangeFrom: string | undefined = date_from;
  let rangeTo: string | undefined = date_to;
  if (period_type || period_value) {
    if (!period_type || !period_value) throw new ValidationError("period_type and period_value must be provided together.");
    if (period_type !== "day" && period_type !== "month" && period_type !== "year") throw new ValidationError("invalid period_type.");
    const range = rangeFromPeriod(period_type, period_value);
    rangeFrom = range.from;
    rangeTo = range.to;
  }
  const delete_scope = (obj.delete_scope as DeleteScope | undefined) ?? "all";
  if (!["all", "sessions", "runtime_events", "source_files"].includes(delete_scope)) {
    throw new ValidationError("invalid delete_scope.");
  }
  if (obj.dry_run !== undefined && typeof obj.dry_run !== "boolean") throw new ValidationError("dry_run must be boolean.");
  if (obj.confirm_delete !== undefined && typeof obj.confirm_delete !== "boolean") throw new ValidationError("confirm_delete must be boolean.");
  if (obj.confirm_plan_id !== undefined && typeof obj.confirm_plan_id !== "string") throw new ValidationError("confirm_plan_id must be string.");

  const dry_run = obj.dry_run === undefined ? true : obj.dry_run;
  const confirm_delete = obj.confirm_delete === undefined ? false : obj.confirm_delete;
  const confirm_plan_id = obj.confirm_plan_id as string | undefined;
  return {
    sources,
    client_surfaces,
    session_kinds,
    date_from: rangeFrom,
    date_to: rangeTo,
    period_type,
    period_value,
    delete_scope,
    dry_run,
    confirm_delete,
    confirm_plan_id
  };
}

function buildFilters(input: DeleteInput): Record<string, unknown> {
  return {
    sources: input.sources,
    client_surfaces: input.client_surfaces ?? null,
    session_kinds: input.session_kinds ?? null,
    date_from: input.date_from ?? null,
    date_to: input.date_to ?? null,
    period_type: null,
    period_value: null,
    delete_scope: input.delete_scope
  };
}

function inputFromFilters(filters: Record<string, unknown>): DeleteInput {
  return parseInput({
    ...filters,
    dry_run: false,
    confirm_delete: false
  });
}

async function matchCounts(db: DatabaseAdapter, input: DeleteInput): Promise<MatchCounts> {
  const hasSessionFilters = (input.client_surfaces && input.client_surfaces.length > 0) || (input.session_kinds && input.session_kinds.length > 0);
  const sessionSources = input.sources.filter((s) => s !== "hook_log");
  const eventSources = input.sources;
  const sessionWhere: string[] = [];
  const sessionParams: unknown[] = [];
  if (sessionSources.length > 0) {
    sessionWhere.push(`source IN (${sessionSources.map(() => "?").join(",")})`);
    sessionParams.push(...sessionSources);
  } else {
    sessionWhere.push("1=0");
  }
  if (input.date_from) {
    sessionWhere.push("updated_at >= ?");
    sessionParams.push(input.date_from);
  }
  if (input.date_to) {
    sessionWhere.push("updated_at < ?");
    sessionParams.push(input.date_to);
  }
  if (input.client_surfaces && input.client_surfaces.length > 0) {
    sessionWhere.push(`COALESCE(client_surface,'unknown') IN (${input.client_surfaces.map(() => "?").join(",")})`);
    sessionParams.push(...input.client_surfaces);
  }
  if (input.session_kinds && input.session_kinds.length > 0) {
    sessionWhere.push(`COALESCE(session_kind,'unknown') IN (${input.session_kinds.map(() => "?").join(",")})`);
    sessionParams.push(...input.session_kinds);
  }

  const eventWhere: string[] = [];
  const eventParams: unknown[] = [];
  if (eventSources.length > 0) {
    eventWhere.push(`source IN (${eventSources.map(() => "?").join(",")})`);
    eventParams.push(...eventSources);
  } else {
    eventWhere.push("1=0");
  }
  if (input.date_from) {
    eventWhere.push("occurred_at >= ?");
    eventParams.push(input.date_from);
  }
  if (input.date_to) {
    eventWhere.push("occurred_at < ?");
    eventParams.push(input.date_to);
  }

  const sessions = await db.get<{ n: number }>(`SELECT COUNT(*) as n FROM sessions WHERE ${sessionWhere.join(" AND ")}`, sessionParams);
  const messages = await db.get<{ n: number }>(
    `SELECT COUNT(*) as n FROM message_metrics WHERE session_id IN (SELECT id FROM sessions WHERE ${sessionWhere.join(" AND ")})`,
    sessionParams
  );
  const eventsSessions = await db.get<{ n: number }>(
    `SELECT COUNT(*) as n FROM runtime_events WHERE session_id IN (SELECT id FROM sessions WHERE ${sessionWhere.join(" AND ")})`,
    sessionParams
  );
  const eventsHook = hasSessionFilters
    ? { n: 0 }
    : await db.get<{ n: number }>(
        `SELECT COUNT(*) as n FROM runtime_events WHERE session_id IS NULL AND ${eventWhere.join(" AND ")}`,
        eventParams
      );
  const sourceFiles = await db.get<{ n: number }>(
    `SELECT COUNT(*) as n FROM source_files WHERE source IN (${input.sources.map(() => "?").join(",")})`,
    input.sources
  );
  return {
    sessions: sessions?.n ?? 0,
    message_metrics: messages?.n ?? 0,
    runtime_events: (eventsSessions?.n ?? 0) + (eventsHook?.n ?? 0),
    source_files: sourceFiles?.n ?? 0
  };
}

function buildPlanId(filters: Record<string, unknown>, matched: MatchCounts): string {
  const payload = JSON.stringify({ filters, matched });
  return createHash("sha256").update(payload).digest("hex").slice(0, 24);
}

async function deleteByScope(db: DatabaseAdapter, input: DeleteInput): Promise<{ deleted: MatchCounts; warnings: Array<{ code: string; message_code: string; severity: "warning" }> }> {
  const warnings: Array<{ code: string; message_code: string; severity: "warning" }> = [];
  const deleted: MatchCounts = { sessions: 0, message_metrics: 0, runtime_events: 0, source_files: 0 };
  const sessionSources = input.sources.filter((s) => s !== "hook_log");
  const sessionWhere: string[] = [];
  const sessionParams: unknown[] = [];
  if (sessionSources.length > 0) {
    sessionWhere.push(`source IN (${sessionSources.map(() => "?").join(",")})`);
    sessionParams.push(...sessionSources);
  } else {
    sessionWhere.push("1=0");
  }
  if (input.date_from) {
    sessionWhere.push("updated_at >= ?");
    sessionParams.push(input.date_from);
  }
  if (input.date_to) {
    sessionWhere.push("updated_at < ?");
    sessionParams.push(input.date_to);
  }
  if (input.client_surfaces && input.client_surfaces.length > 0) {
    sessionWhere.push(`COALESCE(client_surface,'unknown') IN (${input.client_surfaces.map(() => "?").join(",")})`);
    sessionParams.push(...input.client_surfaces);
  }
  if (input.session_kinds && input.session_kinds.length > 0) {
    sessionWhere.push(`COALESCE(session_kind,'unknown') IN (${input.session_kinds.map(() => "?").join(",")})`);
    sessionParams.push(...input.session_kinds);
  }

  const eventWhere: string[] = [];
  const eventParams: unknown[] = [];
  if (input.sources.length > 0) {
    eventWhere.push(`source IN (${input.sources.map(() => "?").join(",")})`);
    eventParams.push(...input.sources);
  }
  if (input.date_from) {
    eventWhere.push("occurred_at >= ?");
    eventParams.push(input.date_from);
  }
  if (input.date_to) {
    eventWhere.push("occurred_at < ?");
    eventParams.push(input.date_to);
  }

  await db.exec("BEGIN IMMEDIATE TRANSACTION");
  try {
    if (input.delete_scope === "sessions" || input.delete_scope === "all") {
      const beforeMm = await db.get<{ n: number }>(`SELECT COUNT(*) as n FROM message_metrics WHERE session_id IN (SELECT id FROM sessions WHERE ${sessionWhere.join(" AND ")})`, sessionParams);
      const beforeEv = await db.get<{ n: number }>(`SELECT COUNT(*) as n FROM runtime_events WHERE session_id IN (SELECT id FROM sessions WHERE ${sessionWhere.join(" AND ")})`, sessionParams);
      await db.run(`DELETE FROM message_metrics WHERE session_id IN (SELECT id FROM sessions WHERE ${sessionWhere.join(" AND ")})`, sessionParams);
      await db.run(`DELETE FROM runtime_events WHERE session_id IN (SELECT id FROM sessions WHERE ${sessionWhere.join(" AND ")})`, sessionParams);
      const resS = await db.run(`DELETE FROM sessions WHERE ${sessionWhere.join(" AND ")}`, sessionParams);
      deleted.sessions += resS.changes;
      deleted.message_metrics += beforeMm?.n ?? 0;
      deleted.runtime_events += beforeEv?.n ?? 0;
    }

    if (input.delete_scope === "runtime_events" || input.delete_scope === "all") {
      const hasSessionFilters = (input.client_surfaces && input.client_surfaces.length > 0) || (input.session_kinds && input.session_kinds.length > 0);
      if (hasSessionFilters) {
        const sessionScopedWhere = [...eventWhere, `session_id IN (SELECT id FROM sessions WHERE ${sessionWhere.join(" AND ")})`];
        const sessionScopedParams = [...eventParams, ...sessionParams];
        const resScoped = await db.run(`DELETE FROM runtime_events WHERE ${sessionScopedWhere.join(" AND ")}`, sessionScopedParams);
        deleted.runtime_events += resScoped.changes;
      } else {
        const resE = await db.run(`DELETE FROM runtime_events WHERE ${eventWhere.join(" AND ")}`, eventParams);
        deleted.runtime_events += resE.changes;
      }
    }

    if (input.delete_scope === "source_files" || input.delete_scope === "all") {
      for (const source of input.sources) {
        const candidates = await db.all<{ id: string }>("SELECT id FROM source_files WHERE source = ?", [source]);
        for (const row of candidates) {
          const refsS = await db.get<{ n: number }>("SELECT COUNT(*) as n FROM sessions WHERE source_file_id = ?", [row.id]);
          const refsE = await db.get<{ n: number }>("SELECT COUNT(*) as n FROM runtime_events WHERE source_file_id = ?", [row.id]);
          if ((refsS?.n ?? 0) > 0 || (refsE?.n ?? 0) > 0) {
            warnings.push({ code: "SOURCE_FILE_STILL_REFERENCED", message_code: "SOURCE_FILE_STILL_REFERENCED", severity: "warning" });
            continue;
          }
          const resF = await db.run("DELETE FROM source_files WHERE id = ?", [row.id]);
          deleted.source_files += resF.changes;
        }
      }
    }
    await db.exec("COMMIT");
  } catch (e) {
    await db.exec("ROLLBACK");
    throw e;
  }
  return { deleted, warnings };
}

export async function deleteImportedData(db: DatabaseAdapter, args: unknown): Promise<Record<string, unknown>> {
  const input = parseInput(args);

  if (input.dry_run) {
    const filters = buildFilters(input);
    const matched = await matchCounts(db, input);
    const planId = buildPlanId(filters, matched);
    const runId = randomUUID();
    await db.run(
      `INSERT INTO delete_runs(id, plan_id, started_at, completed_at, status, dry_run, sources_json, filters_json, matched_json, deleted_json, warnings_json)
       VALUES (?, ?, ?, ?, 'planned', 1, ?, ?, ?, NULL, '[]')`,
      [runId, planId, nowIso(), nowIso(), JSON.stringify(input.sources), JSON.stringify(filters), JSON.stringify(matched)]
    );
    return { ok: true, dry_run: true, delete_plan_id: planId, matched, warnings: [] };
  }

  if (!input.confirm_delete) {
    throw new ValidationError("confirm_delete must be true for non-dry-run.");
  }
  if (!input.confirm_plan_id) {
    throw new ValidationError("confirm_plan_id is required for non-dry-run.");
  }

  const planned = await db.get<{ id: string; plan_id: string; matched_json: string; filters_json: string; sources_json: string }>(
    "SELECT id, plan_id, matched_json, filters_json, sources_json FROM delete_runs WHERE plan_id = ? AND status = 'planned' ORDER BY started_at DESC LIMIT 1",
    [input.confirm_plan_id]
  );
  if (!planned) {
    throw new ValidationError("matching planned delete not found.");
  }

  const storedFilters = JSON.parse(planned.filters_json) as Record<string, unknown>;
  const plannedInput = inputFromFilters(storedFilters);
  const matchedNow = await matchCounts(db, plannedInput);
  const computedPlanId = buildPlanId(storedFilters, matchedNow);
  if (computedPlanId !== input.confirm_plan_id) {
    throw new Error("DELETE_PLAN_CHANGED: plan changed; rerun dry-run.");
  }
  const previousMatched = JSON.parse(planned.matched_json) as MatchCounts;
  if (
    previousMatched.sessions !== matchedNow.sessions ||
    previousMatched.message_metrics !== matchedNow.message_metrics ||
    previousMatched.runtime_events !== matchedNow.runtime_events ||
    previousMatched.source_files !== matchedNow.source_files
  ) {
    throw new Error("DELETE_PLAN_CHANGED: plan changed; rerun dry-run.");
  }

  const runId = randomUUID();
  await db.run(
    `INSERT INTO delete_runs(id, plan_id, started_at, status, dry_run, sources_json, filters_json, matched_json, warnings_json)
     VALUES (?, ?, ?, 'planned', 0, ?, ?, ?, '[]')`,
    [runId, input.confirm_plan_id, nowIso(), planned.sources_json, planned.filters_json, JSON.stringify(matchedNow)]
  );
  const result = await deleteByScope(db, plannedInput);
  await db.run(
    "UPDATE delete_runs SET completed_at = ?, status = 'completed', deleted_json = ?, warnings_json = ? WHERE id = ?",
    [nowIso(), JSON.stringify(result.deleted), JSON.stringify(result.warnings), runId]
  );
  return { ok: true, dry_run: false, delete_plan_id: input.confirm_plan_id, matched: matchedNow, deleted: result.deleted, warnings: result.warnings };
}
