#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { cp, lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createRequire, Module } from "node:module";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const here = dirname(fileURLToPath(import.meta.url));
const platformRoot = resolve(here, "../..");
const engineRoot = resolve(platformRoot, "../NODEDC_ENGINE_INFRA");
const artifactRoot = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || join(here, "../deploy-artifacts"));
const stageArtifact = resolve(
  process.env.NODEDC_N8N_EXTENSION_STAGE_ARTIFACT
    || join(artifactRoot, "nodedc-n8n-private-extension-n8n-nodes-ndc-replace-replay-v1-20260721-009.tgz"),
);
const predecessorArtifact = resolve(
  process.env.NODEDC_N8N_EXTENSION_PREDECESSOR_ARTIFACT
    || join(artifactRoot, "nodedc-engine-n8n-private-extension-20260721-005.tgz"),
);

const stageArtifactSha256 = "4984d4937e2c8de1ecbf5b836cf7fafac131cc0dc64d49c06034507d0cb94577";
const predecessorArtifactSha256 = "037d4790b03c0b245b543fe2a841d28f3cfc7822b9a12f2bff9324bf1328b395";
const releaseId = "0.1.6-25cc2d7a52a0d0ee";
const packageVersion = "0.1.6";
const packageSha256 = "25cc2d7a52a0d0ee288b7a4b3de9f436489c010f234498a94bcccd114d7355af";
const n8nVersion = "2.3.2";
const baseImage = "docker.n8n.io/n8nio/n8n:2.3.2";
const architecture = "amd64";
const generatedAt = "2026-07-21T12:00:00.000Z";
const previouslyIssuedTransitionIds = new Set([
  "20260715-002",
  "20260716-003",
  "20260717-004",
  "20260717-005",
  "20260718-006",
  "20260718-007",
  "20260718-008",
  "20260721-005",
  "20260721-008",
]);
const transitionId = readTransitionId(process.argv.slice(2), process.env.NODEDC_N8N_TRANSITION_ID);
const activationId = `engine-n8n-private-extension-${transitionId}`;
const rollbackId = `engine-n8n-private-extension-rollback-${transitionId}`;

const transitionRoot = "nodedc-source/services/n8n/private-extensions";
const descriptorRel = `${transitionRoot}/ndc-activation.json`;
const overrideRel = `${transitionRoot}/docker-compose.ndc-private-extension.yml`;
const schemaRoot = "nodedc-source/server/assets/n8n/schema/v2.3.2";
const nodesCatalogRel = `${schemaRoot}/nodes.catalog.json`;
const credentialsCatalogRel = `${schemaRoot}/credentials.catalog.json`;
const metaRel = `${schemaRoot}/meta.json`;
const iconRoot = "nodedc-source/server/assets/n8n/icons";
const iconRel = `${iconRoot}/ndc.svg`;
const darkIconRel = `${iconRoot}/ndc.dark.svg`;
const runtimePackagePath = "/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc";

const expectedNodeTypes = [
  "n8n-nodes-ndc.ndcDataProductPublish",
  "n8n-nodes-ndc.ndcDataProductRead",
  "n8n-nodes-ndc.ndcFoundryBinding",
];
const expectedCredentialTypes = [
  "ndcDataProductWriterApi",
  "ndcDataProductReaderApi",
  "ndcFoundryBindingApi",
  "ndcProviderRotatingAccessApi",
];
const predecessorCredentialTypes = [...expectedCredentialTypes];
const nodeModules = [
  ["dist/nodes/NdcDataProductPublish/NdcDataProductPublish.node.js", "NdcDataProductPublish"],
  ["dist/nodes/NdcDataProductRead/NdcDataProductRead.node.js", "NdcDataProductRead"],
  ["dist/nodes/NdcFoundryBinding/NdcFoundryBinding.node.js", "NdcFoundryBinding"],
];
const credentialModules = [
  ["dist/credentials/NdcDataProductWriterApi.credentials.js", "NdcDataProductWriterApi"],
  ["dist/credentials/NdcDataProductReaderApi.credentials.js", "NdcDataProductReaderApi"],
  ["dist/credentials/NdcFoundryBindingApi.credentials.js", "NdcFoundryBindingApi"],
  ["dist/credentials/NdcProviderRotatingAccessApi.credentials.js", "NdcProviderRotatingAccessApi"],
];

