import { createHash, randomUUID } from "node:crypto";
import { lstatSync, readdirSync, readFileSync } from "node:fs";
import { lstat, mkdir, open, rename } from "node:fs/promises";
import { join } from "node:path";
import { kxmUserStateRoot } from "./bindings.ts";
import type { KxmOneShotProcessResult } from "./oneshot-process.ts";

export const ONESHOT_EVIDENCE_SCHEMA = "kxm.oneshot-evidence.v2" as const;
const RETIRED_ONESHOT_EVIDENCE_SCHEMAS = new Set(["kxm.oneshot-evidence.v1"]);

const TEXT_LIMIT = 4 * 1024 * 1024;
const ARGV_LIMIT = 64 * 1024;
const RECORD_LIMIT = 16 * 1024 * 1024;
const digest = (value: string): string => `sha256:${createHash("sha256").update(value).digest("hex")}`;

export interface OneShotEvidenceRecord {
  schema: typeof ONESHOT_EVIDENCE_SCHEMA;
  [key: string]: unknown;
}

/** Brake: retired and unknown persisted schema ids are never aliased or upgraded. */
export function parseOneShotEvidenceRecord(bytes: string): OneShotEvidenceRecord {
  let parsed: unknown;
  try {
    parsed = JSON.parse(bytes) as unknown;
  } catch {
    throw new Error("oneshot_evidence_record_invalid");
  }
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    throw new Error("oneshot_evidence_record_invalid");
  }
  const schema = (parsed as { schema?: unknown }).schema;
  if (typeof schema !== "string") throw new Error("oneshot_evidence_record_invalid");
  if (RETIRED_ONESHOT_EVIDENCE_SCHEMAS.has(schema)) throw new Error("oneshot_evidence_schema_retired");
  if (schema !== ONESHOT_EVIDENCE_SCHEMA) throw new Error("oneshot_evidence_schema_unknown");
  return parsed as OneShotEvidenceRecord;
}

/** Where live one-shot spawns write `intent.json` and `result.json`. */
export function oneShotEvidenceRoot(): string {
  return join(kxmUserStateRoot(), "runtime", "oneshot-evidence");
}

/**
 * Whether the one-shot evidence for this attempt says the direct child exited.
 * Missing, unreadable, or unfinished evidence is `undefined` (do not settle).
 * A recorded `false` stays `false`.
 */
export function readOneShotObservedChildExit(runId: string, attemptId: string, root = oneShotEvidenceRoot()): boolean | undefined {
  let names: string[];
  try {
    names = readdirSync(root);
  } catch {
    return undefined;
  }
  let seenTrue = false;
  let seenFalse = false;
  const capped = names.slice(0, 4096);
  for (const name of capped) {
    if (!name.startsWith("evd_")) continue;
    const dir = join(root, name);
    try {
      const info = lstatSync(dir);
      if (!info.isDirectory() || info.isSymbolicLink()) continue;
      const intent = parseOneShotEvidenceRecord(readFileSync(join(dir, "intent.json"), "utf8"));
      if (intent.runId !== runId || intent.attemptId !== attemptId) continue;
      const result = parseOneShotEvidenceRecord(readFileSync(join(dir, "result.json"), "utf8"));
      if (result.observedChildExit === true) seenTrue = true;
      else if (result.observedChildExit === false) seenFalse = true;
    } catch {
      // An unreadable or unfinished record is not evidence that the child exited.
    }
  }
  if (seenFalse) return false;
  if (seenTrue) return true;
  return undefined;
}

/** Private diagnostic evidence, never a peer reply or acceptance authority.
 * Retained until operator archival; no automatic deletion of failed attempts.
 * Each record is bounded; inability to reserve/write evidence fails closed.
 */
