#!/usr/bin/env node

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

import { validateL2ExecutionPlan } from "../../packages/external-provider-contract/src/index.mjs";
import { geliosProviderPackageV12 } from "../../packages/external-provider-contract/providers/gelios/v12/index.mjs";

const PACKAGE_ID = "gelios.provider.v12";
const PREDECESSOR_VERSION = "12.0.0";
const TARGET_VERSION = "12.0.1";
const UNITS_CAPABILITY_ID = "gelios.units.current.read";
const engineRoot = resolve(
  process.env.NODEDC_ENGINE_SOURCE_ROOT || "../NODEDC_ENGINE_INFRA",
);
const executionCatalogPath = resolve(
  engineRoot,
  "nodedc-source/server/assets/execution-plans/v1/catalog.json",
);
const securityCatalogPath = resolve(
  engineRoot,
  "nodedc-source/server/assets/provider-packages/v1/catalog.json",
);

if (
  geliosProviderPackageV12.id !== PACKAGE_ID
  || geliosProviderPackageV12.version !== TARGET_VERSION
) {
  throw new Error("gelios_v12_units_items_source_version_mismatch");
}

const sourceCapabilities = new Map(
  geliosProviderPackageV12.capabilities.map((capability) => [
    capability.id,
    capability,
  ]),
);
const unitsCapability = sourceCapabilities.get(UNITS_CAPABILITY_ID);
if (
  !unitsCapability
  || unitsCapability.request.response.collectionPaths[0] !== "items"
) {
  throw new Error("gelios_v12_units_items_source_contract_invalid");
}

const executionCatalog = JSON.parse(
  await readFile(executionCatalogPath, "utf8"),
);
const executionPackage = requirePackage(executionCatalog, "execution");
assertMigratableVersion(executionPackage.version, "execution");
executionPackage.version = TARGET_VERSION;
executionPackage.contractDigest = canonicalDigest(geliosProviderPackageV12);
executionPackage.capabilities = geliosProviderPackageV12.capabilities.map(
  (capability) => ({
    id: capability.id,
    contractDigest: canonicalDigest(capability),
    requestDigest: canonicalDigest(capability.request),
    method: capability.request.method,
    url: requestUrl(capability.request),
  }),
);

for (const profile of executionPackage.profiles) {
  const plan = profile.executionPlanTemplate;
  if (!plan) throw new Error(`gelios_v12_registered_plan_missing:${profile.id}`);
  if (plan.compilerVersion !== "1.4.0") {
    throw new Error(`gelios_v12_registered_plan_compiler_drift:${profile.id}`);
  }
  plan.package.version = TARGET_VERSION;
  plan.artifacts.packageDigest = canonicalDigest(geliosProviderPackageV12);

  for (const step of plan.steps) {
    const capabilityId = step.config?.capabilityId;
    if (!capabilityId) continue;
    const capability = sourceCapabilities.get(capabilityId);
    if (!capability) {
      throw new Error(
        `gelios_v12_registered_plan_capability_missing:${profile.id}:${capabilityId}`,
      );
    }
    step.config.capabilityDigest = canonicalDigest(capability);
    if (step.kind === "provider_request") {
      step.config.request = structuredClone(capability.request);
    } else if (step.kind === "extract_items") {
      step.config.response = structuredClone(capability.request.response);
    }
  }

  const stepsById = new Map(plan.steps.map((step) => [step.id, step]));
  for (const node of plan.graphBlueprint.nodes) {
    const step = stepsById.get(node.stepId);
    if (!step) {
      throw new Error(
        `gelios_v12_registered_plan_blueprint_step_missing:${profile.id}:${node.stepId}`,
      );
    }
    node.configDigest = canonicalDigest({
      stepConfig: step.config,
      ...(node.adapterConfig ? { adapterConfig: node.adapterConfig } : {}),
    });
  }
  plan.graphBlueprint.blueprintDigest = digestWithout(
    plan.graphBlueprint,
    "blueprintDigest",
  );
  plan.executionPlanDigest = digestWithout(plan, "executionPlanDigest");

  const validation = validateL2ExecutionPlan(plan);
  if (!validation.ok) {
    throw new Error(
      `gelios_v12_registered_plan_invalid:${profile.id}:`
        + validation.errors.join(","),
    );
  }
}

const technicalProfile = executionPackage.profiles.find(
  (profile) => profile.id === "gelios.units.profile.cold.v1",
);
const technicalExtract = technicalProfile?.executionPlanTemplate?.steps.find(
  (step) => (
    step.kind === "extract_items"
    && step.config?.capabilityId === UNITS_CAPABILITY_ID
  ),
);
if (technicalExtract?.config?.response?.collectionPaths?.[0] !== "items") {
  throw new Error("gelios_v12_technical_profile_items_envelope_missing");
}

const securityCatalog = JSON.parse(
  await readFile(securityCatalogPath, "utf8"),
);
const securityPackage = requirePackage(securityCatalog, "security");
assertMigratableVersion(securityPackage.version, "security");
securityPackage.version = TARGET_VERSION;

await writeFile(
  executionCatalogPath,
  `${JSON.stringify(executionCatalog, null, 2)}\n`,
);
await writeFile(
  securityCatalogPath,
  `${JSON.stringify(securityCatalog, null, 2)}\n`,
);

console.log(JSON.stringify({
  ok: true,
  packageId: PACKAGE_ID,
  predecessorVersion: PREDECESSOR_VERSION,
  targetVersion: TARGET_VERSION,
  executionCatalogPath,
  securityCatalogPath,
  registeredProfiles: executionPackage.profiles.length,
  technicalProfile: technicalProfile.id,
  collectionPaths: technicalExtract.config.response.collectionPaths,
  compilerVersion: technicalProfile.executionPlanTemplate.compilerVersion,
}, null, 2));

function requirePackage(catalog, kind) {
  const matches = catalog.packages.filter(
    (providerPackage) => providerPackage.id === PACKAGE_ID,
  );
  if (matches.length !== 1) {
    throw new Error(`gelios_v12_${kind}_package_cardinality_invalid`);
  }
  return matches[0];
}

function assertMigratableVersion(version, kind) {
  if (![PREDECESSOR_VERSION, TARGET_VERSION].includes(version)) {
    throw new Error(
      `gelios_v12_${kind}_package_version_drift:${String(version)}`,
    );
  }
}

function requestUrl(request) {
  const url = new URL(request.path, request.baseUrl);
  for (const [name, value] of Object.entries(request.query || {})) {
    url.searchParams.append(name, String(value));
  }
  return url.toString();
}

function digestWithout(value, key) {
  const descriptor = structuredClone(value);
  delete descriptor[key];
  return canonicalDigest(descriptor);
}

function canonicalDigest(value) {
  return `sha256:${createHash("sha256").update(
    JSON.stringify(stableValue(value)),
    "utf8",
  ).digest("hex")}`;
}

function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (!value || typeof value !== "object") return value;
  return Object.fromEntries(
    Object.keys(value)
      .filter((key) => value[key] !== undefined)
      .sort()
      .map((key) => [key, stableValue(value[key])]),
  );
}
