import { existsSync, readFileSync } from "node:fs";
import https from "node:https";
import { spawnSync } from "node:child_process";

const namespace = requiredEnv("RUNTIME_NAMESPACE");
const expectedSha = process.env.EXPECTED_SHA || "";
const workloads = parseWorkloads(requiredEnv("WORKLOADS_B64"));
const maxMessageBytes = positiveInt(process.env.MAX_MESSAGE_BYTES, "MAX_MESSAGE_BYTES");
const maxContainers = positiveInt(process.env.MAX_CONTAINERS, "MAX_CONTAINERS");

const useServiceAccount = Boolean(process.env.KUBERNETES_SERVICE_HOST && process.env.KUBERNETES_SERVICE_PORT)
  && existsSync("/var/run/secrets/kubernetes.io/serviceaccount/token")
  && existsSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");

class KubeReadError extends Error {
  constructor(reason, message, details = {}) {
    super(shortText(message) || reason);
    this.name = "KubeReadError";
    this.reason = reason;
    this.statusCode = details.statusCode ?? null;
    this.notFound = details.notFound === true;
    this.path = details.path || null;
  }
}

main().catch((error) => {
  process.stderr.write(error?.message || String(error));
  process.exit(1);
});

async function main() {
  const items = [];
  for (const spec of workloads) {
    items.push(await workloadSummary(spec));
  }
  const targetShas = items.map((item) => item.sourceCommit?.value).filter(Boolean);
  const targetSha = uniqueOrNull(targetShas);
  const ready = items.length > 0 && items.every((item) => item.ready === true);
  const aligned = expectedSha ? targetShas.length > 0 && targetShas.every((value) => value === expectedSha) : null;
  console.log(JSON.stringify({
    ok: items.every((item) => item.ok !== false),
    degradedReason: items.some((item) => item.ok === false) ? "runtime-read-degraded" : null,
    namespace,
    expectedSha: shortSha(expectedSha),
    targetSha: shortSha(targetSha),
    ready,
    aligned,
    firstSeenReadyAt: null,
    firstSeenExpectedShaAt: null,
    lastObservedAt: new Date().toISOString(),
    blockingReason: blockingReason(items, expectedSha),
    workloads: items,
    statusAuthority: useServiceAccount ? "kubernetes-api-serviceaccount" : "target-node-kubectl-raw",
    parsedDownstreamCliOutput: false,
  }));
}

async function workloadSummary(spec) {
  const resource = await getJson(apiPathForWorkload(spec), false);
  if (resource === null) {
    return {
      ok: false,
      degradedReason: "workload-not-found",
      kind: spec.kind,
      name: spec.name,
      namespace,
      message: `${spec.kind} ${namespace}/${spec.name} was not found`,
      ready: false,
      aligned: expectedSha ? false : null,
      sourceCommit: null,
      pods: [],
    };
  }
  const selector = matchLabels(resource.spec?.selector?.matchLabels);
  const pods = selector === null ? [] : await listPods(selector);
  const sourceCommit = sourceCommitSummary(spec, resource, pods);
  const ready = workloadReady(spec.kind, resource);
  return {
    ok: true,
    kind: spec.kind,
    name: spec.name,
    namespace,
    generation: integerOrNull(resource.metadata?.generation),
    observedGeneration: integerOrNull(resource.status?.observedGeneration),
    replicas: integerOrNull(resource.status?.replicas),
    updatedReplicas: integerOrNull(resource.status?.updatedReplicas),
    readyReplicas: integerOrNull(resource.status?.readyReplicas),
    availableReplicas: integerOrNull(resource.status?.availableReplicas),
    unavailableReplicas: integerOrNull(resource.status?.unavailableReplicas),
    ready,
    aligned: expectedSha && sourceCommit.value ? sourceCommit.value === expectedSha : null,
    selector,
    conditions: arrayItems(resource.status?.conditions).slice(-4).map(compactCondition),
    sourceCommit,
    pods: pods.slice(0, 4).map((pod) => podSummary(spec, pod)),
  };
}

function apiPathForWorkload(spec) {
  const plural = spec.kind === "StatefulSet" ? "statefulsets" : "deployments";
  return `/apis/apps/v1/namespaces/${encodeURIComponent(namespace)}/${plural}/${encodeURIComponent(spec.name)}`;
}

async function listPods(selector) {
  const labelSelector = Object.entries(selector).map(([key, value]) => `${key}=${value}`).join(",");
  const list = await getJson(`/api/v1/namespaces/${encodeURIComponent(namespace)}/pods?labelSelector=${encodeURIComponent(labelSelector)}`, false);
  return arrayItems(list?.items).sort((left, right) => String(right.metadata?.creationTimestamp || "").localeCompare(String(left.metadata?.creationTimestamp || "")));
}

