import { createHash } from "node:crypto";
import { spawn } from "node:child_process";
import path from "node:path";
import { lstat, realpath } from "node:fs/promises";

export const RUNRAIL_AGENT_BRIDGE = Object.freeze({
  artifactType: "runrail-agent-bridge-v1",
  receiptArtifactType: "runrail-agent-launch-receipt-v1",
  contractVersion: "1.0.0",
  authority: "none",
  maxReceiptBytes: 32 * 1024
});

const FIXED_ADAPTERS = Object.freeze([
  Object.freeze({
    id: "codex",
    label: "Codex",
    executableNames: Object.freeze(["codex"]),
    launchMode: "interactive"
  }),
  Object.freeze({
    id: "claude",
    label: "Claude",
    executableNames: Object.freeze(["claude"]),
    launchMode: "interactive"
  }),
  Object.freeze({
    id: "gemini",
    label: "Gemini",
    executableNames: Object.freeze(["gemini"]),
    launchMode: "interactive"
  }),
  Object.freeze({
    id: "grok",
    label: "Grok",
    executableNames: Object.freeze(["grok", "grok-cli"]),
    launchMode: "interactive"
  })
]);

const PUBLIC_ADAPTER_KEYS = Object.freeze([
  "adapterId",
  "label",
  "detected",
  "launchable",
  "mode",
  "reason",
  "authority"
]);

const RECEIPT_KEYS = Object.freeze([
  "artifactType",
  "contractVersion",
  "receiptId",
  "packetChecksumSha256",
  "adapterId",
  "startedAt",
  "endedAt",
  "outcome",
  "exitCode",
  "signal",
  "authority",
  "checksumSha256"
]);

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

function stableJson(value) {
  if (Array.isArray(value)) {
    return `[${value.map((item) => stableJson(item)).join(",")}]`;
  }
  if (value && typeof value === "object") {
    return `{${Object.keys(value)
      .sort()
      .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
      .join(",")}}`;
  }
  return JSON.stringify(value);
}

function assertExactKeys(value, keys, label) {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    const error = new Error(`${label} must be an object.`);
    error.code = "agent-bridge-invalid";
    throw error;
  }
  const expected = new Set(keys);
  const actual = Object.keys(value);
  if (
    actual.length !== expected.size ||
    actual.some((key) => !expected.has(key))
  ) {
    const error = new Error(`${label} contains unsupported fields.`);
    error.code = "agent-bridge-invalid";
    throw error;
  }
}

function isInsideOrEqual(candidate, root) {
  const relative = path.relative(root, candidate);
  return relative === "" || (
    relative !== ".." &&
    !relative.startsWith(`..${path.sep}`) &&
    !path.isAbsolute(relative)
  );
}

function boundedId(value, label) {
  const normalized = String(value || "").trim();
  if (!/^[a-z][a-z0-9-]{0,31}$/.test(normalized)) {
    const error = new Error(`${label} is invalid.`);
    error.code = "agent-bridge-adapter-invalid";
    throw error;
  }
  return normalized;
}

function checksum(value, label) {
  const normalized = String(value || "").toLowerCase();
  if (!/^[a-f0-9]{64}$/.test(normalized)) {
    const error = new Error(`${label} must be a SHA-256 checksum.`);
    error.code = "agent-bridge-invalid";
    throw error;
  }
  return normalized;
}

function isoTime(value, label) {
  let normalized = "";
  try {
    normalized = new Date(value).toISOString();
  } catch {}
  if (!normalized || Number.isNaN(Date.parse(normalized))) {
    const error = new Error(`${label} is invalid.`);
    error.code = "agent-bridge-invalid";
    throw error;
  }
  return normalized;
}

