#!/usr/bin/env node

import { readFile } from 'node:fs/promises';
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
import { pathToFileURL } from 'node:url';
import { type } from 'arktype';

import {
  classifyWarning,
  discoverCoverageConfigs,
  loadRootCoverageConfig,
} from './coverage-config.js';

class CoverageReportError extends Error {}

const StatementMap = type({
  '[string]': type({
    start: type({
      line: 'number.integer >= 1',
    }),
  }),
});
const HitMap = type({ '[string]': 'number.integer >= 0' });
const BranchHitMap = type({ '[string]': type('number.integer >= 0').array() });
const BranchMap = type({
  '[string]': type({
    locations: type('unknown').array(),
  }),
});
const FunctionMap = type({ '[string]': 'unknown' });

const IstanbulCoverageRecord = type({
  path: 'string',
  statementMap: StatementMap,
  s: HitMap,
  branchMap: BranchMap,
  b: BranchHitMap,
  fnMap: FunctionMap,
  f: HitMap,
});

const METRICS = ['lines', 'statements', 'branches', 'functions'];

function assertSameKeys(left, right, description) {
  const leftKeys = Object.keys(left).sort();
  const rightKeys = Object.keys(right).sort();
  if (
    leftKeys.length !== rightKeys.length ||
    leftKeys.some((key, index) => key !== rightKeys[index])
  ) {
    throw new CoverageReportError(`Invalid Istanbul coverage record: ${description} keys differ`);
  }
}

function validateCoverageRecord(value, filePath) {
  let record;
  try {
    record = IstanbulCoverageRecord.assert(value);
  } catch (error) {
    throw new CoverageReportError(`Invalid Istanbul coverage record for ${filePath}: ${error}`, {
      cause: error,
    });
  }

  assertSameKeys(record.statementMap, record.s, `${filePath} statementMap/s`);
  assertSameKeys(record.branchMap, record.b, `${filePath} branchMap/b`);
  assertSameKeys(record.fnMap, record.f, `${filePath} fnMap/f`);

  for (const [branchId, hits] of Object.entries(record.b)) {
    if (record.branchMap[branchId].locations.length !== hits.length) {
      throw new CoverageReportError(
        `Invalid Istanbul coverage record: ${filePath} branch ${branchId} locations/hits differ`,
      );
    }
  }

  return record;
}

function normalizeReportPath(root, reportPath) {
  if (reportPath.length === 0 || reportPath.includes('\0')) {
    throw new CoverageReportError(`Invalid coverage report path: ${JSON.stringify(reportPath)}`);
  }
  const absoluteRoot = resolve(root);
  const absolutePath = isAbsolute(reportPath)
    ? resolve(reportPath)
    : resolve(absoluteRoot, reportPath);
  const pathFromRoot = relative(absoluteRoot, absolutePath);
  if (pathFromRoot === '..' || pathFromRoot.startsWith(`..${sep}`) || isAbsolute(pathFromRoot)) {
    throw new CoverageReportError(`Covered path is outside repository root: ${reportPath}`);
  }
  return pathFromRoot.split(sep).join('/');
}

function emptyCount() {
  return { total: 0, covered: 0 };
}

function percentage(count) {
  return {
    ...count,
    pct: count.total === 0 ? 100 : (count.covered / count.total) * 100,
  };
}

function recordMetrics(record) {
  const statements = {
    total: Object.keys(record.s).length,
    covered: Object.values(record.s).filter((hits) => hits > 0).length,
  };
  const branchHits = Object.values(record.b).flat();
  const branches = {
    total: branchHits.length,
    covered: branchHits.filter((hits) => hits > 0).length,
  };
  const functions = {
    total: Object.keys(record.f).length,
    covered: Object.values(record.f).filter((hits) => hits > 0).length,
  };
  const linesByNumber = new Map();
  for (const [statementId, location] of Object.entries(record.statementMap)) {
    const line = location.start.line;
    const covered = record.s[statementId] > 0;
    linesByNumber.set(line, covered || (linesByNumber.get(line) ?? false));
  }
  const lines = {
    total: linesByNumber.size,
    covered: [...linesByNumber.values()].filter(Boolean).length,
  };
  return { lines, statements, branches, functions };
}

