import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);
const defaultRegistryPrefix = "127.0.0.1:5000/hwlab";
const d601NativeKubeconfigPath = "/etc/rancher/k3s/k3s.yaml";
const imagePullBackoffReasons = new Set(["ErrImagePull", "ImagePullBackOff", "InvalidImageName"]);

function commandLine(command, args) {
  return [command, ...args].join(" ");
}

async function run(command, args = [], { timeoutMs = 5000 } = {}) {
  const commandText = commandLine(command, args);
  try {
    const result = await execFileAsync(command, args, {
      timeout: timeoutMs,
      maxBuffer: 1024 * 1024
    });
    return {
      ok: true,
      command: commandText,
      exitCode: 0,
      stdout: result.stdout.trim(),
      stderr: result.stderr.trim()
    };
  } catch (error) {
    return {
      ok: false,
      command: commandText,
      exitCode: typeof error.code === "number" ? error.code : 1,
      stdout: String(error.stdout ?? "").trim(),
      stderr: String(error.stderr ?? "").trim(),
      error: error.message
    };
  }
}

async function commandExists(command, timeoutMs) {
  return (await run("which", [command], { timeoutMs })).ok;
}

function fetchErrorMessage(error) {
  if (!(error instanceof Error)) {
    return String(error);
  }
  const cause = error.cause;
  if (cause && typeof cause === "object") {
    const code = "code" in cause ? String(cause.code) : "";
    const address = "address" in cause ? String(cause.address) : "";
    const port = "port" in cause ? String(cause.port) : "";
    return [error.message, code, address, port].filter(Boolean).join(" ");
  }
  return error.message;
}

async function httpProbe(url, { timeoutMs = 5000 } = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetch(url, {
      method: "GET",
      signal: controller.signal
    });
    const body = await response.text();
    return {
      ok: response.ok,
      url,
      method: "GET",
      status: response.status,
      statusText: response.statusText,
      body: body.slice(0, 500)
    };
  } catch (error) {
    return {
      ok: false,
      url,
      method: "GET",
      error: fetchErrorMessage(error)
    };
  } finally {
    clearTimeout(timer);
  }
}

export function parseRegistryPrefix(prefix = defaultRegistryPrefix) {
  const normalized = prefix.replace(/\/+$/u, "");
  const [hostPort, ...pathParts] = normalized.split("/");
  const host = hostPort.split(":")[0].toLowerCase();
  const port = hostPort.includes(":") ? hostPort.split(":").at(-1) : null;
  return {
    normalized,
    hostPort,
    host,
    port,
    namespace: pathParts.join("/")
  };
}

export function registryApiEndpoint(prefix = defaultRegistryPrefix) {
  const parsed = parseRegistryPrefix(prefix);
  return `http://${parsed.hostPort}/v2/`;
}

function isLoopbackRegistry5000(prefix) {
  const parsed = parseRegistryPrefix(prefix);
  return (parsed.host === "127.0.0.1" || parsed.host === "localhost") && parsed.port === "5000";
}

function summarizeCommand(result) {
  return {
    command: result.command,
    ok: result.ok,
    exitCode: result.exitCode,
    stdout: result.stdout.slice(0, 1000),
    stderr: result.stderr.slice(0, 1000),
    error: result.error ?? null
  };
}

function parseDockerPs(stdout) {
  return stdout
    .split("\n")
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => {
      try {
        return JSON.parse(line);
      } catch {
        return null;
      }
    })
    .filter(Boolean);
}

function registryContainersFromDockerPs(stdout) {
  return parseDockerPs(stdout)
    .filter((row) => {
      const image = String(row.Image ?? "");
      const ports = String(row.Ports ?? "");
      return image.startsWith("registry:") && ports.includes("5000->5000/tcp");
    })
    .map((row) => ({
      id: row.ID ?? null,
      image: row.Image ?? null,
      names: row.Names ?? null,
      ports: row.Ports ?? null,
      state: row.State ?? null,
      status: row.Status ?? null
    }));
}

async function probeProcessHttpAccess(registryPrefix, timeoutMs) {
  const endpoint = registryApiEndpoint(registryPrefix);
  const probe = await httpProbe(endpoint, { timeoutMs });
  const passed = probe.ok || probe.status === 401;
  return {
    id: "process-http-access",
    status: passed ? "pass" : "degraded",
    requiredForPublish: true,
    requiredForDeploy: false,
    endpoint,
    probeKind: "runner-process-http-v2",
    summary: passed
      ? `Runner or BuildKit-side process can reach ${endpoint}.`
      : `Runner process cannot reach ${endpoint}; BuildKit publish may not be able to reach the registry from this execution surface.`,
    evidence: {
      probe,
      publishFailure: false
    }
  };
}

