/**
 * SET 19 / 45a / 45c — pure install-stats aggregation + 2D metrics grid +
 * export serializers + substrate strip / page-efficacy helpers.
 * Consumed by tests and the web Admin view; background.js mirrors the totals shape.
 */

import { formatHms } from './metricsHelpers';

/** SET 34d / 45c — fill/repair outcome bucket for efficacy splits. */
export type EfficacyBucket = { cases: number; filled: number };

export type RunOutcome = 'successful' | 'partial' | 'failed' | 'cancelled' | 'running' | 'refused';

export interface RunRecordLike {
  id?: string;
  ts?: string;
  mode?: string;
  error?: string | null;
  problems?: number | null;
  result?: unknown;
  metrics?: Record<string, unknown> | null;
  cost?: { usdTotal?: number | null } | null;
  /** SET 32e.ii — set on live in-progress snapshots so classifyRunOutcome
   *  returns `running` (never a terminal disposition). Persisted records
   *  never carry this — they always have a terminal outcome. */
  live?: boolean;
  running?: boolean;
  /** SET 51 — checkpoint status (`running` must not count as terminal). */
  status?: string | null;
  /** SET 51a — last successful checkpoint timestamp (viewer honesty). */
  checkpointAt?: string | null;
  /** SET 51a — true only after a finalized end-of-run persist. */
  finalized?: boolean;
  /** SET 33b.2 — project-scope aggregation key. Root run's own id founds the project. */
  projectId?: string | null;
}

export interface StatsTotals {
  runs: { total: number; successful: number; partial: number; failed: number; cancelled: number; running: number; refused: number };
  byMode: Record<string, number>;
  tokens: {
    inSum: number; outSum: number; totalSum: number;
    inMax: number; outMax: number; totalMax: number;
    inMin: number | null; outMin: number | null; totalMin: number | null;
    runsWithTokens: number;
  };
  perCall: {
    avgInSum: number; avgOutSum: number;
    maxInMax: number; maxOutMax: number;
    maxInMin: number | null; maxOutMin: number | null;
    avgInMin: number | null; avgOutMin: number | null;
    avgInMax: number; avgOutMax: number;
    runsWith: number;
  };
  cycles: { sum: number; max: number; min: number | null; runsWith: number };
  irGen: { sum: number; max: number; min: number | null; runsWith: number };
  substrate: {
    fixes: number; attempts: number; refusals: number; noHandler: number;
    mechanical: number; model: number; templated: number;
    sbDraws: number; sbWins: number;
    /** SET 37f — concept ideation draws. */
    conceptDraws: number;
    /** SET 45c — draws whose superbuffer packed a substrate handoff page. */
    pageWithCases: number;
    pageWithFilled: number;
    /** SET 45c — draws whose superbuffer omitted the substrate page. */
    pageWithoutCases: number;
    pageWithoutFilled: number;
  };
  perm: { batches: number; candidates: number; improved: number };
  surgeries: { irCase: number; crash: number };
  duration: { sumMs: number; maxMs: number; minMs: number | null; runsWith: number };
  cost: { sum: number; count: number; max: number; min: number | null };
  /** SET 57d — run-report counter buckets keyed by REPORT_FACET_KEYS.
   * Optional: persisted totals predating SET 57 lack it. */
  report?: Record<string, ReportBucket>;
}

/** SET 57d — per-facet aggregation bucket for run-report counters/scores. */
export interface ReportBucket {
  sum: number; max: number; min: number | null; runsWith: number;
}

/** SET 57a/d — the run-report counter facets (identical labels everywhere). */
export const REPORT_COUNTER_KEYS = [
  'featuresTotal', 'featuresFrozenWithoutRedraw', 'irRedraws',
  'nodesTotal', 'nodesBuildable', 'nodesBuilt', 'nodesBuiltCleanFirstPass',
  'boundedLoopNodes', 'boundedLoopIterations',
  'candidatesDrawn', 'candidatesAccepted',
  'fillCandidatesDrawn', 'repairCandidatesDrawn', 'salvagedCases',
  'sickRedCases', 'testRhaSubstrateFixes', 'testRhaDraws', 'irSurgeriesInRha',
  'reEnqueuedCases', 'recoveredCases',
  'l2Draws', 'l2Wins', 'l2Bullseyes',
  'fixesL1', 'fixesL2', 'fixesL3', 'fixesL4',
  'corpseRejections', 'classifyAttempts', 'classifyCataloged',
  'grammarConformant', 'grammarFallbacks', 'shellLockApplied', 'shellLockFallbacks',
  'judgmentGates', 'judgmentOperator', 'judgmentLlm', 'judgmentDropped',
  'watcherSyntheses', 'watcherReadings', 'watcherObservations',
  'watcherCacheHits', 'watcherCacheMisses', 'watcherWorldStaleSkips',
  'corpseRejectionCeilingBreaches',
  'nodeRepairDraws', 'nodeRepairWins',
  'observationPolls', 'observationsNew', 'observationsCorroborated',
  'stubRouteShortcuts', 'observatoryPollsSkipped', 'l2ShortCircuits', 'quarantineSuspensions',
  'errorsTotal', 'errorsParse', 'errorsConformance', 'errorsExecution', 'errorsCrash',
  // SET 89 §§10.5 / 11.4 — canonical registry-sourced counters. `errors*`
  // above are legacy aliases retained for one release; new UI/exports
  // prefer these.
  'findingOccurrencesTotal', 'findingOccurrencesParse',
  'findingOccurrencesConformance', 'findingOccurrencesExecution',
  'findingOccurrencesCrash', 'findingOccurrencesTestFailure',
  'uniqueDefectsObserved', 'uniqueDefectsResolved', 'uniqueDefectsUnresolved',
  'actionStarts', 'actionTerminals', 'actionDecisions', 'actionPending', 'actionFailed',
  'actionFailedNonfatal',
  'deferredTotal', 'deferredRelitigated', 'deferredResolvedAfterRelitigation', 'deferredPermanent',
  'nodesHealed', 'fixesUnsuccessful', 'fixesUnparseable',
] as const;

export const REPORT_SCORE_KEYS = ['scoreIr', 'scoreBuild', 'scoreRha'] as const;

function reportRatio(numv: number, den: number): number | null {
  if (den <= 0) return null;
  return Math.max(0, Math.min(1, numv / den));
}

/** SET 57b/d — the three function ratings; single source of truth for the
 * Run Report tiles, the Admin mirror, and the export. */
