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

const scriptDir = dirname(fileURLToPath(import.meta.url));
const platformRoot = resolve(scriptDir, "../..");
const sourceRoot = resolve(platformRoot, "device-plane");
const artifactDir = resolve(
  process.env.NODEDC_DEPLOY_ARTIFACT_DIR
    || resolve(scriptDir, "../deploy-artifacts"),
);
const [
  patchId = "device-plane-b2-discovery-ingress-20260726-001",
  ...extra
] = process.argv.slice(2);

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

const files = [
  ".dockerignore",
  "package.json",
  "package-lock.json",
  "docker-compose.device-plane.yml",
  "packages/device-protocol-contract",
  "packages/arusnavi-b2-adapter",
  "services/device-control-core",
  "services/device-gateway",
  "deployment/device-plane-b2-discovery-ingress-v1.json",
];
const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules"]);
const ignoredDirectoryNames = new Set(["test"]);
const stage = await mkdtemp(
  join(tmpdir(), "nodedc-device-plane-b2-discovery-ingress-"),
);
const payload = join(stage, "payload");
const target = join(
  artifactDir,
  `nodedc-device-plane-${patchId}.tgz`,
);

await assertBoundary();

try {
  await mkdir(payload, { recursive: true });
  for (const sourceRelative of files) {
    await copySafe(
      resolve(sourceRoot, sourceRelative),
      join(payload, sourceRelative),
    );
  }
  await writeFile(
    join(stage, "manifest.env"),
    `id=${patchId}\ncomponent=device-plane\ntype=app-overlay\n`,
    "utf8",
  );
  await writeFile(
    join(stage, "files.txt"),
    `${files.join("\n")}\n`,
    "utf8",
  );
  await mkdir(artifactDir, { recursive: true });

  const tar = spawnSync(
    "python3",
    ["-c", canonicalTarScript(), target, stage],
    {
      encoding: "utf8",
      maxBuffer: 128 * 1024 * 1024,
    },
  );
  if (tar.status !== 0) {
    throw new Error(`tar_failed:${tar.stderr || tar.stdout}`);
  }

  const digest = createHash("sha256")
    .update(await readFile(target))
    .digest("hex");
  console.log(JSON.stringify({
    ok: true,
    patchId,
    artifact: target,
    sha256: digest,
    component: "device-plane",
    transition: "verified-b2-discovery-only",
    entries: files,
    services: ["device-control-core", "device-gateway"],
    preservedRuntime: [
      "device-postgres",
      "nodedc-device-plane-postgres-data",
      "Gelios",
    ],
    ingress: {
      transport: "tcp",
      published: "0.0.0.0:9921:9921",
      mode: "discovery-only",
      framing: "verified-read-only",
      lifecycle: "quarantine",
      commandTransport: "disabled",
    },
    rollback: "restore-source-and-predecessor-stateless-runtime",
    excluded: [
      ".env*",
      "node_modules",
      "**/test",
      "docs",
      "runtime",
      "secrets",
    ],
  }, null, 2));
} finally {
  await rm(stage, { recursive: true, force: true });
}

async function assertBoundary() {
  const compose = await readFile(
    resolve(sourceRoot, "docker-compose.device-plane.yml"),
    "utf8",
  );
  for (const fragment of [
    'DEVICE_DISCOVERY_INGEST_ENABLED: "true"',
    'DEVICE_GATEWAY_LISTEN_ENABLED: "true"',
    'DEVICE_GATEWAY_PUBLIC_INGRESS_ENABLED: "true"',
    "DEVICE_GATEWAY_CORE_URL: http://device-control-core:18120",
    "DEVICE_GATEWAY_CORE_TOKEN_FILE: /run/nodedc-secrets/gateway-core-token",
    '"127.0.0.1:18120:18120"',
    '"127.0.0.1:18121:18121"',
    '"0.0.0.0:9921:9921"',
    "name: nodedc-device-plane-private",
    "internal: true",
    "name: nodedc-device-plane-control",
    "internal: false",
    'com.docker.network.bridge.enable_ip_masquerade: "false"',
    "name: nodedc-device-plane-postgres-data",
    "pull_policy: never",
  ]) {
    if (!compose.includes(fragment)) {
      throw new Error(
        `device_plane_b2_ingress_boundary_missing:${fragment}`,
      );
    }
  }
  for (const forbidden of [
    "POSTGRES_PASSWORD:",
    "DEVICE_GATEWAY_CORE_TOKEN:",
    "DEVICE_IDENTIFIER_PEPPER:",
    "DEVICE_GATEWAY_COMMAND",
    "9921:9921/udp",
  ]) {
    if (compose.includes(forbidden)) {
      throw new Error(
        `device_plane_b2_ingress_boundary_violation:${forbidden}`,
      );
    }
  }

  const descriptor = JSON.parse(await readFile(
    resolve(
      sourceRoot,
      "deployment/device-plane-b2-discovery-ingress-v1.json",
    ),
    "utf8",
  ));
  const expected = {
    schemaVersion: "nodedc.device-plane.b2-discovery-ingress.v1",
    mode: "verified-b2-discovery-only",
    predecessorPatchId:
      "device-plane-foundation-network-publication-20260725-003",
    predecessorArtifactSha256:
      "6fdd5a12c310786db1753882fc1378184fe378d2cc533633a8c73c951521b7bf",
    sourceAction: "publish-verified-b2-discovery-ingress-source",
    runtimeAction: "build-and-recreate-stateless-services",
    selectedServices: ["device-control-core", "device-gateway"],
    preservedServices: ["device-postgres"],
    privateNetwork: "nodedc-device-plane-private",
    controlNetwork: "nodedc-device-plane-control",
    publishedPorts: [
      "127.0.0.1:18120:18120",
      "127.0.0.1:18121:18121",
      "0.0.0.0:9921:9921/tcp",
    ],
    protocolProfile: "arusnavi.b2.internal.v1",
    framingSpecification:
      "arusnavi.internal.protocol-sheet.gid-12.v1",
    identityTrust: "claimed-not-ownership-proof",
    discoveryLifecycle: "quarantine",
    commandTransport: "disabled",
    gelios: "untouched",
    databaseVolume: "nodedc-device-plane-postgres-data",
    rollback: "restore-source-and-predecessor-stateless-runtime",
  };
  if (JSON.stringify(descriptor) !== JSON.stringify(expected)) {
    throw new Error("device_plane_b2_ingress_descriptor_mismatch");
  }
}

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");
}

async function copySafe(source, destination) {
  const sourceStat = await lstat(source);
  if (sourceStat.isSymbolicLink()) {
    throw new Error(
      `source_symlink_rejected:${relative(sourceRoot, source)}`,
    );
  }
  if (sourceStat.isFile()) {
    await mkdir(dirname(destination), { recursive: true });
    await cp(source, destination, {
      force: true,
      verbatimSymlinks: true,
    });
    return;
  }
  if (!sourceStat.isDirectory()) {
    throw new Error(`source_type_rejected:${source}`);
  }

  await mkdir(destination, { recursive: true });
  for (const entry of await readdir(source, { withFileTypes: true })) {
    if (
      ignoredBasenames.has(entry.name)
      || entry.name.startsWith(".env")
      || (
        entry.isDirectory()
        && ignoredDirectoryNames.has(entry.name)
      )
    ) {
      continue;
    }
    const childSource = join(source, entry.name);
    const childDestination = join(destination, entry.name);
    if (entry.isSymbolicLink()) {
      throw new Error(
        `source_symlink_rejected:${relative(sourceRoot, childSource)}`,
      );
    }
    await copySafe(childSource, childDestination);
  }
}
