#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import {
  copyFile,
  lstat,
  mkdir,
  mkdtemp,
  readFile,
  readdir,
  rm,
  writeFile,
} from "node:fs/promises";
import { createReadStream } from "node:fs";
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(
  process.env.NODEDC_ENGINE_ROOT || join(platformRoot, "../NODEDC_ENGINE_INFRA"),
);
const artifactRoot = resolve(
  process.env.NODEDC_DEPLOY_ARTIFACT_DIR || join(here, "../deploy-artifacts"),
);

const releaseId = "2.33.2-974a9fb3492f";
const upstreamCommit = "974a9fb3492fe2c4984ee0549085d531cdc6242a";
const imageTag = `nodedc/engine-node-intelligence:${releaseId}`;
const predecessorGatewaySha256 = "6b8c80fa997ef7c438d199a6ee942c5e37aebc4057667af417897fa636131db4";
const predecessorComposeSha256 = "258cebb64ff1943c939655cc55bdce00fc5c4dced67ec291d84d6df066ace50e";
const sourceRootRel = "nodedc-source/server/nodeIntelligence";
const gatewayRel = "nodedc-source/server/routes/engineAgentGateway.js";
const serviceRootRel = "nodedc-source/services/node-intelligence";
const overrideRel = `${serviceRootRel}/docker-compose.immutable-runtime.yml`;
const descriptorRel = `${serviceRootRel}/activation.json`;
const imageArchiveRel = `${serviceRootRel}/image/engine-node-intelligence.tar`;
const activationEntries = [sourceRootRel, gatewayRel, serviceRootRel];
const rollbackEntries = [gatewayRel, serviceRootRel];
const transitionId = readTransitionId(process.argv.slice(2));
const activationId = `engine-node-intelligence-${transitionId}`;
const rollbackId = `engine-node-intelligence-rollback-${transitionId}`;
const activationTarget = join(artifactRoot, `nodedc-${activationId}.tgz`);
const rollbackTarget = join(artifactRoot, `nodedc-${rollbackId}.tgz`);
const commandsTarget = join(
  artifactRoot,
  `engine-node-intelligence-${transitionId}-plan-apply.txt`,
);

await mkdir(artifactRoot, { recursive: true });
for (const path of [
  activationTarget,
  `${activationTarget}.sha256`,
  rollbackTarget,
  `${rollbackTarget}.sha256`,
  commandsTarget,
]) {
  await assertFresh(path);
}
await assertEngineInputs();
const image = inspectLocalImage();

