import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import http from "node:http";
import https from "node:https";
import { parseRuntimeReuseConfig, summarizeRuntimeReuseConfig } from "./reuse-config-summary.mjs";

const gate = requiredEnv("GATE");
const follower = requiredEnv("FOLLOWER_ID");
const repository = requiredEnv("REPOSITORY");
const repoPath = requiredEnv("REPO_PATH");
const sourceBranch = requiredEnv("SOURCE_BRANCH");
const snapshotPrefix = requiredEnv("SNAPSHOT_PREFIX").replace(/\/+$/u, "");
const selectedSource = process.env.SOURCE_COMMIT || "";
const gitopsBranch = process.env.GITOPS_BRANCH || "";
const tektonNamespace = process.env.TEKTON_NAMESPACE || "";
const pipelineRunPrefix = process.env.PIPELINE_RUN_PREFIX || "";
const argoNamespace = process.env.ARGO_NAMESPACE || "";
const argoApplication = process.env.ARGO_APPLICATION || "";
const runtimeNamespace = process.env.RUNTIME_NAMESPACE || "";
const stateNamespace = process.env.STATE_NAMESPACE || "";
const stateConfigMap = process.env.STATE_CONFIGMAP || "";
const workloads = parseWorkloads(process.env.WORKLOADS_B64 || "");
const healthUrl = process.env.HEALTH_URL || "";
const slowTaskSeconds = requiredPositiveIntEnv("SLOW_TASK_SECONDS");
const healthTimeoutMs = requiredPositiveIntEnv("HEALTH_TIMEOUT_MS");
const gateTimeoutMs = optionalPositiveIntEnv("GATE_TIMEOUT_MS") ?? healthTimeoutMs;

const errors = [];
const branchCommit = rev(`refs/heads/${sourceBranch}`);
const sourceCommit = selectedSource || branchCommit || "";
const sourceStageRef = sourceCommit ? `${snapshotPrefix}/${sourceCommit}` : null;
const sourceSnapshot = sourceStageRef ? rev(sourceStageRef) : null;
const source = {
  repository,
  branch: sourceBranch,
  sourceCommit: sourceCommit || null,
  stageRef: sourceStageRef,
  snapshotReady: Boolean(sourceCommit && sourceSnapshot === sourceCommit),
  authority: "k8s-git-mirror-snapshot",
};
const gitMirror = gitMirrorSummary(sourceCommit);

let evidence;
if (gate === "reuse-plan") evidence = await reusePlanEvidence(sourceCommit);
else if (gate === "ci-taskrun-plan") evidence = await ciTaskRunEvidence(sourceCommit);
else if (gate === "cd-rollout-plan") evidence = await cdRolloutEvidence(sourceCommit);
else if (gate === "post-deploy-health") evidence = await postDeployHealthEvidence(sourceCommit);
else if (gate === "runtime-closeout") evidence = await runtimeCloseoutEvidence(sourceCommit);
else fail(`unsupported gate ${gate}`);

const ok = errors.length === 0 && evidence?.ok === true;
const payload = {
  ok,
  gate,
  follower,
  source,
  evidence,
  policy: {
    slowTaskSeconds,
    healthTimeoutMs,
  },
  errors: errors.slice(0, 6),
  statusAuthority: "kubernetes-api-serviceaccount",
  parsedDownstreamCliOutput: false,
  bounded: true,
};
console.log(JSON.stringify(payload));
if (!ok) process.exit(2);

async function reusePlanEvidence(commit) {
  const reuse = readReuseConfig(commit);
  const decisions = reuse.present === true ? reusePlanDecisions(reuse) : [];
  const summary = reusePlanDecisionSummary(decisions);
  return {
    ok: source.snapshotReady && gitMirror.ok === true && reuse.present === true && reuse.serviceCount > 0 && decisions.length > 0,
    gitMirror: compactGitMirrorEvidence(gitMirror),
    reuse: compactReuseEvidence(reuse),
    decisions: boundedDecisions(decisions),
    decisionSummary: summary,
    contract: {
      consumer: "adapter-ci",
      fields: "serviceId sourceIdentity envIdentity runtimeReuse envReuse skipImageBuild buildDecision reusableImageRef reason",
      expectation: "CI consumes skipImageBuild from this plan; it does not re-infer build/skip from TaskRun logs or source file scans.",
    },
  };
}

