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

type JsonRecord = Record<string, unknown>;

interface ServiceCase {
  serviceId: string;
  sourceRepo: string;
  dockerfile: string;
  registryRepository: string;
  composeService: string;
  containerName: string;
  targetImage: string;
  deployEnvPrefix: string;
}

function asRecord(value: unknown, label: string): JsonRecord {
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
    throw new Error(`${label} must be an object: ${JSON.stringify(value)}`);
  }
  return value as JsonRecord;
}

function asArray(value: unknown, label: string): unknown[] {
  if (!Array.isArray(value)) throw new Error(`${label} must be an array: ${JSON.stringify(value)}`);
  return value;
}

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

const commit = "0123456789abcdef0123456789abcdef01234567";

const serviceCases: ServiceCase[] = [
  {
    serviceId: "project-manager",
    sourceRepo: "https://github.com/pikasTech/unidesk",
    dockerfile: "src/components/microservices/project-manager/Dockerfile",
    registryRepository: "unidesk/project-manager",
    composeService: "project-manager",
    containerName: "project-manager-backend",
    targetImage: "project-manager",
    deployEnvPrefix: "UNIDESK_PROJECT_MANAGER_DEPLOY",
  },
  {
    serviceId: "oa-event-flow",
    sourceRepo: "https://github.com/pikasTech/unidesk",
    dockerfile: "src/components/microservices/oa-event-flow/Dockerfile",
    registryRepository: "unidesk/oa-event-flow",
    composeService: "oa-event-flow",
    containerName: "oa-event-flow-backend",
    targetImage: "oa-event-flow",
    deployEnvPrefix: "UNIDESK_OA_EVENT_FLOW_DEPLOY",
  },
  {
    serviceId: "todo-note",
    sourceRepo: "https://gitee.com/Lyon1998/todo_note",
    dockerfile: "Dockerfile",
    registryRepository: "unidesk/todo-note",
    composeService: "todo-note",
    containerName: "todo-note-backend",
    targetImage: "todo-note",
    deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
  },
  {
    serviceId: "baidu-netdisk",
    sourceRepo: "https://github.com/pikasTech/unidesk",
    dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile",
    registryRepository: "unidesk/baidu-netdisk",
    composeService: "baidu-netdisk",
    containerName: "baidu-netdisk-backend",
    targetImage: "baidu-netdisk",
    deployEnvPrefix: "UNIDESK_BAIDU_NETDISK_DEPLOY",
  },
  {
    serviceId: "frontend",
    sourceRepo: "https://github.com/pikasTech/unidesk",
    dockerfile: "src/components/frontend/Dockerfile",
    registryRepository: "unidesk/frontend",
    composeService: "frontend",
    containerName: "unidesk-frontend",
    targetImage: "unidesk-frontend",
    deployEnvPrefix: "UNIDESK_FRONTEND_DEPLOY",
  },
];

const ciCatalog = asRecord(JSON.parse(readFileSync(rootPath("CI.json"), "utf8")) as unknown, "CI.json");
const artifacts = asArray(ciCatalog.artifacts, "CI.json.artifacts").map((item, index) => asRecord(item, `CI.json.artifacts[${index}]`));
const artifactByService = new Map(artifacts.map((item) => [String(item.serviceId), item]));

const backendCoreArtifact = asRecord(artifactByService.get("backend-core"), "CI.json artifact backend-core");
const backendCoreSource = asRecord(backendCoreArtifact.source, "CI.json artifact backend-core.source");
const backendCoreImage = asRecord(backendCoreArtifact.image, "CI.json artifact backend-core.image");
assertCondition(backendCoreArtifact.kind === "source-build", "backend-core producer must be source-build", backendCoreArtifact);
assertCondition(backendCoreArtifact.status === "supported", "backend-core producer must be supported", backendCoreArtifact);
assertCondition(backendCoreArtifact.producer === "ci publish-backend-core", "backend-core producer command must be publish-backend-core", backendCoreArtifact);
assertCondition(backendCoreSource.repo === "https://github.com/pikasTech/unidesk", "backend-core producer source repo mismatch", backendCoreSource);
assertCondition(backendCoreSource.dockerfile === "src/components/backend-core/Dockerfile", "backend-core producer dockerfile mismatch", backendCoreSource);
assertCondition(backendCoreImage.repository === "unidesk/backend-core", "backend-core registry repository mismatch", backendCoreImage);