const work = await mkdtemp(join(tmpdir(), "nodedc-engine-node-intelligence-"));
try {
  const imageArchive = join(work, "engine-node-intelligence.tar");
  run("docker", ["image", "save", "--output", imageArchive, imageTag]);
  const archive = inspectImageArchive(imageArchive);
  const archiveSha256 = await hashFile(imageArchive);

  const source = {
    gatewaySha256: await hashFile(join(engineRoot, gatewayRel)),
    catalogSha256: await hashFile(join(engineRoot, sourceRootRel, "catalog.js")),
    upstreamClientSha256: await hashFile(join(engineRoot, sourceRootRel, "upstreamMcpClient.js")),
    upstreamProjectionSha256: await hashFile(join(engineRoot, sourceRootRel, "upstreamProjection.js")),
    composeOverrideSha256: await hashFile(join(engineRoot, overrideRel)),
    readmeSha256: await hashFile(join(engineRoot, serviceRootRel, "README.md")),
  };
  const activationDescriptor = descriptor({
    action: "activate",
    expectedCurrent: "inactive",
    gatewayPredecessor: predecessorGatewaySha256,
    source,
    image: {
      tag: imageTag,
      archiveRelativePath: imageArchiveRel,
      archiveSha256,
      configSha256: archive.configSha256,
      architecture: "amd64",
      os: "linux",
    },
  });

  await buildArtifact(
    work,
    activationId,
    activationEntries,
    activationTarget,
    async (payload) => {
      await copyExactDirectory(join(engineRoot, sourceRootRel), join(payload, sourceRootRel));
      await copyExactFile(join(engineRoot, gatewayRel), join(payload, gatewayRel));
      await copyExactFile(join(engineRoot, serviceRootRel, "README.md"), join(payload, serviceRootRel, "README.md"));
      await copyExactFile(join(engineRoot, overrideRel), join(payload, overrideRel));
      await copyExactFile(imageArchive, join(payload, imageArchiveRel));
      await writeJson(join(payload, descriptorRel), activationDescriptor);
    },
  );

  const rollbackReadme = [
    "# NDC Engine node intelligence (inactive rollback)",
    "",
    `This exact runner-owned descriptor deactivates ${releaseId}.`,
    "The pinned image and source are retained for audit; the Compose overlay is absent,",
    "the sidecar is removed, and the verified immutable backend is recreated alone.",
    "",
  ].join("\n");
  const rollbackReadmeSha256 = hashBytes(Buffer.from(rollbackReadme, "utf8"));
  const rollbackDescriptor = descriptor({
    action: "rollback-inactive",
    expectedCurrent: releaseId,
    gatewayPredecessor: source.gatewaySha256,
    source: {
      gatewaySha256: predecessorGatewaySha256,
      readmeSha256: rollbackReadmeSha256,
    },
    image: {
      tag: imageTag,
      configSha256: archive.configSha256,
      architecture: "amd64",
      os: "linux",
    },
  });
  await buildArtifact(
    work,
    rollbackId,
    rollbackEntries,
    rollbackTarget,
    async (payload) => {
      await writeGitFile(gatewayRel, join(payload, gatewayRel));
      await mkdir(join(payload, serviceRootRel), { recursive: true });
      await writeFile(join(payload, serviceRootRel, "README.md"), rollbackReadme, "utf8");
      await writeJson(join(payload, descriptorRel), rollbackDescriptor);
    },
  );

  const runnerSha256 = await hashFile(join(here, "nodedc-deploy"));
  const activationSha256 = await hashFile(activationTarget);
  const rollbackSha256 = await hashFile(rollbackTarget);
  await writeFile(
    `${activationTarget}.sha256`,
    `${activationSha256}  ${activationTarget.split("/").at(-1)}\n`,
    "utf8",
  );
  await writeFile(
    `${rollbackTarget}.sha256`,
    `${rollbackSha256}  ${rollbackTarget.split("/").at(-1)}\n`,
    "utf8",
  );
  await writeFile(
    commandsTarget,
    commandPlan({ runnerSha256, activationSha256, rollbackSha256 }),
    "utf8",
  );

  process.stdout.write(`${JSON.stringify({
    ok: true,
    transitionId,
    releaseId,
    upstreamCommit,
    image: {
      tag: imageTag,
      localId: image.Id,
      archiveSha256,
      configSha256: archive.configSha256,
      architecture: archive.architecture,
      os: archive.os,
    },
    runner: { sha256: runnerSha256 },
    activation: { artifact: activationTarget, sha256: activationSha256, entries: activationEntries },
    rollback: { artifact: rollbackTarget, sha256: rollbackSha256, entries: rollbackEntries },
    commands: commandsTarget,
  }, null, 2)}\n`);
} finally {
  await rm(work, { recursive: true, force: true });
}

function readTransitionId(args) {
  if (args.length !== 1) throw new Error("usage: build-engine-node-intelligence-artifacts.mjs YYYYMMDD-NNN");
  const value = String(args[0] || "").trim();
  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 parsed = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
  if (
    parsed.getUTCFullYear() !== Number(match[1])
    || parsed.getUTCMonth() !== Number(match[2]) - 1
    || parsed.getUTCDate() !== Number(match[3])
  ) throw new Error("transition_id_invalid");
  return value;
}

async function assertFresh(path) {
  try {
    await lstat(path);
  } catch (error) {
    if (error?.code === "ENOENT") return;
    throw error;
  }
  throw new Error(`target_already_exists:${path}`);
}