export function reportScoresFromMetrics(m: Record<string, unknown>): {
  scoreIr: number | null; scoreBuild: number | null; scoreRha: number | null;
} {
  return {
    scoreIr: reportRatio(num(m.featuresFrozenWithoutRedraw), num(m.featuresTotal)),
    // SET 79 — BUILD = owned files accepted per node. Falls back to the old
    // formula only for records predating the counter.
    scoreBuild: num(m.nodesBuildable) > 0
      ? reportRatio(num(m.nodesBuilt), num(m.nodesBuildable))
      : reportRatio(num(m.nodesBuiltCleanFirstPass), num(m.nodesTotal)),
    scoreRha: reportRatio(num(m.nodesHealed), num(m.boundedLoopNodes)),
  };
}

export interface FacetCell {
  latest: number | null;
  avg: number | null;
  min: number | null;
  max: number | null;
  lifetime: number | null;
}

export type GridTone = 'ok' | 'warn' | 'bad' | null;
export type CellFormat = 'int' | 'float1' | 'usd' | 'hms' | 'ratio' | 'pct' | 'raw';

export type MetricsGridRow =
  | { kind: 'header'; category: string }
  | {
      kind: 'row';
      category: string;
      key: string;
      label: string;
      cells: FacetCell;
      format: CellFormat;
      tone?: GridTone;
    };

export interface InstallStatsSummary {
  runs: StatsTotals['runs'];
  byMode: Record<string, number>;
  /** Facet map used by the 2D grid. */
  facets: Record<string, FacetCell>;
  /** Latest run vs priced average (±%). Null when either side missing. */
  latestVsAvgPct: number | null;
  /** Lifetime token totals for comparative pricing (Admin). */
  lifetimeTokens: { in: number; out: number; total: number };
  pricingAvailable: boolean;
  latestTs: string | null;
  latestId: string | null;
  ringSize: number;
  totalsRuns: number;
}

function num(v: unknown): number {
  const n = Number(v);
  return Number.isFinite(n) ? n : 0;
}

function numOrNull(v: unknown): number | null {
  if (v == null) return null;
  const n = Number(v);
  return Number.isFinite(n) ? n : null;
}

/** SET 43f — fixes/attempts; null when attempts ≤ 0. */
export function substrateHitRate(fixes: number, attempts: number): number | null {
  if (!Number.isFinite(fixes) || !Number.isFinite(attempts) || attempts <= 0) return null;
  return fixes / attempts;
}

/**
 * SET 45a — metrics-card strip (no UI here; Integration renders the string).
 * `substrate: N actions (F fixes · A attempts · R refusals · C candidates)`
 */
export function formatSubstrateStrip(opts: {
  actions: number;
  fixes: number;
  attempts: number;
  refusals: number;
  candidates: number;
}): string {
  const n = (v: number) => (Number.isFinite(v) ? Math.max(0, Math.floor(v)) : 0);
  return (
    `substrate: ${n(opts.actions)} actions `
    + `(${n(opts.fixes)} fixes · ${n(opts.attempts)} attempts · `
    + `${n(opts.refusals)} refusals · ${n(opts.candidates)} candidates)`
  );
}

/** SET 45c — empty efficacy buckets for WITH/WITHOUT substrate page sampling. */
export function emptySubstratePageEfficacy(): {
  withPage: EfficacyBucket;
  withoutPage: EfficacyBucket;
} {
  return {
    withPage: { cases: 0, filled: 0 },
    withoutPage: { cases: 0, filled: 0 },
  };
}

/** SET 45c — sample one fill/repair outcome into the WITH/WITHOUT buckets. */
export function noteSubstratePageEfficacy(
  buckets: { withPage: EfficacyBucket; withoutPage: EfficacyBucket },
  hadSubstratePage: boolean,
  filled: boolean,
): void {
  const bucket = hadSubstratePage ? buckets.withPage : buckets.withoutPage;
  bucket.cases++;
  if (filled) bucket.filled++;
}

/**
 * SET 45c — report line (34d pattern):
 * `substrate page: N cases, M filled (X%) · no substrate page: N', M' (X'%)`
 */
export function formatSubstratePageEfficacy(
  withPage: EfficacyBucket,
  withoutPage: EfficacyBucket,
): string {
  const pct = (m: number, n: number) => (n === 0 ? 0 : Math.round((100 * m) / n));
  const a = withPage;
  const b = withoutPage;
  return (
    `substrate page: ${a.cases} cases, ${a.filled} filled (${pct(a.filled, a.cases)}%)`
    + ` · no substrate page: ${b.cases} cases, ${b.filled} filled (${pct(b.filled, b.cases)}%)`
  );
}

/** Alias — SET 45c `formatReportLine` name for Integration. */
export const formatSubstratePageEfficacyReportLine = formatSubstratePageEfficacy;

export function emptyStatsTotals(): StatsTotals {
  return {
    runs: { total: 0, successful: 0, partial: 0, failed: 0, cancelled: 0, running: 0, refused: 0 },
    byMode: { rha: 0, vanilla: 0, referee: 0 },
    tokens: {
      inSum: 0, outSum: 0, totalSum: 0,
      inMax: 0, outMax: 0, totalMax: 0,
      inMin: null, outMin: null, totalMin: null,
      runsWithTokens: 0,
    },
    perCall: {
      avgInSum: 0, avgOutSum: 0,
      maxInMax: 0, maxOutMax: 0,
      maxInMin: null, maxOutMin: null,
      avgInMin: null, avgOutMin: null,
      avgInMax: 0, avgOutMax: 0,
      runsWith: 0,
    },
    cycles: { sum: 0, max: 0, min: null, runsWith: 0 },
    irGen: { sum: 0, max: 0, min: null, runsWith: 0 },
    substrate: {
      fixes: 0, attempts: 0, refusals: 0, noHandler: 0,
      mechanical: 0, model: 0, templated: 0,
      sbDraws: 0, sbWins: 0, conceptDraws: 0,
      pageWithCases: 0, pageWithFilled: 0,
      pageWithoutCases: 0, pageWithoutFilled: 0,
    },
    perm: { batches: 0, candidates: 0, improved: 0 },
    surgeries: { irCase: 0, crash: 0 },
    duration: { sumMs: 0, maxMs: 0, minMs: null, runsWith: 0 },
    cost: { sum: 0, count: 0, max: 0, min: null },
    report: {},
  };
}

