import { existsSync, readFileSync } from "node:fs";

import { rootPath } from "./config";

export type CiArtifactStatus = "supported" | "blocked";

export type CiSourceBuildProducer = "ci publish-backend-core" | "ci publish-user-service" | "platform-infra pipelines-as-code";

export type CiCatalogArtifact = CiSourceBuildCatalogArtifact | CiUpstreamImageCatalogArtifact;

export interface CiCatalog {
  schemaVersion: number;
  kind: "ci-artifact-catalog";
  purpose: string;
  summaryContract: {
    requiredOnSuccess: string[];
    fieldSemantics: Record<string, string>;
  };
  defaults: {
    registry: string;
    tagTemplate: string;
    mutableTagsAllowed: boolean;
    runtimeFieldsForbidden: string[];
  };
  artifacts: CiCatalogArtifact[];
}

export interface CiSourceBuildCatalogArtifact {
  serviceId: string;
  kind: "source-build";
  status: CiArtifactStatus;
  producer: CiSourceBuildProducer;
  source: {
    repo: string;
    dockerfile: string;
    root?: string;
  };
  image: {
    repository: string;
  };
  notes?: string;
  blockedReason?: string;
}

export interface CiUpstreamImageCatalogArtifact {
  serviceId: string;
  kind: "upstream-image";
  status: "blocked";
  producer: "ci publish-user-service";
  upstream: {
    imageRef: string;
    digestRef: string;
    sourceRepo?: string;
    sourceRevision?: string;
    mirrorRepository: string;
    mirrorTag: string;
    mirrorDigestRef: string;
  };
  notes?: string;
  blockedReason: string;
}

let cachedCatalog: CiCatalog | null = null;

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

function stringField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = obj[key];
  if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
  return value;
}

function stringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] {
  const value = obj[key];
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) {
    throw new Error(`${path}.${key} must be an array of non-empty strings`);
  }
  return value as string[];
}

function optionalStringField(obj: Record<string, unknown>, key: string, path: string): string | undefined {
  const value = obj[key];
  if (value === undefined) return undefined;
  if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
  return value;
}

function optionalStringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] | undefined {
  const value = obj[key];
  if (value === undefined) return undefined;
  return stringArrayField(obj, key, path);
}

function optionalBooleanField(obj: Record<string, unknown>, key: string, path: string): boolean | undefined {
  const value = obj[key];
  if (value === undefined) return undefined;
  if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
  return value;
}

function requiredCatalogPath(value: string, label: string): string {
  if (value.length === 0 || value.startsWith("/") || value.includes("\0") || value.split("/").includes("..")) {
    throw new Error(`${label} must be a repo-relative path`);
  }
  return value;
}

function requiredImageRepository(value: string, label: string): string {
  if (value.length === 0 || value.startsWith("/") || value.includes("..") || value.includes(":") || value.includes("@") || value.includes("latest") || !/^[a-z0-9._/-]+$/u.test(value)) {
    throw new Error(`${label} must be a registry repository path without registry host, tag, digest, latest, or uppercase characters`);
  }
  return value;
}

function stringRecordField(obj: Record<string, unknown>, key: string, path: string): Record<string, string> {
  const value = asRecord(obj[key], `${path}.${key}`);
  for (const [entryKey, entryValue] of Object.entries(value)) {
    if (typeof entryValue !== "string" || entryValue.length === 0) {
      throw new Error(`${path}.${key}.${entryKey} must be a non-empty string`);
    }
  }
  return value as Record<string, string>;
}

