import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";

export function artifactCatalogAuthorityPath(catalogPath) {
  return `${catalogPath}.authority.json`;
}

export function artifactCatalogSha256(catalog) {
  return createHash("sha256").update(JSON.stringify(catalog)).digest("hex");
}

export function artifactCatalogSourceCommitId(catalog) {
  const direct = text(catalog?.publish?.sourceCommitId) || text(catalog?.sourceCommitId) || text(catalog?.commitId);
  if (direct) return direct;
  const serviceCommits = unique((catalog?.services ?? []).map((service) => text(service?.sourceCommitId)).filter(Boolean));
  return serviceCommits.length === 1 ? serviceCommits[0] : null;
}

export function artifactCatalogServiceCoverage(catalog, selectedServices) {
  if (!Array.isArray(catalog?.services)) throw new Error("artifact catalog services must be an array");
  const selectedServiceIds = normalizedServiceIds(selectedServices, "selected services");
  const catalogServiceIds = normalizedServiceIds(catalog.services.map((service) => service?.serviceId), "artifact catalog services");
  const missingSelectedServices = selectedServiceIds.filter((serviceId) => !catalogServiceIds.includes(serviceId));
  const extraCatalogServices = catalogServiceIds.filter((serviceId) => !selectedServiceIds.includes(serviceId));
  return {
    status: missingSelectedServices.length > 0
      ? (extraCatalogServices.length > 0 ? "divergent" : "partial")
      : (extraCatalogServices.length > 0 ? "superset" : "exact"),
    selectedServiceCount: selectedServiceIds.length,
    selectedServicePresentCount: selectedServiceIds.length - missingSelectedServices.length,
    catalogServiceCount: catalogServiceIds.length,
    selectedServiceIds,
    catalogServiceIds,
    missingSelectedServices,
    extraCatalogServices
  };
}

export function createHydratedArtifactCatalogAuthority({ catalog, catalogPath, gitopsBranch, gitopsCommitId, selectedServices }) {
  return {
    schemaVersion: "v1",
    status: "hydrated",
    authority: "gitops-branch",
    catalogPath: normalizeCatalogPath(catalogPath),
    gitopsBranch: text(gitopsBranch),
    gitopsCommitId: text(gitopsCommitId),
    catalogSourceCommitId: artifactCatalogSourceCommitId(catalog),
    serviceCount: Array.isArray(catalog?.services) ? catalog.services.length : 0,
    catalogSha256: artifactCatalogSha256(catalog),
    coverage: artifactCatalogServiceCoverage(catalog, selectedServices)
  };
}

export function createBootstrapArtifactCatalogAuthority({ catalog, catalogPath, sourceCommitId, reason, selectedServices }) {
  return {
    schemaVersion: "v1",
    status: "bootstrap",
    authority: "source-bootstrap",
    catalogPath: normalizeCatalogPath(catalogPath),
    sourceCommitId: text(sourceCommitId),
    bootstrapReason: text(reason),
    catalogSourceCommitId: artifactCatalogSourceCommitId(catalog),
    serviceCount: Array.isArray(catalog?.services) ? catalog.services.length : 0,
    catalogSha256: artifactCatalogSha256(catalog),
    coverage: artifactCatalogServiceCoverage(catalog, selectedServices)
  };
}

export async function readArtifactCatalogSummary({ repoRoot, catalogPath, authorityPath = artifactCatalogAuthorityPath(catalogPath), catalog = undefined }) {
  const loadedCatalog = catalog === undefined ? await readJsonIfPresent(repoRoot, catalogPath) : catalog;
  const authority = await readJsonIfPresent(repoRoot, authorityPath);
  const normalizedCatalogPath = normalizeCatalogPath(catalogPath);
  if (!loadedCatalog) {
    if (authority) throw new Error(`artifact catalog authority exists without catalog ${normalizedCatalogPath}`);
    return {
      status: "missing",
      authority: "none",
      catalogPath: normalizedCatalogPath,
      authorityPath: null,
      gitopsBranch: null,
      gitopsCommitId: null,
      catalogSourceCommitId: null,
      serviceCount: 0,
      catalogSha256: null,
      coverage: null,
      bootstrapReason: null
    };
  }

  const serviceCount = Array.isArray(loadedCatalog.services) ? loadedCatalog.services.length : 0;
  const catalogSha256 = artifactCatalogSha256(loadedCatalog);
  const catalogSourceCommitId = artifactCatalogSourceCommitId(loadedCatalog);
  if (!authority) {
    return {
      status: "loaded",
      authority: "workspace-file",
      catalogPath: normalizedCatalogPath,
      authorityPath: null,
      gitopsBranch: null,
      gitopsCommitId: null,
      catalogSourceCommitId,
      serviceCount,
      catalogSha256,
      coverage: null,
      bootstrapReason: null
    };
  }

  const coverage = validateCatalogAuthority({ authority, catalog: loadedCatalog, catalogPath: normalizedCatalogPath, serviceCount, catalogSha256 });
  if (authority.authority === "gitops-branch") {
    return {
      status: "hydrated",
      authority: "gitops-branch",
      catalogPath: normalizedCatalogPath,
      authorityPath: normalizeCatalogPath(authorityPath),
      gitopsBranch: text(authority.gitopsBranch),
      gitopsCommitId: text(authority.gitopsCommitId),
      catalogSourceCommitId: text(authority.catalogSourceCommitId) || catalogSourceCommitId,
      serviceCount,
      catalogSha256,
      coverage,
      bootstrapReason: null
    };
  }
  return {
    status: "bootstrap",
    authority: "source-bootstrap",
    catalogPath: normalizedCatalogPath,
    authorityPath: normalizeCatalogPath(authorityPath),
    gitopsBranch: null,
    gitopsCommitId: null,
    sourceCommitId: text(authority.sourceCommitId),
    catalogSourceCommitId: text(authority.catalogSourceCommitId) || catalogSourceCommitId,
    serviceCount,
    catalogSha256,
    coverage,
    bootstrapReason: text(authority.bootstrapReason)
  };
}

