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

const scriptDir = dirname(fileURLToPath(import.meta.url));
const platformRoot = resolve(scriptDir, "../..");
const packageRoot = resolve(platformRoot, "packages/n8n-nodes-ndc");
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
const requireModule = createRequire(import.meta.url);
const expectedPackageVersion = "0.1.6";
const [patchId = "n8n-nodes-ndc-replace-replay-v1-20260721-009", ...extra] = process.argv.slice(2);
const expectedRuntimeNodes = [
  {
    file: "dist/nodes/NdcDataProductPublish/NdcDataProductPublish.node.js",
    exportName: "NdcDataProductPublish",
    name: "ndcDataProductPublish",
  },
  {
    file: "dist/nodes/NdcDataProductRead/NdcDataProductRead.node.js",
    exportName: "NdcDataProductRead",
    name: "ndcDataProductRead",
  },
  {
    file: "dist/nodes/NdcFoundryBinding/NdcFoundryBinding.node.js",
    exportName: "NdcFoundryBinding",
    name: "ndcFoundryBinding",
  },
];
const expectedN8nNodes = expectedRuntimeNodes.map((node) => node.file);
const expectedRuntimeNodeTypes = expectedRuntimeNodes.map((node) => `n8n-nodes-ndc.${node.name}`);
const expectedN8nCredentials = [
  "dist/credentials/NdcDataProductWriterApi.credentials.js",
  "dist/credentials/NdcDataProductReaderApi.credentials.js",
  "dist/credentials/NdcFoundryBindingApi.credentials.js",
  "dist/credentials/NdcProviderRotatingAccessApi.credentials.js",
];
const expectedCredentialTypes = [
  "ndcDataProductWriterApi",
  "ndcDataProductReaderApi",
  "ndcFoundryBindingApi",
  "ndcProviderRotatingAccessApi",
];

if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
  throw new Error("usage: build-n8n-private-extension-artifact.mjs [patch-id]");
}

const packageJson = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
assertPackageSourcePolicy(packageJson);
run("npm", ["test"], packageRoot);
run("npm", ["run", "lint"], packageRoot);
assertRuntimeNodePolicy(packageJson);

const stage = await mkdtemp(join(tmpdir(), "nodedc-n8n-private-extension-"));
const packDir = join(stage, "pack");
const payload = join(stage, "payload");