async function probeDockerDaemonPushAccess(registryPrefix, timeoutMs) {
  const dockerVersion = await run("docker", ["--version"], { timeoutMs });
  if (!dockerVersion.ok) {
    return {
      id: "docker-daemon-push-access",
      status: "degraded",
      requiredForPublish: false,
      requiredForDeploy: false,
      endpoint: registryPrefix,
      probeKind: "docker-cli-read-only",
      summary: "Docker CLI is not available; legacy Docker daemon registry visibility is diagnostic only and is not required for BuildKit publish.",
      evidence: {
        dockerVersion: summarizeCommand(dockerVersion),
        pushAttempted: false,
        mutationAttempted: false
      }
    };
  }

  const dockerPs = await run("docker", ["ps", "--format", "{{json .}}"], { timeoutMs });
  if (!dockerPs.ok) {
    return {
      id: "docker-daemon-push-access",
      status: "degraded",
      requiredForPublish: false,
      requiredForDeploy: false,
      endpoint: registryPrefix,
      probeKind: "docker-ps-read-only",
      summary: "Docker daemon is not readable; legacy Docker daemon registry visibility is diagnostic only and is not required for BuildKit publish.",
      evidence: {
        dockerVersion: summarizeCommand(dockerVersion),
        dockerPs: summarizeCommand(dockerPs),
        pushAttempted: false,
        mutationAttempted: false
      }
    };
  }

  const matchingContainers = registryContainersFromDockerPs(dockerPs.stdout);
  const registryVisible = !isLoopbackRegistry5000(registryPrefix) || matchingContainers.length > 0;
  const k8sNativeLoopbackRegistry =
    isLoopbackRegistry5000(registryPrefix) &&
    process.env.HWLAB_DEV_REGISTRY_K8S_NATIVE === "1";
  const registryAccepted = registryVisible || k8sNativeLoopbackRegistry;
  return {
    id: "docker-daemon-push-access",
    status: registryAccepted ? "pass" : "degraded",
    requiredForPublish: false,
    requiredForDeploy: false,
    endpoint: registryPrefix,
    probeKind: "docker-ps-read-only",
    summary: registryVisible
      ? "Docker daemon can see a local/internal registry target; this legacy diagnostic did not push and is not required for BuildKit publish."
      : (k8sNativeLoopbackRegistry
        ? "Docker daemon did not show the registry container, but HWLAB_DEV_REGISTRY_K8S_NATIVE=1 declares the loopback registry is owned by Kubernetes and reachable through the node network."
        : "Docker daemon did not show a registry container publishing port 5000; this no longer blocks the BuildKit publish path."),
    evidence: {
      dockerVersion: summarizeCommand(dockerVersion),
      registryPrefix,
      loopbackPort5000: isLoopbackRegistry5000(registryPrefix),
      registryContainerVisible: registryVisible,
      k8sNativeLoopbackRegistry,
      matchingContainers,
      pushAttempted: false,
      mutationAttempted: false
    }
  };
}

function parsePodList(stdout) {
  try {
    const parsed = JSON.parse(stdout);
    return Array.isArray(parsed.items) ? parsed.items : [];
  } catch {
    return [];
  }
}

function podPullEvidence(pods, registryPrefix) {
  const parsed = parseRegistryPrefix(registryPrefix);
  const hostNeedle = `${parsed.hostPort}/`;
  const pulledImages = [];
  const pullFailures = [];

  for (const pod of pods) {
    const podName = pod?.metadata?.name ?? "unknown";
    for (const status of pod?.status?.containerStatuses ?? []) {
      const image = String(status.image ?? "");
      const imageID = String(status.imageID ?? "");
      const waitingReason = status.state?.waiting?.reason;
      if (waitingReason && imagePullBackoffReasons.has(waitingReason)) {
        pullFailures.push({
          pod: podName,
          container: status.name ?? null,
          image,
          reason: waitingReason
        });
      }
      if (image.includes(hostNeedle) && imageID.includes("sha256:")) {
        pulledImages.push({
          pod: podName,
          container: status.name ?? null,
          image,
          imageID
        });
      }
    }
  }

  return {
    pulledImages,
    pullFailures
  };
}

