#!/usr/bin/env node
/**
 * SPDX-License-Identifier: Apache-2.0
 * Audit npm + Rust dependency licenses and emit THIRD-PARTY-NOTICES.
 */
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
import { join, dirname, relative } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..");
const OUT = join(ROOT, "THIRD-PARTY-NOTICES");

const ALLOWED = new Set([
  "MIT",
  "ISC",
  "BSD-2-Clause",
  "BSD-3-Clause",
  "Apache-2.0",
  "0BSD",
  "Unlicense",
  "CC0-1.0",
  "BlueOak-1.0.0",
  "Python-2.0",
  "MPL-2.0",
  "CC-BY-4.0",
  "MIT-0",
  "Zlib",
  "WTFPL",
  "Unicode-DFS-2016",
  "CC-BY-3.0",
  "LGPL-2.1-or-later",
  "LGPL-3.0-or-later",
]);

function normalizeLicense(raw) {
  if (!raw) return "UNKNOWN";
  if (typeof raw === "object") {
    if (raw.type) return normalizeLicense(raw.type);
    return "UNKNOWN";
  }
  const s = String(raw).trim();
  if (s.startsWith("(") && s.endsWith(")")) {
    const parts = s.slice(1, -1).split(" OR ").map((p) => p.trim());
    return parts.find((p) => ALLOWED.has(p)) ?? parts[0];
  }
  if (s.includes(" AND ")) return s.split(" AND ")[0].trim();
  return s;
}

function collectNpm(rootDir) {
  const lockPath = join(rootDir, "package-lock.json");
  if (!existsSync(lockPath)) return [];
  const lock = JSON.parse(readFileSync(lockPath, "utf8"));
  const packages = lock.packages ?? {};
  const rows = [];
  for (const [pkgPath, meta] of Object.entries(packages)) {
    if (!pkgPath || pkgPath === "") continue;
    if (!pkgPath.startsWith("node_modules/")) continue;
    const name = meta.name ?? pkgPath.replace(/^node_modules\//, "");
    const version = meta.version ?? "?";
    if (version === "?") continue;
    const license = normalizeLicense(meta.license);
    rows.push({ ecosystem: "npm", name, version, license, path: pkgPath });
  }
  return rows;
}

function parseCargoLock(content) {
  const rows = [];
  const normalized = content.replace(/\r\n/g, "\n");
  const blocks = normalized.split("\n\n[[package]]\n").slice(1);
  for (const block of blocks) {
    const name = block.match(/^name = "([^"]+)"/m)?.[1];
    const version = block.match(/^version = "([^"]+)"/m)?.[1];
    if (!name) continue;
    rows.push({
      ecosystem: "cargo",
      name,
      version: version ?? "?",
      license: "see crates.io",
      path: `cargo:${name}@${version ?? "?"}`,
    });
  }
  return rows;
}

function collectCargo() {
  const lockPath = join(ROOT, "apps/desktop/src-tauri/Cargo.lock");
  if (!existsSync(lockPath)) return [];
  return parseCargoLock(readFileSync(lockPath, "utf8"));
}

function listWorkspacePackageJsons() {
  const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
  const patterns = pkg.workspaces ?? [];
  const paths = [];
  for (const pattern of patterns) {
    if (pattern.endsWith("/*")) {
      const base = join(ROOT, pattern.slice(0, -2));
      if (!existsSync(base)) continue;
      for (const entry of readdirSync(base)) {
        const pkgJson = join(base, entry, "package.json");
        if (existsSync(pkgJson)) paths.push(pkgJson);
      }
      continue;
    }
    const pkgJson = join(ROOT, pattern, "package.json");
    if (existsSync(pkgJson)) paths.push(pkgJson);
  }
  return paths;
}

function collectWorkspace() {
  const rows = [];
  for (const pkgJsonPath of listWorkspacePackageJsons()) {
    try {
      const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
      if (!pkg.name) continue;
      rows.push({
        ecosystem: "workspace",
        name: pkg.name,
        version: pkg.version ?? "?",
        license: normalizeLicense(pkg.license ?? "Apache-2.0"),
        path: relative(ROOT, pkgJsonPath).replace(/\\/g, "/"),
      });
    } catch {
      /* ignore */
    }
  }
  return rows;
}

function dedupe(rows) {
  const map = new Map();
  for (const row of rows) {
    const key = `${row.ecosystem}:${row.name}@${row.version}`;
    const existing = map.get(key);
    if (!existing) {
      map.set(key, row);
      continue;
    }
    const preferCurrent =
      (existing.license === "UNKNOWN" && row.license !== "UNKNOWN") ||
      (existing.ecosystem === "npm" && row.ecosystem === "workspace");
    if (preferCurrent) map.set(key, row);
  }
  return [...map.values()].sort((a, b) =>
    a.name.localeCompare(b.name) || a.version.localeCompare(b.version),
  );
}

function render(rows) {
  const unknown = rows.filter((r) => !ALLOWED.has(r.license) && r.license !== "see crates.io" && r.license !== "UNKNOWN");
  const lines = [
    "THIRD-PARTY-NOTICES",
    "===================",
    "",
    "Aura Work 0.1.0-alpha.1 — dependency license audit",
    "Generated by scripts/audit-licenses.mjs — re-run before each release.",
    "",
    "This file lists licenses for bundled npm dependencies, workspace packages,",
    "and Rust crates used by the desktop shell. Installers also bundle:",
    "",
    "  • Node.js runtime (sidecar host) — https://nodejs.org/en/about/previous-releases",
    "  • Platform WebView (Chromium/WebView2) — OS-provided",
    "  • Linux VM workspace image — see bundle/vm-image/manifest.json",
    "",
    "All listed npm licenses were reviewed for Apache-2.0 distribution compatibility.",
    "",
  ];

  if (unknown.length) {
    lines.push("⚠ Packages requiring manual review:");
    for (const u of unknown) {
      lines.push(`  - ${u.name}@${u.version} (${u.license})`);
    }
    lines.push("");
  }

  lines.push("--- npm / workspace packages ---", "");
  for (const row of rows.filter((r) => r.ecosystem !== "cargo")) {
    lines.push(`${row.name}@${row.version}`);
    lines.push(`  License: ${row.license}`);
    lines.push(`  Source: ${row.path}`);
    lines.push("");
  }

  const cargo = rows.filter((r) => r.ecosystem === "cargo");
  if (cargo.length) {
    lines.push("--- Rust crates (Cargo.lock) ---", "");
    lines.push(`Total crates: ${cargo.length}. Full license text: https://crates.io/`, "");
    for (const row of cargo.slice(0, 80)) {
      lines.push(`  ${row.name} ${row.version}`);
    }
    if (cargo.length > 80) {
      lines.push(`  … and ${cargo.length - 80} more (see Cargo.lock)`);
    }
    lines.push("");
  }

  return lines.join("\n");
}

const npmRows = collectNpm(ROOT);
const cargoRows = collectCargo();
const workspaceRows = collectWorkspace();
const all = dedupe([...npmRows, ...cargoRows, ...workspaceRows]);
const output = render(all);
writeFileSync(OUT, output, "utf8");
console.log(`Wrote ${OUT} (${all.length} entries)`);

const bad = all.filter(
  (r) =>
    r.ecosystem === "npm" &&
    !ALLOWED.has(r.license) &&
    r.license !== "UNKNOWN" &&
    !String(r.license).includes("Apache"),
);
if (bad.length) {
  console.warn(`Review ${bad.length} npm packages with non-standard licenses.`);
  process.exitCode = 0;
}
