import { execFileSync } from "node:child_process";
import { readdirSync, statSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT } from "../../internal/protocol/index.mjs";
import { buildDesiredStatePlan } from "./deploy-desired-state-plan.mjs";
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
import { readStructuredFile } from "./structured-config.mjs";

const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const cloudWebSourceRoot = "web/hwlab-cloud-web/src";

function bunExecutable() {
  return process.env.HWLAB_BUN_BINARY || process.env.BUN_BINARY || "bun";
}

function inspectCloudWebDistFreshness(webRoot) {
  try {
    const stdout = execFileSync(bunExecutable(), ["web/hwlab-cloud-web/scripts/dist-contract.ts", "--inspect", "--root", webRoot], {
      cwd: defaultRepoRoot,
      encoding: "utf8"
    });
    return JSON.parse(stdout);
  } catch (error) {
    return {
      status: "blocked",
      distPath: "web/hwlab-cloud-web/dist",
      buildCommand: "cd web/hwlab-cloud-web && bun run build",
      freshnessCommand: "cd web/hwlab-cloud-web && bun run build",
      files: [],
      mismatches: ["dist-contract-inspect"],
      error: error instanceof Error ? error.message : String(error)
    };
  }
}

async function readCloudWebAppSource(repoRoot) {
  const files = collectCloudWebSourceFiles(path.join(repoRoot, cloudWebSourceRoot));
  const parts = await Promise.all(files.map((file) => readFile(file, "utf8")));
  return parts.join("\n");
}

function collectCloudWebSourceFiles(dir) {
  const files = [];
  for (const entry of readdirSync(dir)) {
    const full = path.join(dir, entry);
    const stat = statSync(full);
    if (stat.isDirectory()) files.push(...collectCloudWebSourceFiles(full));
    else if (/\.(ts|tsx|css)$/u.test(full)) files.push(full);
  }
  return files.sort();
}

const defaultArtifactReport = tempReportPath("dev-artifacts.json");
const defaultArtifactCatalog = "deploy/artifact-catalog.dev.json";
const defaultRuntimeReport = tempReportPath("dev-cloud-workbench-live.json");
const defaultEdgeReport = tempReportPath("dev-edge-health.json");
const defaultOutputReport = tempReportPath("dev-artifact-runtime-readiness.json");
const defaultTargetRef = "origin/main";
const guardCommand = `node scripts/artifact-runtime-readiness-guard.mjs --target-ref ${defaultTargetRef} --check --no-report`;
const expectedBlockedGuardCommand = `${guardCommand} --expect-blocked`;
const digestPattern = /^sha256:[a-f0-9]{64}$/u;
const commitPattern = /^[a-f0-9]{7,40}$/u;
const minCodeAgentFrontendTimeoutMs = 35000;
const defaultCodeAgentBackendTimeoutMs = 1200000;
const minCodeAgentBackendTimeoutMs = 1200000;

export function normalizeCommit(value) {
  if (typeof value !== "string") return "unknown";
  const trimmed = value.trim().toLowerCase();
  return commitPattern.test(trimmed) ? trimmed : "unknown";
}

export function shortCommit(value) {
  const commit = normalizeCommit(value);
  return commit === "unknown" ? "unknown" : commit.slice(0, 7);
}

export function commitsMatch(left, right) {
  const a = normalizeCommit(left);
  const b = normalizeCommit(right);
  return a !== "unknown" && b !== "unknown" && (a === b || a.startsWith(b) || b.startsWith(a));
}

