// SPEC: PJ2026-01060703 CI/CD branch follower AgentRun reuse decisions.
// Responsibility: compute AgentRun per-service reuse decisions and bounded CI consumption evidence.
import { createHash } from "node:crypto";
import { repoRoot } from "./config";
import { runCommand } from "./command";
import type { AgentRunArtifactService } from "./agentrun-manifests";
import type { AgentRunLaneSpec } from "./agentrun-lanes";
import type { FollowerSpec, NativeK8sJobResult } from "./cicd-types";
import { RUNTIME_REUSE_CONFIG_PATH, runtimeReuseService, type RuntimeReuseConfig } from "./cicd-reuse-config";

export interface AgentRunReusePlan {
  ok: boolean;
  sourceCommit: string;
  stageRef: string;
  decisions: AgentRunReuseDecision[];
  decisionSummary: {
    serviceCount: number;
    skipImageBuildCount: number;
    buildImageCount: number;
    skipImageBuildServices: string[];
    buildImageServices: string[];
  };
  errors: string[];
  valuesRedacted: true;
}

export interface AgentRunReuseDecision {
  serviceId: string;
  sourceIdentity: AgentRunIdentityComparison;
  envIdentity: AgentRunIdentityComparison;
  runtimeReuse: { enabled: boolean; hit: boolean; reason: string };
  envReuse: { enabled: boolean; hit: boolean; reason: string };
  skipImageBuild: boolean;
  buildDecision: "skipImageBuild" | "buildImage";
  reusableImageRef: string | null;
  reusableImageRefKnown: boolean;
  reason: string;
}

interface AgentRunIdentityComparison {
  configured: boolean;
  pathCount: number;
  hit: boolean;
  status: "not-configured" | "base-missing" | "hit" | "miss";
  currentMissingCount: number;
  previousMissingCount: number | null;
}

export function buildAgentRunReusePlan(follower: FollowerSpec, reuseConfig: RuntimeReuseConfig, sourceCommit: string, timeoutSeconds: number): AgentRunReusePlan {
  const stageRef = `${follower.source.snapshotPrefix.replace(/\/+$/u, "")}/${sourceCommit}`;
  const service = runtimeReuseService(reuseConfig, ["agentrun-mgr", "manager"]);
  const decisions = service === null ? [] : [agentRunReuseDecision(follower.nativeStatus.source.repoPath, stageRef, service, timeoutSeconds)];
  const summary = agentRunDecisionSummary(decisions);
  return {
    ok: reuseConfig.ok && decisions.length > 0,
    sourceCommit,
    stageRef,
    decisions,
    decisionSummary: summary,
    errors: service === null ? [`${RUNTIME_REUSE_CONFIG_PATH} must declare service agentrun-mgr|manager`] : reuseConfig.errors.slice(0, 5),
    valuesRedacted: true,
  };
}

export function compactAgentRunReusePlan(plan: AgentRunReusePlan): Record<string, unknown> {
  return {
    ok: plan.ok,
    sourceCommit: plan.sourceCommit,
    stageRef: plan.stageRef,
    decisionSummary: plan.decisionSummary,
    decisions: plan.decisions.slice(0, 8).map((decision) => ({
      serviceId: decision.serviceId,
      sourceIdentity: compactIdentity(decision.sourceIdentity),
      envIdentity: compactIdentity(decision.envIdentity),
      runtimeReuse: { enabled: decision.runtimeReuse.enabled, hit: decision.runtimeReuse.hit },
      envReuse: { enabled: decision.envReuse.enabled, hit: decision.envReuse.hit },
      skipImageBuild: decision.skipImageBuild,
      buildDecision: decision.buildDecision,
      reusableImageRef: decision.reusableImageRef,
      reusableImageRefKnown: decision.reusableImageRefKnown,
      reason: decision.reason,
    })),
    errors: plan.errors.slice(0, 5),
    valuesRedacted: true,
  };
}

export function agentRunManagerDecision(plan: AgentRunReusePlan): AgentRunReuseDecision | null {
  return plan.decisions.find((decision) => decision.serviceId === "agentrun-mgr" || decision.serviceId === "manager") ?? null;
}