export function classifyRunOutcome(rec: RunRecordLike): RunOutcome {
  // SET 32e.ii — live in-progress snapshots must never classify as terminal.
  // Callers building a live metrics grid mark the snapshot with `live: true`
  // (or `running: true`); those get `running`, not `failed`, mid-run. Without
  // this the Runs → Failed cell showed 1 for any healthy in-progress run.
  if (rec.live || rec.running || rec.status === 'running') return 'running';
  if (rec.status === 'refused') return 'refused';
  const err = String(rec.error || '').toLowerCase();
  if (err.includes('cancelled') || rec.status === 'cancelled') return 'cancelled';
  if (rec.error || rec.status === 'failed' || rec.status === 'interrupted') return 'failed';
  const n = rec.problems;
  if (n == null) return rec.result || rec.status === 'complete' ? 'successful' : 'running';
  if (n === 0) return 'successful';
  return 'partial';
}

function bumpMin(cur: number | null, v: number): number {
  return cur == null ? v : Math.min(cur, v);
}

function cyclesFromMetrics(m: Record<string, unknown>): number {
  const ppf = m.passesPerFeature;
  if (ppf && typeof ppf === 'object' && Object.keys(ppf as object).length > 0) {
    let sum = 0;
    for (const n of Object.values(ppf as Record<string, unknown>)) sum += num(n);
    return sum;
  }
  return num(m.passesTotal);
}

/** Extract a flat numeric snapshot of one run for Latest / ring stats. */
export function runSnapshot(rec: RunRecordLike): Record<string, number | null> {
  const m = (rec.metrics || {}) as Record<string, unknown>;
  const tin = num(m.tokensIn);
  const tout = num(m.tokensOut);
  const outcome = classifyRunOutcome(rec);
  const mode = rec.mode || 'rha';
  const cost = typeof rec.cost?.usdTotal === 'number' && Number.isFinite(rec.cost.usdTotal)
    ? rec.cost.usdTotal
    : null;
  const mech = num(m.mechanicalFixes);
  const model = num(m.repairDraws);
  const fixes = num(m.substrateFixes) || mech;
  // SET 32e.ii — live snapshots leave disposition cells NULL (not 0), so the
  // grid never shows a terminal classification for an in-progress run.
  const isRunning = outcome === 'running';
  return {
    runsTotal: 1,
    runsClean: isRunning ? null : (outcome === 'successful' ? 1 : 0),
    runsPartial: isRunning ? null : (outcome === 'partial' ? 1 : 0),
    runsFailed: isRunning ? null : (outcome === 'failed' ? 1 : 0),
    runsCancelled: isRunning ? null : (outcome === 'cancelled' ? 1 : 0),
    runsRefused: isRunning ? null : (outcome === 'refused' ? 1 : 0),
    runsRunning: isRunning ? 1 : 0,
    modeRha: mode === 'rha' ? 1 : 0,
    modeVanilla: mode === 'vanilla' ? 1 : 0,
    modeReferee: mode === 'referee' ? 1 : 0,
    tokensIn: tin,
    tokensOut: tout,
    tokensTotal: tin + tout,
    perCallAvgIn: numOrNull(m.avgIn),
    perCallMaxIn: numOrNull(m.maxIn),
    perCallAvgOut: numOrNull(m.avgOut),
    perCallMaxOut: numOrNull(m.maxOut),
    cycles: cyclesFromMetrics(m),
    irGenAttempts: num(m.irGenAttempts),
    substrateFixes: fixes,
    substrateAttempts: num(m.substrateAttempts),
    substrateRefusals: num(m.substrateRefusals),
    substrateNoHandler: num(m.substrateNoHandler),
    substrateHitRate: substrateHitRate(fixes, num(m.substrateAttempts)),
    mechModelRatio: model > 0 ? mech / model : (mech > 0 ? Infinity : null),
    templatedCases: num(m.templatedCases),
    superbufferDraws: num(m.superbufferDraws),
    superbufferWins: num(m.superbufferWins),
    conceptDraws: num(m.conceptDraws),
    // SET 45c — substrate handoff page efficacy (Integration populates metrics).
    substratePageWithCases: num(m.substratePageWithCases),
    substratePageWithFilled: num(m.substratePageWithFilled),
    substratePageWithoutCases: num(m.substratePageWithoutCases),
    substratePageWithoutFilled: num(m.substratePageWithoutFilled),
    permBatches: num(m.permBatches),
    permCandidates: num(m.permCandidates != null ? m.permCandidates : m.permutationDraws),
    permImproved: num(m.permImproved ?? m.permutationWins),
    surgeryIrCase: num(m.irSurgeries),
    surgeryCrash: numOrNull(m.crashSurgeries) ?? null,
    durationMs: numOrNull(m.elapsedMs),
    costUsd: cost,
    // SET 57d — run-report counters + function scores.
    ...Object.fromEntries(REPORT_COUNTER_KEYS.map(k => [k, numOrNull(m[k])])),
    ...reportScoresFromMetrics(m),
  };
}