export function evaluateArtifactRuntimeReadiness({
  artifactReport,
  artifactCatalog,
  desiredStatePlan,
  runtimeReport,
  liveRuntime,
  targetRef,
  targetCommitId
}) {
  const target = {
    ref: targetRef,
    commitId: normalizeCommit(targetCommitId),
    shortCommitId: shortCommit(targetCommitId)
  };
  const artifact = summarizeArtifactReport(artifactReport);
  const catalog = summarizeArtifactCatalog(artifactCatalog);
  const desiredState = summarizeDesiredStatePlan(desiredStatePlan, target);
  const runtime = summarizeRuntimeSources(runtimeReport, liveRuntime);
  const apiRuntime = runtime.api.effective;
  const cloudWebRuntime = runtime.cloudWeb.effective;
  const checks = [];

  addCheck(checks, {
    id: "target-ref-resolved",
    pass: target.commitId !== "unknown",
    type: "contract_blocker",
    summary: `Target ${target.ref} resolves to ${target.shortCommitId}.`,
    blockedSummary: `Target ${target.ref} could not be resolved to a Git commit.`,
    nextTask: "Fetch origin/main and rerun this guard before preparing any DEV apply."
  });
  addCheck(checks, {
    id: "artifact-report-published",
    pass: artifact.publishVerified,
    type: "runtime_blocker",
    summary: `Artifact report status=${artifact.status}; required published=${artifact.publishedRequiredCount}/${artifact.requiredServiceCount}; sha256=${artifact.digestCounts.sha256}; invalid=${artifact.digestCounts.invalid}.`,
    nextTask: "Complete the DEV artifact publish workflow and keep real sha256 digests for every required service."
  });
  addCheck(checks, {
    id: "artifact-report-target-match",
    pass: commitsMatch(artifact.sourceCommitId, target.commitId),
    type: "observability_blocker",
    summary: `Artifact report source=${artifact.shortCommitId}; target=${target.shortCommitId}.`,
    nextTask: "Publish artifacts for latest origin/main or keep rollout readiness blocked."
  });
  addCheck(checks, {
    id: "artifact-catalog-published",
    pass: catalog.publishVerified,
    type: "runtime_blocker",
    summary: `Artifact catalog state=${catalog.artifactState}; ciPublished=${catalog.ciPublished}; registryVerified=${catalog.registryVerified}; sha256=${catalog.digestCounts.sha256}; not_published=${catalog.digestCounts.notPublished}; invalid=${catalog.digestCounts.invalid}.`,
    nextTask: "Refresh deploy/artifact-catalog.dev.json only from a successful DEV artifact publish report."
  });
  addCheck(checks, {
    id: "artifact-catalog-target-match",
    pass: commitsMatch(catalog.commitId, target.commitId),
    type: "observability_blocker",
    summary: `Artifact catalog commit=${catalog.shortCommitId}; target=${target.shortCommitId}.`,
    nextTask: "Do not refresh source desired-state. Let node Tekton promotion write artifact catalog identity to the configured GitOps branch, then verify Argo/runtime revision."
  });
  addCheck(checks, {
    id: "artifact-report-catalog-match",
    pass: commitsMatch(artifact.sourceCommitId, catalog.commitId),
    type: "observability_blocker",
    summary: `Artifact report source=${artifact.shortCommitId}; catalog=${catalog.shortCommitId}.`,
    nextTask: `Keep ${defaultArtifactReport} and deploy/artifact-catalog.dev.json aligned to the same artifact source.`
  });
  addCheck(checks, {
    id: "desired-state-internal-valid",
    pass: desiredState.status !== "blocked" && desiredState.status !== "missing",
    type: "contract_blocker",
    summary: `Desired-state planner status=${desiredState.status}; convergence=${desiredState.targetConvergence.state}.`,
    nextTask: "Repair deploy/deploy.yaml, deploy/artifact-catalog.dev.json, and deploy/k8s/base/workloads.yaml before any apply."
  });
  addCheck(checks, {
    id: "desired-state-target-match",
    pass: desiredState.matchesTarget,
    type: "observability_blocker",
    summary: `Desired-state deploy=${desiredState.deployCommitId}; target=${target.shortCommitId}; convergence=${desiredState.targetConvergence.state}; pending=${desiredState.targetConvergence.pendingTargetFields}.`,
    nextTask: `Run node scripts/deploy-desired-state-plan.mjs --promotion-commit ${target.shortCommitId} --check as a read-only review, then rely on node Tekton promotion for catalog writes.`
  });
  addCheck(checks, {
    id: "rollout-read-access",
    pass: runtime.live.mode === "live-read-only-http",
    type: "observability_blocker",
    summary: `Runtime rollout-read mode=${runtime.live.mode}.`,
    nextTask: "Run this guard with read-only DEV endpoint access; SOURCE/local/static checks cannot claim latest-main deployment."
  });
  addCheck(checks, {
    id: "api-runtime-identity-observed",
    pass: apiRuntime.observed,
    type: "observability_blocker",
    summary: `API /health/live identity status=${apiRuntime.status}; source=${apiRuntime.evidenceSource}; commit=${apiRuntime.shortCommitId}.`,
    nextTask: "Restore read-only access to Cloud API /health/live before treating DEV runtime identity as observed."
  });
  addCheck(checks, {
    id: "api-runtime-target-match",
    pass: apiRuntime.observed && commitsMatch(apiRuntime.commitId, target.commitId),
    type: "runtime_blocker",
    summary: `API /health/live commit=${apiRuntime.shortCommitId}; target=${target.shortCommitId}.`,
    nextTask: "After publish and GitOps promotion, verify Argo DEV rollout and re-observe /health/live."
  });
  addCheck(checks, {
    id: "api-runtime-artifact-match",
    pass: apiRuntime.observed && commitsMatch(apiRuntime.commitId, catalog.commitId) && commitsMatch(apiRuntime.commitId, artifact.sourceCommitId),
    type: "runtime_blocker",
    summary: `API /health/live commit=${apiRuntime.shortCommitId}; catalog=${catalog.shortCommitId}; artifact=${artifact.shortCommitId}.`,
    nextTask: "Do not treat a healthy API route as current unless runtime, artifact report, and catalog identity all match."
  });
  addCheck(checks, {
    id: "cloud-web-runtime-identity-observed",
    pass: cloudWebRuntime.observed,
    type: "observability_blocker",
    summary: `Cloud Web :16666 /health/live identity status=${cloudWebRuntime.status}; source=${cloudWebRuntime.evidenceSource}; commit=${cloudWebRuntime.shortCommitId}.`,
    nextTask: "Observe Cloud Web /health/live on :16666; HTML and static asset checks alone do not prove the deployed artifact revision."
  });
  addCheck(checks, {
    id: "cloud-web-runtime-target-match",
    pass: cloudWebRuntime.observed && commitsMatch(cloudWebRuntime.commitId, target.commitId),
    type: "runtime_blocker",
    summary: `Cloud Web :16666 revision=${cloudWebRuntime.shortCommitId}; target=${target.shortCommitId}.`,
    nextTask: "Roll out the latest Cloud Web artifact through the authorized DEV path, then re-check :16666 /health/live."
  });
  addCheck(checks, {
    id: "cloud-web-runtime-artifact-match",
    pass: cloudWebRuntime.observed && commitsMatch(cloudWebRuntime.commitId, catalog.commitId) && commitsMatch(cloudWebRuntime.commitId, artifact.sourceCommitId),
    type: "runtime_blocker",
    summary: `Cloud Web revision=${cloudWebRuntime.shortCommitId}; catalog=${catalog.shortCommitId}; artifact=${artifact.shortCommitId}.`,
    nextTask: "Block rollout claims until the Cloud Web served runtime identity matches the artifact catalog and publish report."
  });
  addCheck(checks, {
    id: "cloud-web-published-build-freshness",
    pass: artifact.cloudWeb.published && digestPattern.test(artifact.cloudWeb.digest) && artifact.cloudWeb.distFreshnessStatus === "pass",
    type: "runtime_blocker",
    summary: `Cloud Web artifact status=${artifact.cloudWeb.status}; imageTag=${artifact.cloudWeb.imageTag}; digest=${artifact.cloudWeb.digestStatus}; distFreshness=${artifact.cloudWeb.distFreshnessStatus}.`,
    nextTask: "Run the Cloud Web build before publish and keep dist freshness evidence in the artifact publish report."
  });
  addCheck(checks, {
    id: "source-local-static-not-authoritative",
    pass: true,
    type: "safety_blocker",
    summary: "SOURCE, LOCAL, STATIC, and report snapshots remain non-authoritative until runtime and artifact identity checks above pass.",
    nextTask: "Use this guard before apply and after any authorized rollout; never infer deployed latest-main from local source checks."
  });

  const blockers = checks.filter((item) => item.status === "blocked");
  return {
    target,
    artifact,
    catalog,
    desiredState,
    runtime,
    ready: blockers.length === 0,
    canInferFromSourceLocalStatic: false,
    checks,
    blockers
  };
}

