#!/usr/bin/env node

import fs from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';

import { parseMatrixMarkdown, parseAllureResults } from './lib/trust-metrics.mjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const REPO_ROOT = path.resolve(__dirname, '..');

const TRACEABILITY_SOURCES = {
  docxMcp: path.join(REPO_ROOT, 'packages', 'docx-mcp', 'src', 'testing', 'SAFE_DOCX_OPENSPEC_TRACEABILITY.md'),
  docxCore: path.join(REPO_ROOT, 'packages', 'docx-core', 'src', 'testing', 'DOCX_COMPARISON_OPENSPEC_TRACEABILITY.md'),
};

const ALLURE_SOURCES = {
  docxMcp: path.join(REPO_ROOT, 'packages', 'docx-mcp', 'allure-results'),
  docxCore: path.join(REPO_ROOT, 'packages', 'docx-core', 'allure-results'),
};

function parseArgs() {
  const args = process.argv.slice(2);
  let outputPath = path.join(REPO_ROOT, 'site', 'src', 'trust', 'system-card.md');
  for (let i = 0; i < args.length; i += 1) {
    if (args[i] === '--output') {
      const value = args[i + 1];
      if (!value) {
        throw new Error('--output requires a path value');
      }
      outputPath = path.resolve(process.cwd(), value);
      i += 1;
      continue;
    }
    throw new Error(`Unknown argument: ${args[i]}`);
  }
  return { outputPath };
}

function runNodeScript(scriptRelativePath, scriptArgs = []) {
  const scriptPath = path.join(REPO_ROOT, scriptRelativePath);
  execFileSync(process.execPath, [scriptPath, ...scriptArgs], {
    cwd: REPO_ROOT,
    stdio: 'inherit',
  });
}

function utcTimestamp(value) {
  if (value == null) {
    return 'n/a';
  }
  return new Date(value).toISOString().replace('.000', '');
}

function pct(covered, total) {
  if (!total) {
    return '0.0%';
  }
  return `${((covered / total) * 100).toFixed(1)}%`;
}

function formatStatusCount(statusCounts, key) {
  return statusCounts[key] ?? 0;
}

function latestTimestampFromAllure(allureReports) {
  let latest = null;
  for (const report of allureReports) {
    if (typeof report.latestStop !== 'number') {
      continue;
    }
    latest = latest == null ? report.latestStop : Math.max(latest, report.latestStop);
  }
  return latest;
}

function percentValue(numerator, denominator) {
  if (!denominator) {
    return null;
  }
  return (numerator / denominator) * 100;
}

function fixedPercent(value) {
  if (value == null || Number.isNaN(value)) {
    return 'n/a';
  }
  return `${value.toFixed(1)}%`;
}

