import console from "node:console";
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const rootManifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
const components = [];

function npmPurl(name, version) {
  if (name.startsWith("@")) {
    const slash = name.indexOf("/");
    return `pkg:npm/%40${encodeURIComponent(name.slice(1, slash))}/${encodeURIComponent(name.slice(slash + 1))}@${version}`;
  }
  return `pkg:npm/${encodeURIComponent(name)}@${version}`;
}

for (const entry of readdirSync(join(root, "packages"), { withFileTypes: true })) {
  if (!entry.isDirectory()) continue;
  const file = join(root, "packages", entry.name, "package.json");
  try {
    const manifest = JSON.parse(readFileSync(file, "utf8"));
    if (!manifest.name?.startsWith("@wrnexus/")) continue;
    components.push({
      type: "library",
      name: manifest.name,
      version: manifest.version,
      "bom-ref": npmPurl(manifest.name, manifest.version),
      purl: npmPurl(manifest.name, manifest.version),
      properties: [
        { name: "wrnexus:workspace", value: "true" },
        { name: "wrnexus:private", value: String(manifest.publishConfig?.access !== "public") },
      ],
    });
  } catch {
    // Not a workspace package.
  }
}

// Bun's text lockfile is JSON with optional trailing commas. Package tuples use
// ["name@version", resolved, metadata, integrity], which is enough to include
// every resolved transitive dependency without requiring node_modules.
try {
  const lockSource = readFileSync(join(root, "bun.lock"), "utf8").replace(/,\s*([}\]])/g, "$1");
  const lock = JSON.parse(lockSource);
  const external = new Map();
  for (const value of Object.values(lock.packages ?? {})) {
    if (!Array.isArray(value) || typeof value[0] !== "string") continue;
    const specifier = value[0];
    const split = specifier.lastIndexOf("@");
    if (split <= 0) continue;
    const name = specifier.slice(0, split);
    const version = specifier.slice(split + 1);
    if (!name || !version || name.startsWith("@wrnexus/")) continue;
    const ref = npmPurl(name, version);
    const component = {
      type: "library",
      name,
      version,
      "bom-ref": ref,
      purl: ref,
      properties: [{ name: "wrnexus:dependency-source", value: "bun.lock" }],
    };
    const integrity = typeof value[3] === "string" ? value[3] : "";
    if (integrity.startsWith("sha512-")) {
      component.hashes = [{ alg: "SHA-512", content: integrity.slice("sha512-".length) }];
    }
    external.set(ref, component);
  }
  components.push(...external.values());
} catch (error) {
  throw new Error(
    `Unable to parse bun.lock for the SBOM: ${error instanceof Error ? error.message : String(error)}`,
    { cause: error },
  );
}

components.sort((a, b) => {
  const left = `${a.name}@${a.version}`;
  const right = `${b.name}@${b.version}`;
  return left < right ? -1 : left > right ? 1 : 0;
});

function deterministicUuid(value) {
  const hash = createHash("sha256").update(value).digest("hex").slice(0, 32).split("");
  hash[12] = "5";
  hash[16] = ((Number.parseInt(hash[16], 16) & 0x3) | 0x8).toString(16);
  const hex = hash.join("");
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}

const version = String(rootManifest.version);
const identity = JSON.stringify({ framework: "WRNexusJS", version, components });
const document = {
  bomFormat: "CycloneDX",
  specVersion: "1.5",
  serialNumber: `urn:uuid:${deterministicUuid(identity)}`,
  version: 1,
  metadata: {
    component: {
      type: "framework",
      name: "WRNexusJS",
      version,
      "bom-ref": `pkg:generic/WRNexusJS@${version}`,
    },
  },
  components,
};
const reportDir = join(root, ".wrnexus", "reports");
mkdirSync(reportDir, { recursive: true });
const file = join(reportDir, `SBOM-${version}.cdx.json`);
writeFileSync(file, `${JSON.stringify(document, null, 2)}\n`);
console.log(`Generated ${file} with ${components.length} workspace and transitive components.`);