export function applyAgentRunReuseConfig(spec: AgentRunLaneSpec, reuseConfig: RuntimeReuseConfig): AgentRunLaneSpec {
  const service = runtimeReuseService(reuseConfig, ["agentrun-mgr", "manager"]);
  const envReuse = service?.envReuse;
  if (envReuse === undefined || envReuse === null) return spec;
  if (envReuse.enabled === false) return spec;
  const imageBuild = spec.deployment.manager.imageBuild;
  return {
    ...spec,
    deployment: {
      ...spec.deployment,
      manager: {
        ...spec.deployment.manager,
        imageBuild: {
          ...imageBuild,
          buildArgs: Object.keys(envReuse.buildArgs).length === 0 ? imageBuild.buildArgs : envReuse.buildArgs,
          envIdentityFiles: envReuse.envIdentityFiles.length === 0 ? imageBuild.envIdentityFiles : envReuse.envIdentityFiles,
        },
      },
    },
  };
}

export function reusableAgentRunImageArtifact(spec: AgentRunLaneSpec, repoPath: string, sourceCommit: string, timeoutSeconds: number): { image: AgentRunArtifactService | null; evidence: Record<string, unknown> } {
  const artifactRef = `refs/heads/${spec.gitops.branch}:${spec.deployment.artifactCatalogPath}`;
  const result = runCommand(["git", "--git-dir", repoPath, "show", artifactRef], repoRoot, { timeoutMs: budgetMs(timeoutSeconds) });
  if (result.exitCode !== 0) {
    return {
      image: null,
      evidence: {
        ok: false,
        status: "missing",
        artifactRef,
        reason: shortText(result.stderr || result.stdout || "artifact catalog missing"),
        valuesRedacted: true,
      },
    };
  }
  const parsed = parseJsonRecord(result.stdout);
  const services = Array.isArray(parsed?.services) ? parsed.services : [];
  const service = services.find((item) => record(item).serviceId === "agentrun-mgr");
  const row = record(service);
  const digest = stringOrNull(row.digest) ?? stringOrNull(row.envDigest);
  const envIdentity = stringOrNull(row.envIdentity) ?? stringOrNull(row.imageTag);
  const image = stringOrNull(row.image) ?? stringOrNull(row.envImage);
  if (digest === null || envIdentity === null || image === null) {
    return {
      image: null,
      evidence: {
        ok: false,
        status: "invalid",
        artifactRef,
        reason: "artifact catalog lacks image/digest/envIdentity for agentrun-mgr",
        valuesRedacted: true,
      },
    };
  }
  const repositoryDigest = stringOrNull(row.repositoryDigest) ?? `${imageRepository(image)}@${digest}`;
  const artifact: AgentRunArtifactService = {
    serviceId: "agentrun-mgr",
    artifactKind: stringOrNull(row.artifactKind) ?? "env-reuse",
    status: "reused",
    image,
    digest,
    repositoryDigest,
    imageTag: envIdentity,
    envIdentity,
    envImage: stringOrNull(row.envImage) ?? image,
    envDigest: stringOrNull(row.envDigest) ?? digest,
    envRepositoryDigest: stringOrNull(row.envRepositoryDigest) ?? repositoryDigest,
    bootCommit: sourceCommit,
    bootScript: stringOrNull(row.bootScript) ?? "deploy/runtime/boot/agentrun-boot.sh",
    provenance: {
      sourceCommitId: sourceCommit,
      previousSourceCommitId: stringOrNull(parsed?.sourceCommitId),
      source: "unidesk-yaml-only-reuse",
      artifactRef,
      valuesPrinted: false,
    },
  };
  return {
    image: artifact,
    evidence: {
      ok: true,
      status: "reused",
      artifactRef,
      previousSourceCommitId: stringOrNull(parsed?.sourceCommitId),
      image,
      digest,
      envIdentity,
      valuesRedacted: true,
    },
  };
}

export function skippedAgentRunImageBuildResult(namespace: string, jobName: string, summary: Record<string, unknown>): NativeK8sJobResult {
  return {
    ok: true,
    completed: true,
    failed: false,
    timedOut: false,
    created: false,
    reused: true,
    jobName,
    namespace,
    polls: 0,
    elapsedMs: 0,
    logsTail: null,
    summary,
    conditionReason: "Skipped",
    conditionMessage: "reuse-plan skipImageBuild consumed by AgentRun trigger",
    statusAuthority: "kubernetes-api-serviceaccount",
    parsedDownstreamCliOutput: false,
  };
}

