import { randomUUID } from "node:crypto";
import { Database } from "bun:sqlite";
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";

export class IncidentMutationLeaseError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "IncidentMutationLeaseError";
  }
}

export type LaunchPhase = "claimed" | "metadata" | "board" | "started";
export type ResolutionPhase = "verified" | "closed" | "learned" | "quarantined";

export interface ResolutionRecord {
  taskId: number;
  evidenceSha256: string;
  evidenceJson: string;
  phase: ResolutionPhase;
}

export interface PendingResolutionRecord extends ResolutionRecord {
  incidentId: string;
  attemptCount?: number;
  nextAttemptAtMs?: number;
  lastError?: string | null;
}

interface ResolutionRow {
  incident_id: string;
  task_id: number;
  evidence_sha256: string;
  evidence_json: string;
  phase: ResolutionPhase;
  attempt_count: number;
  next_attempt_at_ms: number;
  last_error: string | null;
}

export interface LaunchClaimRecord {
  taskId: number;
  dispatchId: string;
  phase: LaunchPhase;
}

export interface LaunchClaimResult extends LaunchClaimRecord {
  result: "claimed" | "replay" | "lost";
}

interface LeaseRow {
  holder: string;
  until_ms: number;
}

interface LaunchClaimRow {
  task_id: number;
  dispatch_id: string;
  phase: string;
}

const LAUNCH_PHASES: readonly LaunchPhase[] = ["claimed", "metadata", "board", "started"];

function parseLaunchPhase(raw: string | null | undefined): LaunchPhase {
  if (raw && (LAUNCH_PHASES as readonly string[]).includes(raw)) {
    return raw as LaunchPhase;
  }
  return "claimed";
}

/**
 * Cross-process serialization for incident filing and dispatch. Each collector
 * request constructs a fresh IncidentService; in-memory locks cannot coordinate
 * separate instances.
 */
export class IncidentMutationStore {
  private readonly db: Database;