await mkdir(artifactRoot, { recursive: true });
await assertArtifactTargetFresh(join(artifactRoot, `nodedc-${activationId}.tgz`));
await assertArtifactTargetFresh(join(artifactRoot, `nodedc-${rollbackId}.tgz`));
assertSha(await readFile(stageArtifact), stageArtifactSha256, "staging artifact");
assertSha(await readFile(predecessorArtifact), predecessorArtifactSha256, "predecessor artifact");
assertEngineBaseline(await readFile(join(engineRoot, "docker-compose.yml"), "utf8"));

const work = await mkdtemp(join(tmpdir(), "nodedc-engine-n8n-sealed-"));
try {
  extractArchive(stageArtifact, work);
  const stagedRelease = join(work, "payload", "releases", "n8n-nodes-ndc", releaseId);
  const release = JSON.parse(await readFile(join(stagedRelease, "release.json"), "utf8"));
  assertRelease(release);
  assertSha(await readFile(join(stagedRelease, "package.tgz")), packageSha256, "private package");

  const unpacked = join(work, "unpacked");
  await mkdir(unpacked);
  extractArchive(join(stagedRelease, "package.tgz"), unpacked);
  const packageRoot = join(unpacked, "package");
  const packageJson = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
  assertPackage(packageJson);

  const devNodeModules = join(platformRoot, "packages", "n8n-nodes-ndc", "node_modules");
  const nodePath = String(process.env.NODE_PATH || "").split(":").filter(Boolean);
  if (!nodePath.includes(devNodeModules)) nodePath.unshift(devNodeModules);
  process.env.NODE_PATH = nodePath.join(":");
  Module._initPaths();
  const packageRequire = createRequire(join(packageRoot, "package.json"));
  const privateNodes = nodeModules.map(([path, className], index) => {
    const NodeClass = packageRequire(join(packageRoot, path))[className];
    if (typeof NodeClass !== "function") throw new Error(`node_class_missing:${className}`);
    const description = structuredClone(new NodeClass().description);
    description.name = expectedNodeTypes[index];
    description.icon = { light: "file:ndc.svg", dark: "file:ndc.dark.svg" };
    if (Object.prototype.hasOwnProperty.call(description, "usableAsTool")) {
      throw new Error(`tool_variant_forbidden:${description.name}`);
    }
    return description;
  });
  const privateCredentials = credentialModules.map(([path, className]) => {
    const CredentialClass = packageRequire(join(packageRoot, path))[className];
    if (typeof CredentialClass !== "function") throw new Error(`credential_class_missing:${className}`);
    const description = structuredClone(new CredentialClass());
    description.icon = { light: "file:ndc.svg", dark: "file:ndc.dark.svg" };
    return description;
  });
  assertExact(privateNodes.map((item) => item.name), expectedNodeTypes, "node types");
  assertExact(privateCredentials.map((item) => item.name), expectedCredentialTypes, "credential types");
  assertPublishNodeV2(privateNodes[0]);

  const predecessorDescriptor = JSON.parse(predecessorArtifactFile(descriptorRel));
  const predecessorNodes = JSON.parse(predecessorArtifactFile(nodesCatalogRel));
  const predecessorCredentials = JSON.parse(predecessorArtifactFile(credentialsCatalogRel));
  const predecessorMeta = JSON.parse(predecessorArtifactFile(metaRel));
  assertPredecessorCatalogs(
    predecessorDescriptor,
    predecessorNodes,
    predecessorCredentials,
    predecessorMeta,
  );
  const baselineNodes = predecessorNodes.filter(
    (item) => !String(item?.name || "").startsWith("n8n-nodes-ndc."),
  );
  const baselineCredentials = predecessorCredentials.filter(
    (item) => !expectedCredentialTypes.includes(String(item?.name || "")),
  );
  assertBaselineCatalogs(baselineNodes, baselineCredentials);
  const activeNodes = [...baselineNodes, ...privateNodes];
  const activeCredentials = [...baselineCredentials, ...privateCredentials];
  const activeMeta = {
    n8nVersion,
    generatedAt,
    source: `n8n-core+n8n-nodes-ndc@${packageVersion}`,
    nodeCount: activeNodes.length,
    credentialCount: activeCredentials.length,
  };

  const target = {
    releaseId,
    packageVersion,
    packageSha256,
  };
  const predecessor = {
    releaseId: predecessorDescriptor.releaseId,
    packageVersion: predecessorDescriptor.packageVersion,
    packageSha256: predecessorDescriptor.packageSha256,
  };
  const activationDescriptor = descriptor(
    target,
    predecessor.releaseId,
    predecessor.releaseId,
    expectedCredentialTypes,
  );
  const rollbackDescriptor = descriptor(
    predecessor,
    releaseId,
    releaseId,
    predecessorCredentialTypes,
  );
  const override = composeOverride(target);
  const rollbackOverride = composeOverride(predecessor);

  const generatedRoot = join(work, "generated-engine-payload");
  await writeJson(join(generatedRoot, nodesCatalogRel), activeNodes);
  await writeJson(join(generatedRoot, credentialsCatalogRel), activeCredentials);
  await writeJson(join(generatedRoot, metaRel), activeMeta);
  await writeJson(join(generatedRoot, descriptorRel), activationDescriptor);
  await writeFile(join(generatedRoot, overrideRel), override, "utf8");
  await mkdir(join(generatedRoot, iconRoot), { recursive: true });
  await cp(join(packageRoot, "dist/icons/ndc.svg"), join(generatedRoot, iconRel), { force: false });
  await cp(join(packageRoot, "dist/icons/ndc.dark.svg"), join(generatedRoot, darkIconRel), { force: false });

  const activationEntries = [
    descriptorRel,
    overrideRel,
    nodesCatalogRel,
    credentialsCatalogRel,
    metaRel,
    iconRel,
    darkIconRel,
  ];
  const activationArtifact = await buildArtifact(work, activationId, activationEntries, async (payload) => {
    for (const rel of activationEntries) {
      await cp(join(generatedRoot, rel), join(payload, rel), { recursive: true, force: false });
    }
  });

  const rollbackEntries = [...activationEntries];
  const rollbackArtifact = await buildArtifact(work, rollbackId, rollbackEntries, async (payload) => {
    await writeJson(join(payload, descriptorRel), rollbackDescriptor);
    await writeFile(join(payload, overrideRel), rollbackOverride, "utf8");
    await writeJson(join(payload, nodesCatalogRel), predecessorNodes);
    await writeJson(join(payload, credentialsCatalogRel), predecessorCredentials);
    await writeJson(join(payload, metaRel), predecessorMeta);
    await mkdir(join(payload, iconRoot), { recursive: true });
    await cp(join(engineRoot, iconRel), join(payload, iconRel), { force: false });
    await cp(join(engineRoot, darkIconRel), join(payload, darkIconRel), { force: false });
  });

  console.log(JSON.stringify({
    ok: true,
    transitionId,
    releaseId,
    packageSha256,
    nodeTypes: expectedNodeTypes,
    credentialTypes: expectedCredentialTypes,
    activation: activationArtifact,
    rollback: rollbackArtifact,
  }, null, 2));
} finally {
  await rm(work, { recursive: true, force: true });
}

