#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";

import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
import {
  resolveDevArtifactServices,
  serviceInventoryFromServices
} from "./src/dev-artifact-services.mjs";
import { tempReportPath } from "./src/report-paths.mjs";
import { readStructuredFile } from "./src/structured-config.mjs";

const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const defaultLane = process.env.HWLAB_ARTIFACT_LANE || process.env.HWLAB_GITOPS_LANE || "node";
const defaultCatalogPath = process.env.HWLAB_ARTIFACT_CATALOG_PATH || "deploy/artifact-catalog.dev.json";
const defaultDeployPath = process.env.HWLAB_DEPLOY_MANIFEST_PATH || "deploy/deploy.yaml";
const defaultPublishReportPath = tempReportPath("dev-artifacts.json");
const defaultRegistryPrefix = process.env.HWLAB_DEV_REGISTRY_PREFIX || process.env.HWLAB_DEV_REGISTRY || "127.0.0.1:5000/hwlab";
const digestPattern = /^sha256:[a-f0-9]{64}$/;
const commitPattern = /^[a-f0-9]{7,40}$/;
const runtimeArtifactLanePattern = /^v\d{2,}$/u;

const publishReadyStatuses = new Set(["published", "reused"]);
const v02EnvReuseRuntimeMode = "env-reuse-git-mirror-checkout";

function isRuntimeArtifactLane(lane) {
  return runtimeArtifactLanePattern.test(String(lane ?? ""));
}

function laneDeployConfig(deploy, lane) {
  return deploy?.lanes?.[lane] ?? null;
}

function serviceIdsForLane(lane, deploy = null) {
  return isRuntimeArtifactLane(lane) ? runtimeServiceIdsFromDeploy(deploy, lane) : SERVICE_IDS;
}

function runtimeServiceIdsFromDeploy(deploy, lane) {
  const serviceIds = uniquePreserveOrder((Array.isArray(deploy?.lanes?.[lane]?.envReuseServices) ? deploy.lanes[lane].envReuseServices : [])
    .map((item) => String(item ?? "").trim())
    .filter(Boolean));
  assert.ok(serviceIds.length > 0, `deploy.lanes.${lane}.envReuseServices must not be empty`);
  return serviceIds;
}

function uniquePreserveOrder(values) {
  const seen = new Set();
  const result = [];
  for (const value of values) {
    if (seen.has(value)) continue;
    seen.add(value);
    result.push(value);
  }
  return result;
}

function parseArgs(argv) {
  const args = {
    lane: defaultLane,
    catalogPath: defaultCatalogPath,
    deployPath: defaultDeployPath,
    imageTagMode: process.env.HWLAB_ARTIFACT_IMAGE_TAG_MODE || "short",
    targetRef: "HEAD",
    publishReportPath: null,
    blocked: false,
    write: false
  };

  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === "--target-ref") {
      args.targetRef = readOption(argv, ++index, arg);
    } else if (arg === "--lane") {
      args.lane = readOption(argv, ++index, arg);
    } else if (arg === "--catalog-path") {
      args.catalogPath = readOption(argv, ++index, arg);
    } else if (arg === "--deploy-config") {
      args.deployPath = readOption(argv, ++index, arg);
    } else if (arg === "--image-tag-mode") {
      args.imageTagMode = readOption(argv, ++index, arg);
    } else if (arg === "--publish-report") {
      args.publishReportPath = readOption(argv, ++index, arg);
    } else if (arg === "--blocked") {
      args.blocked = true;
    } else if (arg === "--write") {
      args.write = true;
    } else if (arg === "--no-write") {
      args.write = false;
    } else if (arg === "--help" || arg === "-h") {
      args.help = true;
    } else {
      throw new Error(`unknown argument ${arg}`);
    }
  }

  if (!args.help) {
    assert.ok(args.lane === "node" || isRuntimeArtifactLane(args.lane), `unknown artifact lane ${args.lane}`);
    assert.ok(["short", "full"].includes(args.imageTagMode), `unknown image tag mode ${args.imageTagMode}`);
    assert.notEqual(args.blocked && Boolean(args.publishReportPath), true, "--blocked and --publish-report are mutually exclusive");
    assert.ok(args.blocked || args.publishReportPath, `choose --blocked or --publish-report ${defaultPublishReportPath}`);
    assert.ok(!args.write || args.publishReportPath, "--write requires --publish-report; blocked skeleton refresh must stay preview-only");
  }

  return args;
}

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