  constructor(path: string) {
    mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
    this.db = new Database(path, { create: true });
    this.db.exec("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA busy_timeout=5000;");
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS incident_mutation_leases (
        lock_key TEXT PRIMARY KEY,
        holder TEXT NOT NULL,
        until_ms INTEGER NOT NULL
      );
      CREATE TABLE IF NOT EXISTS incident_launch_claims (
        incident_id TEXT PRIMARY KEY,
        task_id INTEGER NOT NULL,
        dispatch_id TEXT NOT NULL,
        claimed_at_ms INTEGER NOT NULL,
        phase TEXT NOT NULL DEFAULT 'claimed'
      );
      CREATE TABLE IF NOT EXISTS incident_resolution_operations (
        incident_id TEXT PRIMARY KEY,
        task_id INTEGER NOT NULL,
        evidence_sha256 TEXT NOT NULL,
        evidence_json TEXT NOT NULL,
        phase TEXT NOT NULL,
        attempt_count INTEGER NOT NULL DEFAULT 0,
        next_attempt_at_ms INTEGER NOT NULL DEFAULT 0,
        last_error TEXT
      );
      CREATE TABLE IF NOT EXISTS incident_deletion_operations (
        incident_id TEXT PRIMARY KEY,
        task_id INTEGER NOT NULL,
        holder TEXT NOT NULL,
        phase TEXT NOT NULL CHECK (phase IN ('intent', 'removed', 'finalized'))
      );
      CREATE TABLE IF NOT EXISTS incident_resolution_reconciliation (
        singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
        cursor TEXT NOT NULL DEFAULT ''
      );
      INSERT OR IGNORE INTO incident_resolution_reconciliation(singleton, cursor) VALUES (1, '');
    `);
    this.ensureLaunchPhaseColumn();
    this.ensureResolutionRetryColumns();
  }

  private ensureResolutionRetryColumns(): void {
    const columns = this.db.query<{ name: string }, []>("PRAGMA table_info(incident_resolution_operations)").all();
    const names = new Set(columns.map((column) => column.name));
    if (!names.has("attempt_count")) this.db.exec("ALTER TABLE incident_resolution_operations ADD COLUMN attempt_count INTEGER NOT NULL DEFAULT 0");
    if (!names.has("next_attempt_at_ms")) this.db.exec("ALTER TABLE incident_resolution_operations ADD COLUMN next_attempt_at_ms INTEGER NOT NULL DEFAULT 0");
    if (!names.has("last_error")) this.db.exec("ALTER TABLE incident_resolution_operations ADD COLUMN last_error TEXT");
  }

  private ensureLaunchPhaseColumn(): void {
    const columns = this.db.query<{ name: string }, []>("PRAGMA table_info(incident_launch_claims)").all();
    if (!columns.some((column) => column.name === "phase")) {
      this.db.exec("ALTER TABLE incident_launch_claims ADD COLUMN phase TEXT NOT NULL DEFAULT 'claimed'");
    }
  }

  acquire(lockKey: string, holder: string, nowMs: number, leaseMs = 120_000): boolean {
    return this.db.transaction(() => {
      const updated = this.db.query(
        `UPDATE incident_mutation_leases SET holder = ?, until_ms = ?
         WHERE lock_key = ? AND until_ms < ?`,
      ).run(holder, nowMs + leaseMs, lockKey, nowMs);
      if (updated.changes === 1) return true;
      const current = this.db.query<LeaseRow, [string]>(
        "SELECT holder, until_ms FROM incident_mutation_leases WHERE lock_key = ?",
      ).get(lockKey);
      if (current && current.until_ms >= nowMs) return false;
      try {
        this.db.query(
          "INSERT INTO incident_mutation_leases(lock_key, holder, until_ms) VALUES (?, ?, ?)",
        ).run(lockKey, holder, nowMs + leaseMs);
        return true;
      } catch {
        const raced = this.db.query<LeaseRow, [string]>(
          "SELECT holder, until_ms FROM incident_mutation_leases WHERE lock_key = ?",
        ).get(lockKey);
        return raced?.holder === holder && raced.until_ms >= nowMs;
      }
    })();
  }

  renewLease(lockKey: string, holder: string, nowMs: number, leaseMs = 120_000): boolean {
    const updated = this.db.query(
      `UPDATE incident_mutation_leases
       SET until_ms = ?
       WHERE lock_key = ? AND holder = ? AND until_ms >= ?`,
    ).run(nowMs + leaseMs, lockKey, holder, nowMs);
    return updated.changes === 1;
  }

  release(lockKey: string, holder: string): void {
    this.db.query(
      "DELETE FROM incident_mutation_leases WHERE lock_key = ? AND holder = ?",
    ).run(lockKey, holder);
  }

  getLaunchClaim(incidentId: string): LaunchClaimRecord | null {
    const row = this.db.query<LaunchClaimRow, [string]>(
      "SELECT task_id, dispatch_id, phase FROM incident_launch_claims WHERE incident_id = ?",
    ).get(incidentId);
    if (!row) return null;
    return {
      taskId: row.task_id,
      dispatchId: row.dispatch_id,
      phase: parseLaunchPhase(row.phase),
    };
  }

  /**
   * Exactly one claimant may own the launch transition for an incident. Replays
   * return the canonical dispatch id and durable phase; a different task id loses.
   */
  claimLaunch(
    incidentId: string,
    taskId: number,
    preferredDispatchId: string | null,
    nowMs: number,
  ): LaunchClaimResult {
    return this.db.transaction((): LaunchClaimResult => {
      const existing = this.db.query<LaunchClaimRow, [string]>(
        "SELECT task_id, dispatch_id, phase FROM incident_launch_claims WHERE incident_id = ?",
      ).get(incidentId);
      if (!existing) {
        const dispatchId = preferredDispatchId ?? randomUUID();
        this.db.query(
          "INSERT INTO incident_launch_claims(incident_id, task_id, dispatch_id, claimed_at_ms, phase) VALUES (?, ?, ?, ?, ?)",
        ).run(incidentId, taskId, dispatchId, nowMs, "claimed");
        return { result: "claimed", taskId, dispatchId, phase: "claimed" };
      }
      if (existing.task_id !== taskId) {
        return {
          result: "lost",
          taskId: existing.task_id,
          dispatchId: existing.dispatch_id,
          phase: parseLaunchPhase(existing.phase),
        };
      }
      return {
        result: "replay",
        taskId: existing.task_id,
        dispatchId: existing.dispatch_id,
        phase: parseLaunchPhase(existing.phase),
      };
    })();
  }

  advanceLaunchPhase(
    incidentId: string,
    fromPhase: LaunchPhase,
    toPhase: LaunchPhase,
    lockKey: string,
    holder: string,
    nowMs: number,
  ): boolean {
    const updated = this.db.query(
      `UPDATE incident_launch_claims SET phase = ?
       WHERE incident_id = ? AND phase = ?
         AND EXISTS (
           SELECT 1 FROM incident_mutation_leases
           WHERE lock_key = ? AND holder = ? AND until_ms >= ?
         )`,
    ).run(toPhase, incidentId, fromPhase, lockKey, holder, nowMs);
    return updated.changes === 1;
  }

  claimResolution(incidentId: string, taskId: number, evidenceSha256: string, evidenceJson: string): ResolutionRecord {
    return this.db.transaction((): ResolutionRecord => {
      const existing = this.db.query<{ task_id: number; evidence_sha256: string; evidence_json: string; phase: ResolutionPhase }, [string]>(
        "SELECT task_id, evidence_sha256, evidence_json, phase FROM incident_resolution_operations WHERE incident_id = ?",
      ).get(incidentId);
      if (existing) {
        if (existing.task_id !== taskId || existing.evidence_sha256 !== evidenceSha256 || existing.evidence_json !== evidenceJson) {
          throw new Error("conflicting resolution evidence");
        }
        return { taskId: existing.task_id, evidenceSha256: existing.evidence_sha256, evidenceJson: existing.evidence_json, phase: existing.phase };
      }
      this.db.query(
        "INSERT INTO incident_resolution_operations(incident_id, task_id, evidence_sha256, evidence_json, phase) VALUES (?, ?, ?, ?, 'verified')",
      ).run(incidentId, taskId, evidenceSha256, evidenceJson);
      return { taskId, evidenceSha256, evidenceJson, phase: "verified" };
    })();
  }

  listPendingResolutions(nowMs = Date.now(), limit = 100): PendingResolutionRecord[] {
    const cursor = this.db.query<{ cursor: string }, []>(
      "SELECT cursor FROM incident_resolution_reconciliation WHERE singleton = 1",
    ).get()?.cursor ?? "";
    let rows = this.db.query<ResolutionRow, [number, string, number]>(
      `SELECT incident_id, task_id, evidence_sha256, evidence_json, phase, attempt_count, next_attempt_at_ms, last_error
       FROM incident_resolution_operations
       WHERE phase IN ('verified', 'closed') AND next_attempt_at_ms <= ? AND incident_id > ?
       ORDER BY incident_id
       LIMIT ?`,
    ).all(nowMs, cursor, limit);
    if (rows.length === 0 && cursor !== "") {
      rows = this.db.query<ResolutionRow, [number, number]>(
        `SELECT incident_id, task_id, evidence_sha256, evidence_json, phase, attempt_count, next_attempt_at_ms, last_error
         FROM incident_resolution_operations
         WHERE phase IN ('verified', 'closed') AND next_attempt_at_ms <= ?
         ORDER BY incident_id
         LIMIT ?`,
      ).all(nowMs, limit);
    }
    return rows.map((row) => ({
      incidentId: row.incident_id,
      taskId: row.task_id,
      evidenceSha256: row.evidence_sha256,
      evidenceJson: row.evidence_json,
      phase: row.phase,
      attemptCount: row.attempt_count,
      nextAttemptAtMs: row.next_attempt_at_ms,
      lastError: row.last_error,
    }));
  }

  advanceResolutionReconciliationCursor(incidentId: string): void {
    this.db.query(
      "UPDATE incident_resolution_reconciliation SET cursor = ? WHERE singleton = 1",
    ).run(incidentId);
  }

  recordResolutionRetry(incidentId: string, attemptCount: number, nextAttemptAtMs: number, error: string): void {
    this.db.query(
      `UPDATE incident_resolution_operations
       SET attempt_count = ?, next_attempt_at_ms = ?, last_error = ?
       WHERE incident_id = ? AND phase IN ('verified', 'closed')`,
    ).run(attemptCount, nextAttemptAtMs, error.slice(0, 1_000), incidentId);
  }

  quarantineResolution(incidentId: string, error = "invalid persisted resolution"): void {
    this.db.query(
      "UPDATE incident_resolution_operations SET phase = 'quarantined', last_error = ? WHERE incident_id = ? AND phase IN ('verified', 'closed')",
    ).run(error.slice(0, 1_000), incidentId);
  }

  advanceResolutionPhase(incidentId: string, fromPhase: ResolutionPhase, toPhase: ResolutionPhase, lockKey: string, holder: string, nowMs: number): boolean {
    const updated = this.db.query(
      `UPDATE incident_resolution_operations SET phase = ?
       WHERE incident_id = ? AND phase = ?
         AND EXISTS (
           SELECT 1 FROM incident_mutation_leases
           WHERE lock_key = ? AND holder = ? AND until_ms >= ?
         )`,
    ).run(toPhase, incidentId, fromPhase, lockKey, holder, nowMs);
    return updated.changes === 1;
  }

  claimDeletion(incidentId: string, taskId: number, lockKey: string, holder: string, nowMs: number): "intent" | "removed" {
    return this.db.transaction(() => {
      const lease = this.db.query<LeaseRow, [string]>("SELECT holder, until_ms FROM incident_mutation_leases WHERE lock_key = ?").get(lockKey);
      if (!lease || lease.holder !== holder || lease.until_ms < nowMs) throw new IncidentMutationLeaseError("deletion lease lost");
      const existing = this.db.query<{ task_id: number; phase: "intent" | "removed" }, [string]>("SELECT task_id, phase FROM incident_deletion_operations WHERE incident_id = ?").get(incidentId);
      if (existing) {
        if (existing.task_id !== taskId) throw new IncidentMutationLeaseError("deletion ownership mismatch");
        return existing.phase;
      }
      this.db.query("INSERT INTO incident_deletion_operations(incident_id, task_id, holder, phase) VALUES (?, ?, ?, 'intent')").run(incidentId, taskId, holder);
      return "intent";
    })();
  }

  getDeletion(incidentId: string): { taskId: number; phase: "intent" | "removed" | "finalized"; holder: string } | null {
    const row = this.db.query<{ task_id: number; phase: "intent" | "removed" | "finalized"; holder: string }, [string]>("SELECT task_id, phase, holder FROM incident_deletion_operations WHERE incident_id = ?").get(incidentId);
    return row ? { taskId: row.task_id, phase: row.phase, holder: row.holder } : null;
  }

  retireFinalizedDeletion(incidentId: string, taskId: number): boolean {
    const removed = this.db.query("DELETE FROM incident_deletion_operations WHERE incident_id = ? AND task_id = ? AND phase = 'finalized'").run(incidentId, taskId);
    return removed.changes === 1;
  }

  quarantineDeletion(incidentId: string, taskId: number, previousHolder: string, lockKey: string, holder: string, nowMs: number): boolean {
    const removed = this.db.query(`DELETE FROM incident_deletion_operations
      WHERE incident_id = ? AND task_id = ? AND holder = ? AND phase = 'intent'
      AND EXISTS (SELECT 1 FROM incident_mutation_leases WHERE lock_key = ? AND holder = ? AND until_ms >= ?)`)
      .run(incidentId, taskId, previousHolder, lockKey, holder, nowMs);
    return removed.changes === 1;
  }

  adoptDeletion(incidentId: string, taskId: number, expectedPhase: "intent" | "removed", previousHolder: string, lockKey: string, holder: string, nowMs: number): "intent" | "removed" {
    const updated = this.db.query(`UPDATE incident_deletion_operations SET holder = ?
      WHERE incident_id = ? AND task_id = ? AND phase = ? AND holder = ?
      AND EXISTS (SELECT 1 FROM incident_mutation_leases WHERE lock_key = ? AND holder = ? AND until_ms >= ?)`)
      .run(holder, incidentId, taskId, expectedPhase, previousHolder, lockKey, holder, nowMs);
    if (updated.changes !== 1) throw new IncidentMutationLeaseError("deletion adoption refused");
    return expectedPhase;
  }

  markDeletionRemoved(incidentId: string, taskId: number, lockKey: string, holder: string, nowMs: number): boolean {
    const updated = this.db.query(`UPDATE incident_deletion_operations SET phase = 'removed'
      WHERE incident_id = ? AND task_id = ? AND holder = ? AND phase = 'intent'
      AND EXISTS (SELECT 1 FROM incident_mutation_leases WHERE lock_key = ? AND holder = ? AND until_ms >= ?)`)
      .run(incidentId, taskId, holder, lockKey, holder, nowMs);
    return updated.changes === 1;
  }

  finalizeDeletion(incidentId: string, taskId: number, lockKey: string, holder: string, nowMs: number): boolean {
    return this.db.transaction(() => {
      const operation = this.db.query<{ phase: string; holder: string }, [string, number]>("SELECT phase, holder FROM incident_deletion_operations WHERE incident_id = ? AND task_id = ?").get(incidentId, taskId);
      const lease = this.db.query<LeaseRow, [string]>("SELECT holder, until_ms FROM incident_mutation_leases WHERE lock_key = ?").get(lockKey);
      if (operation?.phase !== "removed" || operation.holder !== holder || !lease || lease.holder !== holder || lease.until_ms < nowMs) return false;
      this.db.query("DELETE FROM incident_launch_claims WHERE incident_id = ? AND task_id = ?").run(incidentId, taskId);
      this.db.query("DELETE FROM incident_resolution_operations WHERE incident_id = ? AND task_id = ?").run(incidentId, taskId);
      this.db.query("UPDATE incident_deletion_operations SET phase = 'finalized' WHERE incident_id = ? AND task_id = ? AND holder = ? AND phase = 'removed'").run(incidentId, taskId, holder);
      return true;
    })();
  }

  deleteOwnedIncidentState(incidentId: string, taskId: number, lockKey: string, holder: string, nowMs: number): boolean {
    return this.db.transaction(() => {
      const lease = this.db.query<LeaseRow, [string]>(
        "SELECT holder, until_ms FROM incident_mutation_leases WHERE lock_key = ?",
      ).get(lockKey);
      if (!lease || lease.holder !== holder || lease.until_ms < nowMs) return false;
      const launch = this.db.query<{ task_id: number }, [string]>(
        "SELECT task_id FROM incident_launch_claims WHERE incident_id = ?",
      ).get(incidentId);
      const resolution = this.db.query<{ task_id: number }, [string]>(
        "SELECT task_id FROM incident_resolution_operations WHERE incident_id = ?",
      ).get(incidentId);
      if ((launch && launch.task_id !== taskId) || (resolution && resolution.task_id !== taskId)) return false;
      this.db.query("DELETE FROM incident_launch_claims WHERE incident_id = ? AND task_id = ?").run(incidentId, taskId);
      this.db.query("DELETE FROM incident_resolution_operations WHERE incident_id = ? AND task_id = ?").run(incidentId, taskId);
      return true;
    })();
  }

  close(): void {
    this.db.close();
  }
}