function escapeHtml(value) {
  return String(value)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function passRatePercent(report) {
  if (!report.available || !report.total) {
    return null;
  }
  return percentValue(formatStatusCount(report.statusCounts, 'passed'), report.total);
}

function nonPassingCount(report) {
  return formatStatusCount(report.statusCounts, 'failed')
    + formatStatusCount(report.statusCounts, 'broken')
    + formatStatusCount(report.statusCounts, 'unknown');
}

function chartClassForPercent(value) {
  if (value == null) return 'is-na';
  if (value >= 99) return 'is-good';
  if (value >= 95) return 'is-mid';
  return 'is-low';
}

function chartRowHtml({ label, detail, percent }) {
  const width = percent == null ? 100 : Math.max(0, Math.min(100, percent));
  const valueText = fixedPercent(percent);
  const cssClass = chartClassForPercent(percent);
  return [
    '<div class="chart-row">',
    `<div class="chart-label">${escapeHtml(label)}<span class="chart-detail">${escapeHtml(detail)}</span></div>`,
    `<div class="chart-track"><span class="chart-fill ${cssClass}" style="width:${width.toFixed(1)}%"></span></div>`,
    `<div class="chart-value">${escapeHtml(valueText)}</div>`,
    '</div>',
  ].join('');
}

function reliabilityVerdict({ unmappedScenarioCount, totalNonPassingChecks }) {
  if (unmappedScenarioCount === 0 && totalNonPassingChecks === 0) {
    return 'Strong signal in measured scope';
  }
  if (unmappedScenarioCount <= 2 && totalNonPassingChecks <= 2) {
    return 'Moderate signal; review caveats';
  }
  return 'Needs attention before relying on this run';
}

function makeSystemCardMarkdown({ traceability, allure }) {
  const docxMcp = traceability.docxMcp;
  const docxCore = traceability.docxCore;

  const mappingScope = [docxMcp, docxCore];
  const totalScenarios = mappingScope.reduce((sum, item) => sum + item.total, 0);
  const totalMapped = mappingScope.reduce((sum, item) => sum + item.covered, 0);
  const totalUnmapped = mappingScope.reduce((sum, item) => sum + item.missing + item.pending, 0);
  const mappingCoverage = percentValue(totalMapped, totalScenarios);

  const totalChecks = allure.reduce((sum, report) => sum + report.total, 0);
  const totalNonPassingChecks = allure.reduce((sum, report) => sum + nonPassingCount(report), 0);
  const latestAllureTimestamp = latestTimestampFromAllure(allure);

  const unresolved = [
    ...docxMcp.missingScenarios.map((item) => ({ packageLabel: docxMcp.label, ...item })),
    ...docxCore.missingScenarios.map((item) => ({ packageLabel: docxCore.label, ...item })),
  ];

  const conclusion = reliabilityVerdict({
    unmappedScenarioCount: totalUnmapped,
    totalNonPassingChecks,
  });

  const docxMcpPassRate = passRatePercent(allure.find((report) => report.packageLabel === 'DOCX MCP') ?? {});
  const docxCorePassRate = passRatePercent(allure.find((report) => report.packageLabel === 'DOCX Core') ?? {});

  const chartRows = [
    chartRowHtml({
      label: 'DOCX MCP: spec scenario mapping',
      detail: `${docxMcp.covered}/${docxMcp.total} scenarios`,
      percent: percentValue(docxMcp.covered, docxMcp.total),
    }),
    chartRowHtml({
      label: 'DOCX Core: spec scenario mapping',
      detail: `${docxCore.covered}/${docxCore.total} scenarios`,
      percent: percentValue(docxCore.covered, docxCore.total),
    }),
    chartRowHtml({
      label: 'DOCX MCP: test run pass rate',
      detail: `${formatStatusCount((allure.find((report) => report.packageLabel === 'DOCX MCP') ?? { statusCounts: {} }).statusCounts ?? {}, 'passed')} passing checks`,
      percent: docxMcpPassRate,
    }),
    chartRowHtml({
      label: 'DOCX Core: test run pass rate',
      detail: `${formatStatusCount((allure.find((report) => report.packageLabel === 'DOCX Core') ?? { statusCounts: {} }).statusCounts ?? {}, 'passed')} passing checks`,
      percent: docxCorePassRate,
    }),
  ];

  const lines = [];
  lines.push('---');
  lines.push('layout: layouts/base.njk');
  lines.push('title: System Card | Safe DOCX');
  lines.push('description: Reliability summary and evidence for Safe DOCX.');
  lines.push('contentClass: content');
  lines.push('---');
  lines.push('');
  lines.push('# Safe DOCX System Card');
  lines.push('');
  lines.push(`Evidence snapshot (UTC): \`${utcTimestamp(latestAllureTimestamp)}\``);
  lines.push('');
  lines.push('## Executive Summary');
  lines.push('');
  lines.push('<div class="summary-banner">');
  lines.push(`<h2>${escapeHtml(conclusion)}</h2>`);
  lines.push(`<p>${escapeHtml(totalMapped)} of ${escapeHtml(totalScenarios)} spec scenarios are mapped to tests in the currently measured packages. ${escapeHtml(totalChecks)} automated checks were recorded, with ${escapeHtml(totalNonPassingChecks)} non-passing outcomes.</p>`);
  lines.push('</div>');
  lines.push('');
  lines.push('- This card focuses on reliability signals developers can scan quickly.');
  lines.push('- Scope note: both packages shown here include scenario-mapping and run-status summaries.');
  lines.push('');
  lines.push('## Visual Snapshot');
  lines.push('');
  lines.push('<div class="chart">');
  for (const row of chartRows) {
    lines.push(row);
  }
  lines.push('</div>');
  lines.push('');
  lines.push('## Key Results');
  lines.push('');
  lines.push('### 1) Spec Scenario Mapping');
  lines.push('');
  lines.push('This is **spec scenario mapping coverage**, not line/branch code coverage.');
  lines.push('');
  lines.push('| Package | Spec scenarios | Mapped | Unmapped | Coverage |');
  lines.push('|---|---:|---:|---:|---:|');
  lines.push(`| ${docxMcp.label} | ${docxMcp.total} | ${docxMcp.covered} | ${docxMcp.missing + docxMcp.pending} | ${pct(docxMcp.covered, docxMcp.total)} |`);
  lines.push(`| ${docxCore.label} | ${docxCore.total} | ${docxCore.covered} | ${docxCore.missing + docxCore.pending} | ${pct(docxCore.covered, docxCore.total)} |`);
  lines.push(`| Combined measured scope | ${totalScenarios} | ${totalMapped} | ${totalUnmapped} | ${fixedPercent(mappingCoverage)} |`);
  lines.push('');
  if (unresolved.length === 0) {
    lines.push('No unmapped scenarios were found in the currently measured scope for this run.');
    lines.push('');
  } else {
    const previewCount = 10;
    lines.push(`Unmapped scenarios found in this run: ${unresolved.length}`);
    lines.push('Top items:');
    for (const item of unresolved.slice(0, previewCount)) {
      const location = item.change ? ` (${item.change})` : '';
      lines.push(`- [${item.packageLabel}] ${item.scenario}${location} - ${item.status}`);
    }
    if (unresolved.length > previewCount) {
      lines.push(`- ...and ${unresolved.length - previewCount} more (see detailed tables in appendix).`);
    }
    lines.push('');
  }
  lines.push('### 2) Automated Test Run Status');
  lines.push('');
  lines.push('| Package | Recorded checks | Passing | Non-passing | Skipped | Last observed update (UTC) |');
  lines.push('|---|---:|---:|---:|---:|---|');
  for (const report of allure) {
    if (!report.available) {
      lines.push(`| ${report.packageLabel} | 0 | 0 | 0 | 0 | n/a |`);
      continue;
    }
    lines.push(
      `| ${report.packageLabel} | ${report.total} | ${formatStatusCount(report.statusCounts, 'passed')} | ${nonPassingCount(report)} | ${formatStatusCount(report.statusCounts, 'skipped')} | ${utcTimestamp(report.latestStop)} |`,
    );
  }
  lines.push('');
  lines.push('## Discussion');
  lines.push('');
  lines.push('- A high mapping percentage means each spec scenario is represented by at least one test.');
  lines.push('- A high pass rate means recent automated runs did not surface failures in the current result set.');
  lines.push('- Both signals should be read together, along with known limitations below.');
  lines.push('');
  lines.push('## Limitations');
  lines.push('');
  lines.push('- This card does not report line or branch code coverage.');
  lines.push('- Mapping coverage can be 100% and defects can still exist.');
  lines.push('- Run-status counts depend on the current contents of package test-result directories; stale results should be cleaned before release reporting.');
  lines.push('');
  lines.push('## Appendix: Methods (Technical)');
  lines.push('');
  lines.push('- Mapping numbers are read from generated spec-scenario mapping files for DOCX MCP and DOCX Core.');
  lines.push('- Run-status numbers are read from structured test result files in each package result directory.');
  lines.push('- This page is regenerated by running `npm run generate:system-card`.');
  lines.push('');
  lines.push('## Appendix: Detailed Tables');
  lines.push('');
  lines.push('- [DOCX MCP scenario mapping table](../traceability/safe-docx/index.html)');
  lines.push('- [DOCX Core scenario mapping table](../traceability/docx-core/index.html)');
  lines.push('');
  lines.push(`Data snapshot timestamp for run-status metrics: \`${utcTimestamp(latestAllureTimestamp)}\``);
  lines.push('');

  return `${lines.join('\n')}\n`;
}

function wrapMatrixAsPage(title, description, permalink, body) {
  return `---\nlayout: layouts/base.njk\ntitle: ${title}\ndescription: ${description}\ncontentClass: content\npermalink: ${permalink}\n---\n\n${body}`;
}

async function ensureDir(filePath) {
  await fs.mkdir(path.dirname(filePath), { recursive: true });
}

async function main() {
  const { outputPath } = parseArgs();

  // Recompute matrices before reading them to avoid drift.
  // Use default mode so the system card still renders during active feature work
  // while surfacing any unmapped scenarios in the generated output.
  runNodeScript('packages/docx-core/scripts/validate_openspec_coverage.mjs');
  runNodeScript('packages/docx-mcp/scripts/validate_openspec_coverage.mjs');

  const [docxMcpMatrixRaw, docxCoreMatrixRaw] = await Promise.all([
    fs.readFile(TRACEABILITY_SOURCES.docxMcp, 'utf-8'),
    fs.readFile(TRACEABILITY_SOURCES.docxCore, 'utf-8'),
  ]);

  const traceability = {
    docxMcp: parseMatrixMarkdown(docxMcpMatrixRaw, 'DOCX MCP'),
    docxCore: parseMatrixMarkdown(docxCoreMatrixRaw, 'DOCX Core'),
  };

  const allure = await Promise.all([
    parseAllureResults('DOCX MCP', ALLURE_SOURCES.docxMcp),
    parseAllureResults('DOCX Core', ALLURE_SOURCES.docxCore),
  ]);

  const systemCard = makeSystemCardMarkdown({
    traceability,
    allure,
  });

  await ensureDir(outputPath);
  await fs.writeFile(outputPath, systemCard, 'utf-8');

  const traceabilityDir = path.join(path.dirname(outputPath), 'traceability');
  const docxMcpTraceabilityPagePath = path.join(traceabilityDir, 'safe-docx.md');
  const docxCoreTraceabilityPagePath = path.join(traceabilityDir, 'docx-core.md');

  await ensureDir(docxMcpTraceabilityPagePath);

  await fs.writeFile(
    docxMcpTraceabilityPagePath,
    wrapMatrixAsPage(
      'DOCX MCP Traceability Matrix | Safe DOCX',
      'Generated OpenSpec-to-test scenario mapping for the DOCX MCP package.',
      '/trust/traceability/safe-docx/',
      docxMcpMatrixRaw,
    ),
    'utf-8',
  );

  await fs.writeFile(
    docxCoreTraceabilityPagePath,
    wrapMatrixAsPage(
      'DOCX Core Traceability Matrix | Safe DOCX',
      'Generated spec-to-test scenario mapping for the DOCX Core package.',
      '/trust/traceability/docx-core/',
      docxCoreMatrixRaw,
    ),
    'utf-8',
  );

  const relativeOutput = path.relative(REPO_ROOT, outputPath).split(path.sep).join('/');
  console.log(`Generated system card: ${relativeOutput}`);
}

await main();
