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

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

interface DeleteInput {
  sources: AnalyticsSource[];
  client_surfaces?: ClientSurface[];
  session_kinds?: Array<(typeof SESSION_KINDS)[number]>;
  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;
  runtime_event_observations: number;
  source_files: number;
}

interface ExactSelection {
  sessions: string[];
  message_metrics: string[];
  runtime_events: string[];
  runtime_event_observations: string[];
  source_files: string[];
  blocked_source_files: number;
}

interface SelectionDigests {
  sessions: string;
  message_metrics: string;
  runtime_events: string;
  runtime_event_observations: string;
  source_files: string;
  overall: string;
}

interface StoredPlanManifest {
  counts: MatchCounts;
  digests: SelectionDigests;
}

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"
  ]);

  const sources = parseOptionalEnumArray(obj.sources, ALL_SOURCES, "sources") ?? [...ALL_SOURCES];
  const client_surfaces = parseOptionalEnumArray(obj.client_surfaces, CLIENT_SURFACES, "client_surfaces");
  const session_kinds = parseOptionalEnumArray(obj.session_kinds, SESSION_KINDS, "session_kinds");

  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 {
  const normalized = { ...filters };
  for (const key of ["client_surfaces", "session_kinds", "date_from", "date_to", "period_type", "period_value"]) {
    if (normalized[key] === null) delete normalized[key];
  }
  return parseInput({
    ...normalized,
    dry_run: false,
    confirm_delete: false
  });
}

function selectionConditions(input: DeleteInput): {
  sessionWhere: string[]; sessionParams: unknown[]; eventWhere: string[]; eventParams: unknown[]; hasSessionFilters: boolean;
} {
  const hasSessionFilters = Boolean((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);
  }

  return { sessionWhere, sessionParams, eventWhere, eventParams, hasSessionFilters };
}

function digestIds(ids: string[]): string {
  return createHash("sha256").update(JSON.stringify(ids)).digest("hex");
}

function selectionManifest(selection: ExactSelection): StoredPlanManifest {
  const counts: MatchCounts = {
    sessions: selection.sessions.length,
    message_metrics: selection.message_metrics.length,
    runtime_events: selection.runtime_events.length,
    runtime_event_observations: selection.runtime_event_observations.length,
    source_files: selection.source_files.length
  };
  const setDigests = {
    sessions: digestIds(selection.sessions),
    message_metrics: digestIds(selection.message_metrics),
    runtime_events: digestIds(selection.runtime_events),
    runtime_event_observations: digestIds(selection.runtime_event_observations),
    source_files: digestIds(selection.source_files)
  };
  return {
    counts,
    digests: {
      ...setDigests,
      overall: createHash("sha256").update(JSON.stringify(setDigests)).digest("hex")
    }
  };
}

function sameManifest(a: StoredPlanManifest, b: StoredPlanManifest): boolean {
  return a.digests.sessions === b.digests.sessions
    && a.digests.message_metrics === b.digests.message_metrics
    && a.digests.runtime_events === b.digests.runtime_events
    && a.digests.runtime_event_observations === b.digests.runtime_event_observations
    && a.digests.source_files === b.digests.source_files
    && a.digests.overall === b.digests.overall;
}

async function exactSelection(db: DatabaseAdapter, input: DeleteInput): Promise<ExactSelection> {
  const { sessionWhere, sessionParams, eventWhere, eventParams, hasSessionFilters } = selectionConditions(input);
  const sessionRows = input.delete_scope === "sessions" || input.delete_scope === "all"
    ? await db.all<{ id: string }>(`SELECT id FROM sessions WHERE ${sessionWhere.join(" AND ")} ORDER BY id`, sessionParams)
    : [];
  const sessionIds = sessionRows.map((row) => row.id);
  const messageRows = sessionIds.length > 0
    ? await db.all<{ id: string }>(`SELECT id FROM message_metrics WHERE session_id IN (SELECT id FROM sessions WHERE ${sessionWhere.join(" AND ")}) ORDER BY id`, sessionParams)
    : [];
  const eventIds = new Set<string>();
  if (sessionIds.length > 0) {
    const rows = await db.all<{ id: string }>(`SELECT id FROM runtime_events WHERE session_id IN (SELECT id FROM sessions WHERE ${sessionWhere.join(" AND ")})`, sessionParams);
    for (const row of rows) eventIds.add(row.id);
  }
  if (input.delete_scope === "runtime_events" || input.delete_scope === "all") {
    const scopedWhere = hasSessionFilters
      ? [...eventWhere, `session_id IN (SELECT id FROM sessions WHERE ${sessionWhere.join(" AND ")})`]
      : eventWhere;
    const scopedParams = hasSessionFilters ? [...eventParams, ...sessionParams] : eventParams;
    const rows = await db.all<{ id: string }>(`SELECT id FROM runtime_events WHERE ${scopedWhere.join(" AND ")}`, scopedParams);
    for (const row of rows) eventIds.add(row.id);
  }
  const sortedEventIds = [...eventIds].sort();
  const observationIds: string[] = [];
  for (let offset = 0; offset < sortedEventIds.length; offset += 400) {
    const chunk = sortedEventIds.slice(offset, offset + 400);
    const rows = await db.all<{ id: string }>(
      `SELECT id FROM runtime_event_observations WHERE runtime_event_id IN (${chunk.map(() => "?").join(",")}) ORDER BY id`,
      chunk
    );
    observationIds.push(...rows.map((row) => row.id));
  }
  observationIds.sort();
  const selectedSessions = new Set(sessionIds);
  const selectedMessages = new Set(messageRows.map((row) => row.id));
  const selectedEvents = new Set(sortedEventIds);
  const selectedObservations = new Set(observationIds);
  const sourceFileIds: string[] = [];
  let blockedSourceFiles = 0;
  if (input.delete_scope === "source_files" || input.delete_scope === "all") {
    const candidates = await db.all<{ id: string }>(
      `SELECT id FROM source_files WHERE source IN (${input.sources.map(() => "?").join(",")}) ORDER BY id`, input.sources
    );
    for (const candidate of candidates) {
      const sessionRefs = await db.all<{ id: string }>("SELECT id FROM sessions WHERE source_file_id = ?", [candidate.id]);
      const messageRefs = await db.all<{ id: string }>("SELECT id FROM message_metrics WHERE source_file_id = ?", [candidate.id]);
      const eventRefs = await db.all<{ id: string }>("SELECT id FROM runtime_events WHERE source_file_id = ?", [candidate.id]);
      const observationRefs = await db.all<{ id: string }>("SELECT id FROM runtime_event_observations WHERE source_file_id = ?", [candidate.id]);
      if (
        sessionRefs.every((row) => selectedSessions.has(row.id))
        && messageRefs.every((row) => selectedMessages.has(row.id))
        && eventRefs.every((row) => selectedEvents.has(row.id))
        && observationRefs.every((row) => selectedObservations.has(row.id))
      ) {
        sourceFileIds.push(candidate.id);
      } else {
        blockedSourceFiles += 1;
      }
    }
  }
  return {
    sessions: sessionIds,
    message_metrics: messageRows.map((row) => row.id),
    runtime_events: sortedEventIds,
    runtime_event_observations: observationIds,
    source_files: sourceFileIds,
    blocked_source_files: blockedSourceFiles
  };
}

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