function usage() {
  return [
    "usage: node scripts/refresh-artifact-catalog.mjs --target-ref REF (--blocked|--publish-report PATH) [--write|--no-write]",
    "",
    "Preview or explicitly write DEV artifact catalog identity without faking digest evidence.",
    "Default mode is read-only. --write is reserved for node Tekton GitOps promotion and never writes deploy/deploy.yaml.",
    "",
    "examples:",
    "  node scripts/refresh-artifact-catalog.mjs --target-ref HEAD --blocked --no-write",
    `  node scripts/refresh-artifact-catalog.mjs --target-ref HEAD --publish-report ${defaultPublishReportPath} --no-write`,
    `  node scripts/refresh-artifact-catalog.mjs --lane v03 --catalog-path deploy/artifact-catalog.v03.json --image-tag-mode full --target-ref HEAD --publish-report ${defaultPublishReportPath} --write`
  ].join("\n");
}

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

async function readConfigIfPresent(relativePath, fallback = null) {
  try {
    return await readConfig(relativePath);
  } catch (error) {
    if (error?.code === "ENOENT") return fallback;
    throw error;
  }
}

async function writeJson(relativePath, value) {
  await writeFile(path.join(repoRoot, relativePath), `${JSON.stringify(value, null, 2)}\n`);
}

async function gitValue(args) {
  const result = await execFileAsync("git", args, {
    cwd: repoRoot,
    timeout: 5000,
    maxBuffer: 1024 * 1024
  });
  return result.stdout.trim();
}

async function resolveTarget(ref, imageTagMode = "short") {
  const commitId = await gitValue(["rev-parse", ref]);
  const shortCommitId = await gitValue(["rev-parse", "--short=7", ref]);
  assert.match(commitId, /^[a-f0-9]{40}$/, `target ref ${ref} must resolve to a full SHA`);
  return { ref, commitId, shortCommitId, imageTag: imageTagMode === "full" ? commitId : shortCommitId };
}

function parseTaggedImage(image, context) {
  assert.equal(typeof image, "string", `${context} image must be a string`);
  assert.equal(image.includes("@"), false, `${context} image must not include a digest suffix`);
  assert.ok(!/prod|production/iu.test(image), `${context} image must not target prod`);
  const slashIndex = image.lastIndexOf("/");
  const colonIndex = image.lastIndexOf(":");
  assert.ok(colonIndex > slashIndex, `${context} image must be a tagged image reference`);
  return {
    repository: image.slice(0, colonIndex),
    tag: image.slice(colonIndex + 1)
  };
}

function targetImage(serviceId, shortCommitId, registryPrefix = defaultRegistryPrefix) {
  return `${registryPrefix.replace(/\/+$/u, "")}/${serviceId}:${shortCommitId}`;
}

function artifactEnvironment(args) {
  return isRuntimeArtifactLane(args.lane) ? args.lane : ENVIRONMENT_DEV;
}

function artifactNamespace(args, deploy) {
  if (!isRuntimeArtifactLane(args.lane)) return "hwlab-dev";
  const namespace = laneDeployConfig(deploy, args.lane)?.namespace;
  assert.ok(String(namespace ?? "").trim(), `deploy.lanes.${args.lane}.namespace must not be empty`);
  return namespace;
}

function isHttpEndpoint(value) {
  if (typeof value !== "string" || value.trim().length === 0) return false;
  try {
    const url = new URL(value);
    return url.protocol === "http:" || url.protocol === "https:";
  } catch {
    return false;
  }
}