export function buildArtifactRuntimeReadinessReport({
  artifactReport,
  artifactCatalog,
  desiredStatePlan,
  runtimeReport,
  edgeReport,
  liveRuntime,
  cloudWebDistFreshness,
  m3IoSourceReadiness,
  targetRef,
  targetCommitId,
  generatedFromCommit = "unknown",
  explicitLive = false,
  codeAgentTimeoutReadiness = null
}) {
  const readiness = evaluateArtifactRuntimeReadiness({
    artifactReport,
    artifactCatalog,
    desiredStatePlan,
    runtimeReport,
    liveRuntime,
    targetRef,
    targetCommitId
  });
  const status = readiness.ready ? "pass" : "blocked";
  const commanderDecision = buildCommanderReadinessSummary({
    readiness,
    runtimeReport,
    edgeReport,
    liveRuntime,
    cloudWebDistFreshness,
    m3IoSourceReadiness,
    targetRef,
    generatedFromCommit,
    explicitLive,
    codeAgentTimeoutReadiness
  });
  return {
    $schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
    $id: "https://hwlab.pikastech.local/dev-gate/dev-artifact-runtime-readiness.json",
    reportVersion: "v1",
    status,
    issue: "pikasTech/HWLAB#164",
    supports: [
      "pikasTech/HWLAB#7",
      "pikasTech/HWLAB#78",
      "pikasTech/HWLAB#131"
    ],
    taskId: "dev-artifact-runtime-readiness",
    commitId: shortCommit(generatedFromCommit),
    acceptanceLevel: "dev_artifact_runtime_readiness",
    devOnly: true,
    prodDisabled: true,
    generatedAt: new Date().toISOString(),
    evidenceLevel: explicitLive && status === "pass" ? "DEV-LIVE-RUNTIME-IDENTITY-NON-M3-M4-M5" : "BLOCKED",
    devLive: explicitLive && status === "pass",
    devLiveClaim: commanderDecision.devLiveClaim,
    reportLifecycle: activeReportLifecycle("Current latest-main DEV artifact/runtime readiness guard; SOURCE and static checks cannot imply deployed 16666 readiness."),
    sourceContract: {
      status: "pass",
      documents: [
        "docs/reference/node-gitops-cicd.md"
      ],
      summary: "Target commit, artifact publish/catalog identity, desired-state convergence, API runtime identity, and Cloud Web served runtime identity are separate gates."
    },
    validationCommands: [
      "node --check scripts/artifact-runtime-readiness-guard.mjs",
      "node --check scripts/src/artifact-runtime-readiness-guard.mjs",
      "node --test scripts/artifact-runtime-readiness-guard.test.mjs",
      guardCommand,
      expectedBlockedGuardCommand
    ],
    localSmoke: {
      status,
      commands: [expectedBlockedGuardCommand],
      evidence: [
        defaultArtifactReport,
        defaultArtifactCatalog,
        defaultRuntimeReport,
        `${DEV_FRONTEND_ENDPOINT}/health/live`,
        `${DEV_ENDPOINT}/health/live`
      ],
      summary: "The guard is read-only and blocks unless artifact, desired-state, API runtime, and Cloud Web runtime identities all align."
    },
    dryRun: {
      status: "not_applicable",
      commands: [guardCommand],
      evidence: ["No deploy, publish, apply, restart, k8s mutation, PROD access, or secret read is performed."],
      summary: "This is a readiness guard, not a deployment dry-run or rollout substitute."
    },
    devPreconditions: {
      status,
      requirements: [
        "Latest origin/main must resolve to a concrete target commit.",
        "DEV artifact publish report and artifact catalog must match the target and contain real registry digests.",
        "Desired-state deploy, catalog, workload image, and mirror env fields must converge to the target commit before apply.",
        "Cloud API /health/live must report the target runtime identity.",
        "Cloud Web :16666 /health/live must report the target served artifact identity.",
        "M3/M4/M5 acceptance remains out of scope for this guard."
      ],
      commands: [guardCommand],
      summary: readiness.ready
        ? "Latest-main artifact, desired-state, API runtime, and Cloud Web runtime identity are aligned."
        : "Latest-main DEV rollout readiness is blocked; see actionable blockers."
    },
    artifactRuntimeReadiness: {
      version: "v1",
      artifactReportPath: defaultArtifactReport,
      artifactCatalogPath: defaultArtifactCatalog,
      runtimeReportPath: defaultRuntimeReport,
      ...readiness
    },
    commanderDecision,
    blockedReasons: readiness.blockers.map((item) => ({
      scope: item.id,
      type: item.type,
      summary: item.summary,
      nextTask: item.nextTask
    })),
    blockers: readiness.blockers.map((item) => ({
      type: item.type,
      scope: item.id,
      status: "open",
      summary: item.summary,
      nextTask: item.nextTask
    })),
    safety: {
      readOnly: true,
      sourceOnlyWhenBlocked: true,
      publicHttpGetOnly: readiness.runtime.live.mode === "live-read-only-http",
      kubectlApply: false,
      k8sMutation: false,
      registryPush: false,
      imageBuild: false,
      serviceRestart: false,
      prodTouched: false,
      secretsRead: false,
      canInferReadinessFromSourceLocalStatic: false,
      statement: "No SOURCE, LOCAL, STATIC, or report-only check may claim deployed latest-main without matching artifact, desired-state, API runtime, and Cloud Web runtime identity."
    }
  };
}

export async function observeLiveRuntimeIdentities({ live = true, timeoutMs = 5000 } = {}) {
  const endpoints = {
    api: new URL("/health/live", DEV_ENDPOINT).toString(),
    cloudWeb: new URL("/health/live", DEV_FRONTEND_ENDPOINT).toString()
  };
  if (!live) {
    return {
      mode: "disabled",
      endpoints,
      api: notObservedRuntimeIdentity(endpoints.api, "live observation disabled by --no-live", "live-http"),
      cloudWeb: notObservedRuntimeIdentity(endpoints.cloudWeb, "live observation disabled by --no-live", "live-http")
    };
  }
  const [api, cloudWeb] = await Promise.all([
    observeHealthLive(endpoints.api, { evidenceSource: "live-http", timeoutMs }),
    observeHealthLive(endpoints.cloudWeb, { evidenceSource: "live-http", timeoutMs })
  ]);
  return {
    mode: "live-read-only-http",
    endpoints,
    api,
    cloudWeb
  };
}