async function probeK3sPullAccess(registryPrefix, timeoutMs) {
  const kubectlExists = await commandExists("kubectl", timeoutMs);
  if (!kubectlExists) {
    return {
      id: "k3s-pull-access",
      status: "blocked",
      requiredForPublish: false,
      requiredForDeploy: true,
      endpoint: registryPrefix,
      probeKind: "kubectl-read-only",
      summary: "kubectl is not installed, so k3s pull access cannot be assessed from this runner.",
      evidence: {
        kubectlAvailable: false,
        pullAttempted: false,
        mutationAttempted: false
      }
    };
  }

  const nativeKubectlArgs = (args) => ["KUBECONFIG=" + d601NativeKubeconfigPath, "kubectl", ...args];
  const [context, canGetPods, podsResult] = await Promise.all([
    run("env", nativeKubectlArgs(["config", "current-context"]), { timeoutMs }),
    run("env", nativeKubectlArgs(["auth", "can-i", "get", "pods", "-n", "hwlab-dev"]), { timeoutMs }),
    run("env", nativeKubectlArgs(["-n", "hwlab-dev", "get", "pods", "-o", "json"]), { timeoutMs })
  ]);
  const canReadPods = canGetPods.ok && canGetPods.stdout === "yes" && podsResult.ok;
  if (!canReadPods) {
    return {
      id: "k3s-pull-access",
      status: "blocked",
      requiredForPublish: false,
      requiredForDeploy: true,
      endpoint: registryPrefix,
      probeKind: "kubectl-read-only",
      summary: "Read-only kubectl probes could not inspect hwlab-dev pods, so k3s pull access is blocked.",
      evidence: {
        kubectlAvailable: true,
        kubeconfig: d601NativeKubeconfigPath,
        context: summarizeCommand(context),
        canGetPods: summarizeCommand(canGetPods),
        pods: summarizeCommand(podsResult),
        pullAttempted: false,
        mutationAttempted: false
      }
    };
  }

  const pods = parsePodList(podsResult.stdout);
  const evidence = podPullEvidence(pods, registryPrefix);
  const hasFailures = evidence.pullFailures.length > 0;
  const hasPulledImages = evidence.pulledImages.length > 0;
  return {
    id: "k3s-pull-access",
    status: hasFailures ? "blocked" : (hasPulledImages ? "pass" : "degraded"),
    requiredForPublish: false,
    requiredForDeploy: true,
    endpoint: registryPrefix,
    probeKind: "kubectl-read-only",
    summary: hasFailures
      ? "Read-only kubectl observed image pull failures in hwlab-dev."
      : (hasPulledImages
        ? "Read-only kubectl observed hwlab-dev pods with pulled registry images."
        : "Read-only kubectl can inspect hwlab-dev, but no current pod proves k3s pulled this registry prefix."),
    evidence: {
      kubectlAvailable: true,
      kubeconfig: d601NativeKubeconfigPath,
      context: summarizeCommand(context),
      canGetPods: summarizeCommand(canGetPods),
      podCount: pods.length,
      pulledImages: evidence.pulledImages,
      pullFailures: evidence.pullFailures,
      pullAttempted: false,
      mutationAttempted: false
    }
  };
}

function capabilityClassification(dimensions) {
  const values = Object.values(dimensions)
    .filter((dimension) => dimension.requiredForPublish || dimension.requiredForDeploy)
    .map((dimension) => dimension.status);
  if (values.includes("blocked")) return "blocked";
  if (values.includes("degraded")) return "degraded";
  return "pass";
}

export async function probeRegistryCapabilities({
  registryPrefix = defaultRegistryPrefix,
  timeoutMs = 5000
} = {}) {
  const [processHttpAccess, dockerDaemonPushAccess, k3sPullAccess] = await Promise.all([
    probeProcessHttpAccess(registryPrefix, timeoutMs),
    probeDockerDaemonPushAccess(registryPrefix, timeoutMs),
    probeK3sPullAccess(registryPrefix, timeoutMs)
  ]);
  const dimensions = {
    processHttpAccess,
    dockerDaemonPushAccess,
    k3sPullAccess
  };

  return {
    version: "v1",
    registryPrefix,
    generatedAt: new Date().toISOString(),
    classification: capabilityClassification(dimensions),
    dimensions,
    interpretation: {
      processHttpAccess: "Runner/BuildKit-side process HTTP access to /v2/ is the publish-path registry API preflight.",
      dockerDaemonPushAccess: "Docker daemon registry visibility is a legacy read-only diagnostic only. It is not required for the BuildKit publish path and does not push.",
      k3sPullAccess: "k3s pull access is the deploy-path capability. This probe is read-only and does not create pods or read secrets."
    }
  };
}