function artifactEndpoint(args, deploy) {
  if (!isRuntimeArtifactLane(args.lane)) return DEV_ENDPOINT;
  const endpoint = laneDeployConfig(deploy, args.lane)?.endpoint;
  assert.ok(isHttpEndpoint(endpoint), `deploy.lanes.${args.lane}.endpoint must be a non-empty http(s) URL`);
  return endpoint;
}

function artifactCatalogSkeleton({ args, deploy, target, registryPrefix = defaultRegistryPrefix }) {
  const environment = artifactEnvironment(args);
  const namespace = artifactNamespace(args, deploy);
  return {
    catalogVersion: "v1",
    kind: "hwlab-artifact-catalog",
    environment,
    profile: environment,
    namespace,
    endpoint: artifactEndpoint(args, deploy),
    commitId: target.imageTag,
    artifactState: "contract-skeleton",
    publish: {
      ciPublished: false,
      registryVerified: false,
      provenance: "not_available_until_publish",
      note: `${environment} artifact catalog skeleton initialized without DEV/node fallback.`
    },
    healthContract: {
      method: "GET",
      path: "/health/live",
      responseFormat: "json",
      requiredFields: ["serviceId", "environment", "status"]
    },
    allowedProfiles: [environment],
    forbiddenProfiles: isRuntimeArtifactLane(args.lane) ? ["dev", "prod"] : ["prod"],
    services: serviceIdsForLane(args.lane, deploy).map((serviceId) => catalogSkeletonService({ args, serviceId, target, registryPrefix, environment, namespace }))
  };
}

function catalogSkeletonService({ args, serviceId, target, registryPrefix, environment, namespace }) {
  return {
    serviceId,
    commitId: target.imageTag,
    sourceCommitId: target.commitId,
    image: targetImage(serviceId, target.imageTag, registryPrefix),
    imageTag: target.imageTag,
    digest: "not_published",
    publishState: "skeleton-only",
    profile: environment,
    namespace,
    healthPath: "/health/live",
    sourceState: "source-present",
    publishEnabled: true,
    artifactRequired: true,
    artifactScope: "required",
    notPublishedReason: "publish_not_run"
  };
}

function commitMatchesTarget(value, target) {
  return value === target.commitId || value === target.shortCommitId;
}

function shortCommitForRecord(record, target) {
  const value = record?.commitId ?? record?.imageTag ?? null;
  if (typeof value === "string" && commitPattern.test(value)) return target.imageTag.length === 40 ? value : value.slice(0, 7);
  if (typeof record?.sourceCommitId === "string" && commitPattern.test(record.sourceCommitId)) return target.imageTag.length === 40 ? record.sourceCommitId : record.sourceCommitId.slice(0, 7);
  return target.imageTag;
}

function revisionForRecord(record, target) {
  if (typeof record?.sourceCommitId === "string" && commitPattern.test(record.sourceCommitId)) return record.sourceCommitId;
  if (typeof record?.commitId === "string" && commitPattern.test(record.commitId)) return record.commitId;
  return target.commitId;
}

function isEnvReusePublishRecord(service) {
  return service?.runtimeMode === v02EnvReuseRuntimeMode || service?.envReuse === true;
}

function assertPublishedImageTag(service, image, target) {
  if (!isEnvReusePublishRecord(service)) {
    assert.equal(image.tag, target.imageTag, `${service.serviceId} newly published image tag must match target image tag`);
    return;
  }

  assert.equal(service.sourceCommitId, target.commitId, `${service.serviceId} env-reuse sourceCommitId must match target commit`);
  assert.equal(service.bootCommit, target.commitId, `${service.serviceId} env-reuse bootCommit must match target commit`);
  assert.match(service.environmentInputHash ?? "", /^[a-f0-9]{64}$/u, `${service.serviceId} env-reuse environmentInputHash must be a sha256 hex input hash`);
  assert.equal(image.tag, `env-${service.environmentInputHash.slice(0, 12)}`, `${service.serviceId} env-reuse image tag must match environment input hash`);
}