function publicAdapter(adapter) {
  const value = {
    adapterId: adapter.adapterId,
    label: adapter.label,
    detected: adapter.detected === true,
    launchable: adapter.launchable === true,
    mode: adapter.mode,
    reason: adapter.reason,
    authority: "none"
  };
  assertExactKeys(value, PUBLIC_ADAPTER_KEYS, "Agent adapter");
  return Object.freeze(value);
}

async function ordinaryExecutable(candidate, {
  blockedRoots,
  platform,
  lstatFile,
  realpathFile
}) {
  const entry = await lstatFile(candidate).catch(() => null);
  if (!entry) return null;
  if (platform === "win32" && /\.(?:cmd|bat|ps1)$/i.test(candidate)) return null;
  const canonical = await realpathFile(candidate).catch(() => null);
  if (!canonical || !path.isAbsolute(canonical)) return null;
  const canonicalEntry = await lstatFile(canonical).catch(() => null);
  if (!canonicalEntry?.isFile()) return null;
  if (platform !== "win32" && (canonicalEntry.mode & 0o111) === 0) return null;
  if ((canonicalEntry.mode & 0o002) !== 0) return null;
  const parentEntry = await lstatFile(path.dirname(canonical)).catch(() => null);
  if (!parentEntry?.isDirectory() || (parentEntry.mode & 0o002) !== 0) return null;
  if (blockedRoots.some((root) => isInsideOrEqual(canonical, root))) return null;
  return canonical;
}

export async function createRunrailAgentAdapterRegistry({
  env = process.env,
  platform = process.platform,
  blockedRoots = [],
  interactiveAvailable = Boolean(
    process.stdin?.isTTY &&
    process.stdout?.isTTY &&
    process.stderr?.isTTY
  ),
  lstatFile = lstat,
  realpathFile = realpath
} = {}) {
  const roots = [];
  for (const value of blockedRoots) {
    const resolved = path.resolve(String(value || ""));
    if (path.isAbsolute(resolved)) roots.push(resolved);
  }
  const pathEntries = String(env.PATH || "")
    .split(path.delimiter)
    .filter((entry) => path.isAbsolute(entry));
  const resolvedAdapters = new Map();
  const publicAdapters = [];

  for (const definition of FIXED_ADAPTERS) {
    let executablePath = "";
    for (const directory of pathEntries) {
      const names = platform === "win32"
        ? definition.executableNames.map((name) => `${name}.exe`)
        : definition.executableNames;
      for (const name of names) {
        executablePath = await ordinaryExecutable(path.join(directory, name), {
          blockedRoots: roots,
          platform,
          lstatFile,
          realpathFile
        }) || "";
        if (executablePath) break;
      }
      if (executablePath) break;
    }
    const detected = Boolean(executablePath);
    const launchable = detected && interactiveAvailable;
    const adapter = {
      adapterId: definition.id,
      label: definition.label,
      detected,
      launchable,
      mode: definition.launchMode,
      reason: !detected
        ? "not-detected"
        : !interactiveAvailable
          ? "interactive-terminal-unavailable"
          : "ready",
      executablePath
    };
    resolvedAdapters.set(definition.id, Object.freeze(adapter));
    publicAdapters.push(publicAdapter(adapter));
  }

  publicAdapters.push(publicAdapter({
    adapterId: "generic-copy",
    label: "Generic copy",
    detected: true,
    launchable: false,
    mode: "copy",
    reason: "copy-brief-only"
  }));

  return Object.freeze({
    list() {
      return Object.freeze(publicAdapters.slice());
    },
    launchSpec(adapterId) {
      const id = boundedId(adapterId, "Agent adapter ID");
      if (id === "generic-copy") {
        const error = new Error("Generic copy does not launch a process.");
        error.code = "agent-bridge-copy-only";
        throw error;
      }
      const adapter = resolvedAdapters.get(id);
      if (!adapter) {
        const error = new Error("The requested agent adapter is not approved.");
        error.code = "agent-bridge-adapter-invalid";
        throw error;
      }
      if (!adapter.detected) {
        const error = new Error("The requested agent is not installed in an approved location.");
        error.code = "agent-bridge-not-detected";
        throw error;
      }
      if (!adapter.launchable) {
        const error = new Error("An interactive terminal is required to open this agent.");
        error.code = "agent-bridge-terminal-required";
        throw error;
      }
      return Object.freeze({
        adapterId: adapter.adapterId,
        executablePath: adapter.executablePath,
        args: Object.freeze([])
      });
    }
  });
}

