import { z } from "zod";
import { mkdir, open, unlink } from "node:fs/promises";
import { constants } from "node:fs";
import { dirname, join } from "node:path";
import { IncidentStatusProjectionError, type IncidentStatusUpdate } from "./status-writer";

const ResultSchema = z.object({
  kind: z.literal("incident.result"),
  disposition: z.enum(["resolved", "needs-attention"]),
  summary: z.string().trim().min(1).max(2_000),
}).strict();

export type IncidentAgentResult = z.infer<typeof ResultSchema>;

export interface IncidentRunnerRequest {
  incidentId: string;
  taskId: number;
  dispatchId: string;
  brief: string;
  workspace: string;
  wrapper: string;
  wrapperModel: string;
  account: string;
  accountMode: "profile" | "fixed";
  permissionMode: "safe";
  timeoutSeconds: number;
  initialRevision?: number;
}

export interface IncidentProcessResult { exitCode: number; stdout: string; stderr: string }
export type RunIncidentProcess = (argv: string[]) => Promise<IncidentProcessResult>;
export interface IncidentRunnerDeps {
  recordStatus(update: IncidentStatusUpdate): Promise<void>;
  resolveIncident?(incidentId: string, artifact: string, summary: string): Promise<void>;
  runProcess?: RunIncidentProcess;
  now?: () => Date;
  setInterval?: (callback: () => void, milliseconds: number) => unknown;
  clearInterval?: (handle: unknown) => void;
  wait?: (milliseconds: number) => Promise<void>;
}

const MAX_RETAINED_OUTPUT_BYTES = 64 * 1024;

async function drainCapped(stream: ReadableStream<Uint8Array>): Promise<string> {
  const reader = stream.getReader();
  const retained: Uint8Array[] = [];
  let retainedBytes = 0;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    if (retainedBytes < MAX_RETAINED_OUTPUT_BYTES) {
      const available = MAX_RETAINED_OUTPUT_BYTES - retainedBytes;
      const chunk = value.byteLength <= available ? value : value.subarray(0, available);
      retained.push(chunk);
      retainedBytes += chunk.byteLength;
    }
  }
  const output = new Uint8Array(retainedBytes);
  let offset = 0;
  for (const chunk of retained) {
    output.set(chunk, offset);
    offset += chunk.byteLength;
  }
  return new TextDecoder().decode(output);
}

export async function runIncidentProcess(argv: string[]): Promise<IncidentProcessResult> {
  const process = Bun.spawn(argv, { stdout: "pipe", stderr: "pipe" });
  const [exitCode, stdout, stderr] = await Promise.all([
    process.exited,
    drainCapped(process.stdout),
    drainCapped(process.stderr),
  ]);
  return { exitCode, stdout, stderr };
}

const MAX_RESULT_BYTES = 64 * 1024;

async function readDurableResult(path: string): Promise<string> {
  const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
  try {
    const info = await handle.stat();
    if (!info.isFile() || info.size > MAX_RESULT_BYTES) return "";
    return await handle.readFile({ encoding: "utf8" });
  } finally {
    await handle.close();
  }
}

export function parseIncidentAgentResult(stdout: string): IncidentAgentResult | null {
  const lines = stdout.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean);
  const final = lines.at(-1);
  if (!final) return null;
  try {
    const parsed = ResultSchema.safeParse(JSON.parse(final));
    return parsed.success ? parsed.data : null;
  } catch { return null; }
}

function resultFileFor(request: IncidentRunnerRequest): string {
  return join(request.workspace, ".overdeck", `${request.incidentId}.json`);
}

function promptFor(request: IncidentRunnerRequest): string {
  return `${request.brief}\n\nBefore exiting, atomically write the final result to ${resultFileFor(request)} as exactly one JSON object matching: {"kind":"incident.result","disposition":"resolved|needs-attention","summary":"non-empty summary, at most 2000 characters"}. Stdout is not a result channel.`;
}

function wrapperArgv(request: IncidentRunnerRequest): string[] {
  const argv = [
    request.wrapper, "--workspace", request.workspace, "--trust", promptFor(request),
    "--task-slug", `incident-${request.incidentId}`, "--model", request.wrapperModel,
    "--timeout", String(request.timeoutSeconds),
  ];
  if (request.accountMode === "profile") argv.push("--profile", request.account);
  argv.push("--permission-mode", "safe");
  return argv;
}

export function recordIncidentInitializationFailure(
  request: IncidentRunnerRequest,
  commit: (update: IncidentStatusUpdate) => void,
  now: () => Date = () => new Date(),
): void {
  const at = now().toISOString();
  const revision = (request.initialRevision ?? 1) + 1;
  commit({
    incidentId: request.incidentId,
    taskId: request.taskId,
    dispatchId: request.dispatchId,
    revision,
    expectedRevision: revision - 1,
    state: "needs-attention",
    at,
    completedAt: at,
    failureClass: "runner-initialization-refused",
    comment: "Incident runner initialization failed; operator attention is required. No retry was started.",
  });
}