async function ciTaskRunEvidence(commit) {
  if (!commit || !tektonNamespace || !pipelineRunPrefix) return notConfigured("tekton");
  const pipelineRunName = `${pipelineRunPrefix}-${commit.slice(0, 12)}`;
  const reusePlan = await reusePlanEvidence(commit);
  const pipelineRun = await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/pipelineruns/${encodeURIComponent(pipelineRunName)}`, false);
  const prStatus = pipelineRunStatus(pipelineRun);
  const pipelineRef = str(pipelineRun?.spec?.pipelineRef?.name);
  const pipeline = pipelineRef ? await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/pipelines/${encodeURIComponent(pipelineRef)}`, false) : null;
  const taskRuns = await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/taskruns?labelSelector=${encodeURIComponent(`tekton.dev/pipelineRun=${pipelineRunName}`)}`, false);
  const taskSummary = taskRunsSummary(taskRuns);
  const planArtifacts = planArtifactsEvidence(pipelineRunName);
  const storedCiConsumption = await storedAgentRunCiConsumption(commit);
  const buildTaskRunServices = buildTaskServices(taskRuns);
  const ciConsumption = ciConsumptionSummary(reusePlan.decisions || [], planArtifacts, buildTaskRunServices, storedCiConsumption);
  return {
    ok: prStatus.succeeded === true && taskSummary.failedCount === 0 && taskSummary.activeCount === 0 && ciConsumption.ok === true,
    reusePlan: {
      ok: reusePlan.ok,
      decisionSummary: compactDecisionSummary(reusePlan.decisionSummary),
    },
    planArtifacts,
    storedCiConsumption,
    ciConsumption,
    pipelineRun: prStatus,
    pipeline: {
      name: pipelineRef,
      taskCount: Array.isArray(pipeline?.spec?.tasks) ? pipeline.spec.tasks.length : null,
      tasks: Array.isArray(pipeline?.spec?.tasks) ? pipeline.spec.tasks.slice(0, 12).map((task) => ({ name: str(task?.name), runAfter: Array.isArray(task?.runAfter) ? task.runAfter.slice(0, 6) : [] })) : [],
      tasksTruncated: Array.isArray(pipeline?.spec?.tasks) ? pipeline.spec.tasks.length > 12 : false,
    },
    taskRuns: compactCiTaskRuns(taskSummary),
  };
}

function reusePlanDecisions(reuse) {
  const baseRef = sourceStageRef ? parentRef(sourceStageRef) : null;
  return (reuse.serviceSpecs || []).map((service) => {
    const codePaths = service.runtimeReuse?.codeIdentityPaths || [];
    const envPaths = uniqueStrings([...(service.runtimeReuse?.envIdentityPaths || []), ...(service.envReuse?.envIdentityFiles || [])]);
    const sourceIdentity = identityComparison(sourceStageRef, baseRef, codePaths);
    const envIdentity = identityComparison(sourceStageRef, baseRef, envPaths);
    const runtimeEnabled = service.runtimeReuse?.enabled !== false;
    const envEnabled = service.envReuse?.enabled !== false;
    const runtimeHit = runtimeEnabled && sourceIdentity.hit === true && envIdentity.hit === true;
    const envHit = envEnabled && envIdentity.hit === true;
    const skipImageBuild = runtimeHit || envHit;
    return {
      serviceId: service.id,
      sourceIdentity,
      envIdentity,
      runtimeReuse: {
        enabled: runtimeEnabled,
        hit: runtimeHit,
        reason: runtimeHit ? "source-and-env-identity-hit" : missReason(sourceIdentity, envIdentity, runtimeEnabled),
      },
      envReuse: {
        enabled: envEnabled,
        hit: envHit,
        reason: envHit ? "env-identity-hit" : missReason(null, envIdentity, envEnabled),
      },
      skipImageBuild,
      buildDecision: skipImageBuild ? "skipImageBuild" : "buildImage",
      reusableImageRef: null,
      reusableImageRefKnown: false,
      reason: decisionReason({ runtimeHit, envHit, runtimeEnabled, envEnabled, sourceIdentity, envIdentity }),
    };
  });
}

function reusePlanDecisionSummary(decisions) {
  const skip = decisions.filter((item) => item.skipImageBuild === true).map((item) => item.serviceId).sort();
  const build = decisions.filter((item) => item.skipImageBuild !== true).map((item) => item.serviceId).sort();
  return {
    serviceCount: decisions.length,
    skipImageBuildCount: skip.length,
    buildImageCount: build.length,
    skipImageBuildServices: skip,
    buildImageServices: build,
  };
}

function compactDecisionSummary(summary) {
  return {
    serviceCount: summary?.serviceCount ?? null,
    skipImageBuildCount: summary?.skipImageBuildCount ?? null,
    buildImageCount: summary?.buildImageCount ?? null,
  };
}

function identityComparison(currentRef, baseRef, paths) {
  const current = identityDigest(currentRef, paths);
  const previous = baseRef ? identityDigest(baseRef, paths) : null;
  const configured = paths.length > 0;
  const hit = configured && previous !== null && current.sha256 !== null && previous.sha256 !== null && current.sha256 === previous.sha256;
  return {
    configured,
    paths: paths.slice(0, 12),
    pathsTruncated: paths.length > 12,
    pathCount: paths.length,
    current: current.sha256,
    previous: previous?.sha256 ?? null,
    currentMissingCount: current.missingCount,
    previousMissingCount: previous?.missingCount ?? null,
    hit,
    status: !configured ? "not-configured" : previous === null ? "base-missing" : hit ? "hit" : "miss",
  };
}

function identityDigest(ref, paths) {
  if (!ref || paths.length === 0) return { sha256: null, missingCount: 0 };
  const hash = createHash("sha256");
  let missingCount = 0;
  for (const path of paths) {
    const entries = gitTreeEntries(ref, path);
    if (entries.length === 0) missingCount += 1;
    hash.update(path);
    hash.update("\0");
    for (const entry of entries) {
      hash.update(entry);
      hash.update("\0");
    }
  }
  return { sha256: hash.digest("hex"), missingCount };
}

function gitTreeEntries(ref, path) {
  try {
    const out = execFileSync("git", [`--git-dir=${repoPath}`, "ls-tree", "-r", "-z", "--full-tree", ref, "--", path], { encoding: "utf8", maxBuffer: 1024 * 1024 });
    return out.split("\0").filter(Boolean).sort();
  } catch {
    return [];
  }
}

function parentRef(ref) {
  try {
    const out = execFileSync("git", [`--git-dir=${repoPath}`, "rev-parse", "--verify", `${ref}^`], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
    return sha(out) ? out : null;
  } catch {
    return null;
  }
}

function missReason(sourceIdentity, envIdentity, enabled) {
  if (!enabled) return "reuse-disabled";
  if (sourceIdentity?.configured === false || envIdentity?.configured === false) return "identity-not-configured";
  if (sourceIdentity?.status === "base-missing" || envIdentity?.status === "base-missing") return "base-identity-missing";
  if (sourceIdentity?.status === "miss") return "source-identity-miss";
  if (envIdentity?.status === "miss") return "env-identity-miss";
  return "identity-miss";
}

function decisionReason(input) {
  if (input.runtimeHit) return "runtime-reuse-hit";
  if (input.envHit) return "env-reuse-hit";
  if (input.envIdentity.configured === false) return "env-identity-not-configured";
  if (input.envIdentity.status === "base-missing") return "previous-env-identity-unavailable";
  if (input.envIdentity.status === "miss") return "env-identity-changed";
  if (!input.runtimeEnabled && !input.envEnabled) return "reuse-disabled";
  return "reuse-miss";
}

function planArtifactsEvidence(pipelineRunName) {
  if (!tektonNamespace || !pipelineRunName) return { ok: false, degradedReason: "tekton-not-configured" };
  try {
    const text = execFileSync("node", ["./plan-artifacts.mjs", tektonNamespace, pipelineRunName], { encoding: "utf8", maxBuffer: 512 * 1024 });
    return JSON.parse(text);
  } catch (error) {
    return { ok: false, degradedReason: "plan-artifacts-query-failed", reason: shortText(error?.message || String(error)) };
  }
}

function ciConsumptionSummary(decisions, planArtifacts, buildTaskRunServices, storedCiConsumption) {
  const skipExpected = decisions.filter((item) => item.skipImageBuild === true).map((item) => item.serviceId).sort();
  const buildExpected = decisions.filter((item) => item.skipImageBuild !== true).map((item) => item.serviceId).sort();
  if (/agentrun/u.test(follower) && storedCiConsumption?.present === true && storedCiConsumption.ok !== true) {
    return {
      ok: false,
      reason: storedCiConsumption.reason || "ci-consumption-evidence-missing",
      source: "branch-follower-compact-state",
      expected: {
        skipImageBuildCount: skipExpected.length,
        buildImageCount: buildExpected.length,
      },
      observed: {
        buildServicesCount: 0,
        buildTaskRunServices,
        reusedServicesCount: 0,
        skippedOrReusedServicesCount: 0,
        buildSkippedCount: null,
      },
      mismatches: [],
    };
  }
  if (storedCiConsumption?.ok === true) {
    const observed = storedCiConsumption.observed || {};
    const unexpectedStoredBuild = skipExpected.filter((serviceId) => observed.imageBuildDecision === "buildImage" && serviceId === "agentrun-mgr");
    const missingStoredBuild = buildExpected.filter((serviceId) => observed.imageBuildDecision !== "buildImage" && serviceId === "agentrun-mgr");
    const storedOk = unexpectedStoredBuild.length === 0 && missingStoredBuild.length === 0;
    return {
      ok: storedOk,
      reason: storedOk ? "ci-consumed-reuse-plan" : "ci-consumption-mismatch",
      source: "branch-follower-compact-state",
      expected: {
        skipImageBuildCount: skipExpected.length,
        buildImageCount: buildExpected.length,
      },
      observed: {
        buildServicesCount: numberOrNull(observed.buildServicesCount),
        buildTaskRunServices,
        reusedServicesCount: numberOrNull(observed.reusedServicesCount),
        skippedOrReusedServicesCount: numberOrNull(observed.reusedServicesCount),
        buildSkippedCount: numberOrNull(observed.buildSkippedCount),
        imageBuildDecision: str(observed.imageBuildDecision),
        imageBuildStatus: str(observed.imageBuildStatus),
        imageBuildCreated: observed.imageBuildCreated === true,
        imageBuildReused: observed.imageBuildReused === true,
      },
      mismatches: compactCiMismatches(unexpectedStoredBuild, missingStoredBuild),
    };
  }
  const buildObserved = uniqueStrings([...strings(planArtifacts?.buildServices), ...buildTaskRunServices]).sort();
  const reusedObserved = strings(planArtifacts?.reusedServices).sort();
  const skippedObserved = uniqueStrings(reusedObserved).sort();
  const unexpectedBuild = skipExpected.filter((serviceId) => buildObserved.includes(serviceId) && !reusedObserved.includes(serviceId));
  const missingBuild = buildExpected.filter((serviceId) => !buildObserved.includes(serviceId) && !reusedObserved.includes(serviceId));
  const ok = planArtifacts?.ok === true && unexpectedBuild.length === 0 && missingBuild.length === 0;
  return {
    ok,
    reason: unexpectedBuild.length > 0 || missingBuild.length > 0 ? "ci-consumption-mismatch" : planArtifacts?.ok === true ? "ci-consumed-reuse-plan" : "plan-artifacts-event-missing",
    source: "reuse-plan-decisions-compared-to-plan-artifacts-g14-ci-plan",
    expected: {
      skipImageBuildCount: skipExpected.length,
      buildImageCount: buildExpected.length,
    },
    observed: {
      buildServicesCount: buildObserved.length,
      buildTaskRunServices,
      reusedServicesCount: reusedObserved.length,
      skippedOrReusedServicesCount: skippedObserved.length,
      buildSkippedCount: typeof planArtifacts?.buildSkippedCount === "number" ? planArtifacts.buildSkippedCount : null,
    },
    mismatches: compactCiMismatches(unexpectedBuild, missingBuild),
  };
}

async function storedAgentRunCiConsumption(commit) {
  if (!stateNamespace || !stateConfigMap || !follower || !commit) return null;
  const cm = await getJson(`/api/v1/namespaces/${encodeURIComponent(stateNamespace)}/configmaps/${encodeURIComponent(stateConfigMap)}`, false);
  const raw = cm?.data?.[follower];
  if (typeof raw !== "string" || raw.length === 0) return { ok: false, present: false, reason: "compact-state-missing" };
  const state = parseJson(raw);
  if (state === null) return { ok: false, present: true, reason: "state-json-parse-failed" };
  const command = state?.command || {};
  const payload = command.payload || {};
  const agentrun = payload.agentrun || {};
  const sourceCommit = str(agentrun.sourceCommit) || str(command.sourceCommit);
  const ci = agentrun.ciConsumption || null;
  if (sourceCommit === null && (!ci || typeof ci !== "object")) return { ok: false, present: true, reason: "ci-consumption-evidence-missing", sourceCommit: null };
  if (sourceCommit !== commit) return { ok: false, present: true, reason: "state-source-commit-mismatch", sourceCommit };
  if (!ci || typeof ci !== "object") return { ok: false, present: true, reason: "ci-consumption-evidence-missing", sourceCommit };
  return {
    ok: ci.ok === true,
    present: true,
    sourceCommit,
    source: str(ci.source),
    expected: ci.expected || null,
    observed: ci.observed || null,
    mismatches: Array.isArray(ci.mismatches) ? ci.mismatches.slice(0, 6) : [],
    valuesRedacted: true,
  };
}

function compactCiMismatches(unexpectedBuild, missingBuild) {
  return [
    ...(unexpectedBuild.length > 0 ? [{ reason: "expected-skipImageBuild-but-ci-buildServices-includes-service", serviceIds: unexpectedBuild }] : []),
    ...(missingBuild.length > 0 ? [{ reason: "expected-buildImage-but-ci-plan-has-no-build-or-reuse-service", serviceIds: missingBuild }] : []),
  ];
}

function buildTaskServices(list) {
  const items = Array.isArray(list?.items) ? list.items : [];
  return uniqueStrings(items.flatMap((item) => {
    const taskName = str(item?.metadata?.labels?.["tekton.dev/pipelineTask"]) || str(item?.spec?.taskRef?.name) || "";
    const match = /^build-(.+)$/u.exec(taskName);
    return match ? [match[1]] : [];
  })).sort();
}

function compactCiTaskRuns(summary) {
  return {
    count: summary.count,
    slowThresholdSeconds: summary.slowThresholdSeconds,
    failedCount: summary.failedCount,
    activeCount: summary.activeCount,
    slowCount: summary.slowCount,
    failedItems: summary.failedItems.slice(0, 4).map(compactCiTaskItem),
    activeItems: summary.activeItems.slice(0, 4).map(compactCiTaskItem),
    slowItems: summary.slowItems.slice(0, 4).map(compactCiTaskItem),
    timeline: summary.timeline.slice(0, 10),
    timelineTruncated: summary.timeline.length > 10 || summary.timelineTruncated === true,
    performance: summary.performance,
  };
}

function compactCiTaskItem(item) {
  return {
    taskName: item.taskName,
    status: item.status,
    reason: item.reason,
    durationSeconds: item.durationSeconds,
  };
}

async function cdRolloutEvidence(commit) {
  const argo = await argoSummary();
  const runtime = await runtimeSummary(commit);
  return {
    ok: gitMirror.githubInSync !== false && argo.ready === true && runtime.ready === true && runtime.aligned === true,
    gitMirror,
    argo,
    runtime,
  };
}

async function postDeployHealthEvidence(commit) {
  const runtime = await runtimeSummary(commit);
  const health = await healthSummary();
  return {
    ok: runtime.ready === true && runtime.aligned === true && health.ok === true,
    runtime,
    health,
  };
}

async function runtimeCloseoutEvidence(commit) {
  const startedAt = Date.now();
  const refresh = await argoRefresh();
  const deadline = Date.now() + gateTimeoutMs;
  let polls = 0;
  let latest = null;
  while (Date.now() <= deadline) {
    polls += 1;
    latest = await runtimeCloseoutStatus(commit);
    if (latest.ok === true) break;
    if (Date.now() + 2000 > deadline) break;
    await delay(2000);
  }
  return {
    ok: latest?.ok === true,
    sourceCommit: shortSha(commit),
    gitMirror: compactGitMirrorEvidence(gitMirror),
    refresh,
    closeout: latest,
    polls,
    elapsedMs: Date.now() - startedAt,
    writesState: false,
    contract: {
      gate: "argo-runtime-only",
      excludes: ["git-mirror-flush", "state-write"],
      expectation: "Argo refresh and runtime readiness are independently triggerable; git-mirror post-flush is validated by the git-mirror-flush gate.",
    },
  };
}

async function runtimeCloseoutStatus(commit) {
  const pipelineRunName = commit && pipelineRunPrefix ? `${pipelineRunPrefix}-${commit.slice(0, 12)}` : "";
  const pipelineRun = pipelineRunName && tektonNamespace
    ? pipelineRunStatus(await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/pipelineruns/${encodeURIComponent(pipelineRunName)}`, false))
    : { present: null, succeeded: null, reason: "tekton-not-configured" };
  const argo = await argoSummary();
  const runtime = await runtimeSummary(commit);
  return {
    ok: source.snapshotReady === true && pipelineRun.succeeded === true && argo.ready === true && runtime.ready === true && runtime.aligned === true,
    source,
    pipelineRun,
    argo,
    runtime,
  };
}

