import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join, normalize } from "node:path";

export const K3S_WORKSPACE_ROOT = "/var/lib/buildbox/builds";

export const K3S_REMOTE_BUILD_FLAG = "BUILD_REMOTE_K3S";
export const DEFAULT_K3S_KUBECONFIG = join(homedir(), ".kube", "config-buildboxes");
export const K3S_CONFIG_PATH = process.env.BUILD_REMOTE_CONFIG || join(homedir(), ".claude", "build-remote.json");

const SENSITIVE_ENV = /(TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE|API_?KEY|AUTH|COOKIE|_KEY$)/i;

function defaultReadConfig() {
  return JSON.parse(readFileSync(K3S_CONFIG_PATH, "utf8"));
}

export function k3sConfig(processEnv = process.env, readConfig = defaultReadConfig) {
  let raw = null;
  try { raw = readConfig(); } catch {}
  const cfg = raw && typeof raw === "object" ? raw : {};
  const envFlag = processEnv[K3S_REMOTE_BUILD_FLAG];
  const enabled = envFlag === "1" ? true : envFlag === "0" ? false : cfg.k3s_enabled === true;
  return {
    enabled,
    image: processEnv.BUILD_REMOTE_K3S_IMAGE ?? cfg.k3s_image,
    namespace: processEnv.BUILD_REMOTE_K3S_NAMESPACE ?? cfg.k3s_namespace,
    kubeconfig: processEnv.BUILD_REMOTE_K3S_KUBECONFIG || cfg.k3s_kubeconfig || DEFAULT_K3S_KUBECONFIG,
    localOnly: Array.isArray(cfg.local_only) ? cfg.local_only : [],
  };
}

// Stem is the same sanitized (alnum+dash, no path separators) identifier used both in the
// Job name and as the mirror/repo correlation fields the watcher reports back — a real
// derivation of the build key, not a fabricated value.
export function buildKeyStem(key) {
  return String(key).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 38) || "build";
}

function jobName(key, digest) {
  return `rb-${buildKeyStem(key)}-${digest}`;
}

function workspaceContract(workingDir, nodeNames, epoch) {
  if (typeof workingDir !== "string" || !workingDir.startsWith(`${K3S_WORKSPACE_ROOT}/`)
    || normalize(workingDir) !== workingDir || workingDir.includes("\0")) {
    throw new Error(`materialized workspace must be below ${K3S_WORKSPACE_ROOT}`);
  }
  if (!Array.isArray(nodeNames) || nodeNames.length === 0 || nodeNames.some((name) => typeof name !== "string" || !name)) {
    throw new Error("materialized workspace requires at least one target node");
  }
  if (typeof epoch !== "string" || !/^[0-9]+$/.test(epoch)) {
    throw new Error("materialized workspace requires a numeric epoch");
  }
}