async function assertEngineInputs() {
  const baselineGateway = Buffer.from(run("git", ["show", `HEAD:${gatewayRel}`], engineRoot).stdout, "utf8");
  if (hashBytes(baselineGateway) !== predecessorGatewaySha256) throw new Error("gateway_predecessor_mismatch");
  if (await hashFile(join(engineRoot, "docker-compose.yml")) !== predecessorComposeSha256) {
    throw new Error("compose_predecessor_mismatch");
  }
  const sourceFiles = (await readdir(join(engineRoot, sourceRootRel))).sort();
  if (JSON.stringify(sourceFiles) !== JSON.stringify(["catalog.js", "upstreamMcpClient.js", "upstreamProjection.js"])) {
    throw new Error("node_intelligence_source_exact_set_mismatch");
  }
  const serviceFiles = (await readdir(join(engineRoot, serviceRootRel))).sort();
  if (JSON.stringify(serviceFiles) !== JSON.stringify(["README.md", "docker-compose.immutable-runtime.yml"])) {
    throw new Error("node_intelligence_service_exact_set_mismatch");
  }
  const gateway = await readFile(join(engineRoot, gatewayRel), "utf8");
  for (const tool of [
    "engine_get_node_intelligence_status",
    "engine_get_node_guidance",
    "engine_validate_node_configuration",
    "engine_validate_l2_deep",
  ]) {
    if (!gateway.includes(tool)) throw new Error(`gateway_tool_missing:${tool}`);
  }
  const override = await readFile(join(engineRoot, overrideRel), "utf8");
  for (const exact of [
    `image: ${imageTag}`,
    "pull_policy: never",
    'user: "11007:11007"',
    "AUTH_TOKEN_FILE: /run/nodedc-secrets/engine-node-intelligence-auth-token",
    "ENGINE_NODE_INTELLIGENCE_AUTH_TOKEN_FILE: /run/nodedc-secrets/engine-node-intelligence-auth-token",
    "read_only: true",
    "no-new-privileges:true",
  ]) {
    if (!override.includes(exact)) throw new Error(`compose_contract_missing:${exact}`);
  }
  if (/^\s*(?:AUTH_TOKEN|N8N_API_URL|N8N_API_KEY):/m.test(override)) {
    throw new Error("compose_runtime_authority_forbidden");
  }
}

function inspectLocalImage() {
  const raw = run("docker", ["image", "inspect", imageTag]).stdout;
  const images = JSON.parse(raw);
  if (!Array.isArray(images) || images.length !== 1) throw new Error("image_inspect_shape_mismatch");
  const image = images[0];
  const config = image.Config || {};
  const labels = config.Labels || {};
  if (
    !/^sha256:[a-f0-9]{64}$/.test(String(image.Id || ""))
    || image.Architecture !== "amd64"
    || image.Os !== "linux"
    || !(image.RepoTags || []).includes(imageTag)
    || labels["org.opencontainers.image.revision"] !== upstreamCommit
    || JSON.stringify(config.Entrypoint) !== JSON.stringify(["/usr/local/bin/docker-entrypoint.sh"])
    || JSON.stringify(config.Cmd) !== JSON.stringify(["node", "dist/mcp/index.js"])
    || (config.Env || []).some((value) => /^(?:AUTH_TOKEN|N8N_API_URL|N8N_API_KEY)=/.test(String(value)))
  ) throw new Error("image_identity_mismatch");
  return image;
}

function inspectImageArchive(path) {
  const script = [
    "import hashlib,json,pathlib,sys,tarfile",
    "p=pathlib.Path(sys.argv[1])",
    "with tarfile.open(p,'r:') as tf:",
    " m=json.loads(tf.extractfile('manifest.json').read())",
    " if not isinstance(m,list) or len(m)!=1: raise SystemExit('manifest-set')",
    " r=m[0]",
    ` if r.get('RepoTags')!=[${JSON.stringify(imageTag)}]: raise SystemExit('tag')`,
    " n=r.get('Config','')",
    " raw=tf.extractfile(n).read()",
    " digest=n.rsplit('/',1)[-1]",
    " if hashlib.sha256(raw).hexdigest()!=digest: raise SystemExit('config-digest')",
    " c=json.loads(raw)",
    " cfg=c.get('config') or {}",
    ` if c.get('architecture')!='amd64' or c.get('os')!='linux' or (cfg.get('Labels') or {}).get('org.opencontainers.image.revision')!=${JSON.stringify(upstreamCommit)}: raise SystemExit('identity')`,
    " if cfg.get('Entrypoint')!=['/usr/local/bin/docker-entrypoint.sh'] or cfg.get('Cmd')!=['node','dist/mcp/index.js']: raise SystemExit('command')",
    " print(json.dumps({'configSha256':digest,'architecture':c['architecture'],'os':c['os']}))",
  ].join("\n");
  return JSON.parse(run("python3", ["-c", script, path]).stdout);
}