async function argoRefresh() {
  if (!argoNamespace || !argoApplication) return { ok: true, skipped: true, reason: "argo-not-configured" };
  const path = `/apis/argoproj.io/v1alpha1/namespaces/${encodeURIComponent(argoNamespace)}/applications/${encodeURIComponent(argoApplication)}`;
  const patched = await patchJson(path, { metadata: { annotations: { "argocd.argoproj.io/refresh": "hard" } } });
  return {
    ok: patched !== null,
    namespace: argoNamespace,
    application: argoApplication,
    refresh: "hard",
    resourceVersion: str(patched?.metadata?.resourceVersion),
    statusAuthority: "kubernetes-api-serviceaccount",
    parsedDownstreamCliOutput: false,
    valuesRedacted: true,
  };
}

function readReuseConfig(commit) {
  if (!commit || !sourceStageRef) return { present: false, reason: "source-commit-missing" };
  try {
    const text = execFileSync("git", [`--git-dir=${repoPath}`, "show", `${sourceStageRef}:gitops/reuse.ymal`], { encoding: "utf8", maxBuffer: 256 * 1024 });
    const parsed = parseRuntimeReuseConfig(text, { sourceCommit: commit, stageRef: sourceStageRef });
    const summary = summarizeRuntimeReuseConfig(parsed);
    return {
      ...summary,
      serviceSpecs: parsed.services,
      bytes: Buffer.byteLength(text, "utf8"),
      sha256: summary.sha256 ?? createHash("sha256").update(text).digest("hex"),
    };
  } catch (error) {
    return { present: false, path: "gitops/reuse.ymal", reason: shortText(error?.message || String(error)) };
  }
}