function descriptor(target, expectedCurrent, rollbackBaseline, credentialTypes) {
  return {
    schemaVersion: "nodedc.engine-n8n-private-extension-transition/v1",
    action: "activate",
    releaseId: target.releaseId,
    packageVersion: target.packageVersion,
    packageSha256: target.packageSha256,
    n8nVersion,
    baseImage,
    baseImageArchitecture: architecture,
    baseImageIdentityPolicy: "running-container-and-local-tag-must-match",
    sealedReleaseRelativePath: `n8n-private-extensions/releases/n8n-nodes-ndc/${target.releaseId}/package`,
    composeOverride: overrideRel,
    runtimePackagePath,
    topologyServices: ["n8n"],
    expectedCurrent,
    expectedNodeTypes,
    expectedCredentialTypes: credentialTypes,
    rollbackBaseline,
  };
}

function readTransitionId(args, environmentValue) {
  if (args.length > 1) throw new Error("transition_id_argument_count_invalid");
  const argumentValue = args[0] || "";
  const envValue = String(environmentValue || "").trim();
  if (argumentValue && envValue && argumentValue !== envValue) {
    throw new Error("transition_id_sources_conflict");
  }
  const value = argumentValue || envValue;
  if (!value) throw new Error("transition_id_required");
  const match = /^(\d{4})(\d{2})(\d{2})-([0-9]{3})$/.exec(value);
  if (!match || match[4] === "000") throw new Error("transition_id_invalid");
  const year = Number(match[1]);
  const month = Number(match[2]);
  const day = Number(match[3]);
  const parsed = new Date(Date.UTC(year, month - 1, day));
  if (parsed.getUTCFullYear() !== year
      || parsed.getUTCMonth() !== month - 1
      || parsed.getUTCDate() !== day) {
    throw new Error("transition_id_invalid");
  }
  if (previouslyIssuedTransitionIds.has(value)) {
    throw new Error("transition_id_already_issued");
  }
  return value;
}