try {
  await mkdir(packDir, { recursive: true });
  const pack = run("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", packDir], packageRoot);
  const packResult = JSON.parse(pack.stdout);
  if (!Array.isArray(packResult) || packResult.length !== 1) throw new Error("npm_pack_result_invalid");
  const metadata = packResult[0];
  assertPackedFilePolicy(metadata, packageJson);

  const packedPath = join(packDir, metadata.filename);
  const canonicalPackedPath = join(packDir, "n8n-nodes-ndc.canonical.tgz");
  const canonicalPackageScript = [
    "import gzip, io, sys, tarfile",
    "members = []",
    "with tarfile.open(sys.argv[1], 'r:gz') as source:",
    "    for original in source:",
    "        if not (original.isfile() or original.isdir()):",
    "            raise SystemExit('unsupported npm package member')",
    "        content = b''",
    "        if original.isfile():",
    "            extracted = source.extractfile(original)",
    "            if extracted is None:",
    "                raise SystemExit('unreadable npm package member')",
    "            content = extracted.read()",
    "        members.append((original.name, original.isdir(), content))",
    "with open(sys.argv[2], 'wb') as output:",
    "    with gzip.GzipFile(filename='', mode='wb', fileobj=output, compresslevel=9, mtime=0) as compressed:",
    "        with tarfile.open(fileobj=compressed, mode='w', format=tarfile.PAX_FORMAT) as target:",
    "            for name, is_dir, content in sorted(members, key=lambda item: item[0]):",
    "                member = tarfile.TarInfo(name)",
    "                member.uid = member.gid = 0",
    "                member.uname = member.gname = 'root'",
    "                member.mtime = 0",
    "                member.mode = 0o755 if is_dir else 0o644",
    "                member.type = tarfile.DIRTYPE if is_dir else tarfile.REGTYPE",
    "                member.size = 0 if is_dir else len(content)",
    "                target.addfile(member, None if is_dir else io.BytesIO(content))",
  ].join("\n");
  run("python3", ["-c", canonicalPackageScript, packedPath, canonicalPackedPath], stage);
  const packageBytes = await readFile(canonicalPackedPath);
  const packageSha256 = createHash("sha256").update(packageBytes).digest("hex");
  const releaseId = `${packageJson.version}-${packageSha256.slice(0, 16)}`;
  const relativeReleasePath = `releases/n8n-nodes-ndc/${releaseId}`;
  const releaseDir = join(payload, relativeReleasePath);
  await mkdir(releaseDir, { recursive: true });
  await cp(canonicalPackedPath, join(releaseDir, "package.tgz"), { force: false });

  const rollbackBaselinePolicy = {
    allowed: [
      "previous_verified_immutable_release",
      "verified_inactive",
    ],
    firstActivation: "verified_inactive",
    requiresPreActivationVerification: true,
  };
  const release = {
    schemaVersion: "nodedc.n8n-private-extension-release/v2",
    releaseId,
    package: {
      name: "n8n-nodes-ndc",
      version: packageJson.version,
      sha256: packageSha256,
      bytes: packageBytes.byteLength,
      runtimeTypePrefix: "n8n-nodes-ndc.",
    },
    storage: {
      relativePath: relativeReleasePath,
      immutable: true,
    },
    activation: {
      owner: "engine",
      status: "blocked_pending_engine_owned_mount",
      requiredCommunityPackagePath: "/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc",
      requiresAtomicReleaseSwitch: true,
      requiresAllN8nProcessesRestart: true,
      requiresMcpSchemaAcceptance: true,
      rollbackBaselinePolicy,
    },
  };
  const rollback = {
    schemaVersion: "nodedc.n8n-private-extension-rollback/v2",
    releaseId,
    packageSha256,
    mode: "engine-owned-atomic-release-switch",
    baselinePolicy: rollbackBaselinePolicy,
    steps: [
      "select_verified_previous_release_or_preverified_inactive_baseline",
      "switch_engine_owned_mount_atomically",
      "restart_all_n8n_processes",
      "verify_mcp_schema_state_matches_selected_baseline",
    ],
    forbidden: [
      "delete_active_release",
      "mutate_engine_core",
      "live_npm_install",
    ],
  };
  await writeFile(join(releaseDir, "release.json"), `${JSON.stringify(release, null, 2)}\n`, "utf8");
  await writeFile(join(releaseDir, "rollback.json"), `${JSON.stringify(rollback, null, 2)}\n`, "utf8");
  await writeFile(join(stage, "manifest.env"), `id=${patchId}\ncomponent=n8n-private-extension\ntype=app-overlay\n`, "utf8");
  await writeFile(join(stage, "files.txt"), `${relativeReleasePath}\n`, "utf8");

  await mkdir(artifactDir, { recursive: true });
  const target = join(artifactDir, `nodedc-n8n-private-extension-${patchId}.tgz`);
  const tarScript = [
    "import gzip, os, pathlib, sys, tarfile",
    "root = pathlib.Path(sys.argv[2])",
    "def clean(info):",
    "    info.uid = info.gid = 0",
    "    info.uname = info.gname = 'root'",
    "    info.mtime = 0",
    "    info.mode = 0o755 if info.isdir() else 0o644",
    "    return info",
    "with open(sys.argv[1], 'wb') as output:",
    "    with gzip.GzipFile(filename='', mode='wb', fileobj=output, compresslevel=9, mtime=0) as compressed:",
    "        with tarfile.open(fileobj=compressed, mode='w', format=tarfile.PAX_FORMAT) as archive:",
    "            for top in ('manifest.env', 'files.txt', 'payload'):",
    "                path = root / top",
    "                archive.add(path, arcname=top, recursive=False, filter=clean)",
    "                if path.is_dir():",
    "                    for child in sorted(path.rglob('*'), key=lambda item: item.as_posix()):",
    "                        archive.add(child, arcname=child.relative_to(root).as_posix(), recursive=False, filter=clean)",
  ].join("\n");
  run("python3", ["-c", tarScript, target, stage], stage);

  const artifactSha256 = createHash("sha256").update(await readFile(target)).digest("hex");
  console.log(JSON.stringify({
    ok: true,
    patchId,
    artifact: target,
    artifactSha256,
    releaseId,
    packageSha256,
    nodeTypes: expectedRuntimeNodeTypes,
    credentialTypes: expectedCredentialTypes,
    activation: "blocked_pending_engine_owned_mount",
  }, null, 2));
} finally {
  await rm(stage, { recursive: true, force: true });
}