function compactReuseEvidence(reuse) {
  return {
    ok: reuse.ok === true,
    present: reuse.present === true,
    path: reuse.path || "gitops/reuse.ymal",
    sha256: shortFingerprint(reuse.sha256),
    serviceCount: reuse.serviceCount ?? null,
    errors: Array.isArray(reuse.errors) ? reuse.errors.slice(0, 3) : [],
    bytes: reuse.bytes ?? null,
    valuesRedacted: true,
  };
}

function compactGitMirrorEvidence(value) {
  return {
    ok: value.ok === true,
    sourceSnapshotReady: value.sourceSnapshotReady === true,
    githubInSync: value.githubInSync,
    pendingFlush: value.pendingFlush,
  };
}

function boundedDecisions(decisions) {
  return decisions.slice(0, 16).map((item) => ({
    serviceId: item.serviceId,
    sourceIdentity: compactIdentity(item.sourceIdentity),
    envIdentity: compactIdentity(item.envIdentity),
    runtimeReuse: compactReuseHit(item.runtimeReuse),
    envReuse: compactReuseHit(item.envReuse),
    skipImageBuild: item.skipImageBuild,
    buildDecision: item.buildDecision,
    reusableImageRef: item.reusableImageRef,
    reusableImageRefKnown: item.reusableImageRefKnown,
    reason: item.reason,
  }));
}