const CHILD_ENV_KEYS = Object.freeze([
  "APPDATA",
  "COLORTERM",
  "HOME",
  "LANG",
  "LC_ALL",
  "LOCALAPPDATA",
  "LOGNAME",
  "PATH",
  "SHELL",
  "SystemRoot",
  "TEMP",
  "TERM",
  "TMP",
  "TMPDIR",
  "USER",
  "USERPROFILE",
  "WINDIR",
  "XDG_CONFIG_HOME",
  "XDG_DATA_HOME",
  "XDG_STATE_HOME"
]);

export function runrailAgentChildEnvironment(env = process.env) {
  const result = {};
  for (const key of CHILD_ENV_KEYS) {
    if (typeof env[key] === "string" && env[key]) result[key] = env[key];
  }
  return Object.freeze(result);
}

export function createRunrailAgentOperation({
  registry,
  adapterId,
  cwd,
  env = process.env,
  platform = process.platform,
  spawnProcess = spawn,
  terminateProcess = async (child) => {
    if (child?.exitCode === null && child?.signalCode == null) {
      child.kill("SIGTERM");
    }
  },
  now = () => new Date()
}) {
  if (!registry || typeof registry.launchSpec !== "function") {
    const error = new Error("Agent adapter registry is unavailable.");
    error.code = "agent-bridge-invalid";
    throw error;
  }
  const requestedRoot = String(cwd || "");
  if (!path.isAbsolute(requestedRoot)) {
    const error = new Error("Agent working root must be absolute.");
    error.code = "agent-bridge-cwd-invalid";
    throw error;
  }
  const workingRoot = path.resolve(requestedRoot);
  const spec = registry.launchSpec(adapterId);
  const startedAt = now().toISOString();
  const child = spawnProcess(spec.executablePath, [], {
    cwd: workingRoot,
    env: runrailAgentChildEnvironment(env),
    shell: false,
    detached: platform !== "win32",
    stdio: "inherit",
    windowsHide: false
  });
  let stopRequested = false;
  let stopPromise = null;
  const stop = async () => {
    stopRequested = true;
    if (!stopPromise) stopPromise = Promise.resolve(terminateProcess(child));
    await stopPromise;
    return true;
  };
  const result = new Promise((resolve) => {
    let settled = false;
    const finish = (value) => {
      if (settled) return;
      settled = true;
      resolve(Object.freeze({
        adapterId: spec.adapterId,
        startedAt,
        endedAt: now().toISOString(),
        ...value
      }));
    };
    child.once("error", () => {
      finish({
        outcome: "failed-to-start",
        exitCode: null,
        signal: null
      });
    });
    child.once("close", (exitCode, signal) => {
      finish({
        outcome: stopRequested ? "stopped" : "exited",
        exitCode: Number.isInteger(exitCode) && exitCode >= 0 ? exitCode : null,
        signal: signal ? String(signal).slice(0, 32) : null
      });
    });
  });
  return Object.freeze({ child, result, stop });
}

export function createRunrailAgentLaunchReceipt({
  packetChecksumSha256,
  adapterId,
  startedAt,
  endedAt,
  outcome,
  exitCode = null,
  signal = null
}) {
  return Object.freeze(buildRunrailAgentLaunchReceipt({
    packetChecksumSha256,
    adapterId,
    startedAt,
    endedAt,
    outcome,
    exitCode,
    signal
  }));
}