async function assertArtifactTargetFresh(path) {
  try {
    await lstat(path);
  } catch (error) {
    if (error?.code === "ENOENT") return;
    throw error;
  }
  throw new Error("transition_artifact_already_exists");
}

function composeOverride(target) {
  const health = "const http=require('http');const req=http.get('http://127.0.0.1:5678/healthz/readiness',r=>{r.resume();process.exit(r.statusCode===200?0:1)});req.on('error',()=>process.exit(1));req.setTimeout(4000,()=>{req.destroy();process.exit(1)});";
  const sealedReleaseRelativePath = `n8n-private-extensions/releases/n8n-nodes-ndc/${target.releaseId}/package`;
  return [
    "services:",
    "  n8n:",
    `    image: ${baseImage}`,
    "    platform: linux/amd64",
    "    pull_policy: never",
    "    environment:",
    "      N8N_USER_FOLDER: /home/node",
    "      N8N_COMMUNITY_PACKAGES_ENABLED: \"true\"",
    "      N8N_COMMUNITY_PACKAGES_PREVENT_LOADING: \"false\"",
    "      N8N_REINSTALL_MISSING_PACKAGES: \"false\"",
    "    volumes:",
    `      - /volume2/nodedc-demo/${sealedReleaseRelativePath}:${runtimePackagePath}:ro`,
    "    healthcheck:",
    `      test: ${JSON.stringify(["CMD", "node", "-e", health])}`,
    "      interval: 10s",
    "      timeout: 5s",
    "      retries: 30",
    "      start_period: 30s",
    "    labels:",
    `      nodedc.n8n-private-extension.release: ${target.releaseId}`,
    `      nodedc.n8n-private-extension.package-sha256: ${target.packageSha256}`,
    "",
  ].join("\n");
}

async function buildArtifact(workRoot, id, entries, populate) {
  const stage = join(workRoot, id);
  const payload = join(stage, "payload");
  await mkdir(payload, { recursive: true });
  await populate(payload);
  await writeFile(join(stage, "manifest.env"), `id=${id}\ncomponent=engine\ntype=app-overlay\n`, "utf8");
  await writeFile(join(stage, "files.txt"), `${entries.join("\n")}\n`, "utf8");
  const artifact = join(artifactRoot, `nodedc-${id}.tgz`);
  run("python3", ["-c", canonicalTarScript(), artifact, stage]);
  return { id, artifact, sha256: sha(await readFile(artifact)), entries };
}

