import { readFileSync } from "node:fs";
import { rootPath } from "./src/config";
import { runArtifactRegistryCommand } from "./src/artifact-registry";

type Environment = "dev" | "prod";
type ServiceId = "mdtodo" | "claudeqq" | "todo-note";
type JsonRecord = Record<string, unknown>;

type ServiceContract = {
  serviceId: ServiceId;
  desiredCommit: string;
  runtimeCommit: string | null;
  runtimeCommitSource: string;
  artifactExists: boolean;
  devStatus: string;
  prodStatus: string;
  blockedScopes: string[];
  recommendedAction: string;
  sourceRepo: string;
  dockerfile: string;
  registryRepository: string;
  consumerKind: "d601-k3s" | "compose";
};

const contracts: ServiceContract[] = [
  {
    serviceId: "mdtodo",
    desiredCommit: "595de3d320b73ec006794440b32db48b3ad14d2b",
    runtimeCommit: "75fb6757b2504ba86d61f2587fb34a9c9ed4019a",
    runtimeCommitSource: "prod Deployment annotations; desired artifact target was advanced because 75fb6757 predates mdtodo /health.deploy metadata",
    artifactExists: false,
    devStatus: "missing-dev-service",
    prodStatus: "healthy-prod-annotation-stale-after-health-metadata-repin",
    blockedScopes: ["registry-artifact", "dev-service", "runtime-health-metadata-proof", "prod-runtime-commit-drift"],
    recommendedAction: "Publish the desired artifact that includes mdtodo health deploy metadata, create/verify unidesk-dev/mdtodo-dev, then run focused dev smoke before deciding whether prod needs replacement.",
    sourceRepo: "https://github.com/pikasTech/unidesk",
    dockerfile: "src/components/microservices/mdtodo/Dockerfile",
    registryRepository: "unidesk/mdtodo",
    consumerKind: "d601-k3s",
  },
  {
    serviceId: "claudeqq",
    desiredCommit: "203b1f46684c91340ecbbd8a74502bd55e4f2011",
    runtimeCommit: "203b1f46684c91340ecbbd8a74502bd55e4f2011",
    runtimeCommitSource: "prod /health deploy.commit and deploy.requestedCommit",
    artifactExists: false,
    devStatus: "missing-dev-service",
    prodStatus: "healthy-prod-health-aligned-event-api-unverified",
    blockedScopes: ["registry-artifact", "dev-service", "event-api-surface"],
    recommendedAction: "Publish the desired artifact, create/verify unidesk-dev/claudeqq-dev, then resolve or document the event API paths before prod artifact replacement.",
    sourceRepo: "https://gitee.com/lyon1998/agent_skills",
    dockerfile: "claudeqq/Dockerfile",
    registryRepository: "unidesk/claudeqq",
    consumerKind: "d601-k3s",
  },
  {
    serviceId: "todo-note",
    desiredCommit: "a14ce0eb855a685fa17b47adacd54623e72cd2ff",
    runtimeCommit: null,
    runtimeCommitSource: "prod health and container labels do not expose source commit",
    artifactExists: false,
    devStatus: "consumer-plan-only-no-live-dev",
    prodStatus: "healthy-behavior-no-commit-proof",
    blockedScopes: ["registry-artifact", "runtime-commit-proof", "health-deploy-metadata"],
    recommendedAction: "Publish the desired artifact, then use the no-build Compose artifact consumer to recreate only todo-note and verify image labels plus health deploy metadata.",
    sourceRepo: "https://gitee.com/Lyon1998/todo_note",
    dockerfile: "Dockerfile",
    registryRepository: "unidesk/todo-note",
    consumerKind: "compose",
  },
];

function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
  if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
}

function asRecord(value: unknown, label: string): JsonRecord {
  assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, value);
  return value as JsonRecord;
}

function asArray(value: unknown, label: string): unknown[] {
  assertCondition(Array.isArray(value), `${label} must be an array`, value);
  return value as unknown[];
}

function strings(value: unknown, label: string): string[] {
  return asArray(value, label).map(String);
}

function manifestService(manifest: JsonRecord, environment: Environment, serviceId: ServiceId): JsonRecord {
  const environments = asRecord(manifest.environments, "deploy.json.environments");
  const env = asRecord(environments[environment], `deploy.json.environments.${environment}`);
  const services = asArray(env.services, `deploy.json.environments.${environment}.services`);
  const service = services.map((item, index) => asRecord(item, `${environment}.services[${index}]`)).find((item) => item.id === serviceId);
  assertCondition(service !== undefined, `deploy.json ${environment} must include ${serviceId}`, env);
  return service as JsonRecord;
}