function descriptor({ action, expectedCurrent, gatewayPredecessor, source, image }) {
  return {
    schemaVersion: "nodedc.engine-node-intelligence-transition/v1",
    action,
    releaseId,
    expectedCurrent,
    upstream: {
      package: "n8n-mcp",
      version: "2.33.2",
      commit: upstreamCommit,
    },
    image,
    source,
    predecessor: {
      gatewaySha256: gatewayPredecessor,
      composeSha256: predecessorComposeSha256,
      backendRuntime: "verified-derived-retry",
    },
  };
}

async function buildArtifact(workRoot, id, entries, target, 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");
  run("python3", ["-c", canonicalTarScript(), target, stage]);
}

async function copyExactDirectory(source, target) {
  await mkdir(target, { recursive: true });
  for (const name of (await readdir(source)).sort()) {
    await copyExactFile(join(source, name), join(target, name));
  }
}

async function copyExactFile(source, target) {
  const sourceStat = await lstat(source);
  if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) throw new Error(`source_file_unsafe:${source}`);
  await mkdir(dirname(target), { recursive: true });
  await copyFile(source, target, 0);
}

async function writeGitFile(relativePath, target) {
  const value = run("git", ["show", `HEAD:${relativePath}`], engineRoot).stdout;
  await mkdir(dirname(target), { recursive: true });
  await writeFile(target, value, "utf8");
}

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

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 commandPlan({ runnerSha256, activationSha256, rollbackSha256 }) {
  return [
    `transition=${transitionId}`,
    `runner_sha256=${runnerSha256}`,
    `activation_sha256=${activationSha256}`,
    `rollback_sha256=${rollbackSha256}`,
    "",
    "# 1. Read-only gate: no deploy process and no state/deploy.lock.",
    "# 2. Promote the exact staged runner in a standalone sudo step:",
    `sudo sha256sum /volume1/docker/nodedc-deploy/runner-install/candidates/nodedc-deploy.${activationId}`,
    "sudo install -o root -g root -m 0755 \\",
    `  /volume1/docker/nodedc-deploy/runner-install/candidates/nodedc-deploy.${activationId} \\`,
    "  /usr/local/sbin/nodedc-deploy",
    "sudo /usr/local/sbin/nodedc-deploy verify-install",
    "",
    "# 3. Activation is always plan, then explicit apply:",
    `sudo /usr/local/sbin/nodedc-deploy plan /volume1/docker/nodedc-deploy/inbox/${activationTarget.split("/").at(-1)}`,
    `sudo /usr/local/sbin/nodedc-deploy apply /volume1/docker/nodedc-deploy/inbox/${activationTarget.split("/").at(-1)}`,
    "",
    "# 4. Rollback is operator-invoked only after its own plan:",
    `sudo /usr/local/sbin/nodedc-deploy plan /volume1/docker/nodedc-deploy/inbox/${rollbackTarget.split("/").at(-1)}`,
    `sudo /usr/local/sbin/nodedc-deploy apply /volume1/docker/nodedc-deploy/inbox/${rollbackTarget.split("/").at(-1)}`,
    "",
  ].join("\n");
}

function hashBytes(value) {
  return createHash("sha256").update(value).digest("hex");
}

function hashFile(path) {
  return new Promise((resolveHash, rejectHash) => {
    const digest = createHash("sha256");
    const input = createReadStream(path);
    input.on("data", (chunk) => digest.update(chunk));
    input.on("error", rejectHash);
    input.on("end", () => resolveHash(digest.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;
}