function assertRelease(value) {
  if (value?.releaseId !== releaseId
    || value?.package?.name !== "n8n-nodes-ndc"
    || value?.package?.version !== packageVersion
    || value?.package?.sha256 !== packageSha256
    || value?.storage?.relativePath !== `releases/n8n-nodes-ndc/${releaseId}`) {
    throw new Error("staged_release_identity_mismatch");
  }
}

function assertPackage(value) {
  if (value.name !== "n8n-nodes-ndc" || value.version !== packageVersion || value.private !== true) {
    throw new Error("package_identity_mismatch");
  }
  if (value.dependencies !== undefined) throw new Error("runtime_dependencies_forbidden");
  for (const name of ["preinstall", "install", "postinstall", "prepare", "prepack", "postpack"]) {
    if (value.scripts?.[name] !== undefined) throw new Error(`lifecycle_forbidden:${name}`);
  }
  assertExact(value.n8n?.nodes, nodeModules.map(([path]) => path), "package nodes");
  assertExact(value.n8n?.credentials, credentialModules.map(([path]) => path), "package credentials");
}

function assertPredecessorCatalogs(descriptorValue, nodes, credentials, meta) {
  if (descriptorValue?.action !== "activate"
    || descriptorValue?.releaseId !== "0.1.5-3c8ae53f010d7c88"
    || descriptorValue?.packageVersion !== "0.1.5"
    || descriptorValue?.packageSha256 !== "3c8ae53f010d7c88b6e90cb8fc4929e7d0880089ad8c3a9edb876ce034786743") {
    throw new Error("predecessor_descriptor_mismatch");
  }
  if (!Array.isArray(nodes) || nodes.length !== 437
    || !Array.isArray(credentials) || credentials.length !== 389
    || meta?.n8nVersion !== n8nVersion
    || meta?.source !== "n8n-core+n8n-nodes-ndc@0.1.5"
    || meta?.nodeCount !== 437
    || meta?.credentialCount !== 389) {
    throw new Error("predecessor_catalog_mismatch");
  }
  assertExact(
    nodes.filter((item) => String(item?.name || "").startsWith("n8n-nodes-ndc.")).map((item) => item.name),
    expectedNodeTypes,
    "predecessor node types",
  );
  assertExact(
    credentials.filter((item) => predecessorCredentialTypes.includes(String(item?.name || ""))).map((item) => item.name),
    predecessorCredentialTypes,
    "predecessor credential types",
  );
}

function assertPublishNodeV2(node) {
  if (JSON.stringify(node?.version) !== JSON.stringify([1, 2])) {
    throw new Error("publish_node_versions_mismatch");
  }
  const property = node?.properties?.find((item) => item?.name === "publishMode");
  const values = property?.options?.map((item) => item?.value);
  if (JSON.stringify(values) !== JSON.stringify(["upsert", "replace"])
    || !property?.displayOptions?.show?.["@version"]?.includes(2)) {
    throw new Error("publish_node_replace_contract_missing");
  }
}

function assertBaselineCatalogs(nodes, credentials) {
  if (!Array.isArray(nodes) || nodes.length !== 434 || nodes.some((item) => String(item?.name || "").startsWith("n8n-nodes-ndc."))) {
    throw new Error("baseline_node_catalog_mismatch");
  }
  if (!Array.isArray(credentials) || credentials.length !== 385
    || credentials.some((item) => expectedCredentialTypes.includes(String(item?.name || "")))) {
    throw new Error("baseline_credential_catalog_mismatch");
  }
}