export function buildCommanderReadinessSummary({
  readiness,
  runtimeReport = null,
  edgeReport = null,
  liveRuntime = null,
  cloudWebDistFreshness = null,
  m3IoSourceReadiness = null,
  targetRef = defaultTargetRef,
  generatedFromCommit = "unknown",
  explicitLive = false,
  codeAgentTimeoutReadiness = null
}) {
  const sourceCommit = normalizeCommit(generatedFromCommit);
  const deployDesiredState = readiness.desiredState;
  const artifactCatalog = readiness.catalog;
  const cloudWebBuild = summarizeCloudWebBuildFreshness(cloudWebDistFreshness, readiness.artifact.cloudWeb);
  const codeAgentTimeout = codeAgentTimeoutReadiness ?? {
    status: "blocked",
    frontendTimeoutMs: null,
    backendTimeoutMs: defaultCodeAgentBackendTimeoutMs,
    requiredFrontendTimeoutMs: minCodeAgentFrontendTimeoutMs,
    requiredBackendTimeoutMs: minCodeAgentBackendTimeoutMs,
    transportWaitsForBackend: false,
    summary: "Code Agent timeout readiness was not inspected."
  };
  const runtimeDurable = classifyRuntimeDurableBlocker(runtimeReport, readiness.runtime);
  const codeAgentProvider = summarizeCodeAgentProvider(edgeReport, runtimeReport);
  const m3Source = summarizeM3IoSourceReadiness(m3IoSourceReadiness);
  const publishBlockerIds = new Set(["target-ref-resolved", "artifact-report-published"]);
  const applyBlockerIds = new Set([
    "target-ref-resolved",
    "artifact-report-published",
    "artifact-report-target-match",
    "artifact-catalog-published",
    "artifact-catalog-target-match",
    "artifact-report-catalog-match",
    "desired-state-internal-valid",
    "desired-state-target-match",
    "cloud-web-published-build-freshness"
  ]);
  const liveRuntimeBlockerIds = new Set([
    "rollout-read-access",
    "api-runtime-identity-observed",
    "api-runtime-target-match",
    "api-runtime-artifact-match",
    "cloud-web-runtime-identity-observed",
    "cloud-web-runtime-target-match",
    "cloud-web-runtime-artifact-match"
  ]);
  if (explicitLive) {
    for (const id of liveRuntimeBlockerIds) applyBlockerIds.add(id);
  }
  const canPublish = readiness.blockers.every((blocker) => !publishBlockerIds.has(blocker.id)) &&
    cloudWebBuild.sourceDistStatus === "pass" &&
    codeAgentTimeout.status === "pass" &&
    m3Source.status !== "blocked";
  const canApply = canPublish &&
    runtimeDurable.status === "pass" &&
    codeAgentProvider.status !== "blocked" &&
    readiness.blockers.every((blocker) => !applyBlockerIds.has(blocker.id));
  const blockedReasons = [
    ...compactReasons(readiness.blockers, { canPublish, canApply, explicitLive, liveRuntimeBlockerIds }),
    ...extraBlockedReasons({
      canPublish,
      cloudWebBuild,
      codeAgentTimeout,
      codeAgentProvider,
      m3Source,
      runtimeDurable
    })
  ];

  return {
    kind: "hwlab-dev-rollout-readiness",
    status: blockedReasons.length === 0 ? "pass" : "blocked",
    targetRef,
    currentSourceCommit: {
      commitId: sourceCommit,
      shortCommitId: shortCommit(sourceCommit)
    },
    deployDesiredStateCommit: {
      commitId: deployDesiredState.fullDeployCommitId ?? "unknown",
      shortCommitId: deployDesiredState.deployCommitId,
      convergence: deployDesiredState.targetConvergence.state,
      pendingTargetFields: deployDesiredState.targetConvergence.pendingTargetFields
    },
    artifactCatalogState: {
      commitId: artifactCatalog.commitId,
      shortCommitId: artifactCatalog.shortCommitId,
      state: artifactCatalog.artifactState,
      ciPublished: artifactCatalog.ciPublished,
      registryVerified: artifactCatalog.registryVerified,
      publishVerified: artifactCatalog.publishVerified,
      requiredPublished: `${artifactCatalog.publishedRequiredCount}/${artifactCatalog.requiredServiceCount}`,
      digestCounts: artifactCatalog.digestCounts
    },
    cloudWebBuildFreshness: cloudWebBuild,
    codeAgentTimeoutReadiness: codeAgentTimeout,
    codeAgentProviderReadiness: codeAgentProvider,
    m3IoSourceReadiness: m3Source,
    runtimeDurableBlocker: runtimeDurable,
    runtimeIdentity: {
      liveChecksRan: explicitLive,
      liveMode: readiness.runtime.live.mode,
      apiCommit: readiness.runtime.api.effective.shortCommitId,
      cloudWebCommit: readiness.runtime.cloudWeb.effective.shortCommitId
    },
    canPublish,
    canApply,
    devLiveClaim: explicitLive && canApply && readiness.ready,
    blockedReasons: dedupeReasons(blockedReasons),
    nextCommands: nextCommands({ canPublish, canApply, blockedReasons }),
    safety: {
      readOnly: true,
      liveChecksRan: explicitLive,
      publishApplyRestart: false,
      registryPush: false,
      secretsRead: false,
      prodTouched: false
    }
  };
}

export async function runArtifactRuntimeReadinessGuardCli(argv = process.argv.slice(2), options = {}) {
  const repoRoot = options.repoRoot ?? defaultRepoRoot;
  const args = parseArgs(argv);
  if (args.help) {
    process.stdout.write(`${usage()}\n`);
    return 0;
  }

  const [artifactReport, artifactCatalog, runtimeReport, edgeReport, cloudWebDistFreshness, codeAgentTimeoutReadiness, m3IoSourceReadiness, liveRuntime] = await Promise.all([
    readOptionalJson(repoRoot, args.artifactReport),
    readOptionalJson(repoRoot, args.artifactCatalog),
    readOptionalJson(repoRoot, args.runtimeReport),
    readOptionalJson(repoRoot, args.edgeReport),
    inspectCloudWebDistFreshness(path.join(repoRoot, "web/hwlab-cloud-web")),
    inspectCodeAgentTimeoutReadiness(repoRoot),
    inspectM3IoSourceReadiness(repoRoot),
    observeLiveRuntimeIdentities({ live: args.live, timeoutMs: args.timeoutMs })
  ]);
  const targetCommitId = resolveTargetCommit(repoRoot, args.targetRef);
  const desiredStatePlan = await buildDesiredStatePlanOrBlocked(repoRoot, args.targetRef);
  const report = buildArtifactRuntimeReadinessReport({
    artifactReport,
    artifactCatalog,
    desiredStatePlan,
    runtimeReport,
    edgeReport,
    liveRuntime,
    cloudWebDistFreshness,
    m3IoSourceReadiness,
    targetRef: args.targetRef,
    targetCommitId,
    generatedFromCommit: resolveTargetCommit(repoRoot, "HEAD"),
    explicitLive: args.live,
    codeAgentTimeoutReadiness
  });

  if (args.writeReport) {
    const outputPath = ensureNotRepoReportsPath(repoRoot, args.report, "--report");
    await mkdir(path.dirname(outputPath), { recursive: true });
    await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
  }

  const output = args.check ? summaryForCheck(report) : report;
  process.stdout.write(`${JSON.stringify(output, null, args.pretty || args.check ? 2 : 0)}\n`);

  if (args.expectBlocked) {
    return (report.status === "blocked" || report.commanderDecision.canApply === false) ? 0 : 2;
  }
  if (args.check && !report.commanderDecision.canApply) {
    return 2;
  }
  return 0;
}

export function formatArtifactRuntimeReadinessFailure(error) {
  return {
    kind: "hwlab-artifact-runtime-readiness",
    status: "error",
    error: error instanceof Error ? error.message : String(error),
    safety: {
      readOnly: true,
      kubectlApply: false,
      k8sMutation: false,
      registryPush: false,
      imageBuild: false,
      serviceRestart: false,
      prodTouched: false,
      secretsRead: false
    }
  };
}

async function buildDesiredStatePlanOrBlocked(repoRoot, targetRef) {
  try {
    return await buildDesiredStatePlan({ repoRoot, targetRef });
  } catch (error) {
    return {
      kind: "hwlab-deploy-desired-state-plan",
      status: "blocked",
      summary: {
        desiredCommitId: "unknown",
        diagnostics: 1,
        blockers: 1
      },
      target: {
        targetRef,
        convergence: {
          state: "planner_error",
          comparableFields: 0,
          matchingTargetFields: 0,
          pendingTargetFields: 0
        }
      },
      diagnostics: [{
        severity: "blocker",
        code: "planner_error",
        message: error instanceof Error ? error.message : String(error)
      }]
    };
  }
}

