/*
 * render-status — merges new InspectionRecord[] into the human-facing
 * status report `docs/blog-indexing-status.md`.
 *
 *   Usage: tsx render-status.ts --inspections <file.json> [--out file.md]
 *
 * Strategy: the markdown file is regenerated wholesale from a JSON
 * sidecar (`docs/blog-indexing-status.json`) so diffs stay legible and
 * we don't risk parsing markdown to update it. Per-URL latest record
 * wins; older records are kept under "Recent inspections" capped at 50
 * entries.
 *
 * If `--inspections` is empty the script still rewrites the markdown
 * (useful for backfill / formatting changes).
 */
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import {
  type BlogIndexingState,
  type InspectionRecord,
  type SearchAnalyticsRecord,
  REPO_ROOT,
  readJsonFile,
} from './lib.ts';

interface Args {
  inspections?: string;
  analytics?: string;
  out: string;
  state: string;
}

function parseArgs(argv: string[]): Args {
  const args: Partial<Args> = {};
  for (let i = 0; i < argv.length; i++) {
    if (argv[i] === '--inspections') args.inspections = argv[++i];
    else if (argv[i] === '--analytics') args.analytics = argv[++i];
    else if (argv[i] === '--out') args.out = argv[++i];
    else if (argv[i] === '--state') args.state = argv[++i];
  }
  args.out ??= path.join(REPO_ROOT, 'docs/blog-indexing-status.md');
  args.state ??= path.join(REPO_ROOT, 'docs/blog-indexing-status.json');
  return args as Args;
}

function loadState(file: string): BlogIndexingState {
  if (!existsSync(file)) return { latest: {}, history: [] };
  const raw = JSON.parse(readFileSync(file, 'utf8'));
  return {
    latest: raw.latest ?? {},
    history: raw.history ?? [],
    performance: raw.performance ?? {},
    firstInspectedAt: raw.firstInspectedAt ?? raw.firstSeenAt ?? {},
  };
}

function fmtVerdictBadge(record: InspectionRecord): string {
  if ('error' in record.result) return 'error';
  const r = record.result;
  if (r.isIndexed) return 'indexed';
  if (r.verdict === 'PASS') return 'pass';
  if (r.verdict === 'PARTIAL') return 'partial';
  if (r.verdict === 'FAIL') return 'fail';
  return r.verdict.toLowerCase();
}

function fmtCoverage(record: InspectionRecord): string {
  if ('error' in record.result) return record.result.error;
  return record.result.coverageState;
}

function fmtCanonical(record: InspectionRecord): string {
  if ('error' in record.result) return '—';
  const { userCanonical, googleCanonical } = record.result;
  if (!userCanonical && !googleCanonical) return '—';
  if (userCanonical && googleCanonical && userCanonical !== googleCanonical) {
    return `mismatch: user=${userCanonical} google=${googleCanonical}`;
  }
  return googleCanonical ?? userCanonical ?? '—';
}

function fmtDate(iso?: string): string {
  if (!iso) return '—';
  return iso.slice(0, 10);
}

function fmtNum(n?: number): string {
  if (n == null) return '—';
  return new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(n);
}

function fmtPercent(n?: number): string {
  if (n == null) return '—';
  return `${(n * 100).toFixed(1)}%`;
}

function fmtPosition(n?: number): string {
  if (!n) return '—';
  return n.toFixed(1);
}

function renderMarkdown(state: BlogIndexingState): string {
  const latestRows = Object.values(state.latest)
    .sort((a, b) => (a.url < b.url ? -1 : 1))
    .map((r) => {
      const slug = r.url.replace(/^https?:\/\/[^/]+\/blog\//, '').replace(/\/$/, '');
      const lastCrawl =
        'error' in r.result ? '—' : fmtDate(r.result.lastCrawlTime);
      const perf7 = state.performance?.[r.url]?.['7'];
      const perf28 = state.performance?.[r.url]?.['28'];
      return `| ${slug} | ${fmtVerdictBadge(r)} | ${fmtCoverage(r)} | ${lastCrawl} | ${fmtDate(r.inspectedAt)} | ${fmtNum(perf7?.impressions)} | ${fmtNum(perf7?.clicks)} | ${fmtPercent(perf7?.ctr)} | ${fmtPosition(perf28?.position)} |`;
    })
    .join('\n');

  const historyRows = state.history
    .slice(0, 50)
    .map((r) => {
      const slug = r.url.replace(/^https?:\/\/[^/]+\/blog\//, '').replace(/\/$/, '');
      return `| ${fmtDate(r.inspectedAt)} | ${slug} | ${fmtVerdictBadge(r)} | ${fmtCoverage(r)} | ${fmtCanonical(r)} |`;
    })
    .join('\n');

  return `# Blog indexing status

_Generated by \`.github/workflows/blog-indexing-monitor.yml\` and
\`.github/workflows/blog-indexing-on-deploy.yml\`. Do not hand-edit; the
sidecar JSON at \`docs/blog-indexing-status.json\` is the source of
truth and the next workflow run will overwrite this file._

Verdict legend (from Google URL Inspection API):

- **indexed** — \`PASS\` and coverage state contains "Submitted and indexed"
- **pass** — Google has the URL but is not yet indexed
- **partial** — Google sees the URL with one or more soft warnings
- **fail** — page failed inspection (likely fetch / canonical / robots)
- **error** — automation could not call the API for this URL

Traffic columns come from the GSC Search Analytics API. They lag by
roughly two days and represent canonical URL-level performance, not
site-wide totals.

If a URL stays in \`Discovered - currently not indexed\` or
\`Crawled - currently not indexed\` past the T+7 window, the monitor
workflow will open a tracking issue. See
[docs/blog-indexing-automation.md](./blog-indexing-automation.md) for
the full architecture.

## Latest verdict per URL

| Slug | Verdict | Coverage state | Last Google crawl | Last inspected | 7d impressions | 7d clicks | 7d CTR | 28d avg position |
|---|---|---|---|---|---|---|---|---|
${latestRows || '| _no inspections recorded yet_ | — | — | — | — | — | — | — | — |'}

## Recent inspections

| Inspected | Slug | Verdict | Coverage state | Canonical |
|---|---|---|---|---|
${historyRows || '| _no inspections recorded yet_ | — | — | — | — |'}
`;
}

function main() {
  const args = parseArgs(process.argv.slice(2));
  const state = loadState(args.state);

  if (args.inspections && existsSync(args.inspections)) {
    const fresh = readJsonFile<InspectionRecord[]>(args.inspections);
    for (const record of fresh) {
      state.latest[record.url] = record;
      state.history.unshift(record);
      state.firstInspectedAt ??= {};
      state.firstInspectedAt[record.url] ??= record.inspectedAt;
    }
    state.history = state.history.slice(0, 50);
  }

  if (args.analytics && existsSync(args.analytics)) {
    const fresh = readJsonFile<SearchAnalyticsRecord[]>(args.analytics);
    state.performance ??= {};
    for (const record of fresh) {
      state.performance[record.url] ??= {};
      state.performance[record.url][String(record.windowDays) as '7' | '28'] = record;
    }
  }

  writeFileSync(args.state, JSON.stringify(state, null, 2) + '\n');
  writeFileSync(args.out, renderMarkdown(state));
  console.log(
    `Wrote ${args.out} (${Object.keys(state.latest).length} URLs tracked, ${state.history.length} history entries).`,
  );
}

main();