function compactIdentity(value) {
  return {
    configured: value.configured,
    pathCount: value.pathCount,
    hit: value.hit,
    status: value.status,
    missing: {
      current: value.currentMissingCount,
      previous: value.previousMissingCount,
    },
  };
}

function compactReuseHit(value) {
  return {
    enabled: value.enabled,
    hit: value.hit,
  };
}

function gitMirrorSummary(commit) {
  const localSource = rev(`refs/heads/${sourceBranch}`);
  const githubSource = rev(`refs/mirror-stage/heads/${sourceBranch}`);
  const localGitops = gitopsBranch ? rev(`refs/heads/${gitopsBranch}`) : null;
  const githubGitops = gitopsBranch ? rev(`refs/mirror-stage/heads/${gitopsBranch}`) : null;
  return {
    ok: Boolean(localSource) && source.snapshotReady && (!gitopsBranch || !localGitops || localGitops === githubGitops),
    localSource,
    githubSource,
    sourceStageRef,
    sourceSnapshot,
    sourceSnapshotReady: source.snapshotReady,
    gitopsBranch: gitopsBranch || null,
    localGitops,
    githubGitops,
    pendingFlush: gitopsBranch ? Boolean(localGitops && localGitops !== githubGitops) : null,
    githubInSync: gitopsBranch ? Boolean(!localGitops || localGitops === githubGitops) : null,
  };
}

async function argoSummary() {
  if (!argoNamespace || !argoApplication) return { ready: null, reason: "argo-not-configured" };
  const app = await getJson(`/apis/argoproj.io/v1alpha1/namespaces/${encodeURIComponent(argoNamespace)}/applications/${encodeURIComponent(argoApplication)}`, false);
  const sync = app?.status?.sync || {};
  const health = app?.status?.health || {};
  const op = app?.status?.operationState || {};
  const problemWorkloads = await argoProblemWorkloadSummaries(app);
  return {
    name: argoApplication,
    namespace: argoNamespace,
    syncStatus: str(sync.status),
    healthStatus: str(health.status),
    revision: str(sync.revision),
    operationPhase: str(op.phase),
    operationMessage: str(op.message),
    conditions: compactArgoConditions(app?.status?.conditions),
    nonReadyResources: compactArgoNonReadyResources(app?.status?.resources),
    syncResultResources: compactArgoSyncResultResources(op?.syncResult?.resources),
    problemWorkloads,
    ready: sync.status === "Synced" && health.status === "Healthy",
  };
}

async function argoProblemWorkloadSummaries(app) {
  const resources = Array.isArray(app?.status?.resources) ? app.status.resources : [];
  const syncResources = Array.isArray(app?.status?.operationState?.syncResult?.resources) ? app.status.operationState.syncResult.resources : [];
  const names = uniqueStrings([
    ...resources
      .filter((item) => item?.kind === "Deployment" && (item.status === "OutOfSync" || (item.health?.status && item.health.status !== "Healthy")))
      .map((item) => str(item.name))
      .filter(Boolean),
    ...syncResources
      .filter((item) => item?.kind === "Deployment" && problemArgoSyncResource(item))
      .map((item) => str(item.name))
      .filter(Boolean),
  ]).slice(0, 8);
  const rows = [];
  for (const name of names) rows.push(await deploymentProblemSummary(runtimeNamespace || "default", name));
  return rows;
}