export function contributeRunToTotals(totals: StatsTotals, rec: RunRecordLike): StatsTotals {
  const t = totals;
  const outcome = classifyRunOutcome(rec);
  // SET 32e.ii — live snapshots never contribute to persisted totals; they
  // are transient views of an in-flight run, not history.
  if (outcome === 'running') return t;
  t.runs.total += 1;
  t.runs[outcome] = (t.runs[outcome] || 0) + 1;
  const mode = rec.mode || 'rha';
  t.byMode[mode] = (t.byMode[mode] || 0) + 1;

  const m = (rec.metrics || {}) as Record<string, unknown>;
  const tin = num(m.tokensIn);
  const tout = num(m.tokensOut);
  const ttot = tin + tout;
  if (tin || tout || m.llmCalls) {
    const tok = t.tokens;
    tok.inSum += tin; tok.outSum += tout; tok.totalSum += ttot;
    tok.inMax = Math.max(tok.inMax, tin);
    tok.outMax = Math.max(tok.outMax, tout);
    tok.totalMax = Math.max(tok.totalMax, ttot);
    tok.inMin = bumpMin(tok.inMin, tin);
    tok.outMin = bumpMin(tok.outMin, tout);
    tok.totalMin = bumpMin(tok.totalMin, ttot);
    tok.runsWithTokens += 1;
  }

  const avgIn = numOrNull(m.avgIn);
  const avgOut = numOrNull(m.avgOut);
  const maxIn = numOrNull(m.maxIn);
  const maxOut = numOrNull(m.maxOut);
  if (avgIn != null || avgOut != null || maxIn != null || maxOut != null) {
    const pc = t.perCall;
    if (avgIn != null) {
      pc.avgInSum += avgIn;
      pc.avgInMin = bumpMin(pc.avgInMin, avgIn);
      pc.avgInMax = Math.max(pc.avgInMax, avgIn);
    }
    if (avgOut != null) {
      pc.avgOutSum += avgOut;
      pc.avgOutMin = bumpMin(pc.avgOutMin, avgOut);
      pc.avgOutMax = Math.max(pc.avgOutMax, avgOut);
    }
    if (maxIn != null) {
      pc.maxInMax = Math.max(pc.maxInMax, maxIn);
      pc.maxInMin = bumpMin(pc.maxInMin, maxIn);
    }
    if (maxOut != null) {
      pc.maxOutMax = Math.max(pc.maxOutMax, maxOut);
      pc.maxOutMin = bumpMin(pc.maxOutMin, maxOut);
    }
    pc.runsWith += 1;
  }

  const cycles = cyclesFromMetrics(m);
  if (cycles || m.elapsedMs != null) {
    t.cycles.sum += cycles;
    t.cycles.max = Math.max(t.cycles.max, cycles);
    t.cycles.min = bumpMin(t.cycles.min, cycles);
    t.cycles.runsWith += 1;
  }

  const irGen = num(m.irGenAttempts);
  if (irGen || m.irGenAttempts != null) {
    t.irGen.sum += irGen;
    t.irGen.max = Math.max(t.irGen.max, irGen);
    t.irGen.min = bumpMin(t.irGen.min, irGen);
    t.irGen.runsWith += 1;
  }

  const mech = num(m.mechanicalFixes);
  t.substrate.mechanical += mech;
  t.substrate.model += num(m.repairDraws);
  t.substrate.fixes += num(m.substrateFixes) || mech;
  t.substrate.attempts += num(m.substrateAttempts);
  t.substrate.refusals += num(m.substrateRefusals);
  t.substrate.noHandler += num(m.substrateNoHandler);
  t.substrate.templated += num(m.templatedCases);
  t.substrate.sbDraws += num(m.superbufferDraws);
  t.substrate.sbWins += num(m.superbufferWins);
  t.substrate.conceptDraws += num(m.conceptDraws);
  t.substrate.pageWithCases += num(m.substratePageWithCases);
  t.substrate.pageWithFilled += num(m.substratePageWithFilled);
  t.substrate.pageWithoutCases += num(m.substratePageWithoutCases);
  t.substrate.pageWithoutFilled += num(m.substratePageWithoutFilled);

  // SET 35e — never treat permutationDraws as batches (draws ≈ candidates;
  // candidates should be ~3× batches). Legacy rows with only permutationDraws
  // fold into candidates; batches stay 0 unless permBatches is present.
  t.perm.batches += num(m.permBatches);
  t.perm.candidates += num(
    m.permCandidates != null ? m.permCandidates : m.permutationDraws,
  );
  t.perm.improved += num(m.permImproved ?? m.permutationWins);

  t.surgeries.irCase += num(m.irSurgeries);
  t.surgeries.crash += num(m.crashSurgeries);

  const elapsed = numOrNull(m.elapsedMs);
  if (elapsed != null) {
    t.duration.sumMs += elapsed;
    t.duration.maxMs = Math.max(t.duration.maxMs, elapsed);
    t.duration.minMs = bumpMin(t.duration.minMs, elapsed);
    t.duration.runsWith += 1;
  }

  const cost = rec.cost?.usdTotal;
  if (typeof cost === 'number' && Number.isFinite(cost)) {
    t.cost.sum += cost;
    t.cost.count += 1;
    t.cost.max = Math.max(t.cost.max, cost);
    t.cost.min = bumpMin(t.cost.min, cost);
  }

  // SET 57d — run-report counters + function scores (records without the
  // SET 57 counters simply do not contribute to these buckets).
  const rep = (t.report ??= {});
  const bump = (key: string, v: number | null) => {
    if (v == null || !Number.isFinite(v)) return;
    const b = rep[key] ?? (rep[key] = { sum: 0, max: 0, min: null, runsWith: 0 });
    b.sum += v;
    b.max = Math.max(b.max, v);
    b.min = bumpMin(b.min, v);
    b.runsWith += 1;
  };
  const hasReportCounters = REPORT_COUNTER_KEYS.some(k => m[k] != null);
  if (hasReportCounters) {
    for (const k of REPORT_COUNTER_KEYS) bump(k, numOrNull(m[k]));
    const scores = reportScoresFromMetrics(m);
    bump('scoreIr', scores.scoreIr);
    bump('scoreBuild', scores.scoreBuild);
    bump('scoreRha', scores.scoreRha);
  }
  return t;
}

function avg(sum: number, n: number): number | null {
  return n > 0 ? sum / n : null;
}

function cell(
  latest: number | null,
  avgV: number | null,
  minV: number | null,
  maxV: number | null,
  lifetime: number | null,
): FacetCell {
  return { latest, avg: avgV, min: minV, max: maxV, lifetime };
}

function pickLatest(records: RunRecordLike[]): RunRecordLike | null {
  let best: RunRecordLike | null = null;
  let bestTs = -Infinity;
  for (const r of records) {
    const t = r.ts ? Date.parse(r.ts) : NaN;
    const n = Number.isFinite(t) ? t : -Infinity;
    if (n >= bestTs) {
      bestTs = n;
      best = r;
    }
  }
  return best;
}

/**
 * Aggregate install stats from stored run records + optional lifetime totals
 * (accumulator survives the 20-run ring eviction).
 */