export async function runIncident(request: IncidentRunnerRequest, deps: IncidentRunnerDeps): Promise<void> {
  if (request.permissionMode !== "safe") throw new Error("unsafe incident permission mode refused");
  const execute = deps.runProcess ?? runIncidentProcess;
  const now = deps.now ?? (() => new Date());
  const schedule = deps.setInterval ?? ((callback, milliseconds) => globalThis.setInterval(callback, milliseconds));
  const cancel = deps.clearInterval ?? ((handle) => globalThis.clearInterval(handle as ReturnType<typeof setInterval>));
  const wait = deps.wait ?? ((milliseconds) => new Promise((resolve) => globalThis.setTimeout(resolve, milliseconds)));
  async function record(update: IncidentStatusUpdate): Promise<void> {
    const delays = [0, 100, 250];
    let failure: unknown;
    for (const delay of delays) {
      if (delay) await wait(delay);
      try { await deps.recordStatus(update); return; }
      catch (error) { failure = error; }
    }
    throw failure;
  }
  let revision = request.initialRevision ?? 1;
  const startedAt = now().toISOString();
  const resultFile = resultFileFor(request);
  await mkdir(dirname(resultFile), { recursive: true, mode: 0o700 });
  await unlink(resultFile).catch(() => undefined);
  const runningRevision = revision + 1;
  await record({ incidentId: request.incidentId, taskId: request.taskId, dispatchId: request.dispatchId, revision: runningRevision, state: "running", at: startedAt, startedAt, heartbeatAt: startedAt, comment: "Agent wrapper admitted; incident work started." });
  revision = runningRevision;

  let heartbeatTail = Promise.resolve();
  let heartbeatFailure: unknown;
  const timer = schedule(() => {
    heartbeatTail = heartbeatTail.then(async () => {
      const heartbeatAt = now().toISOString();
      const heartbeatRevision = revision + 1;
      await record({ incidentId: request.incidentId, taskId: request.taskId, dispatchId: request.dispatchId, revision: heartbeatRevision, state: "running", at: heartbeatAt, startedAt, heartbeatAt });
      revision = heartbeatRevision;
    }).catch((error) => {
      if (error instanceof IncidentStatusProjectionError && error.durableRevision === revision + 1) {
        revision = error.durableRevision;
      }
      heartbeatFailure ??= error;
    });
  }, 15_000);

  let processResult: IncidentProcessResult;
  try {
    processResult = await execute(wrapperArgv(request));
  } catch {
    cancel(timer);
    await heartbeatTail;
    const completedAt = now().toISOString();
    await record({ incidentId: request.incidentId, taskId: request.taskId, dispatchId: request.dispatchId, revision: revision + 1, state: "engine-down", at: completedAt, startedAt, completedAt, failureClass: "wrapper-precondition-or-down", comment: "Agent wrapper could not complete; operator attention is required." });
    if (heartbeatFailure) throw heartbeatFailure;
    return;
  }
  cancel(timer);
  await heartbeatTail;
  const completedAt = now().toISOString();
  const durableResult = await readDurableResult(resultFile).catch(() => "");
  const result = parseIncidentAgentResult(durableResult);
  let terminal: IncidentStatusUpdate;
  if (processResult.exitCode === 0 && result?.disposition === "resolved") {
    if (deps.resolveIncident) {
      await deps.resolveIncident(request.incidentId, resultFile, result.summary);
      if (heartbeatFailure) throw heartbeatFailure;
      return;
    }
    terminal = { incidentId: request.incidentId, taskId: request.taskId, dispatchId: request.dispatchId, revision: revision + 1, state: "invalid-result", at: completedAt, startedAt, completedAt, exitCode: 0, failureClass: "resolution-verification-unavailable", resultSummary: result.summary, comment: "Agent claimed resolution but the evidence transaction was unavailable; incident remains open." };
  } else if (processResult.exitCode === 0 && result?.disposition === "needs-attention") {
    terminal = { incidentId: request.incidentId, taskId: request.taskId, dispatchId: request.dispatchId, revision: revision + 1, state: "needs-attention", at: completedAt, startedAt, completedAt, exitCode: 0, resultSummary: result.summary, comment: `Agent requested attention: ${result.summary}` };
  } else if (processResult.exitCode === 75) {
    terminal = { incidentId: request.incidentId, taskId: request.taskId, dispatchId: request.dispatchId, revision: revision + 1, state: "rate-limited", at: completedAt, startedAt, completedAt, exitCode: 75, failureClass: "rate-limited", comment: "Agent stopped because its provider was rate limited; no retry was started." };
  } else if (processResult.exitCode === 124) {
    terminal = { incidentId: request.incidentId, taskId: request.taskId, dispatchId: request.dispatchId, revision: revision + 1, state: "timed-out", at: completedAt, startedAt, completedAt, exitCode: 124, failureClass: "timed-out", comment: "Agent reached its deadline; no retry was started." };
  } else if (processResult.exitCode !== 0) {
    terminal = { incidentId: request.incidentId, taskId: request.taskId, dispatchId: request.dispatchId, revision: revision + 1, state: "engine-down", at: completedAt, startedAt, completedAt, exitCode: processResult.exitCode, failureClass: "wrapper-nonzero", comment: "Agent wrapper exited nonzero; operator attention is required." };
  } else {
    terminal = { incidentId: request.incidentId, taskId: request.taskId, dispatchId: request.dispatchId, revision: revision + 1, state: "invalid-result", at: completedAt, startedAt, completedAt, exitCode: 0, failureClass: "invalid-result", comment: "Agent exited without a valid incident.result; operator attention is required." };
  }
  await record(terminal);
  if (heartbeatFailure) throw heartbeatFailure;
}
