import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";

export interface FireRow { id: string; title: string; origin: string; state: string; asked_at: string; }
export interface FireStore {
  askedIncidents(): Promise<FireRow[]>;
  note(id: string, detail: string): Promise<void>;
  ship(id: string, detail: string, proof: string): Promise<void>;
}
export interface PlaybookResult { outcome: "shipped" | "escalate"; detail: string; proof?: string; }
export interface FirePlaybook { run(signature: string): Promise<PlaybookResult>; }
export interface Notifier { send(message: string): Promise<void>; }
export interface AttachmentNotifier extends Notifier { sendAttachment(path: string, message: string): Promise<void>; }
export interface EmergencyAgentResult { code: number; transcript: string; }
export interface EmergencyAgent { run(row: FireRow): Promise<EmergencyAgentResult>; }

export interface PlaybookEntry { pattern: string; script: string; }
interface Edge { signature: string; row: string; askedAt: string; }

/** `*` expands only inside one colon-delimited segment, never across a key boundary. */
export function signatureMatches(pattern: string, signature: string): boolean {
  const expected = pattern.split(":");
  const actual = signature.split(":");
  return expected.length === actual.length && expected.every((part, index) => {
    const expression = `^${part.split("*").map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^:]+")}$`;
    return new RegExp(expression).test(actual[index]!);
  });
}

export function matchingPlaybook(entries: readonly PlaybookEntry[], signature: string): PlaybookEntry | undefined {
  return entries.find((entry) => signatureMatches(entry.pattern, signature));
}
function isDeploySignature(signature: string): boolean {
  return signature.startsWith("deploy-local:") || signature.startsWith("deploy-clone:");
}

function edgeFor(row: FireRow): Edge { return { signature: row.title, row: row.id, askedAt: row.asked_at }; }
function sameEdge(a: Edge, b: Edge): boolean { return a.signature === b.signature && a.row === b.row && a.askedAt === b.askedAt; }
function isEdge(value: unknown): value is Edge {
  return Boolean(value && typeof value === "object" && typeof (value as Edge).signature === "string" && typeof (value as Edge).row === "string" && typeof (value as Edge).askedAt === "string");
}
function readEdges(path: string): Edge[] {
  try {
    const value = JSON.parse(readFileSync(path, "utf8"));
    if (isEdge(value)) return [value]; // Accept the first deployed single-edge form.
    return Array.isArray(value?.handled) ? value.handled.filter(isEdge) : [];
  } catch { return []; }
}
function writeEdges(path: string, edges: readonly Edge[]): void {
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
  const temporary = `${path}.${process.pid}.tmp`;
  writeFileSync(temporary, `${JSON.stringify({ handled: edges.slice(-256) })}\n`, { mode: 0o600 });
  renameSync(temporary, path);
}

/**
 * Processes each asked incident at most once for each `asked_at` crossing. The edge is
 * persisted before invoking a playbook: a crash therefore leaves the row asked rather
 * than risking an automatic retry of a mutation whose result is unknown.
 */
async function notify(notifier: Notifier, message: string): Promise<void> {
  try { await notifier.send(message); } catch { /* Lifecycle announcements are fail-open. */ }
}
async function notifyAttachment(notifier: Notifier, path: string, message: string): Promise<void> {
  try {
    if ("sendAttachment" in notifier && typeof notifier.sendAttachment === "function") await notifier.sendAttachment(path, message);
    else await notifier.send(`${message} Transcript: ${path}`);
  } catch { /* Lifecycle announcements are fail-open. */ }
}

