import { spawn } from "node:child_process";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";

const DEPLOY_DIR = process.env.OVERDECK_DEPLOY_DIR ?? join(homedir(), ".local", "share", "overdeck", "deploy");
const ADAPTERS_PATH = join(DEPLOY_DIR, "modules", "harness", "presets", "adapters.json");
const INCIDENT_WORKSPACE_ROOT = process.env.OVERDECK_INCIDENT_WORKSPACE_ROOT ?? join(homedir(), ".local", "state", "overdeck", "incidents", "workspaces");

async function exit(code: number, msg?: string) {
  if (msg) process.stderr.write(`${msg}\n`);
  process.exit(code);
}

const incidentId = process.argv[2];
if (!incidentId) exit(2, "usage: bun run runner.ts <incident-id>");

const { loadIncidentsConnection } = await import("./src/incidents/connection");
const { KanboardClient } = await import("./src/incidents/kanboard-client");

const conn = loadIncidentsConnection();
if (!conn) exit(3, "Kanboard connection not configured");

const client = new KanboardClient(conn.baseUrl, conn.token);

let task: { id: number; title: string; description: string; column_id: number; swimlane_id: number; project_id: number };
let metadata: Record<string, string>;

try {
  task = await client.call("getTaskByReference", { project_id: 1, reference: incidentId }) as any;
  const projectId = task.project_id;
  metadata = await client.call("getTaskMetadata", { task_id: task.id }) as any;
  const bootstrapResult = await client.call("getProjectByIdentifier", { identifier: "OVERDECKINCIDENTS" }) as any;
  if (!bootstrapResult || (typeof bootstrapResult === "object" && "jsonrpc" in bootstrapResult && (bootstrapResult as any).error)) {
    exit(3, "Kanboard project not bootstrapped");
  }
} catch (err: any) {
  exit(3, `Failed to read incident: ${err.message ?? err}`);
}

const wrapperModel = metadata["overdeck.wrapper_model"];
const cli = metadata["overdeck.cli"];
const unsafe = metadata["overdeck.unsafe"] === "1";
const account = metadata["overdeck.account"];
const brief = metadata["overdeck.dispatch_brief"];

if (!cli || !wrapperModel) exit(3, "Missing dispatch metadata: cli or wrapper_model");

const adaptersRaw = await Bun.file(ADAPTERS_PATH).text();
const adapters = JSON.parse(adaptersRaw);
const adapter = adapters.adapters?.find((a: any) => a.id === cli);
if (!adapter || !adapter.wrapper) exit(3, `Adapter not found for CLI: ${cli}`);

const wrapperPath = adapter.wrapper;
if (!wrapperPath.startsWith("/")) exit(3, `Wrapper path must be absolute: ${wrapperPath}`);

const workspace = join(INCIDENT_WORKSPACE_ROOT, incidentId);
mkdirSync(workspace, { recursive: true });

const prompt = brief
  ? `${brief}\n\n# Situation (owner text)\n\n${task.title}\n\n${removeImmutableMarker(task.description)}`
  : `${task.title}\n\n${removeImmutableMarker(task.description)}`;

writeFileSync(join(workspace, "prompt.md"), prompt);

const permissionMode = unsafe ? "unsafe" : "safe";
const wrapperArgs = [
  "--workspace", workspace,
  "--trust", prompt,
  "--task-slug", `incident-${incidentId}`,
  "--model", wrapperModel,
  "--timeout", String(1800),
  "--permission-mode", permissionMode,
];

if (account) {
  wrapperArgs.push("--profile", account);
}

let revision = Number(metadata["overdeck.status_revision"]) || 0;

async function saveMetadata(values: Record<string, string>) {
  revision += 1;
  await client.call("saveTaskMetadata", {
    task_id: task.id,
    values: { ...values, "overdeck.status_revision": String(revision) },
  });
}

async function moveToColumn(columnName: string) {
  const columns = await client.call("getColumns", { project_id: task.project_id }) as any[];
  const column = columns.find((c: any) => c.title === columnName);
  if (!column) throw new Error(`Column not found: ${columnName}`);
  await client.call("moveTaskPosition", {
    project_id: task.project_id,
    task_id: task.id,
    column_id: column.id,
    position: 1,
    swimlane_id: task.swimlane_id,
  });
}

let currentState: string | null = null;

async function writeState(state: string, extra: Record<string, string> = {}) {
  if (currentState === state) return;
  currentState = state;
  const values: Record<string, string> = { ...extra, "overdeck.dispatch_state": state };
  await saveMetadata(values);
}

async function writeComment(content: string) {
  await client.call("createComment", {
    task_id: task.id,
    content,
    user_id: 0,
  });
}

const startedAt = new Date().toISOString();

try {
  await writeState("running", {
    "overdeck.started_at": startedAt,
    "overdeck.heartbeat_at": startedAt,
  });
  await moveToColumn("Running");
  await writeComment(`Agent started at ${startedAt}`);
} catch (err: any) {
  exit(3, `Failed to write running state: ${err.message ?? err}`);
}

let heartbeatTimer: ReturnType<typeof setInterval> | null = setInterval(async () => {
  try {
    await saveMetadata({ "overdeck.heartbeat_at": new Date().toISOString() });
  } catch { /* heartbeat failure is non-fatal; Kanboard outage will be visible as stale */ }
}, 15000);