function artifactCatalogEntry(catalog: JsonRecord, serviceId: ServiceId): JsonRecord {
  const artifacts = asArray(catalog.artifacts, "CI.json.artifacts").map((item, index) => asRecord(item, `CI.json.artifacts[${index}]`));
  const artifact = artifacts.find((item) => item.serviceId === serviceId);
  assertCondition(artifact !== undefined, `CI.json must include ${serviceId}`, catalog);
  return artifact as JsonRecord;
}

function assertDeployJson(): void {
  const manifest = asRecord(JSON.parse(readFileSync(rootPath("deploy.json"), "utf8")) as unknown, "deploy.json");
  assertCondition(manifest.schemaVersion === 2, "deploy.json must use schemaVersion=2", manifest);
  for (const contract of contracts) {
    for (const environment of ["dev", "prod"] as const) {
      const service = manifestService(manifest, environment, contract.serviceId);
      assertCondition(service.repo === contract.sourceRepo, `${contract.serviceId} ${environment} repo mismatch`, service);
      assertCondition(service.commitId === contract.desiredCommit, `${contract.serviceId} ${environment} desired commit mismatch`, service);
    }
  }
}

function assertCiCatalog(): void {
  const catalog = asRecord(JSON.parse(readFileSync(rootPath("CI.json"), "utf8")) as unknown, "CI.json");
  for (const contract of contracts) {
    const artifact = artifactCatalogEntry(catalog, contract.serviceId);
    const source = asRecord(artifact.source, `${contract.serviceId} CI source`);
    const image = asRecord(artifact.image, `${contract.serviceId} CI image`);
    assertCondition(artifact.kind === "source-build", `${contract.serviceId} producer must be source-build`, artifact);
    assertCondition(artifact.status === "supported", `${contract.serviceId} producer must be supported`, artifact);
    assertCondition(artifact.producer === "ci publish-user-service", `${contract.serviceId} producer command mismatch`, artifact);
    assertCondition(source.repo === contract.sourceRepo, `${contract.serviceId} source repo mismatch`, source);
    assertCondition(source.dockerfile === contract.dockerfile, `${contract.serviceId} Dockerfile mismatch`, source);
    assertCondition(image.repository === contract.registryRepository, `${contract.serviceId} registry repository mismatch`, image);
  }
}

function assertNormalizedStatus(): void {
  for (const contract of contracts) {
    assertCondition(/^[0-9a-f]{40}$/u.test(contract.desiredCommit), `${contract.serviceId} desiredCommit must be a full sha`, contract);
    assertCondition(contract.runtimeCommit === null || /^[0-9a-f]{40}$/u.test(contract.runtimeCommit), `${contract.serviceId} runtimeCommit must be a full sha or null`, contract);
    assertCondition(contract.artifactExists === false, `${contract.serviceId} desired artifact should remain recorded as missing`, contract);
    assertCondition(contract.devStatus.length > 0, `${contract.serviceId} devStatus must be structured`, contract);
    assertCondition(contract.prodStatus.length > 0, `${contract.serviceId} prodStatus must be structured`, contract);
    assertCondition(contract.blockedScopes.length > 0, `${contract.serviceId} blockedScopes must not be empty`, contract);
    assertCondition(contract.recommendedAction.length > 0, `${contract.serviceId} recommendedAction must not be empty`, contract);
  }
}

