#!/usr/bin/env node
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
import { readStructuredFile } from "./src/structured-config.mjs";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");

const catalogPath = "deploy/artifact-catalog.dev.json";
const deployPath = "deploy/deploy.yaml";
const healthContractPath = "deploy/k8s/dev/health-contract.yaml";
const commitPattern = /^[a-f0-9]{7,40}$/;
const digestPattern = /^sha256:[a-f0-9]{64}$/;
const mutableTags = new Set(["latest", "dev", "main", "master", "prod", "production"]);
const catalogModes = new Set(["contract-skeleton", "published"]);
const skeletonProvenances = new Set(["not_available_in_mvp_skeleton", "not_available_until_publish"]);
const sourceStates = new Set(["source-present", "intentionally-disabled"]);
const artifactScopes = new Set(["required", "disabled"]);
const requiredForbiddenItems = [
  "prod-deploy",
  "prod-profile-enabled",
  "prod-namespace",
  "real-ci-publish-claim",
  "real-dev-deploy",
  "secret-material",
  "unidesk-runtime-substitute",
  "force-push"
];
const forbiddenDeployArtifactFields = new Set([
  "commitId",
  "image",
  "imageTag",
  "digest",
  "sourceCommitId",
  "repositoryDigest",
  "buildCreatedAt",
  "buildSource",
  "componentCommitId",
  "componentInputHash",
  "dockerfileHash",
  "baseImageReference",
  "baseImageDigest",
  "buildArgsHash",
  "ciAffected",
  "ciReason",
  "reuse",
  "reusedFrom",
  "publishState"
]);
const forbiddenDeployArtifactEnv = new Set([
  "HWLAB_COMMIT_ID",
  "HWLAB_IMAGE",
  "HWLAB_IMAGE_TAG",
  "HWLAB_IMAGE_DIGEST",
  "HWLAB_REVISION",
  "HWLAB_BUILD_CREATED_AT",
  "HWLAB_BUILD_SOURCE",
  "HWLAB_SKILLS_COMMIT_ID"
]);

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

function assertUnique(name, values) {
  assert.equal(new Set(values).size, values.length, `${name} must be unique`);
}

function assertString(value, context) {
  assert.equal(typeof value, "string", `${context} must be a string`);
  assert.ok(value.length > 0, `${context} must not be empty`);
}

function assertCommitId(value, context) {
  assertString(value, context);
  assert.match(value, commitPattern, `${context} must be a short or full lowercase Git SHA`);
}

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

function assertNoMutableTag(tag, context) {
  assert.equal(mutableTags.has(tag), false, `${context} must not use mutable tag ${tag}`);
}

function assertDevOnlyCatalog(catalog) {
  assert.equal(catalog.catalogVersion, "v1", "catalogVersion");
  assert.equal(catalog.kind, "hwlab-artifact-catalog", "catalog kind");
  assert.equal(catalog.environment, ENVIRONMENT_DEV, "catalog environment");
  assert.equal(catalog.profile, ENVIRONMENT_DEV, "catalog profile");
  assert.equal(catalog.namespace, "hwlab-dev", "catalog namespace");
  assert.equal(catalog.endpoint, DEV_ENDPOINT, "catalog endpoint");
  assertCommitId(catalog.commitId, "catalog commitId");
  assert.ok(catalogModes.has(catalog.artifactState), "catalog artifactState must be contract-skeleton or published");

  assert.deepEqual(catalog.allowedProfiles, [ENVIRONMENT_DEV], "only dev profile is allowed");
  assert.deepEqual(catalog.forbiddenProfiles, ["prod"], "prod profile must be forbidden");
  assert.ok(catalog.publish && typeof catalog.publish === "object", "catalog publish must be an object");

  if (catalog.artifactState === "contract-skeleton") {
    assert.equal(catalog.publish.ciPublished, false, "skeleton catalog must not claim CI publish");
    assert.equal(catalog.publish.registryVerified, false, "skeleton catalog must not claim registry verification");
    assert.ok(skeletonProvenances.has(catalog.publish.provenance), "skeleton catalog provenance must say publish evidence is unavailable");
  } else {
    assert.equal(catalog.publish.ciPublished, true, "published catalog must claim CI publish");
    assert.equal(catalog.publish.registryVerified, true, "published catalog must claim registry verification");
    assertString(catalog.publish.provenance, "published catalog provenance");
    assert.equal(skeletonProvenances.has(catalog.publish.provenance), false, "published catalog provenance must name real publish evidence");
  }

  assert.equal(catalog.healthContract.method, "GET", "health method");
  assert.equal(catalog.healthContract.path, "/health/live", "health path");
  assert.equal(catalog.healthContract.responseFormat, "json", "health response format");
  assert.ok(catalog.healthContract.requiredFields.includes("serviceId"), "health requires serviceId");
  assert.ok(catalog.healthContract.requiredFields.includes("environment"), "health requires environment");
  assert.ok(catalog.healthContract.requiredFields.includes("status"), "health requires status");

  const forbiddenIds = catalog.forbiddenItems.map((item) => item.id);
  assertUnique("catalog forbidden item ids", forbiddenIds);
  for (const required of requiredForbiddenItems) {
    assert.ok(forbiddenIds.includes(required), `catalog forbiddenItems missing ${required}`);
  }

  return catalog.artifactState;
}