function buildRunrailAgentLaunchReceipt({
  packetChecksumSha256,
  adapterId,
  startedAt,
  endedAt,
  outcome,
  exitCode = null,
  signal = null
}) {
  const start = isoTime(startedAt, "Agent launch start");
  const end = isoTime(endedAt, "Agent launch end");
  if (Date.parse(end) < Date.parse(start)) {
    const error = new Error("Agent launch end precedes its start.");
    error.code = "agent-bridge-invalid";
    throw error;
  }
  const normalizedOutcome = String(outcome || "");
  if (!["exited", "stopped", "failed-to-start"].includes(normalizedOutcome)) {
    const error = new Error("Agent launch outcome is invalid.");
    error.code = "agent-bridge-invalid";
    throw error;
  }
  const normalizedExitCode = exitCode === null ? null : Number(exitCode);
  if (
    normalizedExitCode !== null &&
    (!Number.isInteger(normalizedExitCode) || normalizedExitCode < 0)
  ) {
    const error = new Error("Agent launch exit code is invalid.");
    error.code = "agent-bridge-invalid";
    throw error;
  }
  const normalizedSignal = signal === null ? null : String(signal).slice(0, 32);
  const withoutChecksum = {
    artifactType: RUNRAIL_AGENT_BRIDGE.receiptArtifactType,
    contractVersion: RUNRAIL_AGENT_BRIDGE.contractVersion,
    receiptId: `ALR-${sha256([
      packetChecksumSha256,
      adapterId,
      start,
      end,
      normalizedOutcome,
      normalizedExitCode,
      normalizedSignal
    ].join("\0")).slice(0, 24).toUpperCase()}`,
    packetChecksumSha256: checksum(
      packetChecksumSha256,
      "Agent Assurance packet checksum"
    ),
    adapterId: boundedId(adapterId, "Agent adapter ID"),
    startedAt: start,
    endedAt: end,
    outcome: normalizedOutcome,
    exitCode: normalizedExitCode,
    signal: normalizedSignal,
    authority: "none"
  };
  const receipt = {
    ...withoutChecksum,
    checksumSha256: sha256(stableJson(withoutChecksum))
  };
  return receipt;
}

export function verifyRunrailAgentLaunchReceipt(value) {
  assertExactKeys(value, RECEIPT_KEYS, "Agent launch receipt");
  if (
    value.artifactType !== RUNRAIL_AGENT_BRIDGE.receiptArtifactType ||
    value.contractVersion !== RUNRAIL_AGENT_BRIDGE.contractVersion ||
    value.authority !== "none" ||
    !/^ALR-[A-F0-9]{24}$/.test(value.receiptId)
  ) {
    const error = new Error("Agent launch receipt metadata is invalid.");
    error.code = "agent-bridge-invalid";
    throw error;
  }
  const expected = buildRunrailAgentLaunchReceipt({
    packetChecksumSha256: value.packetChecksumSha256,
    adapterId: value.adapterId,
    startedAt: value.startedAt,
    endedAt: value.endedAt,
    outcome: value.outcome,
    exitCode: value.exitCode,
    signal: value.signal
  });
  if (
    expected.receiptId !== value.receiptId ||
    expected.checksumSha256 !== value.checksumSha256
  ) {
    const error = new Error("Agent launch receipt checksum is invalid.");
    error.code = "agent-bridge-invalid";
    throw error;
  }
  return Object.freeze({ ...value });
}

export function stableRunrailAgentLaunchReceiptJson(value) {
  const verified = verifyRunrailAgentLaunchReceipt(value);
  const serialized = `${JSON.stringify(verified, null, 2)}\n`;
  if (Buffer.byteLength(serialized) > RUNRAIL_AGENT_BRIDGE.maxReceiptBytes) {
    const error = new Error("Agent launch receipt exceeds the bounded size.");
    error.code = "agent-bridge-invalid";
    throw error;
  }
  return serialized;
}