/** Process every asked incident once per crossing, with one emergency seat per signature. */
export async function consumeOnce(deps: { store: FireStore; playbooks: ReadonlyMap<string, FirePlaybook>; entries: readonly PlaybookEntry[]; notifier: Notifier; edgeFile: string; emergencyAgent?: EmergencyAgent; }): Promise<void> {
  const handled = readEdges(deps.edgeFile);
  for (const row of await deps.store.askedIncidents()) {
    if (row.origin !== "agent-incident" || row.state !== "asked") continue;
    const edge = edgeFor(row);
    if (handled.some((previous) => sameEdge(previous, edge))) continue;
    // The title is the fire signature by contract. Persist before every external action.
    const seenSignature = handled.some((previous) => previous.signature === row.title);
    handled.push(edge);
    writeEdges(deps.edgeFile, handled);
    const entry = matchingPlaybook(deps.entries, row.title);
    if (!entry) {
      if (!isDeploySignature(row.title)) {
        await notify(deps.notifier, `Incident needs an emergency seat: ${row.title} (no deterministic playbook).`);
        continue;
      }
      if (seenSignature) {
        const detail = `Emergency agent already ran for ${row.title}; stopped after the repeated failure and needs owner review.`;
        await deps.store.note(row.id, detail);
        await notify(deps.notifier, `Deployment incident ${row.title} fired again for row ${row.id}. An emergency agent already handled this failure, so deployment is paused for review.`);
        continue;
      }
      await notify(deps.notifier, `Deployment incident ${row.title} fired for row ${row.id}. An emergency agent is being asked to restore delivery and leave a lasting fix.`);
      // The agent's exit code proves the process ended, never that the deployment was
      // fixed — shipping the row stays the agent's own duty (its brief writes the board
      // with real proof). The consumer only reports honestly what it observed.
      let result: EmergencyAgentResult;
      try {
        result = await (deps.emergencyAgent ?? spawnEmergencyAgent()).run(row);
      } catch (error) {
        const reason = error instanceof Error ? error.message : String(error);
        const detail = `Emergency agent could not be launched for ${row.title}: ${reason}; escalation required.`;
        await deps.store.note(row.id, detail);
        await notify(deps.notifier, `Deployment incident ${row.title} needs review: the emergency agent could not be launched (${reason}).`);
        continue;
      }
      if (result.code === 0) {
        const detail = `Emergency agent finished for ${row.title}; the row ships only with the agent's own proof. Transcript: ${result.transcript}`;
        await notifyAttachment(deps.notifier, result.transcript, `The emergency agent for ${row.title} finished its run — transcript attached. If it repaired the deployment, its proof is on the board row.`);
        await deps.store.note(row.id, detail);
      } else {
        const detail = `Emergency agent died mid-run for ${row.title} (exit ${result.code}); escalation required. Transcript: ${result.transcript}`;
        await notifyAttachment(deps.notifier, result.transcript, `The emergency agent for ${row.title} died mid-run (exit ${result.code}) — partial transcript attached; deployment needs review.`);
        await deps.store.note(row.id, detail);
      }
      continue;
    }
    const playbook = deps.playbooks.get(entry.script);
    if (!playbook) throw new Error(`registered playbook is unavailable: ${entry.script}`);
    let result: PlaybookResult;
    try { result = await playbook.run(row.title); }
    catch (error) {
      const detail = `Playbook ${entry.pattern} crashed before a known result: ${error instanceof Error ? error.message : String(error)}`;
      await deps.store.note(row.id, detail);
      await notify(deps.notifier, `Incident escalation: ${row.title} — ${detail}`);
      continue;
    }
    if (result.outcome === "shipped") {
      await deps.store.ship(row.id, result.detail, result.proof ?? result.detail);
    } else {
      await deps.store.note(row.id, result.detail);
      await notify(deps.notifier, `Incident escalation: ${row.title} — ${result.detail}`);
    }
  }
}

async function output(command: string[], environment: Record<string, string | undefined> = {}): Promise<{ code: number; stdout: string; stderr: string }> {
  const child = Bun.spawn(command, { stdout: "pipe", stderr: "pipe", env: { ...process.env, ...environment } });
  return { code: await child.exited, stdout: await new Response(child.stdout).text(), stderr: await new Response(child.stderr).text() };
}

// od-requests lives in ~/.claude/bin (agent tooling home), not ~/.local/bin — the
// wrong default made the very first live consumer run die on posix_spawn ENOENT.
export function cliStore(cli = process.env.OD_REQUESTS ?? join(homedir(), ".claude", "bin", "od-requests")): FireStore {
  return {
    async askedIncidents() {
      const result = await output([cli, "list", "--state", "asked", "--json"]);
      if (result.code !== 0) throw new Error(`od-requests list failed: ${result.stderr.trim()}`);
      return JSON.parse(result.stdout) as FireRow[];
    },
    async note(id, detail) { const result = await output([cli, "note", id, "--detail", detail]); if (result.code !== 0) throw new Error(`od-requests note failed: ${result.stderr.trim()}`); },
    async ship(id, detail, proof) { const result = await output([cli, "ship", id, "--detail", detail, "--proof-url", proof, "--worker", "fire-consumer"]); if (result.code !== 0) throw new Error(`od-requests ship failed: ${result.stderr.trim()}`); },
  };
}

export function shellPlaybook(script: string): FirePlaybook {
  return { async run(signature) {
    const result = await output([script, signature]);
    if (result.code !== 0) return { outcome: "escalate", detail: result.stderr.trim() || `playbook exited ${result.code}` };
    try { return JSON.parse(result.stdout) as PlaybookResult; } catch { return { outcome: "escalate", detail: `playbook returned invalid result: ${result.stdout.trim()}` }; }
  } };
}

export function botmasterNotifier(): AttachmentNotifier {
  const botmaster = process.env.BOTMASTER_BIN ?? join(homedir(), ".local", "bin", "botmaster");
  return {
    async send(message) { await output([botmaster, "--fyi", message]); },
    async sendAttachment(path, message) { await output([botmaster, "--attachment", path, message]); },
  };
}

export function spawnEmergencyAgent(launcher = join(resolve(import.meta.dir), "spawn-emergency-agent.sh")): EmergencyAgent {
  const stateHome = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
  return { async run(row) {
    const transcript = join(stateHome, "overdeck", "fire-agent", `${row.id}.log`);
    const result = await output([launcher, row.id, row.title, JSON.stringify(row)]);
    return { code: result.code, transcript };
  } };
}

if (import.meta.main) {
  const moduleDir = resolve(import.meta.dir);
  const entries = (JSON.parse(readFileSync(join(moduleDir, "playbooks.json"), "utf8")) as { playbooks: PlaybookEntry[] }).playbooks;
  const playbooks = new Map(entries.map((entry) => [entry.script, shellPlaybook(join(moduleDir, entry.script))]));
  const edgeFile = process.env.OVERDECK_FIRE_CONSUMER_EDGE_FILE ?? join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "overdeck", "fire-consumer.last-handled.json");
  await consumeOnce({ store: cliStore(), playbooks, entries, notifier: botmasterNotifier(), edgeFile });
}