function assertDevOnlyDeployAndCatalog(deploy, catalog) {
  assert.equal(deploy.environment, ENVIRONMENT_DEV, "deploy environment must be dev");
  assert.equal(deploy.namespace, "hwlab-dev", "deploy namespace must be hwlab-dev");
  assert.equal(deploy.endpoint, DEV_ENDPOINT, "deploy endpoint must stay frozen");
  assert.equal(deploy.profiles?.dev?.enabled, true, "deploy dev profile must be enabled");
  assert.equal(deploy.profiles?.prod?.enabled, false, "deploy prod profile must stay disabled");
  assert.deepEqual((deploy.services ?? []).map((service) => service.serviceId), SERVICE_IDS, "deploy services must cover frozen service IDs in order");

  assert.equal(catalog.environment, ENVIRONMENT_DEV, "catalog environment must be dev");
  assert.equal(catalog.profile, ENVIRONMENT_DEV, "catalog profile must be dev");
  assert.equal(catalog.namespace, "hwlab-dev", "catalog namespace must be hwlab-dev");
  assert.equal(catalog.endpoint, DEV_ENDPOINT, "catalog endpoint must stay frozen");
  assert.deepEqual(catalog.allowedProfiles, [ENVIRONMENT_DEV], "catalog allowedProfiles must only allow dev");
  assert.deepEqual(catalog.forbiddenProfiles, ["prod"], "catalog forbiddenProfiles must forbid prod");
  assert.deepEqual((catalog.services ?? []).map((service) => service.serviceId), SERVICE_IDS, "catalog services must cover frozen service IDs in order");
}

function assertRuntimeLaneDeployAndCatalog(deploy, catalog, laneName) {
  const lane = laneDeployConfig(deploy, laneName);
  assert.ok(lane && typeof lane === "object", `deploy.lanes.${laneName} must exist for ${laneName} catalog refresh`);
  assert.ok(String(lane.namespace ?? "").trim(), `deploy.lanes.${laneName}.namespace must not be empty`);
  assert.ok(isHttpEndpoint(lane.endpoint), `deploy.lanes.${laneName}.endpoint must be a non-empty http(s) URL`);
  const serviceIds = serviceIdsForLane(laneName, deploy);
  assert.ok(serviceIds.every((serviceId) => lane.serviceDeclarations?.[serviceId]), `deploy.lanes.${laneName}.serviceDeclarations must cover envReuseServices`);
  assert.ok(serviceIds.every((serviceId) => lane.bootScripts?.[serviceId]), `deploy.lanes.${laneName}.bootScripts must cover envReuseServices`);

  assert.equal(catalog.environment, laneName, `catalog environment must be ${laneName}`);
  assert.equal(catalog.profile, laneName, `catalog profile must be ${laneName}`);
  assert.equal(catalog.namespace, lane.namespace, `catalog namespace must be ${lane.namespace}`);
  if (catalog.endpoint != null) assert.ok(isHttpEndpoint(catalog.endpoint), "catalog endpoint must be a non-empty http(s) URL");
  assert.deepEqual(catalog.allowedProfiles, [laneName], `catalog allowedProfiles must only allow ${laneName}`);
  assert.deepEqual(catalog.forbiddenProfiles, ["dev", "prod"], "catalog forbiddenProfiles must forbid dev/prod");
  assert.deepEqual((catalog.services ?? []).map((service) => service.serviceId), serviceIds, `catalog services must cover deploy.lanes.${laneName}.envReuseServices in order`);
}

function normalizeCatalogForLane(catalog, lane, deploy) {
  if (!catalog || !isRuntimeArtifactLane(lane)) return catalog;
  const allowed = new Set(serviceIdsForLane(lane, deploy));
  return {
    ...catalog,
    services: (catalog.services ?? []).filter((service) => allowed.has(service.serviceId))
  };
}