function summarizeArtifactReport(report) {
  const publish = report?.artifactPublish;
  if (!publish || typeof publish !== "object") {
    return missingArtifactReport();
  }
  const services = Array.isArray(publish.services) ? publish.services : [];
  const required = services.filter((service) => service?.artifactRequired === true);
  const cloudWeb = services.find((service) => service?.serviceId === "hwlab-cloud-web") ?? null;
  const digestCounts = countDigests(required);
  const publishedRequiredCount = required.filter((service) => service?.status === "published").length;
  const requiredServiceCount = Number.isInteger(publish.requiredServiceCount)
    ? publish.requiredServiceCount
    : required.length;
  const allRequiredServicesPublished = requiredServiceCount > 0 &&
    publishedRequiredCount === requiredServiceCount &&
    digestCounts.sha256 === requiredServiceCount;
  return {
    status: sanitizeString(publish.status),
    sourceCommitId: normalizeCommit(publish.sourceCommitId ?? report.commitId),
    shortCommitId: shortCommit(publish.sourceCommitId ?? report.commitId),
    registryPrefix: sanitizeString(publish.registryPrefix),
    serviceCount: Number.isInteger(publish.serviceCount) ? publish.serviceCount : services.length,
    requiredServiceCount,
    publishedRequiredCount,
    allRequiredServicesPublished,
    publishVerified: publish.status === "published" && allRequiredServicesPublished,
    digestCounts,
    cloudWeb: summarizeArtifactService(cloudWeb)
  };
}

function summarizeArtifactCatalog(catalog) {
  if (!catalog || typeof catalog !== "object") {
    return missingArtifactCatalog();
  }
  const services = Array.isArray(catalog.services) ? catalog.services : [];
  const required = services.filter((service) => service?.artifactRequired === true);
  const digestCounts = countDigests(required);
  const publishedRequiredCount = required.filter((service) => service?.publishState === "published").length;
  const allRequiredServicesPublished = required.length > 0 &&
    publishedRequiredCount === required.length &&
    digestCounts.sha256 === required.length;
  return {
    artifactState: sanitizeString(catalog.artifactState),
    commitId: normalizeCommit(catalog.commitId),
    shortCommitId: shortCommit(catalog.commitId),
    ciPublished: catalog.publish?.ciPublished === true,
    registryVerified: catalog.publish?.registryVerified === true,
    serviceCount: services.length,
    requiredServiceCount: required.length,
    publishedRequiredCount,
    digestCounts,
    publishVerified: catalog.artifactState === "published" &&
      catalog.publish?.ciPublished === true &&
      catalog.publish?.registryVerified === true &&
      allRequiredServicesPublished
  };
}

function summarizeDesiredStatePlan(plan, target) {
  if (!plan || typeof plan !== "object") {
    return {
      status: "missing",
      deployCommitId: "unknown",
      shortCommitId: "unknown",
      targetConvergence: emptyConvergence("missing"),
      matchesTarget: false
    };
  }
  const deployCommitId = normalizeCommit(plan.summary?.desiredCommitId);
  const targetConvergence = plan.target?.convergence ?? emptyConvergence("not_requested");
  const pendingTargetFields = Number.isInteger(targetConvergence.pendingTargetFields)
    ? targetConvergence.pendingTargetFields
    : 0;
  return {
    status: sanitizeString(plan.status),
    deployCommitId: shortCommit(deployCommitId),
    fullDeployCommitId: deployCommitId,
    targetConvergence: {
      state: sanitizeString(targetConvergence.state),
      comparableFields: Number.isInteger(targetConvergence.comparableFields) ? targetConvergence.comparableFields : 0,
      matchingTargetFields: Number.isInteger(targetConvergence.matchingTargetFields) ? targetConvergence.matchingTargetFields : 0,
      pendingTargetFields
    },
    matchesTarget: commitsMatch(deployCommitId, target.commitId) && pendingTargetFields === 0
  };
}

function summarizeRuntimeSources(runtimeReport, liveRuntime) {
  const reportApi = normalizeRuntimeIdentity(runtimeReport?.runtimeIdentity, "report-snapshot");
  const live = liveRuntime ?? {
    mode: "missing",
    endpoints: {},
    api: null,
    cloudWeb: null
  };
  return {
    live: {
      mode: sanitizeString(live.mode),
      endpoints: live.endpoints ?? {}
    },
    reportSnapshot: {
      api: reportApi,
      status: sanitizeString(runtimeReport?.status)
    },
    api: {
      live: normalizeRuntimeIdentity(live.api, "live-http"),
      reportSnapshot: reportApi,
      effective: normalizeRuntimeIdentity(live.api, "live-http")
    },
    cloudWeb: {
      live: normalizeRuntimeIdentity(live.cloudWeb, "live-http"),
      reportSnapshot: normalizeRuntimeIdentity(runtimeReport?.cloudWebRuntimeIdentity, "report-snapshot"),
      effective: normalizeRuntimeIdentity(live.cloudWeb, "live-http")
    }
  };
}

function summarizeArtifactService(service) {
  if (!service) {
    return {
      status: "missing",
      published: false,
      imageTag: "unknown",
      digest: "unknown",
      digestStatus: "missing",
      distFreshnessStatus: "missing"
    };
  }
  const digest = sanitizeString(service.digest);
  return {
    status: sanitizeString(service.status),
    published: service.status === "published",
    imageTag: sanitizeString(service.imageTag),
    digest,
    digestStatus: digestPattern.test(digest) ? "sha256" : digest === "not_published" ? "not_published" : "invalid",
    distFreshnessStatus: service.distFreshness?.status ? sanitizeString(service.distFreshness.status) : "missing"
  };
}

function countDigests(services) {
  const counts = {
    sha256: 0,
    notPublished: 0,
    invalid: 0
  };
  for (const service of services) {
    const digest = service?.digest;
    if (digestPattern.test(String(digest ?? ""))) {
      counts.sha256 += 1;
    } else if (digest === "not_published" || digest == null) {
      counts.notPublished += 1;
    } else {
      counts.invalid += 1;
    }
  }
  return counts;
}

async function observeHealthLive(endpoint, { evidenceSource, timeoutMs }) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetch(endpoint, {
      method: "GET",
      headers: { accept: "application/json" },
      signal: controller.signal
    });
    const text = await response.text();
    const body = parseJson(text);
    if (!response.ok || !body) {
      return notObservedRuntimeIdentity(
        endpoint,
        body ? `HTTP ${response.status}` : `HTTP ${response.status}; response was not JSON`,
        evidenceSource,
        response.status
      );
    }
    return observedRuntimeIdentity(endpoint, body, evidenceSource, response.status);
  } catch (error) {
    return notObservedRuntimeIdentity(
      endpoint,
      error instanceof Error ? error.message : String(error),
      evidenceSource
    );
  } finally {
    clearTimeout(timeout);
  }
}