export function agentRunCiConsumptionEvidence(input: {
  sourceCommit: string;
  plan: AgentRunReusePlan;
  imageBuild: { jobName: string; result: NativeK8sJobResult; payload: Record<string, unknown> };
  gitopsPublish: { jobName: string; result: NativeK8sJobResult; payload: Record<string, unknown> };
}): Record<string, unknown> {
  const decision = agentRunManagerDecision(input.plan);
  const expectedSkip = input.plan.decisionSummary.skipImageBuildCount;
  const expectedBuild = input.plan.decisionSummary.buildImageCount;
  const skipped = decision?.skipImageBuild === true;
  const imageBuildConsumed = skipped
    ? input.imageBuild.result.reused === true && input.imageBuild.payload.status === "skipped"
    : input.imageBuild.result.completed === true && input.imageBuild.payload.status !== "skipped";
  return {
    ok: input.plan.ok && imageBuildConsumed && input.gitopsPublish.result.completed === true,
    sourceCommit: input.sourceCommit,
    source: "agentrun-native-trigger",
    expected: {
      skipImageBuildCount: expectedSkip,
      buildImageCount: expectedBuild,
      skipImageBuildServices: input.plan.decisionSummary.skipImageBuildServices,
      buildImageServices: input.plan.decisionSummary.buildImageServices,
    },
    observed: {
      imageBuildDecision: skipped ? "skipImageBuild" : "buildImage",
      imageBuildStatus: stringOrNull(input.imageBuild.payload.status),
      imageBuildJobName: input.imageBuild.jobName,
      imageBuildCreated: input.imageBuild.result.created,
      imageBuildReused: input.imageBuild.result.reused,
      gitopsPublishJobName: input.gitopsPublish.jobName,
      gitopsPublishCompleted: input.gitopsPublish.result.completed,
      buildServicesCount: skipped ? 0 : 1,
      reusedServicesCount: skipped ? 1 : 0,
      buildSkippedCount: skipped ? 1 : 0,
    },
    mismatches: imageBuildConsumed ? [] : [{ reason: "image-build-stage-did-not-consume-reuse-plan", serviceIds: ["agentrun-mgr"] }],
    valuesRedacted: true,
  };
}

export function compactAgentRunPayload(value: Record<string, unknown> | null): Record<string, unknown> | null {
  if (value === null) return null;
  return {
    configPath: stringOrNull(value.configPath),
    sourceCommit: stringOrNull(value.sourceCommit),
    reusePlan: compactRecord(value.reusePlan),
    ciConsumption: compactRecord(value.ciConsumption),
    gitMirrorSync: compactStageRecord(value.gitMirrorSync),
    imageBuild: compactStageRecord(value.imageBuild),
    gitopsPublish: compactStageRecord(value.gitopsPublish),
    gitMirrorFlush: compactStageRecord(value.gitMirrorFlush),
    valuesRedacted: true,
  };
}

function agentRunReuseDecision(repoPath: string, stageRef: string, service: NonNullable<ReturnType<typeof runtimeReuseService>>, timeoutSeconds: number): AgentRunReuseDecision {
  const baseRef = parentRef(repoPath, stageRef, timeoutSeconds);
  const runtime = service.runtimeReuse;
  const envReuse = service.envReuse;
  const codePaths = runtime?.codeIdentityPaths ?? [];
  const envPaths = uniqueStrings([...(runtime?.envIdentityPaths ?? []), ...(envReuse?.envIdentityFiles ?? [])]);
  const sourceIdentity = identityComparison(repoPath, stageRef, baseRef, codePaths, timeoutSeconds);
  const envIdentity = identityComparison(repoPath, stageRef, baseRef, envPaths, timeoutSeconds);
  const runtimeEnabled = runtime?.enabled !== false;
  const envEnabled = envReuse?.enabled !== false;
  const runtimeHit = runtimeEnabled && sourceIdentity.hit && envIdentity.hit;
  const envHit = envEnabled && envIdentity.hit;
  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: runtimeHit ? "runtime-reuse-hit" : envHit ? "env-reuse-hit" : "reuse-miss",
  };
}