export function aggregateInstallStats(
  records: RunRecordLike[],
  lifetimeTotals?: StatsTotals | null,
  opts?: { pricingAvailable?: boolean; statsBaseline?: string | null },
): InstallStatsSummary {
  // SET 30b — "reset your-install stats" stamps a `statsBaseline` ISO ts;
  // records older than it are excluded from aggregation (run HISTORY itself
  // is untouched — this only affects what gets summed into the counters).
  const baseline = opts?.statsBaseline ?? null;
  const eligible = baseline
    ? records.filter(rec => !!rec?.ts && rec.ts >= baseline)
    : records;
  let fromRecords = emptyStatsTotals();
  for (const rec of eligible) {
    if (rec && typeof rec === 'object') fromRecords = contributeRunToTotals(fromRecords, rec);
  }
  const life = lifetimeTotals && (lifetimeTotals.runs?.total || 0) > (fromRecords.runs.total || 0)
    ? lifetimeTotals
    : fromRecords;

  const latestRec = pickLatest(eligible);
  const snap = latestRec ? runSnapshot(latestRec) : null;
  const nTok = life.tokens.runsWithTokens || 0;
  const nPc = life.perCall.runsWith || 0;
  const nCyc = life.cycles.runsWith || 0;
  const nIr = life.irGen.runsWith || 0;
  const nDur = life.duration.runsWith || 0;
  const nCost = life.cost.count || 0;
  const mech = life.substrate.mechanical;
  const model = life.substrate.model;
  const ratioLife = model > 0 ? mech / model : (mech > 0 ? Infinity : null);

  const facets: Record<string, FacetCell> = {
    runsTotal: cell(snap?.runsTotal ?? null, null, null, null, life.runs.total),
    runsClean: cell(snap?.runsClean ?? null, null, null, null, life.runs.successful),
    runsPartial: cell(snap?.runsPartial ?? null, null, null, null, life.runs.partial),
    runsFailed: cell(snap?.runsFailed ?? null, null, null, null, life.runs.failed),
    runsRefused: cell(snap?.runsRefused ?? null, null, null, null, life.runs.refused),
    runsCancelled: cell(snap?.runsCancelled ?? null, null, null, null, life.runs.cancelled),
    modeRha: cell(snap?.modeRha ?? null, null, null, null, life.byMode.rha || 0),
    modeVanilla: cell(snap?.modeVanilla ?? null, null, null, null, life.byMode.vanilla || 0),
    modeReferee: cell(snap?.modeReferee ?? null, null, null, null, life.byMode.referee || 0),

    tokensIn: cell(
      snap?.tokensIn ?? null,
      avg(life.tokens.inSum, nTok),
      life.tokens.inMin,
      life.tokens.inMax,
      life.tokens.inSum,
    ),
    tokensOut: cell(
      snap?.tokensOut ?? null,
      avg(life.tokens.outSum, nTok),
      life.tokens.outMin,
      life.tokens.outMax,
      life.tokens.outSum,
    ),
    tokensTotal: cell(
      snap?.tokensTotal ?? null,
      avg(life.tokens.totalSum, nTok),
      life.tokens.totalMin,
      life.tokens.totalMax,
      life.tokens.totalSum,
    ),

    perCallAvgIn: cell(
      snap?.perCallAvgIn ?? null,
      avg(life.perCall.avgInSum, nPc),
      life.perCall.avgInMin,
      life.perCall.avgInMax || null,
      null,
    ),
    perCallMaxIn: cell(
      snap?.perCallMaxIn ?? null,
      null,
      life.perCall.maxInMin,
      life.perCall.maxInMax || null,
      null,
    ),
    perCallAvgOut: cell(
      snap?.perCallAvgOut ?? null,
      avg(life.perCall.avgOutSum, nPc),
      life.perCall.avgOutMin,
      life.perCall.avgOutMax || null,
      null,
    ),
    perCallMaxOut: cell(
      snap?.perCallMaxOut ?? null,
      null,
      life.perCall.maxOutMin,
      life.perCall.maxOutMax || null,
      null,
    ),

    cycles: cell(
      snap?.cycles ?? null,
      avg(life.cycles.sum, nCyc),
      life.cycles.min,
      life.cycles.max || null,
      life.cycles.sum,
    ),
    irGenAttempts: cell(
      snap?.irGenAttempts ?? null,
      avg(life.irGen.sum, nIr),
      life.irGen.min,
      life.irGen.max || null,
      life.irGen.sum,
    ),

    substrateFixes: cell(snap?.substrateFixes ?? null, null, null, null, life.substrate.fixes),
    substrateAttempts: cell(snap?.substrateAttempts ?? null, null, null, null, life.substrate.attempts),
    substrateRefusals: cell(snap?.substrateRefusals ?? null, null, null, null, life.substrate.refusals),
    substrateNoHandler: cell(snap?.substrateNoHandler ?? null, null, null, null, life.substrate.noHandler),
    // SET 43f — driving metric: fixes/attempts (0–1 ratio). Lifetime from
    // aggregated counters; latest from the newest run's own snapshot.
    substrateHitRate: cell(
      snap?.substrateHitRate ?? null,
      null,
      null,
      null,
      substrateHitRate(life.substrate.fixes, life.substrate.attempts),
    ),
    mechModelRatio: cell(snap?.mechModelRatio ?? null, null, null, null, ratioLife),
    templatedCases: cell(snap?.templatedCases ?? null, null, null, null, life.substrate.templated),
    superbufferDraws: cell(snap?.superbufferDraws ?? null, null, null, null, life.substrate.sbDraws),
    superbufferWins: cell(snap?.superbufferWins ?? null, null, null, null, life.substrate.sbWins),
    conceptDraws: cell(snap?.conceptDraws ?? null, null, null, null, life.substrate.conceptDraws),
    substratePageWithCases: cell(
      snap?.substratePageWithCases ?? null, null, null, null, life.substrate.pageWithCases,
    ),
    substratePageWithFilled: cell(
      snap?.substratePageWithFilled ?? null, null, null, null, life.substrate.pageWithFilled,
    ),
    substratePageWithoutCases: cell(
      snap?.substratePageWithoutCases ?? null, null, null, null, life.substrate.pageWithoutCases,
    ),
    substratePageWithoutFilled: cell(
      snap?.substratePageWithoutFilled ?? null, null, null, null, life.substrate.pageWithoutFilled,
    ),

    permBatches: cell(snap?.permBatches ?? null, null, null, null, life.perm.batches),
    permCandidates: cell(snap?.permCandidates ?? null, null, null, null, life.perm.candidates),
    permImproved: cell(snap?.permImproved ?? null, null, null, null, life.perm.improved),

    surgeryIrCase: cell(snap?.surgeryIrCase ?? null, null, null, null, life.surgeries.irCase),
    surgeryCrash: cell(snap?.surgeryCrash ?? null, null, null, null, life.surgeries.crash || null),

    durationMs: cell(
      snap?.durationMs ?? null,
      avg(life.duration.sumMs, nDur),
      life.duration.minMs,
      life.duration.maxMs || null,
      life.duration.sumMs,
    ),

    costUsd: cell(
      snap?.costUsd ?? null,
      avg(life.cost.sum, nCost),
      life.cost.min,
      life.cost.max || null,
      life.cost.sum,
    ),
  };

  const latestCost = snap?.costUsd ?? null;
  const avgCost = avg(life.cost.sum, nCost);
  let latestVsAvgPct: number | null = null;
  if (latestCost != null && avgCost != null && avgCost > 0) {
    latestVsAvgPct = ((latestCost - avgCost) / avgCost) * 100;
  }
  facets.latestVsAvgPct = cell(latestVsAvgPct, null, null, null, null);

  // SET 57d — run-report facets (counters keep a summed history column;
  // scores keep avg/min/max only — a summed ratio is meaningless).
  const rep = life.report ?? {};
  for (const k of REPORT_COUNTER_KEYS) {
    const b = rep[k];
    facets[k] = cell(
      snap?.[k] ?? null,
      b ? avg(b.sum, b.runsWith) : null,
      b?.min ?? null,
      b ? b.max : null,
      b ? b.sum : null,
    );
  }
  for (const k of REPORT_SCORE_KEYS) {
    const b = rep[k];
    facets[k] = cell(
      snap?.[k] ?? null,
      b ? avg(b.sum, b.runsWith) : null,
      b?.min ?? null,
      b ? b.max : null,
      null,
    );
  }

  return {
    runs: { ...life.runs },
    byMode: { ...life.byMode },
    facets,
    latestVsAvgPct,
    lifetimeTokens: {
      in: life.tokens.inSum,
      out: life.tokens.outSum,
      total: life.tokens.totalSum,
    },
    pricingAvailable: opts?.pricingAvailable !== false && nCost > 0,
    latestTs: latestRec?.ts ?? null,
    latestId: latestRec?.id ?? null,
    ringSize: eligible.length,
    totalsRuns: life.runs.total,
  };
}