function assertLaneDeployAndCatalog(args, deploy, catalog) {
  if (isRuntimeArtifactLane(args.lane)) return assertRuntimeLaneDeployAndCatalog(deploy, catalog, args.lane);
  return assertDevOnlyDeployAndCatalog(deploy, catalog);
}

function publishRecordsFromReport(report, target, serviceIds = SERVICE_IDS) {
  const artifactPublish = report.artifactPublish;
  assert.ok(artifactPublish && typeof artifactPublish === "object", "publish report missing artifactPublish");
  assert.equal(artifactPublish.status, "published", "publish report artifactPublish.status must be published");
  assert.ok(commitMatchesTarget(artifactPublish.sourceCommitId, target), "publish report sourceCommitId must match target ref");
  assert.equal(artifactPublish.serviceCount, serviceIds.length, "publish report must cover every service for the selected lane");
  assert.ok(artifactPublish.publishPlan && typeof artifactPublish.publishPlan === "object", "publish report missing artifactPublish.publishPlan");
  assert.equal(artifactPublish.publishPlan.version, "v2", "publish report plan version must be v2");

  const requiredServiceIds = artifactPublish.publishPlan.services
    .filter((service) => service.required)
    .map((service) => service.serviceId);
  const disabledServiceIds = artifactPublish.publishPlan.services
    .filter((service) => !service.required)
    .map((service) => service.serviceId);
  assert.deepEqual(
    [...requiredServiceIds, ...disabledServiceIds].sort(),
    [...serviceIds].sort(),
    "publish plan must cover every service for the selected lane"
  );
  assert.equal(
    (artifactPublish.publishedCount ?? 0) + (artifactPublish.reusedCount ?? 0),
    requiredServiceIds.length,
    "publish report must publish or reuse every required enabled service"
  );

  const records = new Map();
  const allowed = new Set(serviceIds);
  for (const service of artifactPublish.services ?? []) {
    assert.ok(allowed.has(service.serviceId), `unknown service ${service.serviceId} in publish report`);
    const image = parseTaggedImage(service.image, service.serviceId);
    if (!requiredServiceIds.includes(service.serviceId)) {
      assert.equal(service.artifactRequired, false, `${service.serviceId} disabled service must not be required`);
      assert.equal(service.digest, "not_published", `${service.serviceId} disabled digest must stay not_published`);
      assert.equal(service.notPublishedReason?.startsWith("disabled_"), true, `${service.serviceId} disabled service must state a disabled reason`);
      continue;
    }

    assert.equal(publishReadyStatuses.has(service.status), true, `${service.serviceId} status must be published or reused`);
    assert.equal(service.artifactRequired, true, `${service.serviceId} required service must state artifactRequired=true`);
    if (service.status === "published") {
      assertPublishedImageTag(service, image, target);
    }
    assert.match(service.digest, digestPattern, `${service.serviceId} digest must be a sha256 registry digest`);
    records.set(service.serviceId, {
      serviceId: service.serviceId,
      status: service.status,
      commitId: service.commitId ?? image.tag,
      sourceCommitId: service.sourceCommitId ?? null,
      image: service.image,
      imageTag: image.tag,
      buildCreatedAt: service.buildCreatedAt ?? null,
      buildSource: service.buildSource ?? null,
      buildBackend: service.buildBackend ?? null,
      digest: service.digest,
      repositoryDigest: service.repositoryDigest ?? `${image.repository}@${service.digest}`,
      componentCommitId: service.componentCommitId ?? null,
      componentInputHash: service.componentInputHash ?? null,
      runtimeMode: service.runtimeMode ?? "service-image",
      envReuse: service.envReuse === true,
      envChanged: service.envChanged ?? null,
      codeChanged: service.codeChanged ?? null,
      environmentImage: service.environmentImage ?? null,
      environmentDigest: service.environmentDigest ?? null,
      environmentInputHash: service.environmentInputHash ?? null,
      codeInputHash: service.codeInputHash ?? null,
      bootRepo: service.bootRepo ?? null,
      bootCommit: service.bootCommit ?? null,
      bootSh: service.bootSh ?? null,
      bootEnvEvidence: service.bootEnvEvidence ?? null,
      dockerfileHash: service.dockerfileHash ?? null,
      baseImageReference: service.baseImageReference ?? null,
      baseImageDigest: service.baseImageDigest ?? null,
      buildArgsHash: service.buildArgsHash ?? null,
      ciAffected: service.ciAffected ?? null,
      ciReason: service.ciReason ?? [],
      reuse: service.reuse ?? null,
      reusedFrom: service.reusedFrom ?? null
    });
  }

  assert.deepEqual([...records.keys()], requiredServiceIds, "publish report services must publish required service IDs in order");
  return { records, requiredServiceIds, disabledServiceIds };
}