function assertServiceInventory(catalog) {
  assert.ok(catalog.serviceInventory && typeof catalog.serviceInventory === "object", "catalog serviceInventory must be an object");
  assert.equal(catalog.serviceInventory.version, "v2", "catalog serviceInventory.version");
  assert.deepEqual(catalog.serviceInventory.requiredServiceIds, SERVICE_IDS, "catalog required service IDs");
  assert.deepEqual(catalog.serviceInventory.disabledServiceIds, [], "catalog disabled service IDs");
  assert.equal(catalog.serviceInventory.serviceCount, SERVICE_IDS.length, "catalog serviceInventory.serviceCount");
  assert.equal(catalog.serviceInventory.requiredServiceCount, catalog.serviceInventory.requiredServiceIds.length, "catalog requiredServiceCount");
  assert.equal(catalog.serviceInventory.disabledServiceCount, catalog.serviceInventory.disabledServiceIds.length, "catalog disabledServiceCount");
}

function assertDeployManifest(deployManifest, catalog) {
  assert.equal(deployManifest.manifestVersion, "v1", "deploy manifestVersion");
  assert.equal(deployManifest.environment, ENVIRONMENT_DEV, "deploy environment");
  assert.equal(Object.hasOwn(deployManifest, "commitId"), false, "deploy commitId is generated artifact identity and must stay in artifact catalog");
  assert.equal(deployManifest.namespace, catalog.namespace, "deploy namespace must match catalog");
  assert.equal(deployManifest.endpoint, catalog.endpoint, "deploy endpoint must match catalog");
  assert.equal(deployManifest.profiles.dev.enabled, true, "deploy dev profile must be enabled");
  assert.equal(deployManifest.profiles.dev.namespace, catalog.namespace, "deploy dev namespace must match catalog");
  assert.equal(deployManifest.profiles.dev.endpoint, catalog.endpoint, "deploy dev endpoint must match catalog");
  assert.ok(deployManifest.profiles.prod, "deploy may keep disabled prod placeholder");
  assert.equal(deployManifest.profiles.prod.enabled, false, "deploy prod profile must stay disabled");
  for (const field of Object.keys(deployManifest)) {
    assert.equal(forbiddenDeployArtifactFields.has(field), false, `deploy ${field} is generated artifact identity and must stay in artifact catalog`);
  }
  for (const service of deployManifest.services ?? []) {
    for (const field of Object.keys(service)) {
      assert.equal(forbiddenDeployArtifactFields.has(field), false, `deploy service ${service.serviceId}.${field} is generated artifact identity and must stay in artifact catalog`);
    }
    for (const envName of Object.keys(service.env ?? {})) {
      assert.equal(forbiddenDeployArtifactEnv.has(envName), false, `deploy service ${service.serviceId}.env.${envName} is generated artifact identity and must be rendered from artifact catalog`);
    }
  }
}

