import { Database } from "bun:sqlite";
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
import type { IncidentStatusUpdate } from "./status-writer";

export interface IncidentStatusReceipt {
  operationId: string;
  incidentId: string;
  revision: number;
  update: IncidentStatusUpdate;
}

interface StatusRow { task_id: number; revision: number; state: string; dispatch_id: string; payload: string }
interface ReceiptRow { operation_id: string; incident_id: string; revision: number; payload: string }

const TERMINAL = new Set(["resolved", "needs-attention", "rate-limited", "timed-out", "engine-down", "interrupted", "invalid-result"]);

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

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

function canonical(update: IncidentStatusUpdate): string {
  return JSON.stringify(update, Object.keys(update).sort());
}

export class IncidentStatusStore {
  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_status (
        incident_id TEXT PRIMARY KEY, task_id INTEGER NOT NULL, dispatch_id TEXT NOT NULL,
        revision INTEGER NOT NULL, state TEXT NOT NULL, payload TEXT NOT NULL
      );
      CREATE TABLE IF NOT EXISTS incident_status_receipts (
        operation_id TEXT PRIMARY KEY, incident_id TEXT NOT NULL, revision INTEGER NOT NULL,
        payload TEXT NOT NULL, projected_at TEXT,
        lease_token TEXT, lease_until INTEGER,
        UNIQUE(incident_id, revision)
      );
      CREATE INDEX IF NOT EXISTS incident_status_pending
        ON incident_status_receipts(projected_at, incident_id, revision);
    `);
  }

  commit(update: IncidentStatusUpdate): IncidentStatusReceipt {
    const payload = canonical(update);
    const operationId = `${update.incidentId}:${update.dispatchId}:${update.revision}`;
    return this.db.transaction(() => {
      const duplicate = this.db.query<ReceiptRow, [string]>(
        "SELECT operation_id, incident_id, revision, payload FROM incident_status_receipts WHERE operation_id = ?",
      ).get(operationId);
      if (duplicate) {
        if (duplicate.payload !== payload) throw new IncidentStatusCasError(`operation ${operationId} conflicts with its receipt`);
        return { operationId, incidentId: duplicate.incident_id, revision: duplicate.revision, update };
      }
      const current = this.db.query<StatusRow, [string]>(
        "SELECT task_id, revision, state, dispatch_id, payload FROM incident_status WHERE incident_id = ?",
      ).get(update.incidentId);
      const expected = update.expectedRevision ?? update.revision - 1;
      if (current && current.task_id !== update.taskId) throw new IncidentStatusCasError("status update belongs to a different task");
      if (current && current.dispatch_id !== update.dispatchId) throw new IncidentStatusCasError("status update belongs to a different dispatch");
      if ((current?.revision ?? expected) !== expected) {
        throw new IncidentStatusCasError(`expected status revision ${expected}; current is ${current?.revision ?? "absent"}`);
      }
      if (current && TERMINAL.has(current.state) && update.state === "running") {
        throw new IncidentStatusCasError(`terminal incident cannot return to running from ${current.state}`);
      }
      if (current?.state === "resolved" && update.state !== "resolved") {
        throw new IncidentStatusCasError("resolved incident cannot reopen");
      }
      if (current && TERMINAL.has(current.state) && update.state !== current.state && update.state !== "resolved") {
        throw new IncidentStatusCasError(`terminal incident cannot change from ${current.state} to ${update.state}`);
      }
      this.db.query(`INSERT INTO incident_status(incident_id, task_id, dispatch_id, revision, state, payload)
        VALUES (?, ?, ?, ?, ?, ?)
        ON CONFLICT(incident_id) DO UPDATE SET task_id=excluded.task_id, dispatch_id=excluded.dispatch_id,
          revision=excluded.revision, state=excluded.state, payload=excluded.payload`).run(
        update.incidentId, update.taskId, update.dispatchId, update.revision, update.state, payload,
      );
      this.db.query(`INSERT INTO incident_status_receipts(operation_id, incident_id, revision, payload)
        VALUES (?, ?, ?, ?)`).run(operationId, update.incidentId, update.revision, payload);
      return { operationId, incidentId: update.incidentId, revision: update.revision, update };
    })();
  }

  claim(incidentId: string, token: string, nowMs: number, leaseMs = 30_000): IncidentStatusReceipt | null {
    return this.db.transaction(() => {
      const row = this.db.query<ReceiptRow & { lease_until: number | null }, [string]>(`SELECT operation_id, incident_id, revision, payload, lease_until
        FROM incident_status_receipts WHERE incident_id = ? AND projected_at IS NULL
        ORDER BY revision LIMIT 1`).get(incidentId);
      if (!row) return null;
      if (row.lease_until !== null && row.lease_until >= nowMs) return null;
      const claimed = this.db.query(`UPDATE incident_status_receipts SET lease_token=?, lease_until=?
        WHERE operation_id=? AND projected_at IS NULL AND (lease_until IS NULL OR lease_until < ?)`).run(
        token, nowMs + leaseMs, row.operation_id, nowMs,
      );
      if (claimed.changes !== 1) return null;
      return { operationId: row.operation_id, incidentId: row.incident_id, revision: row.revision, update: JSON.parse(row.payload) };
    })();
  }

  renew(operationId: string, token: string, nowMs: number, leaseMs = 30_000): void {
    const renewed = this.db.query(`UPDATE incident_status_receipts SET lease_until=?
      WHERE operation_id=? AND lease_token=? AND projected_at IS NULL AND lease_until >= ?`).run(
      nowMs + leaseMs, operationId, token, nowMs,
    );
    if (renewed.changes !== 1) throw new IncidentStatusLeaseError(`projection lease lost for ${operationId}`);
  }

  projected(operationId: string, token: string, at: string, nowMs = Date.now()): void {
    const projected = this.db.query(`UPDATE incident_status_receipts SET projected_at=?, lease_token=NULL, lease_until=NULL
      WHERE operation_id=? AND lease_token=? AND projected_at IS NULL AND lease_until >= ?`).run(at, operationId, token, nowMs);
    if (projected.changes !== 1) throw new IncidentStatusLeaseError(`projection lease lost for ${operationId}`);
  }

  release(operationId: string, token: string): void {
    this.db.query(`UPDATE incident_status_receipts SET lease_token=NULL, lease_until=NULL
      WHERE operation_id=? AND lease_token=? AND projected_at IS NULL`).run(operationId, token);
  }

  pendingIncidentIds(): string[] {
    return this.db.query<{ incident_id: string }, []>(
      "SELECT DISTINCT incident_id FROM incident_status_receipts WHERE projected_at IS NULL ORDER BY incident_id",
    ).all().map((row) => row.incident_id);
  }

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