export function buildJobManifest({ key, argv, env = {}, workingDir, nodeNames, epoch }, options = {}) {
  workspaceContract(workingDir, nodeNames, epoch);
  const digest = createHash("sha256").update(`${key}\0${process.pid}\0${Date.now()}`).digest("hex").slice(0, 10);
  const name = options.name ?? jobName(key, digest);
  const namespace = options.namespace ?? process.env.BUILD_REMOTE_K3S_NAMESPACE ?? "default";
  const image = options.image ?? process.env.BUILD_REMOTE_K3S_IMAGE;
  if (!image) throw new Error("BUILD_REMOTE_K3S_IMAGE is required when BUILD_REMOTE_K3S=1");
  const timeoutSec = Number(options.timeoutSec ?? process.env.BUILD_REMOTE_K3S_JOB_TIMEOUT_SEC ?? 1800);
  const forwardedEnv = Object.entries(env)
    .filter(([entry]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(entry) && !SENSITIVE_ENV.test(entry))
    .map(([entry, value]) => ({ name: entry, value: String(value) }));
  // Annotations mirrored onto the pod template (not just the Job) because the Phase 3
  // watcher reads pods, not Jobs, per the newest-pod-wins exit-code doctrine below — Job
  // metadata is never copied onto its pods, pod-template metadata is.
  const reportAnnotations = {
    "overdeck.dev/build-key": String(key),
    "overdeck.dev/mirror": buildKeyStem(key),
    "overdeck.dev/repo": buildKeyStem(key),
    "overdeck.dev/snapshot": digest,
    "overdeck.dev/timeout-sec": String(timeoutSec),
  };
  return {
    apiVersion: "batch/v1",
    kind: "Job",
    metadata: {
      name,
      namespace,
      labels: { "app.kubernetes.io/name": "overdeck-remote-build", "overdeck.dev/build-key": createHash("sha256").update(key).digest("hex").slice(0, 16) },
      annotations: reportAnnotations,
    },
    spec: {
      backoffLimit: 0,
      ttlSecondsAfterFinished: Number(options.ttlSeconds ?? process.env.BUILD_REMOTE_K3S_TTL_SECONDS ?? 600),
      template: {
        metadata: { labels: { "app.kubernetes.io/name": "overdeck-remote-build" }, annotations: reportAnnotations },
        spec: {
          restartPolicy: "Never",
          // Restricts scheduling to exactly the boxes that received this push's mirror
          // (computed from the fan-out's live results) — never a cached/legacy host pick.
          affinity: { nodeAffinity: { requiredDuringSchedulingIgnoredDuringExecution: {
            nodeSelectorTerms: [{ matchExpressions: [{ key: "kubernetes.io/hostname", operator: "In", values: [...nodeNames] }] }],
          } } },
          initContainers: [{
            name: "workspace-guard",
            image,
            imagePullPolicy: options.imagePullPolicy ?? process.env.BUILD_REMOTE_K3S_IMAGE_PULL_POLICY ?? "IfNotPresent",
            command: ["/bin/sh", "-c"],
            args: [
              "set -eu; workspace=$1; expected=$2; "
              + "[ -d \"$workspace\" ] || { echo \"workspace materialization failed: missing $workspace\" >&2; exit 66; }; "
              + "[ -d \"$workspace/.git\" ] || { echo \"workspace materialization failed: checkout absent at $workspace\" >&2; exit 66; }; "
              + "[ -f \"$workspace/.rb-epoch\" ] || { echo \"workspace materialization failed: epoch absent at $workspace\" >&2; exit 66; }; "
              + "actual=$(cat \"$workspace/.rb-epoch\"); [ \"$actual\" = \"$expected\" ] || { echo \"workspace materialization failed: expected epoch $expected, found $actual\" >&2; exit 66; }",
              "overdeck-workspace-guard",
              workingDir,
              epoch,
            ],
            volumeMounts: [{ name: "repo-mirror", mountPath: "/var/lib/buildbox", readOnly: true }],
          }],
          containers: [{
            name: "build",
            image,
            imagePullPolicy: options.imagePullPolicy ?? process.env.BUILD_REMOTE_K3S_IMAGE_PULL_POLICY ?? "IfNotPresent",
            command: ["/bin/sh", "-c"],
            args: ["exec \"$@\"", "overdeck-remote-build", ...argv],
            env: forwardedEnv,
            workingDir,
            resources: {
              requests: { cpu: process.env.BUILD_REMOTE_K3S_CPU_REQUEST ?? "1", memory: process.env.BUILD_REMOTE_K3S_MEMORY_REQUEST ?? "1Gi" },
              limits: { cpu: process.env.BUILD_REMOTE_K3S_CPU_LIMIT ?? "4", memory: process.env.BUILD_REMOTE_K3S_MEMORY_LIMIT ?? "8Gi" },
            },
            // The mirror transport materializes this checkout on the scratch disk before
            // submission. Credentials remain the owner-selected read-only host mount.
            volumeMounts: [
              { name: "repo-mirror", mountPath: "/var/lib/buildbox" },
              { name: "buildbox-credentials", mountPath: "/home/user/.ssh", readOnly: true },
            ],
          }],
          volumes: [
            { name: "repo-mirror", hostPath: { path: "/var/lib/buildbox", type: "Directory" } },
            { name: "buildbox-credentials", hostPath: { path: "/home/user/.ssh", type: "Directory" } },
          ],
        },
      },
    },
  };
}

function defaultKubectl(args, options = {}) {
  return spawnSync("kubectl", args, { encoding: "utf8", shell: false, ...options });
}

function apiUnavailable(result) {
  const text = `${result?.error?.message ?? ""}\n${result?.stderr ?? ""}`;
  return result?.error?.code === "ENOENT" || result?.error?.code === "ETIMEDOUT"
    || /unable to connect|connection refused|i\/o timeout|no such host|context deadline exceeded|tls handshake timeout/i.test(text);
}

function submissionPossiblyAccepted(result) {
  const text = `${result?.error?.message ?? ""}\n${result?.stderr ?? ""}`;
  return result?.error?.code === "ETIMEDOUT" || /i\/o timeout|context deadline exceeded|tls handshake timeout/i.test(text);
}

function exitCodeFromPods(pods) {
  const newest = (pods?.items ?? [])
    .map((pod) => ({
      created: Date.parse(pod?.metadata?.creationTimestamp ?? "") || 0,
      terminated: (pod?.status?.containerStatuses ?? []).find((entry) => entry.name === "build")?.state?.terminated,
    }))
    .filter((entry) => Number.isInteger(entry.terminated?.exitCode))
    .sort((a, b) => b.created - a.created)[0];
  return newest ? newest.terminated.exitCode : null;
}

function workspaceGuardFailureFromPods(pods) {
  const newest = (pods?.items ?? [])
    .map((pod) => ({
      created: Date.parse(pod?.metadata?.creationTimestamp ?? "") || 0,
      terminated: (pod?.status?.initContainerStatuses ?? []).find((entry) => entry.name === "workspace-guard")?.state?.terminated,
    }))
    .filter((entry) => Number.isInteger(entry.terminated?.exitCode) && entry.terminated.exitCode !== 0)
    .sort((a, b) => b.created - a.created)[0];
  return newest?.terminated ?? null;
}

function jobConditions(jobJson) {
  return (jobJson?.status?.conditions ?? []).filter((entry) => entry?.status === "True").map((entry) => entry.type);
}

export function tryK3sBuild(payload, deps = {}) {
  const processEnv = deps.processEnv ?? process.env;
  const cfg = k3sConfig(processEnv, deps.readConfig);
  if (!cfg.enabled) return null;
  const run = deps.kubectl ?? defaultKubectl;
  const log = payload.log ?? (() => {});
  const timeoutSec = Number(processEnv.BUILD_REMOTE_K3S_JOB_TIMEOUT_SEC ?? 1800);
  let manifest;
  try {
    manifest = buildJobManifest(payload, {
      image: cfg.image,
      namespace: cfg.namespace,
      name: deps.name,
      timeoutSec,
    });
  } catch (error) {
    log(`k3s fallback key=${payload.key} reason=config detail=${error.message}`);
    return null;
  }
  const kubeconfig = cfg.kubeconfig;
  const namespace = manifest.metadata.namespace;
  const name = manifest.metadata.name;
  const common = ["--kubeconfig", kubeconfig, "--namespace", namespace];
  const apply = run([...common, "apply", "-f", "-"], { input: `${JSON.stringify(manifest)}\n`, timeout: Number(processEnv.BUILD_REMOTE_K3S_SUBMIT_TIMEOUT_MS ?? 15_000) });
  if (apply.status !== 0) {
    const confirm = run([...common, "get", "job", name, "-o", "json"], { timeout: 15_000 });
    if (confirm.status !== 0) {
      if (/notfound/i.test(confirm.stderr ?? "") || !submissionPossiblyAccepted(apply)) {
        const reason = apiUnavailable(apply) ? "api-unreachable" : "submission-rejected";
        log(`k3s fallback key=${payload.key} reason=${reason}`);
        return null;
      }
      log(`k3s submit-ambiguous key=${payload.key} job=${namespace}/${name}`);
      return { ran: true, status: 125, classification: "k3s-submit-ambiguous" };
    }
  }
  log(`k3s accepted key=${payload.key} job=${namespace}/${name}`);
  const pollMs = Number(processEnv.BUILD_REMOTE_K3S_POLL_MS ?? 5000);
  const sleep = deps.sleep ?? ((ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms));
  const deadline = Date.now() + timeoutSec * 1000;
  let terminal = [];
  for (;;) {
    const jobState = run([...common, "get", "job", name, "-o", "json"], { timeout: 15_000 });
    if (jobState.status === 0) {
      try {
        terminal = jobConditions(JSON.parse(jobState.stdout)).filter((type) => type === "Complete" || type === "Failed");
      } catch {}
      if (terminal.length > 0) break;
    }
    if (Date.now() >= deadline) break;
    sleep(pollMs);
  }
  const logs = run([...common, "logs", `job/${name}`], { timeout: 30_000 });
  if (logs.stdout) (deps.stdout ?? process.stdout).write(logs.stdout);
  if (logs.stderr) (deps.stderr ?? process.stderr).write(logs.stderr);
  const state = run([...common, "get", "pods", "-l", `job-name=${name}`, "-o", "json"], { timeout: 15_000 });
  if (state.status === 0) {
    try {
      const pods = JSON.parse(state.stdout);
      const status = exitCodeFromPods(pods);
      if (status !== null) return { ran: true, status, classification: "k3s-job" };
      const guardFailure = workspaceGuardFailureFromPods(pods);
      if (guardFailure) {
        const guardLogs = run([...common, "logs", `job/${name}`, "-c", "workspace-guard"], { timeout: 30_000 });
        if (guardLogs.stdout) (deps.stdout ?? process.stdout).write(guardLogs.stdout);
        if (guardLogs.stderr) (deps.stderr ?? process.stderr).write(guardLogs.stderr);
        log(`k3s workspace-materialization-failed key=${payload.key} job=${namespace}/${name} rc=${guardFailure.exitCode} reason=${guardFailure.reason ?? "guard-failed"}`);
        return { ran: true, status: 125, classification: "k3s-workspace-materialization-failed" };
      }
    } catch {}
  }
  if (terminal.includes("Complete")) return { ran: true, status: 0, classification: "k3s-job" };
  log(`k3s terminal-unknown key=${payload.key} job=${namespace}/${name}`);
  return { ran: true, status: 125, classification: "k3s-status-unknown" };
}