for (const item of serviceCases) {
  const artifact = asRecord(artifactByService.get(item.serviceId), `CI.json artifact ${item.serviceId}`);
  const source = asRecord(artifact.source, `CI.json artifact ${item.serviceId}.source`);
  const image = asRecord(artifact.image, `CI.json artifact ${item.serviceId}.image`);
  assertCondition(artifact.kind === "source-build", `${item.serviceId} producer must be source-build`, artifact);
  assertCondition(artifact.status === "supported", `${item.serviceId} producer must be supported`, artifact);
  assertCondition(artifact.producer === "ci publish-user-service", `${item.serviceId} producer command must be publish-user-service`, artifact);
  assertCondition(source.repo === item.sourceRepo, `${item.serviceId} producer source repo mismatch`, source);
  assertCondition(source.dockerfile === item.dockerfile, `${item.serviceId} producer dockerfile mismatch`, source);
  assertCondition(image.repository === item.registryRepository, `${item.serviceId} registry repository mismatch`, image);
}

const plans: JsonRecord[] = [];

for (const item of serviceCases) {
  const plan = asRecord(await runArtifactRegistryCommand([
    "deploy-service",
    "--env",
    "prod",
    "--service",
    item.serviceId,
    "--commit",
    commit,
    "--dry-run",
  ]), `${item.serviceId} dry-run plan`);
  const target = asRecord(plan.target, `${item.serviceId} dry-run target`);
  const labels = asRecord(plan.requiredLabels, `${item.serviceId} dry-run labels`);
  const registryProbe = asRecord(plan.registryProbe, `${item.serviceId} dry-run registry probe`);
  const validation = asArray(plan.validation, `${item.serviceId} dry-run validation`).map(String);

  assertCondition(plan.ok === true, `${item.serviceId} dry-run must be ok`, plan);
  assertCondition(plan.dryRun === true && plan.mutation === false, `${item.serviceId} dry-run must be non-mutating`, plan);
  assertCondition(plan.environment === "prod", `${item.serviceId} dry-run must target prod`, plan);
  assertCondition(plan.serviceId === item.serviceId, `${item.serviceId} dry-run service id mismatch`, plan);
  assertCondition(plan.sourceImage === `127.0.0.1:5000/${item.registryRepository}:${commit}`, `${item.serviceId} source image mismatch`, plan);
  assertCondition(labels["unidesk.ai/service-id"] === item.serviceId, `${item.serviceId} required service label missing`, labels);
  assertCondition(labels["unidesk.ai/source-commit"] === commit, `${item.serviceId} required commit label missing`, labels);
  assertCondition(labels["unidesk.ai/dockerfile"] === item.dockerfile, `${item.serviceId} required dockerfile label missing`, labels);
  assertCondition(registryProbe.method === "HEAD", `${item.serviceId} dry-run must only plan registry HEAD probe`, registryProbe);

  assertCondition(target.kind === "compose", `${item.serviceId} target kind must be main-server Compose`, target);
  assertCondition(target.runtimeHost === "main-server", `${item.serviceId} runtime host must be main-server`, target);
  assertCondition(target.composeService === item.composeService, `${item.serviceId} compose service mismatch`, target);
  assertCondition(target.containerName === item.containerName, `${item.serviceId} container mismatch`, target);
  assertCondition(target.targetImage === item.targetImage, `${item.serviceId} stable target image mismatch`, target);
  assertCondition(target.runtimeImage === `${item.targetImage}:${commit}`, `${item.serviceId} runtime image mismatch`, target);
  assertCondition(target.deployEnvPrefix === item.deployEnvPrefix, `${item.serviceId} deploy env prefix mismatch`, target);
  assertCondition(
    target.deployCommandShape === `docker compose up -d --no-build --no-deps --force-recreate ${item.composeService}`,
    `${item.serviceId} dry-run must narrow Compose recreate command`,
    target,
  );
  if (item.serviceId === "baidu-netdisk") {
    const runtimeSecrets = asRecord(plan.runtimeSecrets, "baidu-netdisk dry-run runtimeSecrets");
    const secretSource = asRecord(runtimeSecrets.secretSource, "baidu-netdisk dry-run secretSource");
    const requirements = asArray(runtimeSecrets.requirements, "baidu-netdisk dry-run requirements").map((requirement, index) => asRecord(requirement, `baidu-netdisk dry-run requirement ${index}`));
    assertCondition(runtimeSecrets.check === "runtime-secret-presence", "baidu-netdisk dry-run should expose runtime secret contract", runtimeSecrets);
    assertCondition(secretSource.kind === "compose-env-file", "baidu-netdisk dry-run should expose canonical secret source kind", secretSource);
    assertCondition(secretSource.valuesPrinted === false && runtimeSecrets.valuesPrinted === false, "baidu-netdisk dry-run must not print secret values", runtimeSecrets);
    assertCondition(typeof runtimeSecrets.requiredSecretsPresent === "boolean", "baidu-netdisk requiredSecretsPresent should be boolean", runtimeSecrets);
    assertCondition(Array.isArray(runtimeSecrets.missingSecretKeys), "baidu-netdisk missingSecretKeys should be structured", runtimeSecrets);
    assertCondition(typeof runtimeSecrets.recommendedAction === "string" && runtimeSecrets.recommendedAction.length > 0, "baidu-netdisk recommendedAction should be explicit", runtimeSecrets);
    assertCondition(requirements.length === 3, "baidu-netdisk should list three required source secret keys", requirements);
    assertCondition(requirements.every((requirement) => requirement.valuePrinted === false), "baidu-netdisk should never mark values printed", requirements);
  }

  assertCondition(validation.some((line) => line.includes("registry /v2 manifest")), `${item.serviceId} must plan registry manifest validation`, validation);
  assertCondition(validation.some((line) => line.includes("image labels match service id, source repo, source commit, and Dockerfile")), `${item.serviceId} must plan image label validation`, validation);
  if (item.serviceId === "todo-note") {
    const runtimeProof = asRecord(plan.runtimeProof, "todo-note runtimeProof");
    const requiredEnvKeys = asArray(runtimeProof.requiredEnvKeys, "todo-note runtime proof env keys").map(String);
    assertCondition(runtimeProof.kind === "compose-container-runtime-metadata", "todo-note runtime proof kind mismatch", runtimeProof);
    assertCondition(runtimeProof.sourceDirectoryUsed === false, "todo-note runtime proof must not use source directory guesses", runtimeProof);
    assertCondition(requiredEnvKeys.includes("UNIDESK_DEPLOY_COMMIT"), "todo-note runtime proof should use generic deploy commit env", runtimeProof);
    assertCondition(requiredEnvKeys.includes("UNIDESK_TODO_NOTE_DEPLOY_COMMIT"), "todo-note runtime proof should use service deploy commit env", runtimeProof);
    assertCondition(validation.some((line) => line.includes("Compose container runtime metadata") && line.includes("not source directory guesses")), "todo-note must plan synthetic runtime proof", validation);
  } else {
    assertCondition(validation.some((line) => line.includes("health probe succeeds") && line.includes("deploy.commit/deploy.requestedCommit")), `${item.serviceId} must plan health commit validation`, validation);
  }
  assertCondition(!JSON.stringify(plan).includes("docker compose build"), `${item.serviceId} dry-run must not include compose build`, plan);
  assertCondition(!JSON.stringify(plan).includes("server rebuild"), `${item.serviceId} dry-run must not include server rebuild`, plan);

  plans.push({
    serviceId: item.serviceId,
    sourceImage: plan.sourceImage,
    composeService: target.composeService,
    containerName: target.containerName,
    deployCommandShape: target.deployCommandShape,
  });
}