function assertCommonDryRun(plan: JsonRecord, contract: ServiceContract, environment: Environment): void {
  const source = asRecord(plan.source, `${contract.serviceId} ${environment} source`);
  const registry = asRecord(plan.registry, `${contract.serviceId} ${environment} registry`);
  const build = asRecord(plan.build, `${contract.serviceId} ${environment} build`);
  const labels = asRecord(plan.requiredLabels, `${contract.serviceId} ${environment} labels`);
  const registryProbe = asRecord(plan.registryProbe, `${contract.serviceId} ${environment} registryProbe`);

  assertCondition(plan.ok === true && plan.supported === true, `${contract.serviceId} ${environment} dry-run must be supported`, plan);
  assertCondition(plan.dryRun === true && plan.mutation === false, `${contract.serviceId} ${environment} dry-run must be non-mutating`, plan);
  assertCondition(plan.environment === environment, `${contract.serviceId} ${environment} environment mismatch`, plan);
  assertCondition(plan.providerId === "D601", `${contract.serviceId} ${environment} provider mismatch`, plan);
  assertCondition(plan.serviceId === contract.serviceId, `${contract.serviceId} ${environment} service id mismatch`, plan);
  assertCondition(plan.commit === contract.desiredCommit, `${contract.serviceId} ${environment} commit mismatch`, plan);
  const deployRef = `deploy.json#environments.${environment}.services.${contract.serviceId}`;
  const allowedDeployRefs = [deployRef, `origin/master:${deployRef}`];
  assertCondition(allowedDeployRefs.includes(String(plan.deployRef)), `${contract.serviceId} ${environment} deployRef mismatch`, plan);
  assertCondition(plan.sourceImage === `127.0.0.1:5000/${contract.registryRepository}:${contract.desiredCommit}`, `${contract.serviceId} ${environment} source image mismatch`, plan);
  assertCondition(source.repo === contract.sourceRepo, `${contract.serviceId} ${environment} source repo mismatch`, source);
  assertCondition(source.commit === contract.desiredCommit, `${contract.serviceId} ${environment} source commit mismatch`, source);
  assertCondition(source.dockerfile === contract.dockerfile, `${contract.serviceId} ${environment} source dockerfile mismatch`, source);
  assertCondition(registry.repository === contract.registryRepository, `${contract.serviceId} ${environment} registry repository mismatch`, registry);
  assertCondition(registry.imageRef === plan.sourceImage, `${contract.serviceId} ${environment} registry image mismatch`, registry);
  assertCondition(registry.digest === null, `${contract.serviceId} ${environment} dry-run must not fake a digest`, registry);
  assertCondition(String(registry.digestSource ?? "").includes("live apply must read this digest"), `${contract.serviceId} ${environment} digest source mismatch`, registry);
  assertCondition(registryProbe.method === "HEAD", `${contract.serviceId} ${environment} registry probe must be HEAD`, registryProbe);
  assertCondition(labels["unidesk.ai/service-id"] === contract.serviceId, `${contract.serviceId} ${environment} service label mismatch`, labels);
  assertCondition(labels["unidesk.ai/source-repo"] === contract.sourceRepo, `${contract.serviceId} ${environment} source repo label mismatch`, labels);
  assertCondition(labels["unidesk.ai/source-commit"] === contract.desiredCommit, `${contract.serviceId} ${environment} commit label mismatch`, labels);
  assertCondition(labels["unidesk.ai/dockerfile"] === contract.dockerfile, `${contract.serviceId} ${environment} Dockerfile label mismatch`, labels);
  assertCondition(build.willCompile === false, `${contract.serviceId} ${environment} CD must not compile`, build);
  assertCondition(build.willRunCargoBuild === false, `${contract.serviceId} ${environment} CD must not run cargo build`, build);
  assertCondition(build.willRunDockerBuild === false, `${contract.serviceId} ${environment} CD must not run docker build`, build);
  assertCondition(build.willRunDockerComposeBuild === false, `${contract.serviceId} ${environment} CD must not run docker compose build`, build);
  assertCondition(build.producerBoundary === "ci publish-user-service", `${contract.serviceId} ${environment} producer boundary mismatch`, build);
  assertCondition(String(plan.boundary ?? "").includes("artifact-consumer only"), `${contract.serviceId} ${environment} boundary must be artifact-only`, plan);
  assertCondition(String(plan.boundary ?? "").includes("never builds source"), `${contract.serviceId} ${environment} boundary must forbid source builds`, plan);
}

function assertK3sDryRun(plan: JsonRecord, contract: ServiceContract, environment: Environment): void {
  const target = asRecord(plan.target, `${contract.serviceId} ${environment} target`);
  const validation = strings(plan.validation, `${contract.serviceId} ${environment} validation`);
  const suffix = environment === "dev" ? "-dev" : "";
  assertCondition(target.kind === "d601-k3s", `${contract.serviceId} ${environment} target kind mismatch`, target);
  assertCondition(target.namespace === (environment === "dev" ? "unidesk-dev" : "unidesk"), `${contract.serviceId} ${environment} namespace mismatch`, target);
  assertCondition(target.deployment === `${contract.serviceId}${suffix}`, `${contract.serviceId} ${environment} deployment mismatch`, target);
  assertCondition(target.service === `${contract.serviceId}${suffix}`, `${contract.serviceId} ${environment} service mismatch`, target);
  assertCondition(String(target.deployCommandShape ?? "").includes("kubectl set image"), `${contract.serviceId} ${environment} command must be k3s image update`, target);
  assertCondition(validation.some((line) => line.includes("Kubernetes API service proxy") || line.includes("service health")), `${contract.serviceId} ${environment} validation should include k3s service health`, validation);
}

