#!/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 networkPublicationCompose = resolve(
  scriptDir,
  "fixtures/device-plane-foundation-network-publication-v1.yml",
);
const artifactDir = resolve(
  process.env.NODEDC_DEPLOY_ARTIFACT_DIR
    || resolve(scriptDir, "../deploy-artifacts"),
);
const [
  patchId = "device-plane-foundation-network-publication-20260725-003",
  ...extra
] = process.argv.slice(2);

if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
  throw new Error(
    "usage: build-device-plane-foundation-network-publication-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-foundation-network-publication-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-network-publication-"),
);
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) {
    const source = sourceRelative === "docker-compose.device-plane.yml"
      ? networkPublicationCompose
      : resolve(sourceRoot, sourceRelative);
    await copySafe(
      source,
      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: "failed-foundation-network-publication-correction",
    entries: files,
    build: [],
    services: ["device-control-core", "device-gateway"],
    preservedRuntime: [
      "device-postgres",
      "nodedc-device-plane-postgres-data",
      "Gelios",
    ],
    networkChange: {
      private: "preserved:internal",
      control: "create:non-internal:no-masquerade",
      published: [
        "127.0.0.1:18120:18120",
        "127.0.0.1:18121:18121",
      ],
      disabled: ["9921", "public-ingress", "command-transport"],
    },
    rollback:
      "restore-partial-source-and-internal-only-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(
    networkPublicationCompose,
    "utf8",
  );
  for (const fragment of [
    'DEVICE_DISCOVERY_INGEST_ENABLED: "false"',
    'DEVICE_GATEWAY_LISTEN_ENABLED: "false"',
    '"127.0.0.1:18120:18120"',
    '"127.0.0.1:18121:18121"',
    "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_network_publication_boundary_missing:${fragment}`,
      );
    }
  }
  for (const forbidden of [
    "9921:9921",
    "0.0.0.0:9921",
    'DEVICE_DISCOVERY_INGEST_ENABLED: "true"',
    'DEVICE_GATEWAY_LISTEN_ENABLED: "true"',
    "POSTGRES_PASSWORD:",
  ]) {
    if (compose.includes(forbidden)) {
      throw new Error(
        `device_plane_network_publication_boundary_violation:${forbidden}`,
      );
    }
  }

  const descriptor = JSON.parse(await readFile(
    resolve(
      sourceRoot,
      "deployment/device-plane-foundation-network-publication-v1.json",
    ),
    "utf8",
  ));
  const expected = {
    schemaVersion:
      "nodedc.device-plane.foundation-network-publication.v1",
    mode: "failed-foundation-network-publication-correction",
    failedRecoveryPatchId:
      "device-plane-foundation-recovery-20260725-002",
    failedRecoveryArtifactSha256:
      "9183cc385142584bfd12510bb0a3e6b833b2fd26607436f2486a564c628ea1bf",
    failedRecoveryBackupId:
      "device-plane-device-plane-foundation-recovery-20260725-002-20260725-232447",
    sourceAction: "publish-network-corrected-foundation-source",
    runtimeAction: "recreate-stateless-services-no-build",
    selectedServices: ["device-control-core", "device-gateway"],
    preservedServices: ["device-postgres"],
    privateNetwork: "nodedc-device-plane-private",
    controlNetwork: "nodedc-device-plane-control",
    publishedLoopbackPorts: [
      "127.0.0.1:18120:18120",
      "127.0.0.1:18121:18121",
    ],
    databaseVolume: "nodedc-device-plane-postgres-data",
    rollback:
      "restore-partial-source-and-internal-only-stateless-runtime",
  };
  if (JSON.stringify(descriptor) !== JSON.stringify(expected)) {
    throw new Error(
      "device_plane_network_publication_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);
  }
}