function identityComparison(repoPath: string, currentRef: string, baseRef: string | null, paths: string[], timeoutSeconds: number): AgentRunIdentityComparison {
  const current = identityDigest(repoPath, currentRef, paths, timeoutSeconds);
  const previous = baseRef === null ? null : identityDigest(repoPath, baseRef, paths, timeoutSeconds);
  const configured = paths.length > 0;
  const hit = configured && previous !== null && current.sha256 !== null && previous.sha256 !== null && current.sha256 === previous.sha256;
  return {
    configured,
    pathCount: paths.length,
    hit,
    status: !configured ? "not-configured" : previous === null ? "base-missing" : hit ? "hit" : "miss",
    currentMissingCount: current.missingCount,
    previousMissingCount: previous?.missingCount ?? null,
  };
}

function identityDigest(repoPath: string, ref: string, paths: string[], timeoutSeconds: number): { sha256: string | null; missingCount: number } {
  if (paths.length === 0) return { sha256: null, missingCount: 0 };
  const hash = createHash("sha256");
  let missingCount = 0;
  for (const path of paths) {
    const result = runCommand(["git", "--git-dir", repoPath, "ls-tree", "-r", "-z", "--full-tree", ref, "--", path], repoRoot, { timeoutMs: budgetMs(timeoutSeconds) });
    const entries = result.exitCode === 0 ? result.stdout.split("\0").filter(Boolean).sort() : [];
    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 parentRef(repoPath: string, ref: string, timeoutSeconds: number): string | null {
  const result = runCommand(["git", "--git-dir", repoPath, "rev-parse", "--verify", `${ref}^`], repoRoot, { timeoutMs: budgetMs(timeoutSeconds) });
  const value = result.stdout.trim();
  return result.exitCode === 0 && /^[0-9a-f]{40}$/iu.test(value) ? value : null;
}

function agentRunDecisionSummary(decisions: AgentRunReuseDecision[]): AgentRunReusePlan["decisionSummary"] {
  const skip = decisions.filter((item) => item.skipImageBuild).map((item) => item.serviceId).sort();
  const build = decisions.filter((item) => !item.skipImageBuild).map((item) => item.serviceId).sort();
  return {
    serviceCount: decisions.length,
    skipImageBuildCount: skip.length,
    buildImageCount: build.length,
    skipImageBuildServices: skip,
    buildImageServices: build,
  };
}

function missReason(sourceIdentity: AgentRunIdentityComparison | null, envIdentity: AgentRunIdentityComparison, enabled: boolean): string {
  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 compactIdentity(value: AgentRunIdentityComparison): Record<string, unknown> {
  return {
    configured: value.configured,
    pathCount: value.pathCount,
    hit: value.hit,
    status: value.status,
    missing: {
      current: value.currentMissingCount,
      previous: value.previousMissingCount,
    },
  };
}

function compactStageRecord(value: unknown): Record<string, unknown> | null {
  const stage = record(value);
  if (Object.keys(stage).length === 0) return null;
  const result = record(stage.result);
  const payload = record(stage.payload);
  return {
    jobName: stringOrNull(stage.jobName),
    result: Object.keys(result).length === 0 ? null : {
      ok: result.ok === true,
      completed: result.completed === true,
      failed: result.failed === true,
      timedOut: result.timedOut === true,
      created: result.created === true,
      reused: result.reused === true,
      jobName: stringOrNull(result.jobName),
      namespace: stringOrNull(result.namespace),
      elapsedMs: typeof result.elapsedMs === "number" ? result.elapsedMs : null,
      conditionReason: stringOrNull(result.conditionReason),
      conditionMessage: stringOrNull(result.conditionMessage),
      statusAuthority: stringOrNull(result.statusAuthority),
      parsedDownstreamCliOutput: false,
    },
    payload: compactRecord(payload),
  };
}

function compactRecord(value: unknown): Record<string, unknown> | null {
  const row = record(value);
  return Object.keys(row).length === 0 ? null : JSON.parse(JSON.stringify(row)) as Record<string, unknown>;
}

function parseJsonRecord(text: string): Record<string, unknown> | null {
  try {
    const parsed = JSON.parse(text) as unknown;
    return record(parsed);
  } catch {
    return null;
  }
}

function record(value: unknown): Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}

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

function uniqueStrings(values: string[]): string[] {
  return [...new Set(values.filter((value) => value.length > 0))];
}

function imageRepository(image: string): string {
  const index = image.lastIndexOf(":");
  return index > 0 ? image.slice(0, index) : image;
}

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

function budgetMs(timeoutSeconds: number): number {
  return Math.max(1, timeoutSeconds) * 1000;
}