function observedRuntimeIdentity(endpoint, body, evidenceSource, httpStatus) {
  const commitId = body.commit?.id ?? body.commitId ?? body.revision ?? body.runtime?.commitId;
  const imageTag = body.image?.tag ?? body.imageTag ?? body.runtime?.imageTag;
  const imageDigest = body.image?.digest ?? body.imageDigest ?? body.runtime?.imageDigest;
  return {
    status: "observed",
    observed: true,
    source: "health-live",
    evidenceSource,
    endpoint,
    serviceId: sanitizeString(body.serviceId),
    environment: sanitizeString(body.environment),
    healthStatus: sanitizeString(body.status),
    artifactKind: sanitizeString(body.artifactKind),
    commitId: normalizeCommit(commitId),
    shortCommitId: shortCommit(commitId),
    commitSource: sanitizeString(body.commit?.source ?? body.commitSource ?? (body.revision ? "runtime-env" : "unknown")),
    imageTag: sanitizeString(imageTag),
    imageDigest: sanitizeString(imageDigest),
    observedAt: sanitizeTimestamp(body.observedAt),
    httpStatus,
    reason: null
  };
}

function notObservedRuntimeIdentity(endpoint, reason, evidenceSource, httpStatus = null) {
  return {
    status: "not_observed",
    observed: false,
    source: "health-live",
    evidenceSource,
    endpoint,
    serviceId: "not_observed",
    environment: "not_observed",
    healthStatus: "not_observed",
    artifactKind: "not_observed",
    commitId: "unknown",
    shortCommitId: "unknown",
    commitSource: "not_observed",
    imageTag: "not_observed",
    imageDigest: "not_observed",
    observedAt: new Date().toISOString(),
    httpStatus,
    reason: oneLine(reason)
  };
}

function normalizeRuntimeIdentity(identity, evidenceSource) {
  if (!identity || identity.status !== "observed") {
    return notObservedRuntimeIdentity(identity?.endpoint ?? "unknown", identity?.reason ?? "runtime identity was not observed", evidenceSource);
  }
  return {
    ...identity,
    observed: true,
    evidenceSource: identity.evidenceSource ?? evidenceSource,
    commitId: normalizeCommit(identity.commitId ?? identity.revision),
    shortCommitId: shortCommit(identity.commitId ?? identity.revision),
    serviceId: sanitizeString(identity.serviceId),
    environment: sanitizeString(identity.environment),
    healthStatus: sanitizeString(identity.healthStatus ?? identity.status)
  };
}

function missingArtifactReport() {
  return {
    status: "missing",
    sourceCommitId: "unknown",
    shortCommitId: "unknown",
    registryPrefix: "unknown",
    serviceCount: 0,
    requiredServiceCount: 0,
    publishedRequiredCount: 0,
    allRequiredServicesPublished: false,
    publishVerified: false,
    digestCounts: { sha256: 0, notPublished: 0, invalid: 0 },
    cloudWeb: summarizeArtifactService(null)
  };
}

function missingArtifactCatalog() {
  return {
    artifactState: "missing",
    commitId: "unknown",
    shortCommitId: "unknown",
    ciPublished: false,
    registryVerified: false,
    serviceCount: 0,
    requiredServiceCount: 0,
    publishedRequiredCount: 0,
    digestCounts: { sha256: 0, notPublished: 0, invalid: 0 },
    publishVerified: false
  };
}

function emptyConvergence(state) {
  return {
    state,
    comparableFields: 0,
    matchingTargetFields: 0,
    pendingTargetFields: 0
  };
}

function addCheck(checks, { id, pass, type, summary, blockedSummary = null, nextTask }) {
  checks.push({
    id,
    status: pass ? "pass" : "blocked",
    type,
    summary: pass ? summary : (blockedSummary ?? summary),
    nextTask
  });
}

function summarizeCloudWebBuildFreshness(distFreshness, artifactCloudWeb) {
  const sourceStatus = distFreshness?.status === "pass" ? "pass" : "blocked";
  const artifactStatus = artifactCloudWeb?.distFreshnessStatus ?? "missing";
  const status = sourceStatus === "pass" && (artifactStatus === "pass" || artifactCloudWeb?.published !== true)
    ? "pass"
    : "blocked";
  return {
    status,
    sourceDistStatus: sourceStatus,
    artifactDistFreshnessStatus: artifactStatus,
    mismatches: Array.isArray(distFreshness?.mismatches) ? distFreshness.mismatches.slice(0, 8) : [],
    buildCommand: "bun run --cwd web/hwlab-cloud-web build",
    checkCommand: "bun run --cwd web/hwlab-cloud-web check",
    summary: sourceStatus === "pass"
      ? "Cloud Web source dist freshness is ready for publish review."
      : "Cloud Web dist is stale or missing for source or published artifact evidence."
  };
}

async function inspectCodeAgentTimeoutReadiness(repoRoot) {
  const [appSource, cloudApiEntrySource] = await Promise.all([
    readCloudWebAppSource(repoRoot),
    readFile(path.join(repoRoot, "cmd/hwlab-cloud-api/main.ts"), "utf8")
  ]);
  const frontendTimeoutMs = numericSourceConstant(appSource, "DEFAULT_CODE_AGENT_TIMEOUT_MS");
  const backendTimeoutMs = parseTimeoutFallbackMs(cloudApiEntrySource, "HWLAB_CODE_AGENT_TIMEOUT_MS");
  const frontendReady = Number.isInteger(frontendTimeoutMs) && frontendTimeoutMs >= minCodeAgentFrontendTimeoutMs;
  const backendReady = Number.isInteger(backendTimeoutMs) && backendTimeoutMs >= minCodeAgentBackendTimeoutMs;
  const transportWaitsForBackend = Number.isInteger(frontendTimeoutMs) && Number.isInteger(backendTimeoutMs) && frontendTimeoutMs > backendTimeoutMs;
  return {
    status: frontendReady && backendReady && transportWaitsForBackend ? "pass" : "blocked",
    frontendTimeoutMs,
    backendTimeoutMs,
    requiredFrontendTimeoutMs: minCodeAgentFrontendTimeoutMs,
    requiredBackendTimeoutMs: minCodeAgentBackendTimeoutMs,
    transportWaitsForBackend,
    summary: frontendReady && backendReady && transportWaitsForBackend
      ? "Code Agent browser/backend timeout budget is ready: browser transport waits for the structured backend result."
      : "Code Agent timeout layering is unsafe; long requests can be misclassified before cloud-api returns a structured result."
  };
}

function numericSourceConstant(source, name) {
  const match = source.match(new RegExp(`const\\s+${name}\\s*=\\s*([0-9_]+)\\s*;`, "u"));
  if (!match) return null;
  return Number.parseInt(match[1].replace(/_/gu, ""), 10);
}

function parseTimeoutFallbackMs(source, envName) {
  const escaped = envName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
  const match = source.match(new RegExp(`parseTimeout\\(process\\.env\\.${escaped},\\s*([0-9_]+)`, "u"));
  if (!match) return null;
  return Number.parseInt(match[1].replace(/_/gu, ""), 10);
}

