// Managed by HOL Guard. Re-run `hol-guard install opencode` after moving Guard home.
import { spawn as nodeSpawn } from "node:child_process";
import { accessSync, constants as fsConstants, lstatSync, realpathSync } from "node:fs";
import { homedir } from "node:os";
import { join as joinPath, resolve as resolvePath } from "node:path";

const GUARD_HOME = __GUARD_HOME__;
const GUARD_PYTHON = __GUARD_PYTHON__;
const GUARD_FROZEN = __GUARD_FROZEN__;
const GUARD_NATIVE_BRIDGE: {
  runtimePath: string;
  bootstrap: string;
  config: Record<string, unknown>;
} | null = __GUARD_NATIVE_BRIDGE__;
const GUARD_HOOK_LAUNCHER = __GUARD_HOOK_LAUNCHER__;
const GUARD_HOOK_ENV = __GUARD_HOOK_ENV__;
const GUARD_INHERIT_ENV_KEYS = __GUARD_INHERIT_ENV_KEYS__;
const GUARD_TASKKILL_PATH = __GUARD_TASKKILL_PATH__;
const INTERCEPT_TOOLS = new Set(__INTERCEPT_TOOLS__);
const GUARD_HOOK_TIMEOUT_MS = 30_000;
const GUARD_WINDOWS_JOB_MARKER = "HOL_GUARD_WINDOWS_JOB_CONTAINED\n";
let fallbackInFlight = false;
let fallbackContainmentFailed = false;

const GUARD_RUNTIME_MISSING =
  "HOL Guard's OpenCode plugin cannot find the Guard runtime that generated it. " +
  "Run `hol-guard install opencode` in a host terminal outside OpenCode, then restart OpenCode.";

export function resolveGuardPythonTarget(): string {
  try {
    const invocationStat = lstatSync(GUARD_PYTHON.invocationPath, { bigint: true });
    let invocationType = "other";
    if (invocationStat.isSymbolicLink()) {
      invocationType = "symlink";
    } else if (invocationStat.isFile()) {
      invocationType = "file";
    }
    if (invocationType !== "file" && invocationType !== "symlink") {
      throw new Error("invocation is not a file");
    }
    const resolved = realpathSync(GUARD_PYTHON.invocationPath);
    const targetStat = lstatSync(resolved, { bigint: true });
    if (!targetStat.isFile() || realpathSync(resolved) !== resolved) {
      throw new Error("target is not a regular file");
    }
    // Guard updates replace the interpreter in place. Keep reviewing with the
    // same launcher path instead of blocking every OpenCode shell tool.
    return resolved;
  } catch {
    throw new Error(GUARD_RUNTIME_MISSING);
  }
}

// The native runtime answers a hook from the Guard daemon and runs the Python
// bridge for everything else. A missing or non-executable runtime keeps the
// Python launcher.
export function resolveGuardNativeRuntime(): string | null {
  if (GUARD_NATIVE_BRIDGE === null || process.platform === "win32") {
    return null;
  }
  try {
    const resolved = realpathSync(GUARD_NATIVE_BRIDGE.runtimePath);
    if (!lstatSync(resolved).isFile()) {
      return null;
    }
    accessSync(resolved, fsConstants.X_OK);
    return resolved;
  } catch {
    return null;
  }
}

function nativeHookArgs(pythonTarget: string, guardArgv: string[]): string[] {
  if (GUARD_NATIVE_BRIDGE === null) {
    throw new Error("HOL Guard native hook bridge is not installed");
  }
  const config = { ...GUARD_NATIVE_BRIDGE.config, python_executable: pythonTarget, cli_args: guardArgv };
  return ["bounded-hook-bridge", pythonTarget, "-I", "-c", GUARD_NATIVE_BRIDGE.bootstrap, JSON.stringify(config)];
}

export function verifyGuardPythonIdentity(): void {
  resolveGuardPythonTarget();
}