const backendCoreDevPlan = asRecord(await runArtifactRegistryCommand([
  "deploy-service",
  "--env",
  "dev",
  "--service",
  "backend-core",
  "--commit",
  commit,
  "--dry-run",
]), "backend-core dev dry-run plan");
const backendCoreDevTarget = asRecord(backendCoreDevPlan.target, "backend-core dev dry-run target");
const backendCoreDevLabels = asRecord(backendCoreDevPlan.requiredLabels, "backend-core dev dry-run labels");
const backendCoreDevRegistry = asRecord(backendCoreDevPlan.registry, "backend-core dev dry-run registry");
const backendCoreDevBuild = asRecord(backendCoreDevPlan.build, "backend-core dev dry-run build");
const backendCoreDevSource = asRecord(backendCoreDevPlan.source, "backend-core dev dry-run source");
const backendCoreDevValidation = asArray(backendCoreDevPlan.validation, "backend-core dev dry-run validation").map(String);

assertCondition(backendCoreDevPlan.ok === true, "backend-core dev dry-run must be ok", backendCoreDevPlan);
assertCondition(backendCoreDevPlan.dryRun === true && backendCoreDevPlan.mutation === false, "backend-core dev dry-run must be non-mutating", backendCoreDevPlan);
assertCondition(backendCoreDevPlan.environment === "dev", "backend-core dev dry-run must target dev", backendCoreDevPlan);
assertCondition(backendCoreDevPlan.serviceId === "backend-core", "backend-core dev service id mismatch", backendCoreDevPlan);
assertCondition(backendCoreDevPlan.sourceImage === `127.0.0.1:5000/unidesk/backend-core:${commit}`, "backend-core dev source image mismatch", backendCoreDevPlan);
assertCondition(backendCoreDevRegistry.imageRef === backendCoreDevPlan.sourceImage, "backend-core dev registry imageRef mismatch", backendCoreDevRegistry);
assertCondition(backendCoreDevRegistry.digest === null, "backend-core dev dry-run must not fake a digest", backendCoreDevRegistry);
assertCondition(String(backendCoreDevRegistry.digestSource ?? "").includes("manifest HEAD"), "backend-core dev digest source should name manifest HEAD", backendCoreDevRegistry);
assertCondition(backendCoreDevSource.repo === "https://github.com/pikasTech/unidesk", "backend-core dev source repo mismatch", backendCoreDevSource);
assertCondition(backendCoreDevSource.commit === commit, "backend-core dev source commit mismatch", backendCoreDevSource);
assertCondition(backendCoreDevSource.dockerfile === "src/components/backend-core/Dockerfile", "backend-core dev source dockerfile mismatch", backendCoreDevSource);
assertCondition(backendCoreDevBuild.willCompile === false, "backend-core dev CD dry-run must not compile", backendCoreDevBuild);
assertCondition(backendCoreDevBuild.willRunCargoBuild === false, "backend-core dev CD dry-run must not run cargo build", backendCoreDevBuild);
assertCondition(backendCoreDevBuild.willRunDockerBuild === false, "backend-core dev CD dry-run must not run docker build", backendCoreDevBuild);
assertCondition(backendCoreDevBuild.willRunDockerComposeBuild === false, "backend-core dev CD dry-run must not run docker compose build", backendCoreDevBuild);
assertCondition(backendCoreDevBuild.producerBoundary === "ci publish-backend-core", "backend-core dev producer boundary mismatch", backendCoreDevBuild);
assertCondition(backendCoreDevLabels["unidesk.ai/service-id"] === "backend-core", "backend-core dev required service label missing", backendCoreDevLabels);
assertCondition(backendCoreDevLabels["unidesk.ai/source-commit"] === commit, "backend-core dev required commit label missing", backendCoreDevLabels);
assertCondition(backendCoreDevLabels["unidesk.ai/dockerfile"] === "src/components/backend-core/Dockerfile", "backend-core dev required dockerfile label missing", backendCoreDevLabels);
assertCondition(backendCoreDevTarget.kind === "d601-k3s", "backend-core dev target must be D601 k3s", backendCoreDevTarget);
assertCondition(backendCoreDevTarget.namespace === "unidesk-dev", "backend-core dev namespace mismatch", backendCoreDevTarget);
assertCondition(backendCoreDevTarget.deployment === "backend-core-dev", "backend-core dev deployment mismatch", backendCoreDevTarget);
assertCondition(backendCoreDevTarget.service === "backend-core-dev", "backend-core dev service mismatch", backendCoreDevTarget);
assertCondition(backendCoreDevTarget.stableImage === "unidesk-backend-core:dev", "backend-core dev stable image mismatch", backendCoreDevTarget);
assertCondition(backendCoreDevTarget.runtimeImage === `unidesk-backend-core:${commit}`, "backend-core dev runtime image mismatch", backendCoreDevTarget);
assertCondition(backendCoreDevTarget.manifestRepoPath === "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml", "backend-core dev manifest mismatch", backendCoreDevTarget);
assertCondition(backendCoreDevValidation.some((line) => line.includes("registry /v2 manifest")), "backend-core dev must plan registry manifest validation", backendCoreDevValidation);
assertCondition(backendCoreDevValidation.some((line) => line.includes("native k3s containerd")), "backend-core dev must plan containerd import validation", backendCoreDevValidation);
assertCondition(backendCoreDevValidation.some((line) => line.includes("deploy.commit") && line.includes("deploy.requestedCommit")), "backend-core dev must plan health commit validation", backendCoreDevValidation);
assertCondition(!JSON.stringify(backendCoreDevPlan).includes("cargo build"), "backend-core dev dry-run must not include cargo build", backendCoreDevPlan);
assertCondition(!JSON.stringify(backendCoreDevPlan).includes("docker build"), "backend-core dev dry-run must not include docker build", backendCoreDevPlan);
assertCondition(!JSON.stringify(backendCoreDevPlan).includes("docker compose build"), "backend-core dev dry-run must not include compose build", backendCoreDevPlan);
assertCondition(!JSON.stringify(backendCoreDevPlan).includes("server rebuild"), "backend-core dev dry-run must not include server rebuild", backendCoreDevPlan);

plans.push({
  serviceId: "backend-core",
  environment: "dev",
  sourceImage: backendCoreDevPlan.sourceImage,
  deployment: backendCoreDevTarget.deployment,
  service: backendCoreDevTarget.service,
  deployCommandShape: backendCoreDevTarget.deployCommandShape,
});

process.stdout.write(`${JSON.stringify({
  ok: true,
  checks: [
    "CI.json producer catalog covers backend-core publish-backend-core",
    "dev backend-core dry-run is a non-mutating D601 k3s artifact consumer",
    "dev backend-core dry-run exposes registry/source/build boundaries and does not compile",
    "CI.json producer catalog covers the main-server direct artifact services",
    "prod dry-run plans are non-mutating and commit-pinned",
    "dry-run targets only the matching main-server Compose service with --no-build --no-deps --force-recreate",
    "dry-run plans image-label and health deploy commit validation",
  ],
  plans,
}, null, 2)}\n`);