/**
 * SET 33b.2 — "this project" scope: aggregate only the records belonging to
 * one project (root run founds the project; continuations share its id via
 * `projectId`). Pure filter + delegate to `aggregateInstallStats` — no
 * lifetime carry-over (that concept is install-wide, not per-project).
 *
 * SET 47g — in-flight epochs count toward project totalsRuns (never silent
 * zeros while a live epoch is open). Live snapshots are thawed into a
 * countable stand-in; install-wide aggregation still excludes raw `live`.
 */
export function aggregateProjectStats(
  records: RunRecordLike[],
  projectId: string,
  opts?: { pricingAvailable?: boolean },
): InstallStatsSummary & { liveEpochInProgress?: boolean } {
  const filtered = records.filter(rec => !!rec && (rec.projectId || rec.id) === projectId);
  const live = filtered.filter(rec => !!(rec.live || rec.running));
  const terminal = filtered.filter(rec => !(rec.live || rec.running));
  if (live.length === 0) return aggregateInstallStats(terminal, null, opts);
  const thawedLive: RunRecordLike[] = live.map((rec, i) => ({
    ...rec,
    live: false,
    running: false,
    id: rec.id ? `${rec.id}__inflight` : `inflight-${i}`,
    projectId: rec.projectId || projectId,
    // Countable without forcing a false terminal disposition in the grid:
    // problems=0 + result → successful for the in-flight stand-in is OK for
    // totalsRuns; UI labels the scope "epoch in progress" separately.
    error: null,
    problems: typeof rec.problems === 'number' ? rec.problems : 0,
    result: rec.result ?? {},
  }));
  const merged = aggregateInstallStats([...terminal, ...thawedLive], null, opts);
  return { ...merged, liveEpochInProgress: true };
}