async function deploymentProblemSummary(namespace, name) {
  const deployment = await getJson(`/apis/apps/v1/namespaces/${encodeURIComponent(namespace)}/deployments/${encodeURIComponent(name)}`, false);
  const selector = deploymentSelector(deployment);
  const pods = selector ? await getJson(`/api/v1/namespaces/${encodeURIComponent(namespace)}/pods?labelSelector=${encodeURIComponent(selector)}`, false) : null;
  const conditions = Array.isArray(deployment?.status?.conditions)
    ? deployment.status.conditions
        .filter((item) => item?.status !== "True" || item?.type === "Progressing")
        .slice(-4)
        .map((item) => ({
          type: str(item?.type),
          status: str(item?.status),
          reason: str(item?.reason),
          message: shortText(str(item?.message) || ""),
        }))
    : [];
  return {
    kind: "Deployment",
    namespace,
    name,
    desired: deployment?.spec?.replicas ?? 1,
    readyReplicas: deployment?.status?.readyReplicas ?? 0,
    updatedReplicas: deployment?.status?.updatedReplicas ?? 0,
    unavailableReplicas: deployment?.status?.unavailableReplicas ?? null,
    conditions,
    pods: compactProblemPods(pods),
  };
}

function deploymentSelector(deployment) {
  const labels = deployment?.spec?.selector?.matchLabels;
  if (!labels || typeof labels !== "object" || Array.isArray(labels)) return null;
  const pairs = Object.entries(labels)
    .filter((entry) => typeof entry[0] === "string" && typeof entry[1] === "string")
    .map(([key, value]) => `${key}=${value}`);
  return pairs.length === 0 ? null : pairs.join(",");
}

function compactProblemPods(list) {
  const items = Array.isArray(list?.items) ? list.items : [];
  return items.slice(0, 6).map((pod) => {
    const statuses = Array.isArray(pod?.status?.containerStatuses) ? pod.status.containerStatuses : [];
    return {
      name: str(pod?.metadata?.name),
      phase: str(pod?.status?.phase),
      ready: statuses.every((item) => item?.ready === true),
      containers: statuses.slice(0, 4).map((item) => ({
        name: str(item?.name),
        ready: item?.ready === true,
        restartCount: typeof item?.restartCount === "number" ? item.restartCount : null,
        waitingReason: str(item?.state?.waiting?.reason),
        waitingMessage: shortText(str(item?.state?.waiting?.message) || ""),
        terminatedReason: str(item?.lastState?.terminated?.reason) || str(item?.state?.terminated?.reason),
        exitCode: typeof item?.lastState?.terminated?.exitCode === "number" ? item.lastState.terminated.exitCode : typeof item?.state?.terminated?.exitCode === "number" ? item.state.terminated.exitCode : null,
      })),
    };
  });
}

function compactArgoConditions(value) {
  return Array.isArray(value)
    ? value.slice(0, 5).map((item) => ({
        type: str(item?.type),
        message: shortText(str(item?.message) || ""),
        lastTransitionTime: str(item?.lastTransitionTime),
      }))
    : [];
}

function compactArgoNonReadyResources(value) {
  return Array.isArray(value)
    ? value
        .filter((item) => item?.health?.status && item.health.status !== "Healthy")
        .slice(0, 5)
        .map((item) => ({
          kind: str(item?.kind),
          namespace: str(item?.namespace),
          name: str(item?.name),
          status: str(item?.status),
          healthStatus: str(item?.health?.status),
          healthMessage: shortText(str(item?.health?.message) || ""),
        }))
    : [];
}

function compactArgoSyncResultResources(value) {
  return Array.isArray(value)
    ? value
        .filter(problemArgoSyncResource)
        .slice(0, 8)
        .map((item) => ({
          group: str(item?.group),
          kind: str(item?.kind),
          namespace: str(item?.namespace),
          name: str(item?.name),
          status: str(item?.status),
          hookPhase: str(item?.hookPhase),
          syncPhase: str(item?.syncPhase),
          message: shortText(str(item?.message) || ""),
        }))
    : [];
}

function problemArgoSyncResource(item) {
  const message = String(item?.message || "");
  return (item?.status && item.status !== "Synced")
    || (item?.hookPhase && item.hookPhase !== "Succeeded")
    || /fail|error|backoff|forbidden|invalid|denied|exceeded/iu.test(message);
}

async function runtimeSummary(expected) {
  if (!runtimeNamespace || workloads.length === 0) return { ready: null, aligned: null, reason: "runtime-not-configured" };
  const rows = [];
  for (const workload of workloads) {
    const resource = workload.kind === "StatefulSet" ? "statefulsets" : "deployments";
    const item = await getJson(`/apis/apps/v1/namespaces/${encodeURIComponent(runtimeNamespace)}/${resource}/${encodeURIComponent(workload.name)}`, false);
    rows.push(workloadSummary(workload, item, expected));
  }
  return {
    namespace: runtimeNamespace,
    expectedSha: shortSha(expected),
    ready: rows.length > 0 && rows.every((row) => row.ready === true),
    aligned: rows.length > 0 && rows.every((row) => row.aligned === true),
    workloads: rows,
  };
}

async function healthSummary() {
  if (!healthUrl) return { ok: null, reason: "health-url-not-configured" };
  const targets = healthProbeTargets(healthUrl);
  const probes = [];
  for (const url of targets) probes.push(await httpProbe(url));
  return { ok: probes.every((probe) => probe.ok), probes };
}