function addMetrics(target, source) {
  for (const metric of METRICS) {
    target[metric].total += source[metric].total;
    target[metric].covered += source[metric].covered;
  }
}

function packagePathSort(left, right) {
  return left.packageDir.localeCompare(right.packageDir);
}

function isWithinPackage(sourcePath, packageDir) {
  return sourcePath === packageDir || sourcePath.startsWith(`${packageDir}/`);
}

export function aggregateCoverage({ root, report, packageConfigs, skippedPackageDirs = [] }) {
  if (report === null || typeof report !== 'object' || Array.isArray(report)) {
    throw new CoverageReportError('Coverage report must be an object keyed by source path');
  }

  const owners = [...packageConfigs].sort(
    (left, right) => right.packageDir.length - left.packageDir.length,
  );
  const aggregateByPackage = new Map(
    packageConfigs.map(({ packageDir, config }) => [
      packageDir,
      {
        packageDir,
        thresholds: config.thresholds,
        files: [],
        present: false,
        counts: Object.fromEntries(METRICS.map((metric) => [metric, emptyCount()])),
      },
    ]),
  );

  for (const [reportPath, value] of Object.entries(report)) {
    const sourcePath = normalizeReportPath(root, reportPath);
    if (skippedPackageDirs.some((packageDir) => isWithinPackage(sourcePath, packageDir))) {
      continue;
    }
    const owner = owners.find(({ packageDir }) => isWithinPackage(sourcePath, packageDir));
    if (!owner) {
      throw new CoverageReportError(
        `Covered path has no configured package owner: ${sourcePath || reportPath}`,
      );
    }
    const record = validateCoverageRecord(value, sourcePath);
    const aggregate = aggregateByPackage.get(owner.packageDir);
    aggregate.present = true;
    aggregate.files.push(sourcePath);
    addMetrics(aggregate.counts, recordMetrics(record));
  }

  return [...aggregateByPackage.values()].sort(packagePathSort).map((aggregate) => ({
    packageDir: aggregate.packageDir,
    thresholds: aggregate.thresholds,
    files: aggregate.files.sort(),
    present: aggregate.present,
    metrics: Object.fromEntries(
      METRICS.map((metric) => [metric, percentage(aggregate.counts[metric])]),
    ),
  }));
}

export function checkThresholds(metrics, thresholds) {
  const deficits = [];
  for (const metric of METRICS) {
    const threshold = thresholds[metric];
    if (threshold !== undefined && metrics[metric].pct < threshold) {
      deficits.push({ metric, actual: metrics[metric].pct, threshold });
    }
  }
  return deficits;
}

export function suggestThresholdIncreases(metrics, thresholds, margin = 5, cap = 95) {
  const suggestions = {};
  for (const metric of METRICS) {
    const current = thresholds[metric];
    if (current === undefined || metrics[metric].pct < current + margin) continue;
    const suggested = Math.min(cap, Math.floor(metrics[metric].pct));
    if (suggested > current) {
      suggestions[metric] = { current, actual: metrics[metric].pct, suggested };
    }
  }
  return suggestions;
}