function assertComposeDryRun(plan: JsonRecord, contract: ServiceContract, environment: Environment): void {
  const target = asRecord(plan.target, `${contract.serviceId} ${environment} target`);
  const validation = strings(plan.validation, `${contract.serviceId} ${environment} validation`);
  assertCondition(target.kind === "compose", `${contract.serviceId} ${environment} target kind mismatch`, target);
  assertCondition(target.runtimeHost === "main-server", `${contract.serviceId} ${environment} runtime host mismatch`, target);
  assertCondition(target.composeService === "todo-note", `${contract.serviceId} ${environment} compose service mismatch`, target);
  assertCondition(target.containerName === "todo-note-backend", `${contract.serviceId} ${environment} container mismatch`, target);
  assertCondition(target.deployCommandShape === "docker compose up -d --no-build --no-deps --force-recreate todo-note", `${contract.serviceId} ${environment} command shape mismatch`, target);
  const runtimeProof = asRecord(plan.runtimeProof, `${contract.serviceId} ${environment} runtimeProof`);
  const requiredEnvKeys = asArray(runtimeProof.requiredEnvKeys, `${contract.serviceId} ${environment} runtime proof env keys`).map(String);
  assertCondition(runtimeProof.kind === "compose-container-runtime-metadata", `${contract.serviceId} ${environment} runtime proof kind mismatch`, runtimeProof);
  assertCondition(runtimeProof.sourceDirectoryUsed === false, `${contract.serviceId} ${environment} runtime proof must not use source directory guesses`, runtimeProof);
  assertCondition(requiredEnvKeys.includes("UNIDESK_DEPLOY_COMMIT"), `${contract.serviceId} ${environment} runtime proof must use generic commit env`, runtimeProof);
  assertCondition(requiredEnvKeys.includes("UNIDESK_TODO_NOTE_DEPLOY_COMMIT"), `${contract.serviceId} ${environment} runtime proof must use service commit env`, runtimeProof);
  assertCondition(validation.some((line) => line.includes("deploy.commit/deploy.requestedCommit") && line.includes("Compose container runtime metadata")), `${contract.serviceId} ${environment} validation must require synthetic health deploy metadata`, validation);
}

async function assertDryRuns(): Promise<void> {
  for (const contract of contracts) {
    for (const environment of ["dev", "prod"] as const) {
      const plan = asRecord(await runArtifactRegistryCommand([
        "deploy-service",
        "--env",
        environment,
        "--service",
        contract.serviceId,
        "--commit",
        contract.desiredCommit,
        "--dry-run",
      ]), `${contract.serviceId} ${environment} artifact dry-run`);
      assertCommonDryRun(plan, contract, environment);
      if (contract.consumerKind === "d601-k3s") assertK3sDryRun(plan, contract, environment);
      else assertComposeDryRun(plan, contract, environment);
    }
  }
}

async function main(): Promise<void> {
  assertDeployJson();
  assertCiCatalog();
  assertNormalizedStatus();
  await assertDryRuns();

  process.stdout.write(`${JSON.stringify({
    ok: true,
    checks: [
      "deploy.json dev/prod desired commits match the issue #9 service matrix",
      "CI.json keeps mdtodo, claudeqq and todo-note on supported ci publish-user-service source-build producers",
      "normalized status records expose desiredCommit, runtimeCommit, artifactExists, devStatus, prodStatus, blockedScopes and recommendedAction",
      "dev/prod artifact-registry dry-runs are non-mutating, commit-pinned and no-build",
      "mdtodo and claudeqq dry-runs target D601 k3s consumers",
      "todo-note dry-runs target the main-server Compose consumer with no-build/no-deps recreate shape",
    ],
    services: contracts.map((contract) => ({
      serviceId: contract.serviceId,
      desiredCommit: contract.desiredCommit,
      runtimeCommit: contract.runtimeCommit,
      runtimeCommitSource: contract.runtimeCommitSource,
      artifactExists: contract.artifactExists,
      devStatus: contract.devStatus,
      prodStatus: contract.prodStatus,
      blockedScopes: contract.blockedScopes,
      recommendedAction: contract.recommendedAction,
    })),
  }, null, 2)}\n`);
}

if (import.meta.main) {
  await main();
}