function validateCatalogAuthority({ authority, catalog, catalogPath, serviceCount, catalogSha256 }) {
  const gitopsAuthority = authority.schemaVersion === "v1" && authority.status === "hydrated" && authority.authority === "gitops-branch";
  const bootstrapAuthority = authority.schemaVersion === "v1" && authority.status === "bootstrap" && authority.authority === "source-bootstrap";
  if (!gitopsAuthority && !bootstrapAuthority) {
    throw new Error(`artifact catalog authority for ${catalogPath} is not a supported authority record`);
  }
  if (normalizeCatalogPath(authority.catalogPath) !== catalogPath) {
    throw new Error(`artifact catalog authority path mismatch: expected ${catalogPath}, got ${authority.catalogPath ?? "missing"}`);
  }
  if (authority.serviceCount !== serviceCount) {
    throw new Error(`artifact catalog authority service count mismatch for ${catalogPath}: expected ${serviceCount}, got ${authority.serviceCount ?? "missing"}`);
  }
  if (authority.catalogSha256 !== catalogSha256) {
    throw new Error(`artifact catalog authority fingerprint mismatch for ${catalogPath}`);
  }
  const coverage = artifactCatalogServiceCoverage(catalog, authority.coverage?.selectedServiceIds);
  validateCoverageRecord(authority.coverage, coverage, catalogPath);
  if (gitopsAuthority && (!text(authority.gitopsBranch) || !/^[a-f0-9]{40}$/u.test(text(authority.gitopsCommitId) ?? ""))) {
    throw new Error(`artifact catalog authority gitops provenance is incomplete for ${catalogPath}`);
  }
  if (bootstrapAuthority) {
    const sourceCommitId = text(authority.sourceCommitId);
    if (!/^[a-f0-9]{40}$/u.test(sourceCommitId ?? "") || !text(authority.bootstrapReason)) {
      throw new Error(`artifact catalog bootstrap authority is incomplete for ${catalogPath}`);
    }
    if (sourceCommitId !== artifactCatalogSourceCommitId(catalog)) {
      throw new Error(`artifact catalog bootstrap source commit mismatch for ${catalogPath}`);
    }
  }
  return coverage;
}

async function readJsonIfPresent(repoRoot, filePath) {
  const resolved = path.isAbsolute(filePath) ? filePath : path.join(repoRoot, filePath);
  try {
    return JSON.parse(await readFile(resolved, "utf8"));
  } catch (error) {
    if (error?.code === "ENOENT") return null;
    throw error;
  }
}

function normalizeCatalogPath(value) {
  return String(value ?? "").replaceAll("\\", "/").replace(/^\.\//u, "").trim();
}

function text(value) {
  const result = typeof value === "string" ? value.trim() : "";
  return result || null;
}

function unique(values) {
  return [...new Set(values)];
}

function normalizedServiceIds(values, label) {
  if (!Array.isArray(values) || values.length === 0) throw new Error(`${label} must be a non-empty array`);
  const normalized = values.map(text);
  if (normalized.some((value) => value === null)) throw new Error(`${label} contains an empty service id`);
  const deduplicated = unique(normalized);
  if (deduplicated.length !== normalized.length) throw new Error(`${label} contains duplicate service ids`);
  return deduplicated.sort();
}

function validateCoverageRecord(actual, expected, catalogPath) {
  const scalarFields = ["status", "selectedServiceCount", "selectedServicePresentCount", "catalogServiceCount"];
  const arrayFields = ["selectedServiceIds", "catalogServiceIds", "missingSelectedServices", "extraCatalogServices"];
  const expectedFields = [...scalarFields, ...arrayFields].sort();
  const actualFields = actual && typeof actual === "object" && !Array.isArray(actual)
    ? Object.keys(actual).sort()
    : [];
  if (!sameArray(actualFields, expectedFields)) {
    throw new Error(`artifact catalog authority coverage fields mismatch for ${catalogPath}`);
  }
  for (const field of scalarFields) {
    if (actual[field] !== expected[field]) {
      throw new Error(`artifact catalog authority coverage ${field} mismatch for ${catalogPath}`);
    }
  }
  for (const field of arrayFields) {
    if (!sameArray(actual[field], expected[field])) {
      throw new Error(`artifact catalog authority coverage ${field} mismatch for ${catalogPath}`);
    }
  }
}

function sameArray(left, right) {
  return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => value === right[index]);
}