export function processCoverageReport({ root, report, packageConfigs, rootConfig, now }) {
  const excluded = new Set(rootConfig.excludedPackages.map((path) => `packages/${path}`));
  const configuredPackages = packageConfigs.filter(({ packageDir }) => !excluded.has(packageDir));
  const warningByPackage = new Map(
    rootConfig.warningOnly.map((entry) => [`packages/${entry.package}`, entry]),
  );
  const expiredWarnings = rootConfig.warningOnly
    .map((entry) => ({ entry, ...classifyWarning(entry, now) }))
    .filter(({ active }) => !active);
  const expiredPackages = new Set(expiredWarnings.map(({ entry }) => `packages/${entry.package}`));
  const warnings = [];
  const failures = [];

  const packages = aggregateCoverage({
    root,
    report,
    packageConfigs: configuredPackages,
    skippedPackageDirs: [...excluded],
  }).map((packageResult) => {
    const deficits = checkThresholds(packageResult.metrics, packageResult.thresholds);
    const warningEntry = warningByPackage.get(packageResult.packageDir);
    const warningState = warningEntry ? classifyWarning(warningEntry, now) : null;
    const hasThresholds = Object.keys(packageResult.thresholds).length > 0;
    let status = hasThresholds ? 'passed' : 'no-threshold';

    if (!packageResult.present && warningState?.active) {
      status = 'missing-warning';
      warnings.push({
        packageDir: packageResult.packageDir,
        reason: 'absent-from-report',
        entry: warningEntry,
        expiryDate: warningState.expiryDate,
      });
    } else if (!packageResult.present && hasThresholds) {
      status = expiredPackages.has(packageResult.packageDir) ? 'expired-warning' : 'missing';
      failures.push({
        packageDir: packageResult.packageDir,
        reason: 'absent-from-report',
      });
    } else if (expiredPackages.has(packageResult.packageDir)) {
      status = 'expired-warning';
      if (deficits.length > 0) {
        failures.push({ packageDir: packageResult.packageDir, deficits });
      }
    } else if (deficits.length > 0 && warningState?.active) {
      status = 'warning';
      warnings.push({
        packageDir: packageResult.packageDir,
        deficits,
        entry: warningEntry,
        expiryDate: warningState.expiryDate,
      });
    } else if (deficits.length > 0) {
      status = 'failed';
      failures.push({ packageDir: packageResult.packageDir, deficits });
    }

    const suggestions =
      status === 'passed' && packageResult.present
        ? suggestThresholdIncreases(packageResult.metrics, packageResult.thresholds)
        : {};
    return { ...packageResult, deficits, status, suggestions };
  });

  return {
    ok: failures.length === 0 && expiredWarnings.length === 0,
    packages,
    failures,
    warnings,
    expiredWarnings,
    skipped: [...excluded].sort(),
  };
}

function formatDeficits(deficits, indent) {
  return deficits
    .map(
      ({ metric, actual, threshold }) =>
        `${indent}${metric}: ${actual.toFixed(2)}% < ${threshold}%`,
    )
    .join('\n');
}

