import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";
import { normalizeRecord, type SessionRecord } from "./ledger";
import { loadBuildboxRegistry, type BuildboxRegistry } from "../buildbox-registry";
import type { TmuxCommandResult, TmuxSpawn } from "./tmux";

export interface BoxResidentSession {
  host: string;
  sshAlias: string;
  slug: string;
  tmuxSession: string;
  containerStatus: string;
}

export interface RemoteHostProbe {
  host: string;
  status: "ok" | "failed" | "not-reachable";
  error: string | null;
  at: string;
  sessions: SessionRecord[];
  resident: BoxResidentSession[];
}

export interface ProbeOptions {
  registryPath?: string;
  localHost: string;
  now?: () => number;
  execImpl?: (host: string, alias: string) => Promise<string>;
}

interface RegistryHost {
  name: string;
  state: string;
  ssh_alias: string;
}

const probePath = join(dirname(fileURLToPath(import.meta.url)), "remote-probe.mjs");
const SSH_TIMEOUT_MS = 5_000;

function oneLine(value: unknown): string {
  return String(value instanceof Error ? value.message : value).split(/\r?\n/, 1)[0]!.trim();
}

function shellQuote(value: string): string {
  return `'${value.replaceAll("'", `'\\''`)}'`;
}

/** ssh passes its trailing command through the remote login shell, so quote every argv
 * element before joining it into that single command string. */
export function remoteCommandArgv(alias: string, argv: string[]): string[] {
  return [
    "ssh",
    "-o", "BatchMode=yes",
    "-o", "ConnectTimeout=4",
    "-o", "StrictHostKeyChecking=accept-new",
    alias,
    argv.map(shellQuote).join(" "),
  ];
}

export async function resolveReachableSshAlias(
  host: string,
  loadRegistry: () => Promise<BuildboxRegistry> = () => loadBuildboxRegistry(),
): Promise<string> {
  const registry = await loadRegistry();
  const entry = registry.hosts.find((candidate) => candidate.name === host);
  if (!entry) throw new Error(`${host} is not listed in the buildbox registry`);
  if (entry.state !== "reachable") {
    throw new Error(`${host} is not marked reachable in the buildbox registry (state: ${entry.state})`);
  }
  return entry.ssh_alias;
}

/** Construct a TmuxSpawn which executes the exact tmux argv on a registry-approved host. */
export function remoteTmuxSpawn(
  alias: string,
  spawnImpl: TmuxSpawn = defaultSshSpawn,
): TmuxSpawn {
  return (argv) => spawnImpl(remoteCommandArgv(alias, argv));
}

export async function defaultSshSpawn(argv: string[], timeoutMs = SSH_TIMEOUT_MS): Promise<TmuxCommandResult> {
  return await new Promise((resolve) => {
    const child = spawn(argv[0]!, argv.slice(1), { stdio: ["ignore", "pipe", "pipe"] });
    let stdout = "";
    let stderr = "";
    let settled = false;
    let timer: ReturnType<typeof setTimeout>;
    const finish = (result: TmuxCommandResult) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      resolve(result);
    };
    child.stdout.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
    child.stderr.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
    child.once("error", (error) => finish({ rc: 1, stdout, stderr: oneLine(error) }));
    child.once("close", (code) => finish({ rc: code ?? 1, stdout, stderr: stderr.trim() }));
    timer = setTimeout(() => {
      child.kill("SIGKILL");
      finish({ rc: 124, stdout, stderr: `ssh timed out after ${timeoutMs / 1000}s` });
    }, timeoutMs);
  });
}

async function defaultExec(_host: string, alias: string): Promise<string> {
  const payload = await readFile(probePath, "utf8");
  return await new Promise<string>((resolve, reject) => {
    const child = spawn("ssh", [
      "-o", "BatchMode=yes",
      "-o", "ConnectTimeout=6",
      "-o", "StrictHostKeyChecking=accept-new",
      alias,
      "/usr/local/bin/node",
      "--input-type=module",
    ], { stdio: ["pipe", "pipe", "pipe"] });
    let stdout = "";
    let stderr = "";
    let timedOut = false;
    const timer = setTimeout(() => {
      timedOut = true;
      child.kill("SIGKILL");
    }, 20_000);
    child.stdout.setEncoding("utf8").on("data", (chunk) => stdout += chunk);
    child.stderr.setEncoding("utf8").on("data", (chunk) => stderr += chunk);
    child.once("error", (error) => {
      clearTimeout(timer);
      reject(error);
    });
    child.once("close", (code) => {
      clearTimeout(timer);
      if (timedOut) reject(new Error("ssh timed out after 20s"));
      else if (code !== 0) reject(new Error(`ssh exit ${code}${stderr.trim() ? `: ${oneLine(stderr)}` : ""}`));
      else resolve(stdout);
    });
    child.stdin.end(payload);
  });
}

export async function probeRemoteHosts(options: ProbeOptions): Promise<RemoteHostProbe[]> {
  const now = options.now ?? Date.now;
  const at = new Date(now()).toISOString();
  const registryPath = options.registryPath ?? join(homedir(), ".claude", "buildbox-hosts.json");
  let hosts: RegistryHost[];
  try {
    const parsed = JSON.parse(await readFile(registryPath, "utf8")) as { hosts?: unknown };
    if (!Array.isArray(parsed.hosts)) throw new Error("registry has no hosts array");
    hosts = parsed.hosts.filter((host): host is RegistryHost => {
      if (!host || typeof host !== "object") return false;
      const value = host as Partial<RegistryHost>;
      return typeof value.name === "string" && typeof value.state === "string" && typeof value.ssh_alias === "string";
    });
  } catch (error) {
    return [{ host: "registry", status: "failed", error: `registry: ${oneLine(error)}`, at, sessions: [], resident: [] }];
  }

  const execImpl = options.execImpl ?? defaultExec;
  return await Promise.all(hosts.filter((host) => host.name !== options.localHost).map(async (host) => {
    if (host.state !== "reachable") {
      return { host: host.name, status: "not-reachable" as const, error: `registry state: ${host.state}`, at, sessions: [], resident: [] };
    }
    try {
      const raw = JSON.parse(await execImpl(host.name, host.ssh_alias)) as { ledger?: unknown; resident?: unknown };
      if (!Array.isArray(raw.ledger)) throw new Error("probe output has no ledger array");
      const sessions = raw.ledger
        .map(normalizeRecord)
        .filter((record): record is SessionRecord => record !== null)
        .map((record) => ({ ...record, host: host.name }));
      const resident: BoxResidentSession[] = Array.isArray(raw.resident)
        ? raw.resident
            .filter((entry): entry is { slug: string; tmuxSession: string; containerStatus: string } =>
              !!entry && typeof entry === "object" &&
              typeof (entry as { slug?: unknown }).slug === "string" &&
              typeof (entry as { tmuxSession?: unknown }).tmuxSession === "string" &&
              typeof (entry as { containerStatus?: unknown }).containerStatus === "string")
            .map((entry) => ({ host: host.name, sshAlias: host.ssh_alias, ...entry }))
        : [];
      return { host: host.name, status: "ok" as const, error: null, at, sessions, resident };
    } catch (error) {
      return { host: host.name, status: "failed" as const, error: oneLine(error) || "remote probe failed", at, sessions: [], resident: [] };
    }
  }));
}