const GRID_SPEC: {
  category: string;
  rows: { key: string; label: string; format: CellFormat; tone?: GridTone }[];
}[] = [
  {
    category: 'Runs',
    rows: [
      { key: 'runsTotal', label: 'Total', format: 'int', tone: 'ok' },
      { key: 'runsClean', label: 'Clean', format: 'int', tone: 'ok' },
      { key: 'runsPartial', label: 'Partial', format: 'int', tone: 'warn' },
      { key: 'runsFailed', label: 'Failed', format: 'int', tone: 'bad' },
      { key: 'runsRefused', label: 'Refused', format: 'int', tone: 'warn' },
      { key: 'runsCancelled', label: 'Cancelled', format: 'int', tone: 'warn' },
      { key: 'modeRha', label: 'Mode · rha', format: 'int' },
      { key: 'modeVanilla', label: 'Mode · vanilla', format: 'int' },
      { key: 'modeReferee', label: 'Mode · referee', format: 'int' },
    ],
  },
  {
    category: 'Tokens',
    rows: [
      { key: 'tokensIn', label: 'IN', format: 'int' },
      { key: 'tokensOut', label: 'OUT', format: 'int' },
      { key: 'tokensTotal', label: 'TOTAL', format: 'int', tone: 'ok' },
    ],
  },
  {
    category: 'Per-call',
    rows: [
      { key: 'perCallAvgIn', label: 'Avg in', format: 'int' },
      { key: 'perCallMaxIn', label: 'Max in', format: 'int' },
      { key: 'perCallAvgOut', label: 'Avg out', format: 'int' },
      { key: 'perCallMaxOut', label: 'Max out', format: 'int' },
    ],
  },
  {
    category: 'Cycles',
    rows: [
      { key: 'cycles', label: 'Cycles', format: 'float1', tone: 'ok' },
      { key: 'irGenAttempts', label: 'IR gen attempts', format: 'float1' },
    ],
  },
  {
    category: 'Substrate',
    rows: [
      { key: 'substrateFixes', label: 'Fixes', format: 'int', tone: 'ok' },
      { key: 'substrateAttempts', label: 'Attempts', format: 'int' },
      { key: 'substrateHitRate', label: 'Hit-rate (fixes/attempts)', format: 'ratio', tone: 'ok' },
      { key: 'substrateRefusals', label: 'Refusals', format: 'int', tone: 'warn' },
      { key: 'substrateNoHandler', label: 'No-handler', format: 'int', tone: 'warn' },
      { key: 'mechModelRatio', label: 'Mech:model ratio', format: 'ratio', tone: 'ok' },
      { key: 'templatedCases', label: 'Templated cases', format: 'int', tone: 'ok' },
      { key: 'superbufferDraws', label: 'Superbuffer draws', format: 'int' },
      { key: 'superbufferWins', label: 'Superbuffer wins', format: 'int', tone: 'ok' },
      { key: 'conceptDraws', label: 'Concept draws', format: 'int' },
      { key: 'substratePageWithCases', label: 'Substrate page cases', format: 'int' },
      { key: 'substratePageWithFilled', label: 'Substrate page filled', format: 'int', tone: 'ok' },
      { key: 'substratePageWithoutCases', label: 'No substrate page cases', format: 'int' },
      { key: 'substratePageWithoutFilled', label: 'No substrate page filled', format: 'int' },
    ],
  },
  {
    category: 'Permutations',
    rows: [
      { key: 'permBatches', label: 'Batches', format: 'int' },
      { key: 'permCandidates', label: 'Candidates', format: 'int' },
      { key: 'permImproved', label: 'Improved', format: 'int', tone: 'ok' },
    ],
  },
  {
    category: 'Surgeries',
    rows: [
      { key: 'surgeryIrCase', label: 'IR-case', format: 'int' },
      { key: 'surgeryCrash', label: 'Crash', format: 'int', tone: 'warn' },
    ],
  },
  {
    category: 'Duration',
    rows: [
      { key: 'durationMs', label: 'Compute time', format: 'hms' },
    ],
  },
  {
    category: 'Cost',
    rows: [
      { key: 'costUsd', label: 'Est. USD (priced runs)', format: 'usd' },
      { key: 'latestVsAvgPct', label: 'Latest vs own avg', format: 'pct' },
    ],
  },
  // SET 57d — run-report mirror: identical labels to the RunReport module.
  {
    category: 'Scorecard',
    rows: [
      { key: 'scoreIr', label: 'IR contract', format: 'ratio', tone: 'ok' },
      { key: 'scoreBuild', label: 'Build', format: 'ratio', tone: 'ok' },
      { key: 'scoreRha', label: 'RHA', format: 'ratio', tone: 'ok' },
      { key: 'featuresTotal', label: 'Features total', format: 'int' },
      { key: 'featuresFrozenWithoutRedraw', label: 'Frozen w/o redraw', format: 'int', tone: 'ok' },
      { key: 'irRedraws', label: 'IR redraws', format: 'int' },
      { key: 'nodesTotal', label: 'Nodes total', format: 'int' },
      { key: 'nodesBuilt', label: 'Nodes fully built', format: 'int', tone: 'ok' },
      { key: 'nodesBuildable', label: 'Nodes with owned files', format: 'int' },
      { key: 'nodesBuiltCleanFirstPass', label: 'Green on first measure (rare by design)', format: 'int' },
    ],
  },
  {
    category: 'Bounded loops',
    rows: [
      { key: 'boundedLoopNodes', label: 'Nodes entered', format: 'int' },
      { key: 'boundedLoopIterations', label: 'Iterations', format: 'int' },
      { key: 'repairCandidatesDrawn', label: 'Loop draws', format: 'int' },
      { key: 'fillCandidatesDrawn', label: 'Fill draws (IR)', format: 'int' },
      { key: 'candidatesDrawn', label: 'Candidates drawn (all layers)', format: 'int' },
      { key: 'candidatesAccepted', label: 'Accepted as fills', format: 'int', tone: 'ok' },
      { key: 'salvagedCases', label: 'Salvaged from losers', format: 'int', tone: 'ok' },
      { key: 'reEnqueuedCases', label: 'Re-enqueued (warm retry)', format: 'int' },
      { key: 'recoveredCases', label: 'Recovered at end-of-run', format: 'int', tone: 'ok' },
    ],
  },
  {
    category: 'Errors',
    rows: [
      { key: 'errorsTotal', label: 'Total', format: 'int', tone: 'bad' },
      { key: 'errorsParse', label: 'Parse', format: 'int' },
      { key: 'errorsConformance', label: 'Conformance', format: 'int' },
      { key: 'errorsExecution', label: 'Execution', format: 'int' },
      { key: 'errorsCrash', label: 'Crash-quarantined', format: 'int' },
    ],
  },
  {
    category: 'Deferred',
    rows: [
      { key: 'deferredTotal', label: 'Total', format: 'int' },
      { key: 'deferredRelitigated', label: 'Relitigated', format: 'int' },
      { key: 'deferredResolvedAfterRelitigation', label: 'Resolved after relitigation', format: 'int', tone: 'ok' },
      { key: 'deferredPermanent', label: 'Permanent', format: 'int' },
    ],
  },
  // SET 64 — layer attribution + classification floor + corpse rejections.
  {
    category: 'Layers',
    rows: [
      { key: 'fixesL1', label: 'L1 fixes (substrate)', format: 'int', tone: 'ok' },
      { key: 'fixesL2', label: 'L2 fixes (schema-guided)', format: 'int', tone: 'ok' },
      { key: 'fixesL3', label: 'L3 fixes (free permutation)', format: 'int' },
      { key: 'fixesL4', label: 'L4 fixes (entropy reset)', format: 'int', tone: 'warn' },
      { key: 'l2Draws', label: 'L2 draws', format: 'int' },
      { key: 'l2Bullseyes', label: 'L2 bullseyes', format: 'int', tone: 'ok' },
      { key: 'corpseRejections', label: 'Corpse rejections (0 exec)', format: 'int' },
      { key: 'classifyAttempts', label: 'Classify attempts', format: 'int' },
      { key: 'classifyCataloged', label: 'Classify cataloged', format: 'int', tone: 'ok' },
      { key: 'grammarConformant', label: 'Grammar conformant fills', format: 'int', tone: 'ok' },
      { key: 'grammarFallbacks', label: 'Grammar fallbacks', format: 'int', tone: 'warn' },
      { key: 'shellLockApplied', label: 'Shell-locked builds', format: 'int', tone: 'ok' },
      { key: 'shellLockFallbacks', label: 'Shell-lock fallbacks', format: 'int', tone: 'warn' },
      { key: 'judgmentGates', label: 'Judgment gates', format: 'int' },
      { key: 'judgmentOperator', label: 'Judgment · operator rulings', format: 'int', tone: 'ok' },
      { key: 'judgmentLlm', label: 'Judgment · LLM rulings (provisional)', format: 'int', tone: 'warn' },
      { key: 'judgmentDropped', label: 'Judgment · dropped', format: 'int' },
      { key: 'watcherSyntheses', label: 'Watcher syntheses (triggers)', format: 'int' },
      { key: 'watcherReadings', label: 'Watcher model readings', format: 'int' },
      { key: 'watcherCacheHits', label: 'Watcher cache hits', format: 'int', tone: 'ok' },
      { key: 'watcherCacheMisses', label: 'Watcher cache misses', format: 'int' },
      { key: 'watcherWorldStaleSkips', label: 'Watcher stale-world skips', format: 'int' },
      { key: 'corpseRejectionCeilingBreaches', label: 'Corpse ceiling breaches', format: 'int', tone: 'warn' },
      { key: 'nodeRepairDraws', label: 'Node-compliant repairs attempted', format: 'int' },
      { key: 'nodeRepairWins', label: 'Node-compliant repairs accepted', format: 'int', tone: 'ok' },
      { key: 'observationPolls', label: 'Observation polls (decoupled)', format: 'int' },
      { key: 'observationsNew', label: 'Observations added', format: 'int', tone: 'ok' },
      { key: 'observationsCorroborated', label: 'Observations corroborated', format: 'int', tone: 'ok' },
      { key: 'observatoryPollsSkipped', label: 'Observatory stale-world skips', format: 'int' },
      { key: 'stubRouteShortcuts', label: 'Stub-route L2 shortcuts', format: 'int', tone: 'ok' },
      { key: 'l2ShortCircuits', label: 'L2 short-circuits', format: 'int' },
      { key: 'quarantineSuspensions', label: 'Quarantine suspensions', format: 'int', tone: 'warn' },
      { key: 'watcherObservations', label: 'Watcher observations (advisory)', format: 'int' },
    ],
  },
  {
    category: 'RHA efficacy',
    rows: [
      { key: 'nodesHealed', label: 'Nodes healed', format: 'int', tone: 'ok' },
      { key: 'fixesUnsuccessful', label: 'Unsuccessful fixes', format: 'int', tone: 'warn' },
      { key: 'fixesUnparseable', label: 'Unparseable fixes', format: 'int', tone: 'bad' },
      { key: 'sickRedCases', label: 'Sick-red at freeze', format: 'int' },
      { key: 'testRhaSubstrateFixes', label: 'Freeze-gate fixes', format: 'int', tone: 'ok' },
      { key: 'testRhaDraws', label: 'Freeze-gate redraws', format: 'int' },
      { key: 'irSurgeriesInRha', label: 'IR surgeries in RHA', format: 'int', tone: 'bad' },
    ],
  },
];