async function inspectM3IoSourceReadiness(repoRoot) {
  const files = await listTrackedFiles(repoRoot);
  const candidateFiles = files.filter((file) =>
    /\.(?:ts|mjs|js|json|md|html|css|yaml|yml)$/u.test(file) &&
    file !== "scripts/src/artifact-runtime-readiness-guard.mjs" &&
    file !== "scripts/artifact-runtime-readiness-guard.test.mjs"
  );
  const matches = [];
  const contractFiles = [];
  for (const relativePath of candidateFiles) {
    const text = await readFile(path.join(repoRoot, relativePath), "utf8").catch(() => "");
    if (/\/v1\/m3\/io/u.test(text)) {
      matches.push(relativePath);
    }
    if (/m3-io-control-v1|M3_IO_CONTROL_ROUTE|same-origin \/v1\/m3\/io/u.test(text)) {
      contractFiles.push(relativePath);
    }
  }
  const present = matches.length > 0;
  const sourceReady = contractFiles.length > 0 &&
    matches.some((file) => file.startsWith(`${cloudWebSourceRoot}/`)) &&
    matches.some((file) => file === "internal/cloud/m3-io-control.ts" || file === "scripts/src/m3-io-control-e2e.mjs");
  return {
    status: present ? (sourceReady ? "pass" : "blocked") : "absent",
    present,
    paths: matches.slice(0, 12),
    contractPaths: contractFiles.slice(0, 12),
    requiredForApply: false,
    summary: present
      ? sourceReady
        ? "M3 /v1/m3/io source surface is present with same-origin web/API contract evidence."
        : "M3 /v1/m3/io source surface is present but missing expected web/API source contract evidence."
      : "M3 /v1/m3/io source surface is not present; no M3 IO source readiness gate is applied."
  };
}

async function listTrackedFiles(repoRoot) {
  try {
    const output = execFileSync("git", ["ls-files"], {
      cwd: repoRoot,
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
      timeout: 5000
    });
    return output.split(/\r?\n/u).filter(Boolean);
  } catch {
    return [];
  }
}

function summarizeM3IoSourceReadiness(readiness) {
  if (!readiness) {
    return {
      status: "absent",
      present: false,
      requiredForApply: false,
      summary: "M3 /v1/m3/io source surface was not inspected."
    };
  }
  if (readiness.status === "present") {
    return { ...readiness, status: "pass" };
  }
  return readiness;
}

function summarizeCodeAgentProvider(edgeReport, runtimeReport) {
  const blockers = [
    ...(edgeReport?.blockers ?? []),
    ...(runtimeReport?.blockers ?? [])
  ];
  const providerBlocker = blockers.find((blocker) =>
    String(blocker.scope ?? "").includes("code-agent") ||
    /Code Agent|provider Secret|OPENAI_API_KEY|provider_unavailable/iu.test(`${blocker.summary ?? ""} ${blocker.classification ?? ""}`)
  );
  if (providerBlocker) {
    return {
      status: "blocked",
      blocker: sanitizeString(providerBlocker.scope),
      classification: sanitizeString(providerBlocker.classification),
      summary: oneLine(providerBlocker.summary),
      separateFromDbLive: true
    };
  }
  const liveJourney = (runtimeReport?.checks ?? []).find((check) => check.id === "live-code-agent-browser-journey");
  return {
    status: liveJourney?.status === "pass" ? "pass" : "not_observed",
    blocker: null,
    classification: liveJourney?.status === "pass" ? "completed-browser-journey" : "not_observed",
    summary: liveJourney?.summary ?? "Code Agent provider readiness was not observed by this dry-run/source guard.",
    separateFromDbLive: true
  };
}

function classifyRuntimeDurableBlocker(runtimeReport, runtimeSources) {
  const identity = runtimeReport?.runtimeIdentity ?? runtimeSources?.api?.reportSnapshot ?? runtimeSources?.api?.effective ?? null;
  const runtime = identity?.runtime ?? {};
  const readiness = identity?.readiness ?? {};
  const durability = readiness?.durability ?? {};
  const blockerCodes = Array.isArray(identity?.blockerCodes) ? identity.blockerCodes : [];
  const durableBlocked = runtime?.blocker === "runtime_durable_adapter_query_blocked" ||
    durability?.blocker === "runtime_durable_adapter_query_blocked" ||
    blockerCodes.includes("runtime_durable_adapter_query_blocked") ||
    durability?.blockedLayer === "durability_query" ||
    runtime?.connection?.queryResult === "query_blocked" ||
    durability?.queryResult === "query_blocked";
  const ready = identity?.ready === true &&
    runtime?.durable === true &&
    (runtime?.ready === true || readiness?.ready === true) &&
    durability?.ready === true &&
    !durableBlocked;
  return {
    status: ready ? "pass" : "blocked",
    blocker: ready ? null : sanitizeString(durability?.blocker ?? runtime?.blocker ?? blockerCodes[0]),
    blockedLayer: sanitizeString(durability?.blockedLayer),
    queryResult: sanitizeString(durability?.queryResult ?? runtime?.connection?.queryResult),
    runtimeDurable: runtime?.durable === true,
    runtimeReady: runtime?.ready === true,
    durabilityReady: durability?.ready === true,
    classification: durableBlocked ? "runtime_durable_adapter_query_blocked" : ready ? "durable-ready" : "runtime-durability-not-ready",
    summary: ready
      ? "Runtime durable adapter is ready."
      : "Runtime durable adapter is not ready; durable read/query blockers stay separate from source/apply readiness."
  };
}

function compactReasons(blockers, { canPublish, canApply, explicitLive, liveRuntimeBlockerIds }) {
  return blockers
    .filter((blocker) => explicitLive || !liveRuntimeBlockerIds.has(blocker.id))
    .map((blocker) => ({
    id: blocker.id,
    type: blocker.type,
    class: reasonClassForBlocker(blocker.id, { canPublish, canApply }),
    summary: oneLine(blocker.summary),
    next: oneLine(blocker.nextTask)
  }));
}