function validateSourceBuildArtifact(item: Record<string, unknown>, index: number): CiSourceBuildCatalogArtifact {
  const path = `artifacts[${index}]`;
  const source = asRecord(item.source, `${path}.source`);
  const image = asRecord(item.image, `${path}.image`);
  const status = stringField(item, "status", path);
  if (status !== "supported" && status !== "blocked") throw new Error(`${path}.status must be supported or blocked`);
  const producer = stringField(item, "producer", path);
  if (producer !== "ci publish-backend-core" && producer !== "ci publish-user-service" && producer !== "platform-infra pipelines-as-code") {
    throw new Error(`${path}.producer must be ci publish-backend-core, ci publish-user-service, or platform-infra pipelines-as-code`);
  }
  const artifact: CiSourceBuildCatalogArtifact = {
    serviceId: stringField(item, "serviceId", path),
    kind: "source-build",
    status,
    producer,
    source: {
      repo: stringField(source, "repo", `${path}.source`),
      dockerfile: requiredCatalogPath(stringField(source, "dockerfile", `${path}.source`), `${path}.source.dockerfile`),
      ...(source.root === undefined ? {} : { root: requiredCatalogPath(stringField(source, "root", `${path}.source`), `${path}.source.root`) }),
    },
    image: {
      repository: requiredImageRepository(stringField(image, "repository", `${path}.image`), `${path}.image.repository`),
    },
    ...(optionalStringField(item, "notes", path) === undefined ? {} : { notes: optionalStringField(item, "notes", path) }),
    ...(optionalStringField(item, "blockedReason", path) === undefined ? {} : { blockedReason: optionalStringField(item, "blockedReason", path) }),
  };
  if (artifact.status === "blocked" && artifact.blockedReason === undefined) {
    throw new Error(`${path}.blockedReason is required when status=blocked`);
  }
  if (artifact.status === "supported" && artifact.blockedReason !== undefined) {
    throw new Error(`${path}.blockedReason is only allowed when status=blocked`);
  }
  return artifact;
}

function validateUpstreamImageArtifact(item: Record<string, unknown>, index: number): CiUpstreamImageCatalogArtifact {
  const path = `artifacts[${index}]`;
  const upstream = asRecord(item.upstream, `${path}.upstream`);
  const producer = stringField(item, "producer", path);
  if (producer !== "ci publish-user-service") throw new Error(`${path}.producer must be ci publish-user-service`);
  const artifact: CiUpstreamImageCatalogArtifact = {
    serviceId: stringField(item, "serviceId", path),
    kind: "upstream-image",
    status: "blocked",
    producer,
    upstream: {
      imageRef: stringField(upstream, "imageRef", `${path}.upstream`),
      digestRef: stringField(upstream, "digestRef", `${path}.upstream`),
      ...(optionalStringField(upstream, "sourceRepo", `${path}.upstream`) === undefined ? {} : { sourceRepo: optionalStringField(upstream, "sourceRepo", `${path}.upstream`) }),
      ...(optionalStringField(upstream, "sourceRevision", `${path}.upstream`) === undefined ? {} : { sourceRevision: optionalStringField(upstream, "sourceRevision", `${path}.upstream`) }),
      mirrorRepository: requiredImageRepository(stringField(upstream, "mirrorRepository", `${path}.upstream`), `${path}.upstream.mirrorRepository`),
      mirrorTag: stringField(upstream, "mirrorTag", `${path}.upstream`),
      mirrorDigestRef: stringField(upstream, "mirrorDigestRef", `${path}.upstream`),
    },
    blockedReason: stringField(item, "blockedReason", path),
    ...(optionalStringField(item, "notes", path) === undefined ? {} : { notes: optionalStringField(item, "notes", path) }),
  };
  return artifact;
}

function validateCatalogArtifact(item: unknown, index: number): CiCatalogArtifact {
  const record = asRecord(item, `artifacts[${index}]`);
  const kind = stringField(record, "kind", `artifacts[${index}]`);
  if (kind === "source-build") return validateSourceBuildArtifact(record, index);
  if (kind === "upstream-image") return validateUpstreamImageArtifact(record, index);
  throw new Error(`artifacts[${index}].kind must be source-build or upstream-image`);
}