function healthProbeTargets(value) {
  const trimmed = value.replace(/\/+$/u, "");
  try {
    const parsed = new URL(trimmed);
    const pathname = parsed.pathname.replace(/\/+$/u, "");
    if (pathname.endsWith("/health") || pathname.endsWith("/api/health") || pathname.endsWith("/health/readiness") || pathname.endsWith("/health/live")) {
      return [trimmed];
    }
  } catch {
    // Fall back to the historical base-url contract when HEALTH_URL is not a full URL.
  }
  return [`${trimmed}/health/readiness`, `${trimmed}/health/live`];
}

function workloadSummary(spec, item, expected) {
  const status = item?.status || {};
  const desired = item?.spec?.replicas ?? 1;
  const readyCount = spec.kind === "StatefulSet" ? status.readyReplicas ?? 0 : status.availableReplicas ?? 0;
  const updated = status.updatedReplicas ?? readyCount;
  const commit = workloadCommit(spec, item);
  return {
    kind: spec.kind,
    name: spec.name,
    ready: readyCount >= desired && updated >= desired,
    desired,
    readyReplicas: readyCount,
    updatedReplicas: updated,
    sourceCommit: shortSha(commit),
    aligned: Boolean(expected && commit && commit === expected),
  };
}

function workloadCommit(spec, item) {
  const meta = item?.metadata || {};
  const tmpl = item?.spec?.template || {};
  const podMeta = tmpl.metadata || {};
  for (const key of spec.sourceCommit.labels || []) if (sha(meta.labels?.[key])) return meta.labels[key];
  for (const key of spec.sourceCommit.annotations || []) if (sha(meta.annotations?.[key])) return meta.annotations[key];
  for (const key of spec.sourceCommit.podLabels || []) if (sha(podMeta.labels?.[key])) return podMeta.labels[key];
  for (const key of spec.sourceCommit.podAnnotations || []) if (sha(podMeta.annotations?.[key])) return podMeta.annotations[key];
  for (const envName of spec.sourceCommit.env || []) {
    for (const container of tmpl.spec?.containers || []) {
      for (const env of container.env || []) if (env?.name === envName && sha(env.value)) return env.value;
    }
  }
  return null;
}

function pipelineRunStatus(value) {
  const condition = (value?.status?.conditions || []).find((item) => item?.type === "Succeeded") || null;
  return {
    name: str(value?.metadata?.name),
    namespace: str(value?.metadata?.namespace),
    pipelineRefName: str(value?.spec?.pipelineRef?.name),
    present: value !== null,
    succeeded: condition?.status === "True" ? true : condition?.status === "False" ? false : null,
    reason: str(condition?.reason),
    startTime: str(value?.status?.startTime),
    completionTime: str(value?.status?.completionTime),
    durationSeconds: durationSeconds(value?.status?.startTime, value?.status?.completionTime),
  };
}

function taskRunsSummary(list) {
  const items = Array.isArray(list?.items) ? list.items : [];
  const rows = items.map((item) => {
    const condition = (item?.status?.conditions || []).find((entry) => entry?.type === "Succeeded") || {};
    return {
      name: str(item?.metadata?.name),
      taskName: str(item?.metadata?.labels?.["tekton.dev/pipelineTask"]) || str(item?.spec?.taskRef?.name),
      status: str(condition.status) || "Unknown",
      reason: str(condition.reason),
      startTime: str(item?.status?.startTime),
      completionTime: str(item?.status?.completionTime),
      durationSeconds: durationSeconds(item?.status?.startTime, item?.status?.completionTime),
    };
  });
  const failed = rows.filter((item) => item.status === "False");
  const active = rows.filter((item) => item.status !== "True" && item.status !== "False");
  const slow = rows
    .filter((item) => typeof item.durationSeconds === "number" && item.durationSeconds >= slowTaskSeconds)
    .sort((left, right) => (right.durationSeconds || 0) - (left.durationSeconds || 0));
  const timeline = rows.slice().sort(compareTaskRunRows);
  const performance = taskRunPerformance(rows);
  return {
    count: rows.length,
    slowThresholdSeconds: slowTaskSeconds,
    failedCount: failed.length,
    activeCount: active.length,
    slowCount: slow.length,
    failedItems: failed.slice(0, 6).map(compactNamedTaskRunRow),
    activeItems: active.slice(0, 6).map(compactNamedTaskRunRow),
    slowItems: slow.slice(0, 6).map(compactNamedTaskRunRow),
    timeline: timeline.slice(0, 16).map((item) => compactTimelineTaskRunRow(item, performance.firstStart)),
    timelineTruncated: timeline.length > 16,
    performance,
  };
}

function taskRunPerformance(rows) {
  const starts = rows.map((item) => Date.parse(item.startTime || "")).filter((value) => Number.isFinite(value));
  const finishes = rows.map((item) => Date.parse(item.completionTime || "")).filter((value) => Number.isFinite(value));
  const durations = rows.map((item) => item.durationSeconds).filter((value) => typeof value === "number");
  const firstStart = starts.length === 0 ? null : new Date(Math.min(...starts)).toISOString();
  const lastCompletion = finishes.length === 0 ? null : new Date(Math.max(...finishes)).toISOString();
  return {
    firstStart,
    lastCompletion,
    spanSeconds: firstStart && lastCompletion ? durationSeconds(firstStart, lastCompletion) : null,
    taskDurationSumSeconds: durations.length === 0 ? null : Math.round(durations.reduce((sum, value) => sum + value, 0) * 10) / 10,
    maxTaskDurationSeconds: durations.length === 0 ? null : Math.max(...durations),
  };
}

function compareTaskRunRows(left, right) {
  const leftTime = Date.parse(left.startTime || "");
  const rightTime = Date.parse(right.startTime || "");
  if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) return leftTime - rightTime;
  if (Number.isFinite(leftTime)) return -1;
  if (Number.isFinite(rightTime)) return 1;
  return String(left.name || "").localeCompare(String(right.name || ""));
}

