import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { readFileSync, realpathSync } from "node:fs";
import { lstat, readFile, readlink } from "node:fs/promises";
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
import { createAccountLeaseManager } from "./runtime-account-lease.mjs";
import { readRuntimeReleaseDescriptor } from "./runtime-release.mjs";

const SHA256 = /^[a-f0-9]{64}$/;
const DISPATCH_KEYS = ["accountRef", "accountRegistry", "budget", "effort", "harness", "model", "preset", "repo", "runId", "runtimeDescriptor", "runtimeDigest", "runtimeReleaseRoot", "schemaVersion"];

function hasControl(value) {
  return Array.from(value).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127);
}

function text(value, name, max = 4096) {
  if (typeof value !== "string" || value.length < 1 || value.length > max || hasControl(value)) throw new Error(`dispatch-${name}-invalid`);
  return value;
}

function absolute(value, name) {
  const path = text(value, name);
  if (!isAbsolute(path) || resolve(path) !== path) throw new Error(`dispatch-${name}-invalid`);
  return path;
}

function plain(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

export function validateRunDispatchV1(value) {
  if (!plain(value) || Object.keys(value).sort().join("|") !== DISPATCH_KEYS.join("|") || value.schemaVersion !== 1) {
    throw new Error("run-dispatch-schema-invalid");
  }
  const budget = value.budget;
  if (!plain(budget) || Object.keys(budget).sort().join("|") !== "idleSeconds|wallSeconds") throw new Error("dispatch-budget-invalid");
  for (const key of ["idleSeconds", "wallSeconds"]) {
    if (!Number.isInteger(budget[key]) || budget[key] < 1 || budget[key] > 86_400) throw new Error("dispatch-budget-invalid");
  }
  if (!SHA256.test(value.runtimeDigest)) throw new Error("dispatch-runtime-digest-invalid");
  const accountRef = text(value.accountRef, "account-ref", 256);
  if (!/^codex:[a-z0-9][a-z0-9._-]{0,127}$/.test(accountRef)) throw new Error("dispatch-account-ref-invalid");
  return Object.freeze({
    schemaVersion: 1,
    runId: text(value.runId, "run-id", 128),
    runtimeDigest: value.runtimeDigest,
    runtimeDescriptor: absolute(value.runtimeDescriptor, "runtime-descriptor"),
    runtimeReleaseRoot: absolute(value.runtimeReleaseRoot, "runtime-release-root"),
    preset: text(value.preset, "preset", 128),
    harness: text(value.harness, "harness", 128),
    model: text(value.model, "model", 256),
    effort: text(value.effort, "effort", 64),
    accountRef,
    accountRegistry: absolute(value.accountRegistry, "account-registry"),
    repo: absolute(value.repo, "repo"),
    budget: Object.freeze({ idleSeconds: budget.idleSeconds, wallSeconds: budget.wallSeconds }),
  });
}

function digest(value) {
  return createHash("sha256").update(value).digest("hex");
}

async function selectedRuntime(dispatch, startedRuntimePath) {
  const descriptor = readRuntimeReleaseDescriptor(dispatch.runtimeDescriptor);
  if (descriptor.artifactSha256 !== dispatch.runtimeDigest) throw new Error("runtime-descriptor-digest-mismatch");
  const release = join(dispatch.runtimeReleaseRoot, "releases", dispatch.runtimeDigest);
  if (resolve(dispatch.runtimeDescriptor) !== join(release, "descriptor.json")) throw new Error("runtime-descriptor-path-mismatch");
  const target = await readlink(join(dispatch.runtimeReleaseRoot, "current"));
  if (target !== `releases/${dispatch.runtimeDigest}/runtime`) throw new Error("runtime-release-not-selected");
  const runtime = realpathSync(join(release, "runtime"));
  const started = realpathSync(startedRuntimePath);
  if (started !== runtime && !started.startsWith(`${runtime}${sep}`)) throw new Error("runtime-started-process-mutable");
}

async function accountResolver(dispatch, accountRef) {
  const [tool, slug] = accountRef.split(":", 2);
  if (tool !== "codex") return null;
  const registryStat = await lstat(dispatch.accountRegistry);
  if (!registryStat.isFile() || registryStat.isSymbolicLink()) throw new Error("account-registry-invalid");
  const registry = JSON.parse(await readFile(dispatch.accountRegistry, "utf8"));
  if (!plain(registry) || !Array.isArray(registry.accounts)) throw new Error("account-registry-invalid");
  const matches = registry.accounts.filter((entry) => plain(entry) && entry.slug === slug);
  if (matches.length !== 1) return null;
  const authPath = join(dirname(dispatch.accountRegistry), "accounts", slug, "PI_HOME", "auth.json");
  const authStat = await lstat(authPath);
  if (!authStat.isFile() || authStat.isSymbolicLink()) throw new Error("account-auth-invalid");
  const provider = dispatch.model.split("/", 1)[0];
  const auth = JSON.parse(await readFile(authPath, "utf8"));
  const identity = auth?.[provider]?.accountId;
  if (typeof identity !== "string" || !identity.trim() || hasControl(identity)) throw new Error("account-identity-invalid");
  const expectedIdentity = identity.trim();
  return {
    expectedIdentity,
    accountFingerprint: digest(`account\0${accountRef}\0${expectedIdentity}`),
    concurrentSafe: false,
    accountHandle: { authPath, provider },
  };
}

function processSnapshot(processHandle) {
  const pid = Number(processHandle?.pid ?? processHandle);
  if (!Number.isInteger(pid) || pid < 1) throw new Error("process-invalid");
  const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
  const end = stat.lastIndexOf(")");
  const fields = stat.slice(end + 2).split(" ");
  const startTime = fields[19];
  if (!startTime) throw new Error("process-invalid");
  return `${pid}:${startTime}`;
}

async function verifyProcessIdentity({ processIdentity, codingAgentDir }, provider) {
  const pid = processIdentity.split(":", 1)[0];
  const environ = (await readFile(`/proc/${pid}/environ`)).toString("utf8").split("\0");
  if (!environ.includes(`PI_CODING_AGENT_DIR=${codingAgentDir}`)) throw new Error("process-account-dir-mismatch");
  const authPath = join(codingAgentDir, "auth.json");
  const stat = await lstat(authPath);
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("process-account-auth-invalid");
  const auth = JSON.parse(await readFile(authPath, "utf8"));
  return { identities: [auth?.[provider]?.accountId].filter((item) => typeof item === "string") };
}

function waitForChild(child) {
  return new Promise((resolveExit, reject) => {
    child.once("error", reject);
    child.once("close", (code, signal) => resolveExit({ code, signal }));
  });
}

export async function runRuntimeDispatch({ dispatch: rawDispatch, argv, startedRuntimePath, leaseRoot, stdio = "inherit" }) {
  const dispatch = validateRunDispatchV1(rawDispatch);
  if (!Array.isArray(argv) || argv.length === 0 || argv.some((arg) => typeof arg !== "string" || arg.includes("\0"))) throw new Error("dispatch-command-invalid");
  await selectedRuntime(dispatch, startedRuntimePath);
  const manager = createAccountLeaseManager({
    leaseRoot: absolute(leaseRoot, "lease-root"),
    resolveAccount: (accountRef) => accountResolver(dispatch, accountRef),
    provisionerModuleUrl: new URL("./runtime-account-provisioner.mjs", import.meta.url).href,
    snapshotProcessIdentity: processSnapshot,
    verifyIdentity: (input) => verifyProcessIdentity(input, dispatch.model.split("/", 1)[0]),
  });
  const lease = await manager.acquire({ runId: dispatch.runId, accountRef: dispatch.accountRef });
  let child = null;
  try {
    child = spawn(argv[0], argv.slice(1), {
      cwd: dispatch.repo,
      env: { ...process.env, ...lease.env },
      stdio,
      detached: false,
    });
    await new Promise((resolveSpawn, reject) => {
      child.once("spawn", resolveSpawn);
      child.once("error", reject);
    });
    await lease.verifyAfterStartup(child);
    const trace = lease.trace();
    const { code, signal } = await waitForChild(child);
    return { exitCode: code ?? 1, signal, trace };
  } catch (error) {
    if (child?.pid && child.exitCode === null) {
      try { child.kill("SIGTERM"); } catch (killError) { if (killError?.code !== "ESRCH") throw killError; }
    }
    if (error && typeof error === "object" && !error.trace) error.trace = lease.trace();
    throw error;
  } finally {
    await lease.release();
  }
}