function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInventory, provenancePath, registryPrefix, lane = "node", serviceIds = SERVICE_IDS }) {
  const published = Boolean(publishRecords);
  const records = publishRecords?.records ?? null;
  const requiredIds = new Set(publishRecords?.requiredServiceIds ?? serviceInventory.requiredServiceIds);
  const catalogByService = new Map(catalog.services.map((service) => [service.serviceId, service]));
  const deployByService = deployServiceMapForLane(deploy, lane, serviceIds);
  const inventoryByService = new Map(serviceInventory.services.map((service) => [service.serviceId, service]));
  const refreshedServices = [];
  const nextCatalogServices = [];

  catalog.commitId = target.imageTag;
  if (isRuntimeArtifactLane(lane)) catalog.endpoint = artifactEndpoint({ lane }, deploy);
  catalog.artifactState = published ? "published" : "contract-skeleton";
  catalog.serviceInventory = serviceInventory;
  catalog.publish = {
    ...catalog.publish,
    ciPublished: published,
    registryVerified: published,
    provenance: published ? provenancePath : "not_available_until_publish",
    note: published
      ? "Digest fields were copied from a successful DEV artifact publish report for this source commit."
      : "Artifact identity was refreshed to this source commit, but no publish report proved registry digests."
  };

  for (const serviceId of serviceIds) {
    const deployService = deployByService.get(serviceId);
    const catalogService = catalogByService.get(serviceId);
    assert.ok(deployService, `${serviceId} missing from deploy manifest`);
    assert.ok(catalogService, `${serviceId} missing from catalog`);

    const inventory = inventoryByService.get(serviceId);
    const required = requiredIds.has(serviceId);
    const publishRecord = records?.get(serviceId) ?? null;
    const envReuse = publishRecord?.runtimeMode === v02EnvReuseRuntimeMode
      || publishRecord?.envReuse === true
      || catalogService.runtimeMode === v02EnvReuseRuntimeMode
      || catalogService.envReuse === true;
    const fallbackEnvTag = `env-${String(publishRecord?.environmentInputHash ?? catalogService.environmentInputHash ?? target.commitId).slice(0, 12)}`;
    const fallbackImage = envReuse ? targetImage(`${serviceId}-env`, fallbackEnvTag, registryPrefix) : targetImage(serviceId, target.imageTag, registryPrefix);
    const image = publishRecord?.image ?? publishRecord?.environmentImage ?? fallbackImage;
    const imageTag = publishRecord?.imageTag ?? parseTaggedImage(image, `${serviceId} catalog image`).tag;
    const serviceCommitId = shortCommitForRecord(publishRecord, target);
    const sourceCommitId = revisionForRecord(publishRecord, target);

    catalogService.commitId = serviceCommitId;
    catalogService.sourceCommitId = sourceCommitId;
    catalogService.image = image;
    catalogService.imageTag = imageTag;
    catalogService.buildCreatedAt = publishRecord?.buildCreatedAt ?? null;
    catalogService.buildSource = publishRecord?.buildSource ?? null;
    catalogService.buildBackend = publishRecord ? publishRecord.buildBackend ?? null : catalogService.buildBackend ?? null;
    catalogService.digest = publishRecord?.digest ?? "not_published";
    catalogService.componentCommitId = publishRecord?.componentCommitId ?? catalogService.componentCommitId ?? null;
    catalogService.componentInputHash = publishRecord?.componentInputHash ?? catalogService.componentInputHash ?? null;
    catalogService.runtimeMode = publishRecord?.runtimeMode ?? catalogService.runtimeMode ?? "service-image";
    catalogService.envReuse = envReuse;
    catalogService.envChanged = publishRecord?.envChanged ?? catalogService.envChanged ?? null;
    catalogService.codeChanged = publishRecord?.codeChanged ?? catalogService.codeChanged ?? null;
    catalogService.environmentImage = publishRecord?.environmentImage ?? (envReuse ? image : catalogService.environmentImage ?? null);
    catalogService.environmentDigest = publishRecord?.environmentDigest ?? catalogService.environmentDigest ?? null;
    catalogService.environmentInputHash = publishRecord?.environmentInputHash ?? catalogService.environmentInputHash ?? null;
    catalogService.codeInputHash = publishRecord?.codeInputHash ?? catalogService.codeInputHash ?? null;
    catalogService.bootRepo = publishRecord?.bootRepo ?? catalogService.bootRepo ?? null;
    catalogService.bootCommit = publishRecord?.bootCommit ?? target.commitId;
    catalogService.bootSh = publishRecord?.bootSh ?? catalogService.bootSh ?? null;
    catalogService.bootEnvEvidence = publishRecord?.bootEnvEvidence ?? catalogService.bootEnvEvidence ?? null;
    catalogService.dockerfileHash = publishRecord?.dockerfileHash ?? catalogService.dockerfileHash ?? null;
    catalogService.baseImageReference = publishRecord?.baseImageReference ?? catalogService.baseImageReference ?? null;
    catalogService.baseImageDigest = publishRecord?.baseImageDigest ?? catalogService.baseImageDigest ?? null;
    catalogService.buildArgsHash = publishRecord?.buildArgsHash ?? catalogService.buildArgsHash ?? null;
    catalogService.ciAffected = publishRecord?.ciAffected ?? catalogService.ciAffected ?? null;
    catalogService.ciReason = publishRecord?.ciReason ?? catalogService.ciReason ?? [];
    catalogService.reuse = publishRecord?.reuse ?? catalogService.reuse ?? null;
    catalogService.reusedFrom = publishRecord?.reusedFrom ?? catalogService.reusedFrom ?? null;
    catalogService.publishState = publishRecord?.status === "reused" ? "reused" : publishRecord ? "published" : "skeleton-only";
    catalogService.publishEnabled = inventory?.publishEnabled ?? required;
    catalogService.artifactRequired = required;
    catalogService.artifactScope = required ? "required" : "disabled";
    catalogService.notPublishedReason = publishRecord
      ? null
      : inventory?.disabledReason ?? (published ? "not_required" : "publish_not_run");
    refreshedServices.push({
      serviceId,
      image,
      imageTag,
      buildCreatedAt: catalogService.buildCreatedAt,
      sourceCommitId: catalogService.sourceCommitId,
      buildBackend: catalogService.buildBackend,
      digest: catalogService.digest,
      componentCommitId: catalogService.componentCommitId,
      componentInputHash: catalogService.componentInputHash,
      runtimeMode: catalogService.runtimeMode,
      environmentDigest: catalogService.environmentDigest ?? null,
      environmentInputHash: catalogService.environmentInputHash ?? null,
      codeInputHash: catalogService.codeInputHash ?? null,
      bootCommit: catalogService.bootCommit ?? null,
      bootSh: catalogService.bootSh ?? null,
      dockerfileHash: catalogService.dockerfileHash,
      ciAffected: catalogService.ciAffected,
      ciReason: catalogService.ciReason,
      reusedFrom: catalogService.reusedFrom ?? null,
      publishState: catalogService.publishState,
      artifactRequired: catalogService.artifactRequired,
      notPublishedReason: catalogService.notPublishedReason
    });
    nextCatalogServices.push(catalogService);
  }

  catalog.services = nextCatalogServices;

  return refreshedServices;
}

