#!/usr/bin/env node
// scripts/coverage/aggregate.mjs — L2 of the coverage architecture (ADR-020).
//
// Discovers every per-package lcov (`packages/*/coverage/lcov.info`,
// `apps/*/coverage/lcov.info`), normalizes their paths to repo-relative,
// merges into `coverage/lcov.info` at the repo root, and emits
// `coverage/summary.json` — the committed trend store.
//
// Output:
//   - coverage/lcov.info       (gitignored — large)
//   - coverage/summary.json    (committed — trend via `git log -- ...`)
//   - stdout: short status line
// Exit: 0 on success, 1 if no lcov files found.
//
// Usage:
//   pnpm coverage:aggregate                 # default discovery + emit
//   pnpm coverage:aggregate -- --json       # print summary to stdout
//
// Implementation: zero deps. Pure Node ESM.

import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";

/**
 * Find every per-package / per-app lcov.info file under packages/* and apps/*.
 * Returns absolute paths.
 */
export function discoverLcovs(repoRoot) {
  const results = [];
  for (const root of ["packages", "apps"]) {
    const dir = path.join(repoRoot, root);
    if (!fs.existsSync(dir)) continue;
    for (const pkg of fs.readdirSync(dir)) {
      const lcov = path.join(dir, pkg, "coverage", "lcov.info");
      if (fs.existsSync(lcov)) {
        results.push({
          packageDir: path.join(root, pkg), // repo-relative
          lcov,
        });
      }
    }
  }
  return results.sort((a, b) => a.packageDir.localeCompare(b.packageDir));
}

/**
 * Normalize lcov text so every SF line is repo-relative. Vitest emits paths
 * relative to the package's vitest.config.ts (e.g. `src/foo.ts`), so we
 * prepend `packages/<pkg>/` (or `apps/<pkg>/`) to each SF.
 */
export function normalizeLcov(text, packageDir) {
  return text
    .split("\n")
    .map((line) => {
      if (!line.startsWith("SF:")) return line;
      const p = line.slice(3);
      // If already absolute or already prefixed with packages/apps, leave alone
      if (path.isAbsolute(p) || p.startsWith(packageDir + "/")) return line;
      return `SF:${packageDir}/${p}`;
    })
    .join("\n");
}

/**
 * Compute lcov-level summary stats from a parsed lcov map.
 * Returns { statements, branches, functions, lines } as percentages
 * (statements ≈ lines in V8's lcov output).
 *
 * Algorithm: walk all SF blocks (each record has LF/LH for line totals,
 * BRF/BRH for branches, FNF/FNH for functions). Sum across files; divide.
 */
export function summarizeLcov(lcovText) {
  let lf = 0,
    lh = 0,
    brf = 0,
    brh = 0,
    fnf = 0,
    fnh = 0;
  for (const line of lcovText.split("\n")) {
    if (line.startsWith("LF:")) lf += Number(line.slice(3));
    else if (line.startsWith("LH:")) lh += Number(line.slice(3));
    else if (line.startsWith("BRF:")) brf += Number(line.slice(4));
    else if (line.startsWith("BRH:")) brh += Number(line.slice(4));
    else if (line.startsWith("FNF:")) fnf += Number(line.slice(4));
    else if (line.startsWith("FNH:")) fnh += Number(line.slice(4));
  }
  const pct = (hit, found) =>
    found === 0 ? 100 : Math.round((hit / found) * 10000) / 100;
  return {
    statements: pct(lh, lf), // V8 lcov: statements ≈ lines
    branches: pct(brh, brf),
    functions: pct(fnh, fnf),
    lines: pct(lh, lf),
    counts: { lf, lh, brf, brh, fnf, fnh },
  };
}

/**
 * Aggregate the discovered lcovs. Returns:
 *   {
 *     mergedLcov: string,
 *     summary: { generatedAt, commit, repo: {...}, byPackage: { ... } }
 *   }
 */
export function aggregate(repoRoot, opts = {}) {
  const lcovs = opts.lcovs ?? discoverLcovs(repoRoot);
  if (lcovs.length === 0) {
    return { mergedLcov: "", summary: null, lcovs: [] };
  }

  const merged = [];
  const byPackage = {};

  for (const { packageDir, lcov } of lcovs) {
    const text = fs.readFileSync(lcov, "utf8");
    const normalized = normalizeLcov(text, packageDir);
    merged.push(normalized);
    // The package's @repo/<name> identifier comes from its package.json
    const pkgJsonPath = path.join(repoRoot, packageDir, "package.json");
    let pkgName = packageDir;
    if (fs.existsSync(pkgJsonPath)) {
      try {
        pkgName =
          JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).name ?? packageDir;
      } catch {
        // fall through
      }
    }
    byPackage[pkgName] = summarizeLcov(normalized);
  }

  const mergedLcov = merged.join("\n");
  const repo = summarizeLcov(mergedLcov);

  let commit = "unknown";
  try {
    commit = execSync("git rev-parse --short HEAD", {
      cwd: repoRoot,
      encoding: "utf8",
    }).trim();
  } catch {
    // not in a git repo, leave as "unknown"
  }

  return {
    mergedLcov,
    lcovs,
    summary: {
      generatedAt: opts.now ?? new Date().toISOString(),
      commit,
      repo,
      byPackage,
    },
  };
}

// ---- CLI ----

function parseArgs(argv) {
  const out = { json: false };
  for (let i = 2; i < argv.length; i++) {
    const a = argv[i];
    if (a === "--json") out.json = true;
    else if (a === "--help" || a === "-h") {
      console.log("Usage: pnpm coverage:aggregate [-- --json]");
      process.exit(0);
    }
  }
  return out;
}

function main() {
  const args = parseArgs(process.argv);
  const repoRoot = process.cwd();
  const { mergedLcov, summary, lcovs } = aggregate(repoRoot);

  if (lcovs.length === 0) {
    process.stderr.write(
      `[coverage:aggregate] No per-package lcov.info files found.\n` +
        `Run \`pnpm test -- --coverage\` first.\n`,
    );
    process.exit(1);
  }

  const outDir = path.join(repoRoot, "coverage");
  fs.mkdirSync(outDir, { recursive: true });
  fs.writeFileSync(path.join(outDir, "lcov.info"), mergedLcov);
  fs.writeFileSync(
    path.join(outDir, "summary.json"),
    JSON.stringify(summary, null, 2) + "\n",
  );

  if (args.json) {
    process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
  } else {
    process.stdout.write(
      `[coverage:aggregate] Merged ${lcovs.length} lcov(s); ` +
        `repo coverage: statements ${summary.repo.statements}%, ` +
        `branches ${summary.repo.branches}%, ` +
        `functions ${summary.repo.functions}%, ` +
        `lines ${summary.repo.lines}%\n` +
        `Wrote coverage/lcov.info + coverage/summary.json\n`,
    );
  }
}

const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
if (invokedDirectly) {
  main();
}
