// Runs one gate0 check on a buildbox. The gate is the machine's most frequent
// heavy workload and gate0 strips ~/.claude/bin from PATH, so the shim chain
// (pnpm/tsc/vitest -> cpu-guard -> local-gate) never sees it; this is the gate's
// own dispatch seam instead.
import { createHash } from "node:crypto";
import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { loadRemoteConfig, TRANSIENT_REMOTE_REASONS, tryRemoteBuild } from "./remote-build.mjs";

const HOME = homedir();
const STATE_DIR = join(HOME, ".claude", "run", "local-gate");
const LOG_PATH = join(HOME, ".claude", "local-gate.log");

export function parseArgs(argv) {
  const out = { cwd: "", check: "", decision: "", env: {}, command: [] };
  for (let i = 0; i < argv.length; i += 1) {
    const a = argv[i];
    if (a === "--") {
      out.command = argv.slice(i + 1);
      break;
    }
    if (a === "--cwd") out.cwd = argv[++i] || "";
    else if (a === "--check") out.check = argv[++i] || "";
    else if (a === "--decision") out.decision = argv[++i] || "";
    else if (a === "--env") {
      const pair = argv[++i] || "";
      const eq = pair.indexOf("=");
      if (eq <= 0) throw new Error(`malformed --env: ${pair}`);
      out.env[pair.slice(0, eq)] = pair.slice(eq + 1);
    } else throw new Error(`unknown arg: ${a}`);
  }
  for (const field of ["cwd", "check", "decision"]) {
    if (!out[field]) throw new Error(`missing --${field}`);
  }
  if (!out.command.length) throw new Error("missing command after --");
  return out;
}

// Distinct per worktree: the remote keyfile dedupe is keyed on this alone, and a
// second worktree running the same check must not be answered by the first.
export function dispatchKey(cwd, check) {
  const scope = createHash("sha256").update(cwd).digest("hex").slice(0, 12);
  const name = check.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 48);
  return `gate0_${scope}_${name}`;
}

function log(line) {
  try {
    mkdirSync(dirname(LOG_PATH), { recursive: true });
    writeFileSync(LOG_PATH, `${new Date().toISOString()} ${line}\n`, { flag: "a" });
  } catch {}
}

export function dispatch(args, deps = {}) {
  const loadConfig = deps.loadRemoteConfig ?? loadRemoteConfig;
  const attempt = deps.tryRemoteBuild ?? tryRemoteBuild;
  const record = deps.writeDecision ?? ((verdict, reason) => writeFileSync(args.decision, `${verdict} ${reason}\n`));
  const stateDir = deps.stateDir ?? STATE_DIR;
  const emit = deps.log ?? log;
  const key = dispatchKey(args.cwd, args.check);

  if (!loadConfig()) {
    record("local", "disabled");
    return 0;
  }
  mkdirSync(stateDir, { recursive: true });

  const run = () => attempt({ key, argv: args.command, cwd: args.cwd, stateDir, log: emit, env: args.env });
  let result = run();
  for (let i = 0; i < 2 && !result.ran && TRANSIENT_REMOTE_REASONS.has(String(result.reason)); i += 1) {
    emit(`gate0 remote transient retry key=${key} attempt=${i + 1} reason=${result.reason}`);
    result = run();
  }
  // A deduped answer is another process's success, not this check's — the gate
  // may only report a result it observed, so it falls through to a local run.
  if (result.ran && !result.deduped) {
    emit(`gate0 remote done key=${key} status=${result.status}`);
    record("remote", "");
    return result.status;
  }
  const reason = result.deduped ? "deduped" : String(result.reason);
  // A fleet that never answered is not a decline: running the check here would put
  // the machine's heaviest workload back on the workstation, so the gate stops.
  if (reason.startsWith("dispatch-timeout:")) {
    emit(`gate0 remote blocked key=${key} reason=${reason}`);
    record("blocked", reason);
    return 1;
  }
  emit(`gate0 remote skip key=${key} reason=${reason}`);
  record("local", reason);
  return 0;
}

// ~/.claude/lib is a symlink into the repository, so the path as typed and the
// resolved module url disagree for every caller.
const resolvePath = (path) => {
  try {
    return realpathSync(path);
  } catch {
    return path;
  }
};
const isMain = process.argv[1] !== undefined && resolvePath(fileURLToPath(import.meta.url)) === resolvePath(process.argv[1]);
if (isMain) {
  try {
    process.exit(dispatch(parseArgs(process.argv.slice(2))));
  } catch (err) {
    console.error(`gate-dispatch: ${err.message}`);
    process.exit(2);
  }
}