function podSummary(spec, pod) {
  const readyCondition = conditionByType(pod.status, "Ready");
  const sourceCommit = podSourceCommitSummary(spec, pod);
  return {
    name: pod.metadata?.name || null,
    phase: pod.status?.phase || null,
    createdAt: pod.metadata?.creationTimestamp || null,
    startTime: pod.status?.startTime || null,
    ready: readyCondition?.status || null,
    readyReason: readyCondition?.reason || null,
    readyMessage: shortText(readyCondition?.message || null),
    readyLastTransitionTime: readyCondition?.lastTransitionTime || null,
    sourceCommit,
    containers: allContainerStatuses(pod).slice(0, maxContainers).map(containerSummary),
  };
}

function sourceCommitSummary(spec, resource, pods) {
  const checks = [];
  for (const key of arrayItems(spec.sourceCommit?.labels)) checks.push({ source: "workload-label", key, value: resource.metadata?.labels?.[key] || null });
  for (const key of arrayItems(spec.sourceCommit?.annotations)) checks.push({ source: "workload-annotation", key, value: resource.metadata?.annotations?.[key] || null });
  const template = resource.spec?.template?.metadata || {};
  for (const key of arrayItems(spec.sourceCommit?.podLabels)) checks.push({ source: "template-label", key, value: template.labels?.[key] || null });
  for (const key of arrayItems(spec.sourceCommit?.podAnnotations)) checks.push({ source: "template-annotation", key, value: template.annotations?.[key] || null });
  const podCommit = pods.map((pod) => podSourceCommitSummary(spec, pod).value).find(Boolean) || null;
  if (podCommit) checks.push({ source: "selected-pod", key: "pod", value: podCommit });
  const found = checks.find((item) => item.value);
  return {
    value: found?.value || null,
    short: shortSha(found?.value || null),
    source: found?.source || null,
    key: found?.key || null,
    checks: checks.map((item) => ({ source: item.source, key: item.key, short: shortSha(item.value) })),
  };
}

function podSourceCommitSummary(spec, pod) {
  const checks = [];
  for (const key of arrayItems(spec.sourceCommit?.podLabels)) checks.push({ source: "pod-label", key, value: pod.metadata?.labels?.[key] || null });
  for (const key of arrayItems(spec.sourceCommit?.podAnnotations)) checks.push({ source: "pod-annotation", key, value: pod.metadata?.annotations?.[key] || null });
  for (const envName of arrayItems(spec.sourceCommit?.env)) {
    const value = envValue(pod, envName);
    checks.push({ source: "pod-env", key: envName, value });
  }
  const found = checks.find((item) => item.value);
  return {
    value: found?.value || null,
    short: shortSha(found?.value || null),
    source: found?.source || null,
    key: found?.key || null,
    checks: checks.map((item) => ({ source: item.source, key: item.key, short: shortSha(item.value) })),
  };
}

function envValue(pod, envName) {
  for (const container of arrayItems(pod.spec?.containers)) {
    for (const env of arrayItems(container.env)) {
      if (env.name === envName && typeof env.value === "string") return env.value;
      if (env.name === envName && env.valueFrom) return null;
    }
  }
  return null;
}

function workloadReady(kind, resource) {
  const status = resource.status || {};
  if (kind === "Deployment") {
    const desired = integerOrNull(resource.spec?.replicas) ?? 1;
    const available = integerOrNull(status.availableReplicas) ?? 0;
    const updated = integerOrNull(status.updatedReplicas) ?? 0;
    return available >= desired && updated >= desired;
  }
  const desired = integerOrNull(resource.spec?.replicas) ?? 1;
  const ready = integerOrNull(status.readyReplicas) ?? 0;
  return ready >= desired;
}

function blockingReason(items, expected) {
  const missing = items.find((item) => item.ok === false);
  if (missing) return `${missing.kind}/${missing.name} not-found`;
  const notReady = items.find((item) => item.ready !== true);
  if (notReady) return `${notReady.kind}/${notReady.name} not-ready`;
  if (expected) {
    const stale = items.find((item) => item.sourceCommit?.value && item.sourceCommit.value !== expected);
    if (stale) return `${stale.kind}/${stale.name} source-mismatch`;
  }
  return null;
}

async function getJson(path, required) {
  let text = "";
  try {
    text = await getText(path, required);
  } catch (error) {
    if (!required && isNotFoundError(error)) return null;
    throw error;
  }
  try {
    return JSON.parse(text);
  } catch (error) {
    throw new KubeReadError("invalid-json", `kube api returned non-JSON for ${path}: ${error?.message || String(error)}`, { path });
  }
}