export function isGuardSelfRepairCommand(command: string): boolean {
  const normalized = command.trim();
  if (/[&|;`$<>()\n]/.test(normalized)) {
    return false;
  }
  const argv = normalized.split(/\s+/);
  const binary = argv[0] ?? "";
  const name = binary.replace(/\\/g, "/").split("/").pop() ?? "";
  if (name !== "hol-guard" && name !== "hol-guard.exe") {
    return false;
  }
  const rest = argv.slice(1).join(" ");
  return rest === "update" || rest === "doctor" || rest === "start" || rest === "install opencode";
}

function hookProcessEnv(guardArgv: string[]) {
  const env: Record<string, string> = { ...GUARD_HOOK_ENV };
  for (const key of GUARD_INHERIT_ENV_KEYS) {
    const value = process.env[key];
    if (typeof value === "string" && value.length > 0) {
      env[key] = value;
    }
  }
  env.HOL_GUARD_HOOK_ARGV = JSON.stringify(guardArgv);
  return env;
}

function normalizeCommand(command: unknown): string | null {
  if (typeof command === "string" && command.trim()) {
    return command.trim();
  }
  if (Array.isArray(command) && command.length > 0 && command.every((part) => typeof part === "string")) {
    return command.join(" ");
  }
  return null;
}

function effectiveWorkingDirectory(directory: string, workdir: unknown): string {
  // Directory names can contain spaces. Review the same path the tool uses.
  const baseDirectory = directory || process.cwd();
  if (workdir === undefined) return baseDirectory;
  if (typeof workdir !== "string") {
    throw new Error("HOL Guard could not review this command: workdir must be a string.");
  }
  let target = workdir;
  if (process.platform === "win32") {
    const drive = target.match(/^\/(?:(?:cygdrive|mnt)\/)?([a-zA-Z])(?:\/|$)/)
      ?? target.match(/^\/([a-zA-Z]):(?:[\\/]|$)/);
    if (drive) target = `${drive[1].toUpperCase()}:/${target.slice(drive[0].length)}`;
  }
  if (target === "~") {
    target = homedir();
  } else if (target.startsWith("~/") || (process.platform === "win32" && target.startsWith("~\\"))) {
    target = joinPath(homedir(), target.slice(2));
  }
  return resolvePath(baseDirectory, target);
}

function waitForGuardProcessExit(
  proc: ReturnType<typeof nodeSpawn>,
  timeoutMs: number,
): Promise<boolean> {
  if (proc.exitCode !== null || proc.signalCode !== null) return Promise.resolve(true);
  return new Promise((resolve) => {
    let settled = false;
    const finish = (exited: boolean) => {
      if (settled) return;
      settled = true;
      clearTimeout(watchdog);
      resolve(exited);
    };
    const watchdog = setTimeout(
      () => finish(proc.exitCode !== null || proc.signalCode !== null),
      timeoutMs,
    );
    proc.once("exit", () => finish(true));
  });
}

function guardProcessErrorCode(error: unknown): string | null {
  return (
    typeof error === "object" &&
    error !== null &&
    "code" in error &&
    typeof error.code === "string"
  ) ? error.code : null;
}

function guardProcessGroupExited(processGroupId: number): boolean {
  try {
    process.kill(-processGroupId, 0);
    return false;
  } catch (error) {
    return guardProcessErrorCode(error) === "ESRCH";
  }
}

async function waitForGuardProcessGroupExit(
  processGroupId: number,
  timeoutMs: number,
): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (guardProcessGroupExited(processGroupId)) return true;
    await new Promise((resolve) => setTimeout(resolve, 10));
  }
  return guardProcessGroupExited(processGroupId);
}

async function terminateGuardProcessGroup(
  proc: ReturnType<typeof nodeSpawn>,
  windowsJobContained: boolean,
): Promise<boolean> {
  try {
    const processGroupId = proc.pid;
    if (
      process.platform === "win32" &&
      windowsJobContained &&
      (proc.exitCode !== null || proc.signalCode !== null)
    ) return true;
    if (process.platform === "win32" && typeof processGroupId === "number") {
      if (GUARD_TASKKILL_PATH !== null) {
        const treeKilled = await new Promise<boolean>((resolve) => {
          let taskkill: ReturnType<typeof nodeSpawn>;
          try {
            taskkill = nodeSpawn(
              GUARD_TASKKILL_PATH,
              ["/PID", String(processGroupId), "/T", "/F"],
              { stdio: "ignore", windowsHide: true },
            );
          } catch {
            resolve(false);
            return;
          }
          let settled = false;
          const finish = (killed: boolean) => {
            if (settled) return;
            settled = true;
            clearTimeout(watchdog);
            resolve(killed);
          };
          const watchdog = setTimeout(() => {
            try {
              taskkill.kill("SIGKILL");
            } catch {}
            finish(false);
          }, 200);
          taskkill.once("error", () => finish(false));
          taskkill.once("close", (status) => finish(status === 0));
        });
        if (!treeKilled) {
          try {
            proc.kill("SIGKILL");
          } catch {}
          const parentExited = await waitForGuardProcessExit(proc, 200);
          return windowsJobContained && parentExited;
        }
        return waitForGuardProcessExit(proc, 200);
      }
      try {
        proc.kill("SIGKILL");
      } catch {}
      await waitForGuardProcessExit(proc, 200);
      return false;
    }
    try {
      if (process.platform === "win32") {
        proc.kill("SIGTERM");
      } else if (typeof processGroupId === "number") {
        process.kill(-processGroupId, "SIGTERM");
      }
    } catch {}
    await waitForGuardProcessExit(proc, 100);
    try {
      if (process.platform === "win32") {
        proc.kill("SIGKILL");
      } else if (typeof processGroupId === "number") {
        // The direct parent may have exited while descendants still hold hook pipes.
        process.kill(-processGroupId, "SIGKILL");
      }
    } catch {}
    const parentExited = await waitForGuardProcessExit(proc, 200);
    const groupExited =
      process.platform === "win32" ||
      (typeof processGroupId === "number" && await waitForGuardProcessGroupExit(processGroupId, 200));
    return parentExited && groupExited;
  } catch {
    return false;
  }
}

export async function spawnGuardProcess(options: {
  args: string[];
  nativeArgs?: (pythonTarget: string) => string[];
  cwd: string;
  deadlineMs: number;
  env: Record<string, string>;
  stdin: string;
}): Promise<{ exitCode: number; stdout: string; stderr: string }> {
  return new Promise((resolve, reject) => {
    if (fallbackContainmentFailed) {
      reject(new Error("HOL Guard fallback containment previously failed"));
      return;
    }
    if (fallbackInFlight) {
      reject(new Error("HOL Guard fallback review is already in progress"));
      return;
    }
    const remainingMs = options.deadlineMs - Date.now();
    if (remainingMs <= 0) {
      reject(new Error("HOL Guard fallback review deadline expired"));
      return;
    }
    fallbackInFlight = true;
    let settled = false;
    let timedOut = false;
    let timer: ReturnType<typeof setTimeout> | undefined;
    const finish = (
      outcome:
        | { kind: "resolve"; value: { exitCode: number; stdout: string; stderr: string } }
        | { kind: "reject"; error: unknown },
    ) => {
      if (settled) {
        return;
      }
      settled = true;
      if (timer !== undefined) {
        clearTimeout(timer);
      }
      fallbackInFlight = false;
      if (outcome.kind === "resolve") {
        resolve(outcome.value);
      } else {
        reject(outcome.error);
      }
    };
    let pythonTarget: string;
    try {
      pythonTarget = resolveGuardPythonTarget();
    } catch (error) {
      finish({ kind: "reject", error });
      return;
    }
    let command = pythonTarget;
    let args = options.args;
    const nativeRuntime = options.nativeArgs === undefined ? null : resolveGuardNativeRuntime();
    if (nativeRuntime !== null && options.nativeArgs !== undefined) {
      command = nativeRuntime;
      args = options.nativeArgs(pythonTarget);
    }
    const spawnOptions = {
      cwd: options.cwd,
      detached: process.platform !== "win32",
      env: options.env,
      stdio: ["pipe", "pipe", "pipe"] as ["pipe", "pipe", "pipe"],
    };
    let proc: ReturnType<typeof nodeSpawn>;
    try {
      proc = nodeSpawn(command, args, spawnOptions);
    } catch (error) {
      if (command === pythonTarget) {
        finish({ kind: "reject", error });
        return;
      }
      command = pythonTarget;
      args = options.args;
      try {
        proc = nodeSpawn(command, args, spawnOptions);
      } catch (fallbackError) {
        finish({ kind: "reject", error: fallbackError });
        return;
      }
    }
    let stdout = "";
    let stderr = "";
    let windowsJobContained = false;
    proc.stdout?.setEncoding("utf8");
    proc.stderr?.setEncoding("utf8");
    proc.stdout?.on("data", (chunk: string) => {
      stdout += chunk;
    });
    proc.stderr?.on("data", (chunk: string) => {
      if (chunk.includes(GUARD_WINDOWS_JOB_MARKER)) {
        windowsJobContained = true;
        chunk = chunk.replaceAll(GUARD_WINDOWS_JOB_MARKER, "");
      }
      stderr += chunk;
    });
    const launched = proc;
    const usePythonLauncher = (cause: unknown) => {
      if (command === pythonTarget || stdout !== "" || stderr !== "") {
        finish({ kind: "reject", error: cause });
        return;
      }
      command = pythonTarget;
      args = options.args;
      try {
        proc = nodeSpawn(command, args, spawnOptions);
      } catch (fallbackError) {
        finish({ kind: "reject", error: fallbackError });
        return;
      }
      proc.stdout?.setEncoding("utf8");
      proc.stderr?.setEncoding("utf8");
      proc.stdout?.on("data", (chunk: string) => {
        stdout += chunk;
      });
      proc.stderr?.on("data", (chunk: string) => {
        if (chunk.includes(GUARD_WINDOWS_JOB_MARKER)) {
          windowsJobContained = true;
          chunk = chunk.replaceAll(GUARD_WINDOWS_JOB_MARKER, "");
        }
        stderr += chunk;
      });
      proc.on("error", (fallbackError) => {
        if (timedOut) return;
        finish({ kind: "reject", error: fallbackError });
      });
      proc.on("close", (code: number | null) => {
        if (timedOut) return;
        finish({ kind: "resolve", value: { exitCode: code ?? 1, stdout, stderr } });
      });
      proc.stdin?.on("error", () => {});
      proc.stdin?.end(options.stdin);
    };
    proc.on("error", (error) => {
      if (timedOut || launched !== proc) return;
      usePythonLauncher(error);
    });
    proc.on("close", (code: number | null) => {
      if (timedOut || launched !== proc) return;
      if (command !== pythonTarget && stdout === "" && stderr === "" && (code ?? 1) !== 0) {
        usePythonLauncher(new Error("HOL Guard native runtime launch produced no output"));
        return;
      }
      finish({ kind: "resolve", value: { exitCode: code ?? 1, stdout, stderr } });
    });
    timer = setTimeout(() => {
      timedOut = true;
      const containmentFailure = () => {
        fallbackContainmentFailed = true;
        finish({
          kind: "reject",
          error: new Error("HOL Guard fallback containment could not be confirmed"),
        });
      };
      void terminateGuardProcessGroup(proc, windowsJobContained).then(
        (terminated) => {
          if (!terminated) {
            containmentFailure();
            return;
          }
          finish({ kind: "reject", error: new Error("HOL Guard fallback review timed out") });
        },
        containmentFailure,
      );
    }, remainingMs);
    timer.unref();
    proc.stdin?.on("error", () => {});
    proc.stdin?.end(options.stdin);
  });
}