export function formatCoverageReport(result) {
  const lines = ['='.repeat(80), 'COVERAGE REPORT SUMMARY', '='.repeat(80), ''];

  if (result.expiredWarnings.length > 0) {
    lines.push('EXPIRED COVERAGE WARNINGS - BLOCKING', '-'.repeat(80));
    for (const { entry, expiryDate } of result.expiredWarnings) {
      lines.push(`  packages/${entry.package}`);
      lines.push(`    Reason: ${entry.reason}`);
      lines.push(`    Added: ${entry.addedDate} | Expired: ${expiryDate}`);
      if (entry.assignee) lines.push(`    Assignee: ${entry.assignee}`);
      if (entry.linear) lines.push(`    Linear: ${entry.linear}`);
    }
    lines.push(
      '  Action required: remove the exception after recovery or extend it with justification.',
      '',
    );
  }

  if (result.warnings.length > 0) {
    lines.push('TECHNICAL DEBT - NONBLOCKING COVERAGE WARNINGS', '-'.repeat(80));
    for (const warning of result.warnings) {
      lines.push(`  ${warning.packageDir}`);
      lines.push(`    Reason: ${warning.entry.reason}`);
      lines.push(`    Added: ${warning.entry.addedDate} | Expires: ${warning.expiryDate}`);
      if (warning.entry.assignee) lines.push(`    Assignee: ${warning.entry.assignee}`);
      if (warning.entry.linear) lines.push(`    Linear: ${warning.entry.linear}`);
      if (warning.reason === 'absent-from-report') {
        lines.push('    Missing from coverage report');
      } else {
        lines.push(formatDeficits(warning.deficits, '    '));
      }
    }
    lines.push('');
  }

  if (result.failures.length > 0) {
    lines.push('BLOCKING COVERAGE FAILURES', '-'.repeat(80));
    for (const failure of result.failures) {
      lines.push(`  ${failure.packageDir}`);
      if (failure.reason === 'absent-from-report') {
        lines.push('    Missing from coverage report');
      } else {
        lines.push(formatDeficits(failure.deficits, '    '));
      }
    }
    lines.push('');
  }

  const passing = result.packages.filter(({ status }) => status === 'passed');
  if (passing.length > 0) {
    lines.push(
      'PASSING',
      '-'.repeat(80),
      ...passing.map(({ packageDir }) => `  ${packageDir}`),
      '',
    );
  }

  const noThreshold = result.packages.filter(
    ({ present, status }) => present && status === 'no-threshold',
  );
  if (noThreshold.length > 0) {
    lines.push(
      'NO THRESHOLDS',
      '-'.repeat(80),
      ...noThreshold.map(({ packageDir }) => `  ${packageDir}`),
      '',
    );
  }

  const absentWithoutThresholds = result.packages.filter(
    ({ present, status }) => !present && status === 'no-threshold',
  );
  if (absentWithoutThresholds.length > 0) {
    lines.push(
      'ABSENT FROM REPORT',
      '-'.repeat(80),
      '  No source entries; no thresholds configured (empty metrics are 100%):',
      ...absentWithoutThresholds.map(({ packageDir }) => `  ${packageDir}`),
      '',
    );
  }

  const suggestions = result.packages.filter(
    ({ suggestions: packageSuggestions }) => Object.keys(packageSuggestions).length > 0,
  );
  if (suggestions.length > 0) {
    lines.push('THRESHOLD INCREASE SUGGESTIONS', '-'.repeat(80));
    for (const packageResult of suggestions) {
      lines.push(`  ${packageResult.packageDir}`);
      for (const metric of METRICS) {
        const suggestion = packageResult.suggestions[metric];
        if (suggestion) {
          lines.push(
            `    ${metric}: ${suggestion.current} -> ${suggestion.suggested} (actual ${suggestion.actual.toFixed(2)}%)`,
          );
        }
      }
    }
    lines.push('');
  }

  if (result.skipped.length > 0) {
    lines.push(
      'SKIPPED BY ROOT POLICY',
      '-'.repeat(80),
      ...result.skipped.map((packageDir) => `  ${packageDir}`),
      '',
    );
  }

  const statusCounts = Object.fromEntries(
    [
      'passed',
      'failed',
      'missing',
      'warning',
      'missing-warning',
      'expired-warning',
      'no-threshold',
    ].map((status) => [
      status,
      result.packages.filter((packageResult) => packageResult.status === status).length,
    ]),
  );
  lines.push('='.repeat(80));
  lines.push(
    `Total: ${result.packages.length} | Passed: ${statusCounts.passed} | Failures: ${result.failures.length} | Warnings: ${statusCounts.warning + statusCounts['missing-warning']} | Expired: ${result.expiredWarnings.length} | No thresholds: ${statusCounts['no-threshold']} | Skipped: ${result.skipped.length}`,
  );
  lines.push('='.repeat(80));
  return `${lines.join('\n')}\n`;
}

async function loadCoverageJson(root) {
  const reportPath = join(root, 'coverage', 'coverage-final.json');
  let source;
  try {
    source = await readFile(reportPath, 'utf8');
  } catch (error) {
    throw new CoverageReportError(`Missing coverage report: ${reportPath}`, { cause: error });
  }
  try {
    return JSON.parse(source);
  } catch (error) {
    throw new CoverageReportError(`Malformed coverage report: ${reportPath}`, { cause: error });
  }
}

export async function runCoverageReport({ root = resolve(process.cwd()), now = new Date() } = {}) {
  const rootConfig = loadRootCoverageConfig(root);
  const packageConfigs = discoverCoverageConfigs(root);
  const report = await loadCoverageJson(root);
  return processCoverageReport({ root, report, packageConfigs, rootConfig, now });
}

export async function main() {
  const result = await runCoverageReport();
  process.stdout.write(formatCoverageReport(result));
  if (!result.ok) process.exitCode = 1;
  return result;
}

const isDirectExecution =
  process.argv[1] !== undefined && pathToFileURL(resolve(process.argv[1])).href === import.meta.url;

if (isDirectExecution) {
  main().catch((error) => {
    console.error(`Coverage report failed: ${error.message}`);
    process.exitCode = 1;
  });
}