function extraBlockedReasons({
  canPublish,
  cloudWebBuild,
  codeAgentTimeout,
  codeAgentProvider,
  m3Source,
  runtimeDurable
}) {
  const reasons = [];
  if (cloudWebBuild.sourceDistStatus !== "pass") {
    reasons.push({
      id: "cloud-web-build-freshness",
      type: "runtime_blocker",
      class: "publish",
      summary: cloudWebBuild.summary,
      next: cloudWebBuild.buildCommand
    });
  }
  if (cloudWebBuild.artifactDistFreshnessStatus !== "pass" && canPublish) {
    reasons.push({
      id: "cloud-web-artifact-build-freshness",
      type: "runtime_blocker",
      class: "apply",
      summary: "Cloud Web published artifact is missing fresh dist evidence.",
      next: "Publish a Cloud Web artifact that records artifactPublish.services[].distFreshness.status=pass."
    });
  }
  if (codeAgentTimeout.status !== "pass") {
    reasons.push({
      id: "code-agent-timeout-readiness",
      type: "agent_blocker",
      class: "runtime",
      summary: codeAgentTimeout.summary,
      next: "Increase the Cloud Web Code Agent request timeout and rerun web/check plus focused readiness tests."
    });
  }
  if (codeAgentProvider.status === "blocked") {
    reasons.push({
      id: `code-agent-provider:${codeAgentProvider.blocker}`,
      type: "agent_blocker",
      class: "runtime",
      summary: codeAgentProvider.summary,
      next: "Restore provider Secret/key-presence and DEV egress readiness without printing secret values."
    });
  }
  if (runtimeDurable.status !== "pass") {
    reasons.push({
      id: `runtime-durable:${runtimeDurable.classification}`,
      type: "runtime_blocker",
      class: "runtime",
      summary: runtimeDurable.summary,
      next: "Repair durable runtime auth/schema/migration/read readiness, then rerun read-only health checks."
    });
  }
  if (m3Source.present && !["pass", "present"].includes(m3Source.status)) {
    reasons.push({
      id: "m3-io-source-readiness",
      type: "contract_blocker",
      class: "source",
      summary: m3Source.summary,
      next: "Repair the source-only M3 IO contract before apply review."
    });
  }
  return reasons;
}

function reasonClassForBlocker(id, { canPublish, canApply }) {
  if (!canPublish && [
    "target-ref-resolved",
    "artifact-report-published"
  ].includes(id)) {
    return "publish";
  }
  if (!canApply) return "apply";
  return "runtime";
}

function dedupeReasons(reasons) {
  const byId = new Map();
  for (const reason of reasons) {
    if (!byId.has(reason.id)) byId.set(reason.id, reason);
  }
  return [...byId.values()].slice(0, 20);
}

function nextCommands({ canPublish, canApply, blockedReasons }) {
  if (!canPublish) {
    return [
      "bun run --cwd web/hwlab-cloud-web build",
      "bun run --cwd web/hwlab-cloud-web check",
      "node scripts/artifact-publish.mjs --preflight --no-report",
      "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked --no-write",
      guardCommand
    ];
  }
  if (!canApply) {
    return [
      "node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --pretty",
      "node scripts/deploy-desired-state-plan.mjs --promotion-commit <origin-main-sha> --check",
      "node scripts/gitops-render.mjs --no-write",
      guardCommand
    ];
  }
  if (blockedReasons.length > 0) {
    return [
      "node scripts/dev-edge-health-smoke.mjs --live",
      "node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run",
      guardCommand
    ];
  }
  return [
    "node scripts/gitops-render.mjs --no-write",
    "node scripts/artifact-runtime-readiness-guard.mjs --target-ref origin/main --check --live --no-report"
  ];
}

async function readJson(repoRoot, relativePath) {
  return readStructuredFile(repoRoot, relativePath);
}

async function readOptionalJson(repoRoot, relativePath) {
  try {
    return await readJson(repoRoot, relativePath);
  } catch {
    return null;
  }
}

function resolveTargetCommit(repoRoot, ref) {
  try {
    return execFileSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], {
      cwd: repoRoot,
      encoding: "utf8",
      stdio: ["ignore", "pipe", "pipe"],
      timeout: 5000
    }).trim();
  } catch {
    return "unknown";
  }
}

function parseArgs(argv) {
  const args = {
    targetRef: defaultTargetRef,
    artifactReport: defaultArtifactReport,
    artifactCatalog: defaultArtifactCatalog,
    runtimeReport: defaultRuntimeReport,
    edgeReport: defaultEdgeReport,
    report: defaultOutputReport,
    timeoutMs: 5000,
    live: false,
    writeReport: false,
    check: false,
    expectBlocked: false,
    pretty: false,
    help: false
  };

  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === "--target-ref") {
      args.targetRef = requireValue(argv, index, arg);
      index += 1;
    } else if (arg === "--artifact-report") {
      args.artifactReport = requireValue(argv, index, arg);
      index += 1;
    } else if (arg === "--artifact-catalog") {
      args.artifactCatalog = requireValue(argv, index, arg);
      index += 1;
    } else if (arg === "--runtime-report") {
      args.runtimeReport = requireValue(argv, index, arg);
      index += 1;
    } else if (arg === "--edge-report") {
      args.edgeReport = requireValue(argv, index, arg);
      index += 1;
    } else if (arg === "--report") {
      args.report = requireValue(argv, index, arg);
      index += 1;
    } else if (arg === "--timeout-ms") {
      args.timeoutMs = Number.parseInt(requireValue(argv, index, arg), 10);
      index += 1;
    } else if (arg === "--no-report") {
      args.writeReport = false;
    } else if (arg === "--no-live") {
      args.live = false;
    } else if (arg === "--live") {
      args.live = true;
    } else if (arg === "--check") {
      args.check = true;
    } else if (arg === "--expect-blocked") {
      args.expectBlocked = true;
    } else if (arg === "--pretty") {
      args.pretty = true;
    } else if (arg === "--help" || arg === "-h") {
      args.help = true;
    } else {
      throw new Error(`unknown argument: ${arg}`);
    }
  }

  if (!Number.isInteger(args.timeoutMs) || args.timeoutMs <= 0) {
    throw new Error("--timeout-ms must be a positive integer");
  }
  return args;
}

function requireValue(argv, index, arg) {
  const value = argv[index + 1];
  if (!value || value.startsWith("--")) {
    throw new Error(`${arg} requires a value`);
  }
  return value;
}

function usage() {
  return [
    `usage: ${guardCommand} [--report ${defaultOutputReportPath}] [--live] [--expect-blocked]`,
    "",
    "Read-only latest-main DEV artifact/runtime readiness guard.",
    "",
    "The command compares target main, artifact publish/catalog identity, desired-state convergence,",
    "Cloud API /health/live identity, Cloud Web :16666 /health/live identity, source build freshness,",
    "Code Agent timeout handling, M3 /v1/m3/io source readiness if present, and durable runtime blockers.",
    "By default it does not run live HTTP checks and prints devLiveClaim=false; pass --live for explicit read-only live checks.",
    "It does not deploy, publish, apply, restart services, mutate k8s, touch PROD, or read secrets."
  ].join("\n");
}

function summaryForCheck(report) {
  return report.commanderDecision;
}

function sanitizeString(value) {
  return typeof value === "string" && value.trim().length > 0 ? oneLine(value) : "unknown";
}

function sanitizeTimestamp(value) {
  return typeof value === "string" && !Number.isNaN(Date.parse(value)) ? value : new Date().toISOString();
}

function parseJson(text) {
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }
}

function oneLine(value) {
  return String(value ?? "unknown").replace(/\s+/gu, " ").trim().slice(0, 240) || "unknown";
}