function assertEngineBaseline(compose) {
  const exactImage = `image: docker.n8n.io/n8nio/n8n:\${N8N_IMAGE_TAG:-${n8nVersion}}`;
  if (!compose.includes(exactImage)) throw new Error("engine_n8n_version_mismatch");
  if ((compose.match(/^  n8n:\s*$/gm) || []).length !== 1) throw new Error("engine_n8n_topology_mismatch");
  if (/^  n8n-(?:worker|webhook)|^  (?:worker|webhook):/gm.test(compose)) throw new Error("unexpected_n8n_process_service");
  if (compose.includes("N8N_CUSTOM_EXTENSIONS") || compose.includes("CUSTOM.")) throw new Error("custom_extension_loader_forbidden");
}

function assertExact(actual, expected, label) {
  if (!Array.isArray(actual) || JSON.stringify(actual) !== JSON.stringify(expected)) {
    throw new Error(`${label.replaceAll(" ", "_")}_mismatch`);
  }
}

function assertSha(bytes, expected, label) {
  const actual = sha(bytes);
  if (actual !== expected) throw new Error(`${label.replaceAll(" ", "_")}_sha256_mismatch:${actual}`);
}

function predecessorArtifactFile(rel) {
  const script = [
    "import pathlib,sys,tarfile",
    "archive=pathlib.Path(sys.argv[1])",
    "member='payload/'+sys.argv[2]",
    "with tarfile.open(archive,'r:gz') as source:",
    "  extracted=source.extractfile(member)",
    "  if extracted is None: raise SystemExit('predecessor member missing')",
    "  sys.stdout.buffer.write(extracted.read())",
  ].join("\n");
  return run("python3", ["-c", script, predecessorArtifact, rel]).stdout;
}

async function writeJson(path, value) {
  await mkdir(dirname(path), { recursive: true });
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}

function extractArchive(archive, destination) {
  const script = [
    "import pathlib, sys, tarfile",
    "src=pathlib.Path(sys.argv[1]); dst=pathlib.Path(sys.argv[2]).resolve()",
    "with tarfile.open(src, 'r:gz') as tf:",
    "  for m in tf:",
    "    p=pathlib.PurePosixPath(m.name)",
    "    if p.is_absolute() or '..' in p.parts or any(x.startswith('._') for x in p.parts) or not (m.isfile() or m.isdir()): raise SystemExit('unsafe archive member')",
    "    target=dst.joinpath(*p.parts)",
    "    target.mkdir(parents=True, exist_ok=True) if m.isdir() else target.parent.mkdir(parents=True, exist_ok=True)",
    "    if m.isfile():",
    "      source=tf.extractfile(m)",
    "      with open(target, 'xb') as out: out.write(source.read())",
  ].join("\n");
  run("python3", ["-c", script, archive, destination]);
}

function canonicalTarScript() {
  return [
    "import gzip, io, pathlib, sys, tarfile",
    "root=pathlib.Path(sys.argv[2])",
    "with open(sys.argv[1], 'wb') as out:",
    "  with gzip.GzipFile(filename='', mode='wb', fileobj=out, compresslevel=9, mtime=0) as gz:",
    "    with tarfile.open(fileobj=gz, mode='w', format=tarfile.PAX_FORMAT) as tar:",
    "      for top in ('manifest.env','files.txt','payload'):",
    "        p=root/top; paths=[p] + (sorted(p.rglob('*')) if p.is_dir() else [])",
    "        for x in paths:",
    "          info=tar.gettarinfo(str(x), arcname=x.relative_to(root).as_posix())",
    "          info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
    "          with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info, src if info.isfile() else None)",
  ].join("\n");
}

function sha(bytes) {
  return createHash("sha256").update(bytes).digest("hex");
}

function run(command, args, cwd) {
  const result = spawnSync(command, args, {
    cwd,
    encoding: "utf8",
    maxBuffer: 128 * 1024 * 1024,
    stdio: ["ignore", "pipe", "pipe"],
  });
  if (result.status !== 0) throw new Error(`${command}_failed:${result.stderr || result.stdout}`);
  return result;
}