export async function beginOneShotEvidence(root: string, intent: {
  runId: string; stepId: string; attemptId: string; assignmentId: string;
  harness: string; provider: string; model: string; cwd: string;
  command: string; args: readonly string[]; input?: string | undefined;
  timeoutMs?: number | undefined;
}, sensitive: readonly string[]): Promise<{
  id: string;
  finish(result: KxmOneShotProcessResult, observation: unknown): Promise<string>;
}> {
  await mkdir(root, { recursive: true, mode: 0o700 });
  const stat = await lstat(root);
  if (!stat.isDirectory() || stat.isSymbolicLink()
    || (process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.()))) {
    throw new Error("oneshot_evidence_root_not_private");
  }
  const id = `evd_${randomUUID().replaceAll("-", "")}`;
  const dir = join(root, id);
  await mkdir(dir, { mode: 0o700 });
  const secrets = [...new Set(sensitive.filter((value) => value.length >= 8))].sort((a, b) => b.length - a.length);
  const redact = (value: string): string => {
    let text = value;
    for (const secret of secrets) text = text.replaceAll(secret, "[REDACTED]");
    return text
      .replace(/(\b(?:authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret)\b["']?\s*[:=]\s*["']?)(?:Bearer\s+)?[^\s"',;}]+/gi, "$1[REDACTED]")
      .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]");
  };
  const bounded = (value: string, max = TEXT_LIMIT): { text: string; truncated: boolean; sha256: string } => {
    const bytes = Buffer.from(redact(value));
    return { text: bytes.subarray(0, max).toString("utf8"), truncated: bytes.length > max, sha256: digest(redact(value)) };
  };
  const write = async (name: string, value: unknown): Promise<string> => {
    const current = await lstat(dir);
    if (!current.isDirectory() || current.isSymbolicLink()
      || (process.platform !== "win32" && ((current.mode & 0o077) !== 0 || current.uid !== process.getuid?.()))) {
      throw new Error("oneshot_evidence_directory_changed");
    }
    const bytes = JSON.stringify(value);
    if (Buffer.byteLength(bytes) > RECORD_LIMIT) throw new Error("oneshot_evidence_record_limit");
    parseOneShotEvidenceRecord(bytes);
    const target = join(dir, name);
    const partial = `${target}.partial`;
    const file = await open(partial, "wx", 0o600);
    try { await file.writeFile(bytes); await file.sync(); }
    finally { await file.close(); }
    await rename(partial, target);
    if (process.platform !== "win32") {
      const directory = await open(dir, "r");
      try { await directory.sync(); } finally { await directory.close(); }
    }
    return digest(bytes);
  };
  await write("intent.json", {
    schema: ONESHOT_EVIDENCE_SCHEMA, id, recordedAt: new Date().toISOString(),
    runId: intent.runId, stepId: intent.stepId, attemptId: intent.attemptId, assignmentId: intent.assignmentId,
    harness: intent.harness, provider: intent.provider, requestedModel: intent.model,
    ...(intent.timeoutMs !== undefined ? { timeoutMs: intent.timeoutMs } : {}),
    cwd: intent.cwd, command: redact(intent.command),
    argv: bounded(JSON.stringify(intent.args), ARGV_LIMIT), stdin: intent.input === undefined ? null : bounded(intent.input),
    env: null,
    acceptance: false,
  });
  let finished = false;
  return {
    id,
    async finish(result, observation) {
      if (finished) throw new Error("oneshot_evidence_already_finished");
      finished = true;
      return write("result.json", {
        schema: ONESHOT_EVIDENCE_SCHEMA, id, recordedAt: new Date().toISOString(),
        stdout: bounded(result.stdout), stderr: bounded(result.stderr),
        code: result.code, signal: result.signal ?? null, started: result.started ?? null,
        observedChildExit: result.observedChildExit ?? null, terminationRequested: result.terminationRequested ?? null,
        unverifiedDescendants: result.unverifiedDescendants ?? null,
        error: result.error ? bounded(result.error.message) : null,
        observation: bounded(JSON.stringify(observation)), acceptance: false,
      });
    },
  };
}