function assertUniqueServiceIds(artifacts: CiCatalogArtifact[]): void {
  const seen = new Set<string>();
  for (const artifact of artifacts) {
    if (seen.has(artifact.serviceId)) throw new Error(`CI.json.artifacts contains duplicate serviceId: ${artifact.serviceId}`);
    seen.add(artifact.serviceId);
  }
}

export function loadCiCatalog(): CiCatalog {
  if (cachedCatalog !== null) return cachedCatalog;
  const path = rootPath("CI.json");
  if (!existsSync(path)) throw new Error(`CI.json not found at ${path}`);
  const parsed = asRecord(JSON.parse(readFileSync(path, "utf8")) as unknown, "CI.json");
  const schemaVersion = parsed.schemaVersion;
  if (schemaVersion !== 2) throw new Error("CI.json schemaVersion must be 2");
  const kind = stringField(parsed, "kind", "CI.json");
  if (kind !== "ci-artifact-catalog") throw new Error("CI.json kind must be ci-artifact-catalog");
  const summaryContract = asRecord(parsed.summaryContract, "CI.json.summaryContract");
  const defaults = asRecord(parsed.defaults, "CI.json.defaults");
  const artifacts = Array.isArray(parsed.artifacts) ? parsed.artifacts.map((item, index) => validateCatalogArtifact(item, index)) : [];
  if (artifacts.length === 0) throw new Error("CI.json.artifacts must not be empty");
  assertUniqueServiceIds(artifacts);
  cachedCatalog = {
    schemaVersion,
    kind,
    purpose: stringField(parsed, "purpose", "CI.json"),
    summaryContract: {
      requiredOnSuccess: stringArrayField(summaryContract, "requiredOnSuccess", "CI.json.summaryContract"),
      fieldSemantics: stringRecordField(summaryContract, "fieldSemantics", "CI.json.summaryContract"),
    },
    defaults: {
      registry: stringField(defaults, "registry", "CI.json.defaults"),
      tagTemplate: stringField(defaults, "tagTemplate", "CI.json.defaults"),
      mutableTagsAllowed: optionalBooleanField(defaults, "mutableTagsAllowed", "CI.json.defaults") ?? false,
      runtimeFieldsForbidden: optionalStringArrayField(defaults, "runtimeFieldsForbidden", "CI.json.defaults") ?? [],
    },
    artifacts,
  };
  return cachedCatalog;
}

export function findCiCatalogArtifact(serviceId: string): CiCatalogArtifact | null {
  return loadCiCatalog().artifacts.find((artifact) => artifact.serviceId === serviceId) ?? null;
}

export function supportedSourceBuildArtifactIds(producer?: CiSourceBuildProducer): string[] {
  return loadCiCatalog().artifacts
    .filter((artifact): artifact is CiSourceBuildCatalogArtifact => artifact.kind === "source-build" && artifact.status === "supported" && (producer === undefined || artifact.producer === producer))
    .map((artifact) => artifact.serviceId);
}

export function blockedCatalogArtifactIds(): string[] {
  return loadCiCatalog().artifacts.filter((artifact) => artifact.status === "blocked").map((artifact) => artifact.serviceId);
}

export function catalogSummary(): {
  totalArtifacts: number;
  sourceBuildArtifacts: number;
  supportedSourceBuildArtifacts: number;
  upstreamImageArtifacts: number;
  blockedArtifacts: number;
} {
  const catalog = loadCiCatalog();
  return {
    totalArtifacts: catalog.artifacts.length,
    sourceBuildArtifacts: catalog.artifacts.filter((artifact) => artifact.kind === "source-build").length,
    supportedSourceBuildArtifacts: catalog.artifacts.filter((artifact) => artifact.kind === "source-build" && artifact.status === "supported").length,
    upstreamImageArtifacts: catalog.artifacts.filter((artifact) => artifact.kind === "upstream-image").length,
    blockedArtifacts: catalog.artifacts.filter((artifact) => artifact.status === "blocked").length,
  };
}