async function deleteIds(db: DatabaseAdapter, table: string, ids: string[]): Promise<number> {
  let changes = 0;
  for (let offset = 0; offset < ids.length; offset += 400) {
    const chunk = ids.slice(offset, offset + 400);
    const result = await db.run(`DELETE FROM ${table} WHERE id IN (${chunk.map(() => "?").join(",")})`, chunk);
    changes += result.changes;
  }
  return changes;
}

async function deleteExactSelection(db: DatabaseAdapter, selection: ExactSelection): Promise<{ deleted: MatchCounts; warnings: Array<{ code: string; message_code: string; severity: "warning" }> }> {
  const warnings: Array<{ code: string; message_code: string; severity: "warning" }> = Array.from(
    { length: selection.blocked_source_files },
    () => ({ code: "SOURCE_FILE_STILL_REFERENCED", message_code: "SOURCE_FILE_STILL_REFERENCED", severity: "warning" as const })
  );
  const deleted: MatchCounts = {
    runtime_event_observations: await deleteIds(db, "runtime_event_observations", selection.runtime_event_observations),
    message_metrics: await deleteIds(db, "message_metrics", selection.message_metrics),
    runtime_events: await deleteIds(db, "runtime_events", selection.runtime_events),
    sessions: await deleteIds(db, "sessions", selection.sessions),
    source_files: await deleteIds(db, "source_files", selection.source_files)
  };
  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);
    await db.exec("BEGIN TRANSACTION");
    try {
      const selection = await exactSelection(db, input);
      const manifest = selectionManifest(selection);
      const matched = manifest.counts;
      const runId = randomUUID();
      const planId = buildPlanId(filters, manifest, runId);
      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 (?, ?, ?, NULL, 'planned', 1, ?, ?, ?, NULL, '[]')`,
        [runId, planId, nowIso(), JSON.stringify(input.sources), JSON.stringify(filters), JSON.stringify(manifest)]
      );
      await db.exec("COMMIT");
      return { ok: true, dry_run: true, delete_plan_id: planId, matched, warnings: [] };
    } catch (error) {
      try { await db.exec("ROLLBACK"); } catch { /* preserve the original planning error */ }
      throw error;
    }
  }

  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.");
  }

  await db.exec("BEGIN IMMEDIATE TRANSACTION");
  try {
    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 storedManifest = JSON.parse(planned.matched_json) as StoredPlanManifest;
    if (!storedManifest.counts || !storedManifest.digests) throw new Error("DELETE_PLAN_CHANGED: legacy plan must be regenerated.");
    const selection = await exactSelection(db, plannedInput);
    const currentManifest = selectionManifest(selection);
    if (!sameManifest(storedManifest, currentManifest) || buildPlanId(storedFilters, currentManifest, planned.id) !== input.confirm_plan_id) {
      throw new Error("DELETE_PLAN_CHANGED: plan changed; rerun dry-run.");
    }
    const consumed = await db.run(
      "UPDATE delete_runs SET status = 'completed', dry_run = 0 WHERE id = ? AND plan_id = ? AND status = 'planned'",
      [planned.id, input.confirm_plan_id]
    );
    if (consumed.changes !== 1) throw new Error("DELETE_PLAN_CONSUMED: delete plan is no longer available.");
    const result = await deleteExactSelection(db, selection);
    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), planned.id]
    );
    await db.exec("COMMIT");
    return { ok: true, dry_run: false, delete_plan_id: input.confirm_plan_id, matched: currentManifest.counts, deleted: result.deleted, warnings: result.warnings };
  } catch (error) {
    try { await db.exec("ROLLBACK"); } catch { /* preserve the original confirmation error */ }
    throw error;
  }
}