const proc = spawn(wrapperPath, wrapperArgs, {
  cwd: workspace,
  stdio: ["ignore", "pipe", "pipe"],
  env: { ...process.env, HOME: homedir() },
});

let stdout = "";
let stderr = "";

proc.stdout?.on("data", (chunk: Buffer) => { stdout += chunk.toString(); });
proc.stderr?.on("data", (chunk: Buffer) => { stderr += chunk.toString(); });

proc.on("error", async (err: any) => {
  if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
  const completedAt = new Date().toISOString();
  try {
    await writeState("engine-down", {
      "overdeck.completed_at": completedAt,
      "overdeck.exit_code": "1",
      "overdeck.failure_class": "wrapper-spawn-failed",
    });
    await moveToColumn("Needs attention");
    await writeComment(`Engine failed to start: ${err.message ?? String(err)}`);
  } catch { /* best effort */ }
  exit(3);
});

proc.on("close", async (exitCode, signal) => {
  if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
  const completedAt = new Date().toISOString();
  const rc = exitCode ?? (signal ? 128 + (signal === "SIGTERM" ? 15 : signal === "SIGKILL" ? 9 : 0) : 1);

  try {
    if (rc === 124 || signal !== null) {
      await writeState("timed-out", {
        "overdeck.completed_at": completedAt,
        "overdeck.exit_code": String(rc),
        "overdeck.failure_class": rc === 124 ? "timeout" : "killed",
      });
      await moveToColumn("Needs attention");
      await writeComment(`Agent ${rc === 124 ? "timed out" : `killed by ${signal}`} at ${completedAt}`);
      exit(rc === 0 ? 0 : 1);
      return;
    }

    if (rc === 75) {
      await writeState("rate-limited", {
        "overdeck.completed_at": completedAt,
        "overdeck.exit_code": "75",
        "overdeck.failure_class": "rate-limited",
      });
      await moveToColumn("Needs attention");
      await writeComment(`Agent rate-limited at ${completedAt}`);
      exit(1);
      return;
    }

    if (rc === 0) {
      const result = parseIncidentResult(stdout);
      if (result) {
        if (result.disposition === "resolved") {
          const summary = result.summary.slice(0, 2000);
          await client.call("saveTaskMetadata", {
            task_id: task.id,
            values: {
              ...metadata,
              "overdeck.dispatch_state": "resolved",
              "overdeck.completed_at": completedAt,
              "overdeck.exit_code": "0",
              "overdeck.result_summary": summary,
              "overdeck.status_revision": String(revision + 1),
            },
          });
          await client.call("moveTaskPosition", {
            project_id: task.project_id,
            task_id: task.id,
            column_id: (await client.call("getColumns", { project_id: task.project_id }) as any[]).find((c: any) => c.title === "Resolved")!.id,
            position: 1,
            swimlane_id: task.swimlane_id,
          });
          await client.call("closeTask", { task_id: task.id });
          await writeComment(`Resolved — ${summary}`);
          exit(0);
          return;
        } else {
          await writeState("needs-attention", {
            "overdeck.completed_at": completedAt,
            "overdeck.exit_code": "0",
            "overdeck.result_summary": result.summary.slice(0, 2000),
          });
          await moveToColumn("Needs attention");
          await writeComment(`Agent reported needs-attention: ${result.summary.slice(0, 500)}`);
          exit(0);
          return;
        }
      } else {
        await writeState("invalid-result", {
          "overdeck.completed_at": completedAt,
          "overdeck.exit_code": "0",
          "overdeck.failure_class": "invalid-result",
        });
        await moveToColumn("Needs attention");
        await writeComment("Agent exited 0 but produced no valid incident.result");
        exit(0);
        return;
      }
    }

    await writeState("interrupted", {
      "overdeck.completed_at": completedAt,
      "overdeck.exit_code": String(rc),
      "overdeck.failure_class": `exit-${rc}`,
    });
    await moveToColumn("Needs attention");
    await writeComment(`Agent exited ${rc} at ${completedAt}`);
    exit(1);
  } catch (err: any) {
    exit(3, `Failed to write terminal state: ${err.message ?? err}`);
  }
});

function parseIncidentResult(raw: string): { disposition: string; summary: string } | null {
  try {
    const trimmed = raw.trim();
    if (!trimmed) return null;

    const lastLine = trimmed.split("\n").filter(Boolean).at(-1)?.trim();
    if (!lastLine || !lastLine.startsWith("{")) return null;
    const parsed = JSON.parse(lastLine);
    if (parsed && typeof parsed === "object" && parsed.kind === "incident.result") {
      const disposition = parsed.disposition;
      const summary = parsed.summary;
      if ((disposition === "resolved" || disposition === "needs-attention") && typeof summary === "string" && summary.trim()) {
        return { disposition, summary: summary.trim().slice(0, 2000) };
      }
    }
    return null;
  } catch {
    return null;
  }
}

function removeImmutableMarker(desc: string): string {
  const marker = /^Overdeck-Incident: [^\n]*$/m;
  return desc.replace(marker, "").trim();
}