function assertPackageSourcePolicy(value) {
  if (value.name !== "n8n-nodes-ndc") throw new Error("package_name_invalid");
  if (value.version !== expectedPackageVersion) throw new Error("package_version_invalid");
  if (value.private !== true) throw new Error("package_must_remain_private");
  if (value.dependencies !== undefined) throw new Error("runtime_dependencies_forbidden");
  for (const lifecycle of ["preinstall", "install", "postinstall", "prepack", "prepare", "postpack"]) {
    if (value.scripts?.[lifecycle] !== undefined) throw new Error(`lifecycle_script_forbidden:${lifecycle}`);
  }
  assertExactRegistration(value.n8n?.nodes, expectedN8nNodes, "n8n_nodes");
  assertExactRegistration(value.n8n?.credentials, expectedN8nCredentials, "n8n_credentials");
}

function assertRuntimeNodePolicy(packageJson) {
  const observedTypes = [];
  for (const expected of expectedRuntimeNodes) {
    const loaded = requireModule(join(packageRoot, expected.file));
    const NodeClass = loaded?.[expected.exportName];
    if (typeof NodeClass !== "function") throw new Error(`runtime_node_export_missing:${expected.exportName}`);
    const description = new NodeClass()?.description;
    if (!description || description.name !== expected.name) {
      throw new Error(`runtime_node_name_mismatch:${expected.exportName}`);
    }
    if ("usableAsTool" in description) {
      throw new Error(`runtime_tool_variant_forbidden:${expected.name}`);
    }
    observedTypes.push(`${packageJson.name}.${description.name}`);
  }
  if (JSON.stringify(observedTypes) !== JSON.stringify(expectedRuntimeNodeTypes)) {
    throw new Error("runtime_node_types_mismatch");
  }
}

function assertExactRegistration(actual, expected, label) {
  if (!Array.isArray(actual)) throw new Error(`${label}_missing`);
  if (actual.length !== new Set(actual).size) throw new Error(`${label}_duplicate`);
  if (actual.length !== expected.length || expected.some((value) => !actual.includes(value))) {
    throw new Error(`${label}_mismatch`);
  }
}

function assertPackedFilePolicy(metadata, sourcePackage) {
  if (metadata.name !== sourcePackage.name || metadata.version !== sourcePackage.version) {
    throw new Error("npm_pack_identity_mismatch");
  }
  if (!Number.isSafeInteger(metadata.size) || metadata.size < 1024 || metadata.size > 32 * 1024 * 1024) {
    throw new Error("npm_pack_size_invalid");
  }
  if (!Array.isArray(metadata.files) || metadata.files.length > 512) throw new Error("npm_pack_file_list_invalid");
  const paths = new Set();
  for (const file of metadata.files) {
    if (!file || typeof file.path !== "string" || paths.has(file.path)) throw new Error("npm_pack_file_invalid");
    paths.add(file.path);
    if (!(file.path === "README.md" || file.path === "package.json" || file.path.startsWith("dist/"))) {
      throw new Error(`npm_pack_path_forbidden:${file.path}`);
    }
    const parts = file.path.split("/");
    if (
      file.path.includes("\\")
      || parts.some((part) => !part || part === "." || part === ".." || part.startsWith("."))
      || file.mode !== 0o644
    ) {
      throw new Error(`npm_pack_path_unsafe:${file.path}`);
    }
  }
  assertExactRegistration(
    [...paths].filter((value) => /^dist\/nodes\/.+\.node\.js$/.test(value)),
    expectedN8nNodes,
    "npm_pack_nodes",
  );
  assertExactRegistration(
    [...paths].filter((value) => /^dist\/credentials\/.+\.credentials\.js$/.test(value)),
    expectedN8nCredentials,
    "npm_pack_credentials",
  );
  for (const registered of [...sourcePackage.n8n.nodes, ...sourcePackage.n8n.credentials]) {
    if (!paths.has(registered)) throw new Error(`npm_pack_registration_missing:${registered}`);
  }
}

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