export function buildMetricsGrid(summary: InstallStatsSummary): MetricsGridRow[] {
  const out: MetricsGridRow[] = [];
  for (const cat of GRID_SPEC) {
    out.push({ kind: 'header', category: cat.category });
    for (const r of cat.rows) {
      const cells = summary.facets[r.key] ?? cell(null, null, null, null, null);
      out.push({
        kind: 'row',
        category: cat.category,
        key: r.key,
        label: r.label,
        cells,
        format: r.format,
        tone: r.tone ?? null,
      });
    }
  }
  return out;
}

/** Token facets whose Avg/Max columns borrow the per-call sibling facets. */
const RUN_GRID_PER_CALL: Record<string, { avgKey: string; maxKey: string }> = {
  tokensIn: { avgKey: 'perCallAvgIn', maxKey: 'perCallMaxIn' },
  tokensOut: { avgKey: 'perCallAvgOut', maxKey: 'perCallMaxOut' },
};

/**
 * SET 24b — single-run metrics grid (This run / Avg / Max), reusing GRID_SPEC
 * so the run view and Admin's lifetime grid share one facet catalogue. `snap`
 * is the flat shape produced by `runSnapshot`. Only tokensIn/tokensOut carry
 * Avg/Max (from their per-call sibling facets); every other row only
 * populates `latest` (This run) — Min/Lifetime are always null here.
 */
export function buildRunMetricsGrid(snap: Record<string, number | null>): MetricsGridRow[] {
  const out: MetricsGridRow[] = [];
  for (const cat of GRID_SPEC) {
    out.push({ kind: 'header', category: cat.category });
    for (const r of cat.rows) {
      const perCall = RUN_GRID_PER_CALL[r.key];
      out.push({
        kind: 'row',
        category: cat.category,
        key: r.key,
        label: r.label,
        cells: cell(
          snap[r.key] ?? null,
          perCall ? (snap[perCall.avgKey] ?? null) : null,
          null,
          perCall ? (snap[perCall.maxKey] ?? null) : null,
          null,
        ),
        format: r.format,
        tone: r.tone ?? null,
      });
    }
  }
  return out;
}

export function formatUsd(n: number | null | undefined, digits = 4): string {
  if (n == null || !Number.isFinite(n)) return '—';
  if (n === 0) return '$0.00';
  if (Math.abs(n) < 0.0001) return `$${n.toExponential(2)}`;
  return `$${n.toFixed(digits)}`;
}

export function formatCell(value: number | null, format: CellFormat): string {
  if (value == null) return '—';
  if (value === Infinity) return '∞';
  if (!Number.isFinite(value)) return '—';
  switch (format) {
    case 'int':
      return Math.round(value).toLocaleString();
    case 'float1':
      return (Math.round(value * 10) / 10).toLocaleString(undefined, { maximumFractionDigits: 1 });
    case 'usd':
      return formatUsd(value);
    case 'hms':
      return formatHms(value / 1000);
    case 'ratio':
      return (Math.round(value * 100) / 100).toFixed(2);
    case 'pct':
      return `${value >= 0 ? '+' : ''}${value.toFixed(0)}%`;
    case 'raw':
    default:
      return String(value);
  }
}

export function metricsGridToJson(
  summary: InstallStatsSummary & { installHash?: string },
  grid: MetricsGridRow[],
): object {
  const rows = grid.filter((r): r is Extract<MetricsGridRow, { kind: 'row' }> => r.kind === 'row')
    .map(r => ({
      category: r.category,
      key: r.key,
      label: r.label,
      latest: r.cells.latest,
      avg: r.cells.avg,
      min: r.cells.min,
      max: r.cells.max,
      lifetime: r.cells.lifetime,
      format: r.format,
    }));
  return {
    installHash: summary.installHash ?? null,
    generatedAt: new Date().toISOString(),
    latestTs: summary.latestTs,
    latestId: summary.latestId,
    ringSize: summary.ringSize,
    totalsRuns: summary.totalsRuns,
    runs: summary.runs,
    byMode: summary.byMode,
    lifetimeTokens: summary.lifetimeTokens,
    latestVsAvgPct: summary.latestVsAvgPct,
    rows,
  };
}

export function metricsGridToCsv(
  summary: InstallStatsSummary & { installHash?: string },
  grid: MetricsGridRow[],
): string {
  const esc = (s: string) => {
    if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
    return s;
  };
  const lines = [
    `# installHash=${summary.installHash ?? ''}`,
    `# generatedAt=${new Date().toISOString()}`,
    'category,facet,latest,avg,min,max,lifetime',
  ];
  for (const r of grid) {
    if (r.kind !== 'row') continue;
    lines.push([
      esc(r.category),
      esc(r.label),
      formatCell(r.cells.latest, r.format),
      formatCell(r.cells.avg, r.format),
      formatCell(r.cells.min, r.format),
      formatCell(r.cells.max, r.format),
      formatCell(r.cells.lifetime, r.format),
    ].join(','));
  }
  return lines.join('\n') + '\n';
}