function compactNamedTaskRunRow(item) {
  return {
    name: item.name,
    taskName: item.taskName,
    status: item.status,
    reason: item.reason,
    durationSeconds: item.durationSeconds,
  };
}

function compactTimelineTaskRunRow(item, firstStart) {
  return {
    taskName: item.taskName,
    status: item.status,
    startOffsetSeconds: firstStart && item.startTime ? durationSeconds(firstStart, item.startTime) : null,
    durationSeconds: item.durationSeconds,
  };
}

async function getJson(path, required) {
  const host = process.env.KUBERNETES_SERVICE_HOST;
  const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443");
  const tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
  const caPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt";
  if (!host || !existsSync(tokenPath) || !existsSync(caPath)) fail("kubernetes serviceaccount is unavailable");
  const token = readFileSync(tokenPath, "utf8").trim();
  const ca = readFileSync(caPath);
  return await 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", () => {
        if (res.statusCode === 404 && !required) return resolve(null);
        if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300) return reject(new Error(shortText(body || `kube api ${res.statusCode}`)));
        try { resolve(JSON.parse(body)); } catch (error) { reject(error); }
      });
    });
    req.on("error", reject);
    req.end();
  }).catch((error) => {
    errors.push(`${path}: ${shortText(error?.message || String(error))}`);
    return null;
  });
}

async function patchJson(path, value) {
  const host = process.env.KUBERNETES_SERVICE_HOST;
  const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443");
  const tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
  const caPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt";
  if (!host || !existsSync(tokenPath) || !existsSync(caPath)) fail("kubernetes serviceaccount is unavailable");
  const token = readFileSync(tokenPath, "utf8").trim();
  const ca = readFileSync(caPath);
  const payload = JSON.stringify(value);
  return await new Promise((resolve, reject) => {
    const req = https.request({
      host,
      port,
      path,
      method: "PATCH",
      ca,
      headers: {
        authorization: `Bearer ${token}`,
        "content-type": "application/merge-patch+json",
        "content-length": Buffer.byteLength(payload),
      },
    }, (res) => {
      let body = "";
      res.setEncoding("utf8");
      res.on("data", (chunk) => { body += chunk; });
      res.on("end", () => {
        if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300) return reject(new Error(shortText(body || `kube api ${res.statusCode}`)));
        try { resolve(JSON.parse(body)); } catch (error) { reject(error); }
      });
    });
    req.on("error", reject);
    req.write(payload);
    req.end();
  }).catch((error) => {
    errors.push(`${path}: ${shortText(error?.message || String(error))}`);
    return null;
  });
}

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function httpProbe(url) {
  const client = url.startsWith("https:") ? https : http;
  const started = Date.now();
  return await new Promise((resolve) => {
    const req = client.get(url, { timeout: healthTimeoutMs }, (res) => {
      res.resume();
      res.on("end", () => resolve({ url, ok: (res.statusCode || 0) >= 200 && (res.statusCode || 0) < 300, statusCode: res.statusCode || null, elapsedMs: Date.now() - started }));
    });
    req.on("timeout", () => { req.destroy(); resolve({ url, ok: false, statusCode: null, elapsedMs: Date.now() - started, reason: "timeout" }); });
    req.on("error", (error) => resolve({ url, ok: false, statusCode: null, elapsedMs: Date.now() - started, reason: shortText(error?.message || String(error)) }));
  });
}

function parseWorkloads(value) {
  if (!value) return [];
  try {
    const parsed = JSON.parse(Buffer.from(value, "base64").toString("utf8"));
    return Array.isArray(parsed) ? parsed.filter((item) => item && typeof item === "object").slice(0, 8) : [];
  } catch {
    return [];
  }
}

function optionalPositiveIntEnv(name) {
  const raw = process.env[name] || "";
  if (!raw) return null;
  const value = Number.parseInt(raw, 10);
  if (!Number.isInteger(value) || value <= 0) fail(`${name} must be a positive integer`);
  return value;
}

function rev(ref) {
  try {
    const out = execFileSync("git", [`--git-dir=${repoPath}`, "rev-parse", "--verify", `${ref}^{commit}`], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
    return sha(out) ? out : null;
  } catch {
    return null;
  }
}

function durationSeconds(start, finish) {
  if (!start || !finish) return null;
  const value = (Date.parse(finish) - Date.parse(start)) / 1000;
  return Number.isFinite(value) && value >= 0 ? Math.round(value * 10) / 10 : null;
}

function notConfigured(name) {
  return { ok: false, reason: `${name}-not-configured` };
}

function str(value) {
  return typeof value === "string" && value.length > 0 ? value : null;
}

function parseJson(value) {
  try {
    const parsed = JSON.parse(value);
    return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

function numberOrNull(value) {
  return typeof value === "number" && Number.isFinite(value) ? value : null;
}

function strings(value) {
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
}

function uniqueStrings(value) {
  return [...new Set(value.filter((item) => typeof item === "string" && item.length > 0))];
}

function sha(value) {
  return typeof value === "string" && /^[0-9a-f]{40}$/iu.test(value);
}

function shortSha(value) {
  return sha(value) ? value.slice(0, 12) : null;
}

function shortFingerprint(value) {
  return typeof value === "string" && value.length >= 12 ? value.slice(0, 12) : null;
}

function shortText(value) {
  const text = String(value || "").replace(/\s+/gu, " ").trim();
  return text.length <= 300 ? text : text.slice(0, 300);
}

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

function requiredPositiveIntEnv(name) {
  const value = requiredEnv(name);
  const parsed = Number(value);
  if (!Number.isInteger(parsed) || parsed <= 0) fail(`${name} must be a positive integer`);
  return parsed;
}

function fail(message) {
  console.error(message);
  process.exit(1);
}