async function getText(path, required) {
  if (useServiceAccount) return kubeApiGet(path, required);
  const result = spawnSync("kubectl", ["get", "--raw", path], { encoding: "utf8", maxBuffer: 1024 * 1024 });
  if (result.error) throw new KubeReadError("transport-error", `kubectl get --raw transport error for ${path}: ${result.error.message}`, { path });
  if (result.status === 0) return result.stdout;
  const body = result.stderr || result.stdout || `kubectl get --raw failed with exit ${result.status}`;
  throw new KubeReadError(isNotFoundText(body) ? "not-found" : "kube-api-error", `kubectl get --raw failed for ${path}: ${body}`, {
    path,
    notFound: isNotFoundText(body),
  });
}

function kubeApiGet(path, required) {
  const host = process.env.KUBERNETES_SERVICE_HOST;
  const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443");
  const token = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8").trim();
  const ca = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
  return new Promise((resolve, reject) => {
    const req = https.request({ host, port, path, method: "GET", ca, headers: { authorization: `Bearer ${token}` } }, (res) => {
      let body = "";
      res.setEncoding("utf8");
      res.on("data", (chunk) => { body += chunk; });
      res.on("end", () => {
        const code = res.statusCode || 0;
        if (code >= 200 && code < 300) resolve(body);
        else reject(new KubeReadError(code === 404 || isNotFoundText(body) ? "not-found" : "kube-api-error", `kube api GET ${path} status ${code}: ${body || "-"}`, {
          path,
          statusCode: code,
          notFound: code === 404 || isNotFoundText(body),
        }));
      });
    });
    req.on("error", (error) => reject(new KubeReadError("transport-error", `kube api GET ${path} transport error: ${error?.message || String(error)}`, { path })));
    req.end();
  });
}

function parseWorkloads(value) {
  try {
    const parsed = JSON.parse(Buffer.from(value, "base64").toString("utf8"));
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    return [];
  }
}

function matchLabels(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string" && entry[1].length > 0);
  return entries.length === 0 ? null : Object.fromEntries(entries);
}

function allContainerStatuses(pod) {
  const status = pod?.status || {};
  return [
    ...arrayItems(status.initContainerStatuses),
    ...arrayItems(status.containerStatuses),
    ...arrayItems(status.ephemeralContainerStatuses),
  ];
}

function containerSummary(item) {
  return {
    name: item.name || null,
    ready: item.ready === true,
    restartCount: integerOrNull(item.restartCount),
    waiting: compactWaiting(item.state?.waiting),
    running: item.state?.running ? { startedAt: item.state.running.startedAt || null } : null,
    terminated: compactTerminated(item.state?.terminated || item.lastState?.terminated),
    imageID: shortImageId(item.imageID || null),
  };
}

function conditionByType(status, type) {
  return arrayItems(status?.conditions).find((item) => item?.type === type) || null;
}

function compactCondition(value) {
  if (!value) return null;
  return { type: value.type || null, status: value.status || null, reason: value.reason || null, message: shortText(value.message || null), lastTransitionTime: value.lastTransitionTime || null };
}

function compactTerminated(value) {
  if (!value) return null;
  return { reason: value.reason || null, exitCode: integerOrNull(value.exitCode), message: shortText(value.message || null), startedAt: value.startedAt || null, finishedAt: value.finishedAt || null };
}

function compactWaiting(value) {
  if (!value) return null;
  return { reason: value.reason || null, message: shortText(value.message || null) };
}

function uniqueOrNull(values) {
  const unique = [...new Set(values.filter(Boolean))];
  return unique.length === 1 ? unique[0] : null;
}

function shortImageId(value) {
  if (!value) return null;
  const text = String(value);
  const at = text.lastIndexOf("@");
  return at >= 0 ? text.slice(at + 1, at + 25) : shortText(text);
}

function shortSha(value) {
  if (!value) return null;
  const text = String(value);
  return text.length > 12 ? text.slice(0, 12) : text;
}

function shortText(value) {
  if (value === null || value === undefined) return null;
  const text = String(value).replace(/\s+/gu, " ").trim();
  return text.length <= maxMessageBytes ? text : `${text.slice(0, Math.max(0, maxMessageBytes - 3))}...`;
}

function arrayItems(value) {
  return Array.isArray(value) ? value : [];
}

function integerOrNull(value) {
  return Number.isInteger(value) ? value : null;
}

function isNotFoundError(error) {
  return error instanceof KubeReadError && error.notFound === true;
}

function isNotFoundText(value) {
  return /\b404\b|not found|NotFound/u.test(String(value || ""));
}

function positiveInt(value, name) {
  const parsed = Number(value);
  if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${name} must be a positive integer`);
  return parsed;
}

function requiredEnv(name) {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required`);
  return value;
}