function assertCatalogServices(catalog, deployManifest, catalogMode) {
  assert.ok(Array.isArray(catalog.services), "catalog services must be an array");
  assert.ok(Array.isArray(deployManifest.services), "deploy services must be an array");

  const catalogServiceIds = catalog.services.map((service) => service.serviceId);
  const deployServiceIds = deployManifest.services.map((service) => service.serviceId);
  assert.deepEqual(catalogServiceIds, SERVICE_IDS, "catalog must cover frozen service ids in order");
  assert.deepEqual(deployServiceIds, SERVICE_IDS, "deploy manifest must cover frozen service ids in order");
  assertUnique("catalog service ids", catalogServiceIds);
  assertUnique("deploy service ids", deployServiceIds);

  const deployByServiceId = new Map(deployManifest.services.map((service) => [service.serviceId, service]));
  const requiredServiceIds = new Set(catalog.serviceInventory.requiredServiceIds);
  const disabledServiceIds = new Set(catalog.serviceInventory.disabledServiceIds);
  for (const service of catalog.services) {
    const context = `service ${service.serviceId}`;
    const deployService = deployByServiceId.get(service.serviceId);
    assert.ok(deployService, `${context} missing from deploy manifest`);

    assertCommitId(service.commitId, `${context} commitId`);
    assert.equal(service.imageTag, service.commitId.slice(0, 7), `${context} imageTag must be the short commit`);
    assertNoMutableTag(service.imageTag, `${context} imageTag`);

    const image = imageParts(service.image, `${context} image`);
    assert.equal(image.repositoryName, service.serviceId, `${context} image repository must end with serviceId`);
    assert.equal(image.tag, service.imageTag, `${context} image tag`);

    assert.equal(service.profile, ENVIRONMENT_DEV, `${context} profile`);
    assert.equal(service.profile, deployService.profile, `${context} profile must match deploy manifest`);
    assert.equal(service.namespace, catalog.namespace, `${context} namespace`);
    assert.equal(service.namespace, deployService.namespace, `${context} namespace must match deploy manifest`);
    assert.equal(service.healthPath, catalog.healthContract.path, `${context} healthPath`);
    assert.equal(service.healthPath, deployService.healthPath, `${context} healthPath must match deploy manifest`);
    assert.ok(sourceStates.has(service.sourceState), `${context} sourceState must be source-present or intentionally-disabled`);
    if (service.sourceState === "intentionally-disabled") {
      assert.equal(deployService.replicas, 0, `${context} intentionally-disabled services must have zero deploy replicas`);
    }

    assert.ok(artifactScopes.has(service.artifactScope), `${context} artifactScope`);
    assert.equal(typeof service.publishEnabled, "boolean", `${context} publishEnabled`);
    assert.equal(typeof service.artifactRequired, "boolean", `${context} artifactRequired`);
    assert.equal(service.artifactRequired, requiredServiceIds.has(service.serviceId), `${context} artifactRequired must match serviceInventory`);
    if (requiredServiceIds.has(service.serviceId)) {
      assert.equal(service.artifactScope, "required", `${context} artifactScope`);
    } else {
      assert.ok(disabledServiceIds.has(service.serviceId), `${context} disabled service must be in serviceInventory`);
      assert.equal(service.artifactScope, "disabled", `${context} artifactScope`);
      assert.equal(service.publishEnabled, false, `${context} publishEnabled`);
      assert.equal(service.notPublishedReason.startsWith("disabled_"), true, `${context} notPublishedReason must explain disabled service`);
    }

    if (catalogMode === "contract-skeleton") {
      assert.equal(service.publishState, "skeleton-only", `${context} publishState`);
      assert.equal(service.digest, "not_published", `${context} digest`);
    } else {
      if (service.artifactRequired) {
        assert.ok(["published", "reused"].includes(service.publishState), `${context} publishState`);
        assert.match(service.digest, digestPattern, `${context} digest`);
      } else {
        assert.equal(service.publishState, "skeleton-only", `${context} publishState`);
        assert.equal(service.digest, "not_published", `${context} digest`);
      }
    }
  }
}

function assertHealthContract(healthContract, catalog) {
  assert.equal(healthContract.kind, "ConfigMap", "health contract kind");
  assert.equal(healthContract.metadata.namespace, catalog.namespace, "health contract namespace");
  assert.equal(healthContract.metadata.labels["hwlab.pikastech.local/profile"], ENVIRONMENT_DEV, "health contract profile label");
  assert.equal(healthContract.data.endpoint, catalog.endpoint, "health contract endpoint");
  assert.ok(healthContract.data["cloud-api"].includes(catalog.healthContract.path), "cloud-api health contract path");
  assert.ok(healthContract.data["edge-proxy"].includes(catalog.healthContract.path), "edge-proxy health contract path");
  assert.ok(
    healthContract.data["runtime-substitute-policy"].includes("Do not replace HWLAB runtime"),
    "health contract must include runtime substitute policy"
  );
}

const catalog = await readJSON(catalogPath);
const deployManifest = await readJSON(deployPath);
const healthContract = await readJSON(healthContractPath);

const catalogMode = assertDevOnlyCatalog(catalog);
assertServiceInventory(catalog);
assertDeployManifest(deployManifest, catalog);
assertCatalogServices(catalog, deployManifest, catalogMode);
assertHealthContract(healthContract, catalog);

console.log(`validated ${catalog.services.length} DEV artifact catalog services at ${catalog.commitId} (${catalogMode})`);