function deployServiceMapForLane(deploy, lane, serviceIds) {
  const base = new Map((deploy.services ?? []).map((service) => [service.serviceId, service]));
  if (!isRuntimeArtifactLane(lane)) return base;
  const overlay = new Map((laneDeployConfig(deploy, lane)?.services ?? []).map((service) => [service.serviceId, service]));
  return new Map(serviceIds.map((serviceId) => [serviceId, {
    serviceId,
    ...(base.get(serviceId) ?? {}),
    ...(overlay.get(serviceId) ?? {})
  }]));
}

function publishReportProvenance(publishReport, fallbackPath) {
  const producer = publishReport?.artifactPublish?.producer;
  if (producer?.kind && producer?.runId) return `${producer.kind}:${producer.runId}`;
  return fallbackPath;
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  if (args.help) {
    console.log(usage());
    return;
  }

  const target = await resolveTarget(args.targetRef, args.imageTagMode);
  const [deploy, publishReport] = await Promise.all([
    readConfig(args.deployPath),
    args.publishReportPath ? readConfig(args.publishReportPath) : Promise.resolve(null)
  ]);
  let catalog = await readConfigIfPresent(args.catalogPath, null);
  if (!catalog && isRuntimeArtifactLane(args.lane)) catalog = artifactCatalogSkeleton({ args, deploy, target, registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix });
  catalog = normalizeCatalogForLane(catalog, args.lane, deploy);
  assert.ok(catalog, `${args.catalogPath} is required for ${args.lane} catalog refresh`);
  assertLaneDeployAndCatalog(args, deploy, catalog);

  const serviceIds = serviceIdsForLane(args.lane, deploy);
  const publishRecords = publishReport ? publishRecordsFromReport(publishReport, target, serviceIds) : null;
  const serviceInventory = publishReport?.artifactPublish?.serviceInventory ?? serviceInventoryFromServices(
    await resolveDevArtifactServices(repoRoot, serviceIds, serviceIds)
  );
  const services = refreshDocuments({
    deploy,
    catalog,
    target,
    publishRecords,
    serviceInventory,
    provenancePath: publishReportProvenance(publishReport, args.publishReportPath ?? defaultPublishReportPath),
    registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix,
    lane: args.lane,
    serviceIds
  });

  if (args.write) {
    await writeJson(args.catalogPath, catalog);
  }

  const publishedRecords = [...(publishRecords?.records.values() ?? [])];
  const publishedCount = publishedRecords.filter((record) => record.status === "published").length;
  const reusedCount = publishedRecords.filter((record) => record.status === "reused").length;

  console.log(JSON.stringify({
    status: publishRecords ? "published" : "blocked",
    targetRef: target.ref,
    sourceCommitId: target.commitId,
    artifactCommitId: target.imageTag,
    lane: args.lane,
    catalogPath: args.catalogPath,
    endpoint: catalog.endpoint ?? null,
    imageTagMode: args.imageTagMode,
    wrote: args.write ? [args.catalogPath] : [],
    deployTruthPolicy: "deploy.yaml is human-authored runtime config; artifact promotion must not rewrite it",
    ciPublished: Boolean(publishRecords),
    registryVerified: Boolean(publishRecords),
    publishedCount,
    reusedCount,
    requiredServiceCount: serviceInventory.requiredServiceCount,
    disabledServiceCount: serviceInventory.disabledServiceCount,
    notPublishedCount: services.filter((service) => service.digest === "not_published").length,
    digestPolicy: publishRecords
      ? "catalog digests copied only for required services from a published DEV artifact report; disabled services stay not_published"
      : "all catalog digests remain not_published",
    services
  }, null, 2));
}

main().catch((error) => {
  console.error(JSON.stringify({
    status: "failed",
    error: error instanceof Error ? error.message : String(error)
  }, null, 2));
  process.exitCode = 1;
});
