/**
 * SET 57b — the Run Report: 3-function scorecard + six category groups.
 *
 * Pure props in. THIS-RUN scope only — install-wide and site-wide aggregates
 * are Admin-page material and must never render here.
 * Every ratio is a deterministic counter quotient (SET 57a); a 0/0 renders
 * as an em dash, never NaN.
 */

import type { ReactNode } from 'react';
import { reportScoresFromMetrics } from '../../src/tier1/statsAggregate';

/** Rating bands — ≥ok green, ≥warn yellow, ≥friction dark orange, else red. */
export const SCORE_BANDS = { ok: 0.85, warn: 0.6, friction: 0.35 } as const;

export interface RunReportMetrics {
  featuresTotal?: number;
  featuresFrozenWithoutRedraw?: number;
  irRedraws?: number;
  nodesTotal?: number;
  nodesBuildable?: number;
  nodesBuilt?: number;
  nodesBuiltCleanFirstPass?: number;
  boundedLoopNodes?: number;
  boundedLoopIterations?: number;
  candidatesDrawn?: number;
  candidatesAccepted?: number;
  fillCandidatesDrawn?: number;
  repairCandidatesDrawn?: number;
  salvagedCases?: number;
  sickRedCases?: number;
  testRhaSubstrateFixes?: number;
  testRhaDraws?: number;
  irSurgeriesInRha?: number;
  reEnqueuedCases?: number;
  recoveredCases?: number;
  l2Draws?: number;
  l2Wins?: number;
  l2Bullseyes?: number;
  fixesL1?: number;
  fixesL2?: number;
  fixesL3?: number;
  fixesL4?: number;
  corpseRejections?: number;
  classifyAttempts?: number;
  classifyCataloged?: number;
  judgmentGates?: number;
  judgmentOperator?: number;
  judgmentLlm?: number;
  judgmentDropped?: number;
  watcherSyntheses?: number;
  watcherReadings?: number;
  watcherCacheHits?: number;
  watcherCacheMisses?: number;
  watcherWorldStaleSkips?: number;
  corpseRejectionCeilingBreaches?: number;
  nodeRepairDraws?: number;
  nodeRepairWins?: number;
  observationPolls?: number;
  observationsNew?: number;
  observationsCorroborated?: number;
  watcherObservations?: number;
  errorsTotal?: number;
  errorsParse?: number;
  errorsConformance?: number;
  errorsExecution?: number;
  errorsCrash?: number;
  // SET 89 §§10.5 / 11.4 — canonical counters. When present, replace the
  // legacy `errors*` display block. When ABSENT on historical runs the UI
  // renders "not recorded" (never zero — a run without the registry
  // simply did not report those categories).
  findingOccurrencesTotal?: number;
  findingOccurrencesParse?: number;
  findingOccurrencesConformance?: number;
  findingOccurrencesExecution?: number;
  findingOccurrencesCrash?: number;
  findingOccurrencesTestFailure?: number;
  uniqueDefectsObserved?: number;
  uniqueDefectsResolved?: number;
  uniqueDefectsUnresolved?: number;
  actionStarts?: number;
  actionTerminals?: number;
  actionDecisions?: number;
  actionPending?: number;
  actionFailed?: number;
  // SET 89 §11.4 Gap E — nonfatal-action counter. Distinct from
  // `actionFailed` (which sums fatal + nonfatal) so the UI can show
  // "host-callback threw once" without dressing the whole run in red.
  actionFailedNonfatal?: number;
  deferredTotal?: number;
  deferredRelitigated?: number;
  deferredResolvedAfterRelitigation?: number;
  deferredPermanent?: number;
  nodesHealed?: number;
  fixesUnsuccessful?: number;
  fixesUnparseable?: number;
  substrateAttempts?: number;
  substrateFixes?: number;
  substrateRefusals?: number;
  substrateNoHandler?: number;
  tokensIn?: number;
  tokensOut?: number;
  avgIn?: number;
  avgOut?: number;
  minIn?: number;
  minOut?: number;
  maxIn?: number;
  maxOut?: number;
}

export function ratioOf(num: number | undefined, den: number | undefined): number | null {
  if (den == null || den <= 0) return null;
  return Math.max(0, Math.min(1, (num ?? 0) / den));
}

export function scoreTone(r: number | null): 'ok' | 'warn' | 'friction' | 'bad' | 'dim' {
  if (r == null) return 'dim';
  if (r >= SCORE_BANDS.ok) return 'ok';
  if (r >= SCORE_BANDS.warn) return 'warn';
  if (r >= SCORE_BANDS.friction) return 'friction';
  return 'bad';
}

export function pct(r: number | null): string {
  return r == null ? '—' : `${Math.round(r * 100)}%`;
}

function nz(n: number | undefined): number {
  return typeof n === 'number' && Number.isFinite(n) ? n : 0;
}

function fmt(n: number | undefined): string {
  return n == null ? '—' : String(n);
}

/** The three function ratings — delegated to statsAggregate so the tiles,
 * the Admin mirror, and the export share ONE formula. */
export function functionScores(m: RunReportMetrics): {
  ir: number | null; build: number | null; rha: number | null;
} {
  const s = reportScoresFromMetrics(m as Record<string, unknown>);
  return { ir: s.scoreIr, build: s.scoreBuild, rha: s.scoreRha };
}

function ScoreTile({ label, r, sub }: { label: string; r: number | null; sub: string }) {
  const tone = scoreTone(r);
  return (
    <div className={`scoretile score-${tone}`}>
      <div className="scoretile-label">{label}</div>
      <div className="scoretile-pct">{pct(r)}</div>
      <div className="scoretile-sub dim small">{sub}</div>
    </div>
  );
}

function Row({ label, value, tone }: { label: string; value: string; tone?: string }) {
  return (
    <div className="report-row">
      <span className="report-label dim">{label}</span>
      <span className={`report-value${tone ? ` ${tone}` : ''}`}>{value}</span>
    </div>
  );
}

function Group({ title, children }: { title: string; children: ReactNode }) {
  return (
    <div className="report-group">
      <div className="report-group-title">{title}</div>
      {children}
    </div>
  );
}

export function RunReport({ metrics, costUsd }: {
  metrics: RunReportMetrics;
  costUsd?: number | null;
}) {
  const m = metrics;
  const s = functionScores(m);
  const substrateHitRate = ratioOf(m.substrateFixes, m.substrateAttempts);
  const acceptance = ratioOf(m.candidatesAccepted, m.candidatesDrawn);
  const healRate = ratioOf(m.nodesHealed, m.boundedLoopNodes);
  const avgIterations = nz(m.boundedLoopNodes) > 0
    ? (nz(m.boundedLoopIterations) / nz(m.boundedLoopNodes)).toFixed(1)
    : '—';
  return (
    <div data-rha-report="run">
      <div className="scorecard">
        <ScoreTile
          label="IR CONTRACT"
          r={s.ir}
          sub={`${fmt(m.featuresFrozenWithoutRedraw)}/${fmt(m.featuresTotal)} frozen clean · ${fmt(m.irRedraws)} redraws`}
        />
        <ScoreTile
          label="BUILD"
          r={s.build}
          sub={nz(m.nodesBuildable) > 0
            ? `${fmt(m.nodesBuilt)}/${fmt(m.nodesBuildable)} nodes fully built`
            : `${fmt(m.nodesBuiltCleanFirstPass)}/${fmt(m.nodesTotal)} clean first pass`}
        />
        <ScoreTile
          label="RHA"
          r={s.rha}
          sub={`${fmt(m.nodesHealed)} healed · ${fmt(m.fixesUnsuccessful)} unsuccessful · ${fmt(m.fixesUnparseable)} unparseable`}
        />
      </div>
      <div className="report-groups">
        <Group title="SUBSTRATE">
          {nz(m.substrateAttempts) === 0 ? (
            // SET 84 — 0 attempts is not a weak substrate; it means the repair
            // ladder never executed. Say which, in words.
            <Row
              label="status"
              value={nz(m.nodesTotal) === 0
                ? 'never ran — no node entered the repair ladder'
                : 'never ran — nodes entered the ladder but produced no classified findings'}
              tone="warn"
            />
          ) : null}
          <Row label="attempts" value={fmt(m.substrateAttempts)} />
          <Row label="deterministic fixes" value={fmt(m.substrateFixes)} tone="ok" />
          <Row label="refusals" value={fmt(m.substrateRefusals)} />
          <Row label="no-handler candidates" value={fmt(m.substrateNoHandler)} />
          <Row
            label="hit-rate (0 tok)"
            value={nz(m.substrateAttempts) === 0 ? 'n/a — no attempts' : pct(substrateHitRate)}
          />
        </Group>
        <Group title="BOUNDED LOOPS">
          <Row label="nodes entered" value={fmt(m.boundedLoopNodes)} />
          <Row label="iterations" value={fmt(m.boundedLoopIterations)} />
          <Row label="loop draws" value={fmt(m.repairCandidatesDrawn ?? m.candidatesDrawn)} />
          <Row label="fill draws (IR)" value={fmt(m.fillCandidatesDrawn)} />
          <Row label="accepted as fills" value={fmt(m.candidatesAccepted)} tone="ok" />
          <Row label="salvaged from losers" value={fmt(m.salvagedCases)} tone="ok" />
          <Row label="re-enqueued (warm retry)" value={fmt(m.reEnqueuedCases)} />
          <Row label="recovered at end-of-run" value={fmt(m.recoveredCases)} tone="ok" />
          <Row label="acceptance (all layers)" value={pct(acceptance)} />
          <Row label="avg iterations / node" value={avgIterations} />
        </Group>
        <Group title="TOKENS">
          <Row label="sent avg / min / max" value={`${fmt(m.avgIn)} / ${fmt(m.minIn)} / ${fmt(m.maxIn)}`} />
          <Row label="received avg / min / max" value={`${fmt(m.avgOut)} / ${fmt(m.minOut)} / ${fmt(m.maxOut)}`} />
          <Row label="totals in / out" value={`${fmt(m.tokensIn)} / ${fmt(m.tokensOut)}`} />
          {costUsd != null && <Row label="est. cost" value={`$${costUsd.toFixed(4)}`} />}
        </Group>
        <Group title="FINDING OCCURRENCES">
          {/* SET 89 §10.5 — occurrences accrue at authoritative measurement
              sites (freeze-time + terminal). Categories partition all
              occurrences; the sum is exact by construction. When the
              registry did not record this run (pre-SET 89 record), show
              "not recorded" so a zero cannot be misread as clean. */}
          {m.findingOccurrencesTotal == null ? (
            <Row label="status" value="not recorded" tone="dim" />
          ) : (
            <>
              <Row label="total" value={fmt(m.findingOccurrencesTotal)}
                tone={nz(m.findingOccurrencesTotal) > 0 ? 'bad' : undefined} />
              <Row label="parse" value={fmt(m.findingOccurrencesParse)} />
              <Row label="conformance" value={fmt(m.findingOccurrencesConformance)} />
              <Row label="execution" value={fmt(m.findingOccurrencesExecution)} />
              <Row label="crash" value={fmt(m.findingOccurrencesCrash)} />
              <Row label="test-failure" value={fmt(m.findingOccurrencesTestFailure)} />
            </>
          )}
        </Group>
        <Group title="UNIQUE DEFECTS">
          {/* SET 89 §10.4 — a defect is unique by canonical identity;
              resolution requires a confirming authoritative measurement
              whose scope no longer contains the identity. */}
          {m.uniqueDefectsObserved == null ? (
            <Row label="status" value="not recorded" tone="dim" />
          ) : (
            <>
              <Row label="observed" value={fmt(m.uniqueDefectsObserved)} />
              <Row label="resolved" value={fmt(m.uniqueDefectsResolved)} tone="ok" />
              <Row label="unresolved" value={fmt(m.uniqueDefectsUnresolved)}
                tone={nz(m.uniqueDefectsUnresolved) > 0 ? 'bad' : undefined} />
            </>
          )}
        </Group>
        <Group title="ACTION LEDGER">
          {/* SET 89 §11.4 — every action has exactly one terminal;
              `pending` at the end of the run is a conservation bug. */}
          {m.actionStarts == null ? (
            <Row label="status" value="not recorded" tone="dim" />
          ) : (
            <>
              <Row label="starts" value={fmt(m.actionStarts)} />
              <Row label="terminals" value={fmt(m.actionTerminals)} />
              <Row label="decisions" value={fmt(m.actionDecisions)} />
              <Row label="pending" value={fmt(m.actionPending)}
                tone={nz(m.actionPending) > 0 ? 'bad' : undefined} />
              {/* SET 89 §11.4 Gap E — surface fatal and nonfatal
                  failures separately. Nonfatal failures (host-callback
                  throws, judgment resolver failures) don't fail the
                  run but MUST be visible. When the field is missing
                  (older runs before Gap E landed) the row is hidden. */}
              <Row label="failed"
                value={m.actionFailedNonfatal != null && nz(m.actionFailedNonfatal) > 0
                  ? `${fmt(m.actionFailed)} (${fmt(m.actionFailedNonfatal)} nonfatal)`
                  : fmt(m.actionFailed)}
                tone={nz(m.actionFailed) > nz(m.actionFailedNonfatal ?? 0)
                  ? 'bad'
                  : (nz(m.actionFailedNonfatal ?? 0) > 0 ? 'dim' : undefined)} />
            </>
          )}
        </Group>
        <Group title="DEFERRED">
          <Row label="total" value={fmt(m.deferredTotal)} />
          <Row label="relitigated" value={fmt(m.deferredRelitigated)} />
          <Row label="resolved after relitigation" value={fmt(m.deferredResolvedAfterRelitigation)} tone="ok" />
          <Row label="permanent" value={fmt(m.deferredPermanent)} />
        </Group>
        <Group title="RHA EFFICACY">
          <Row label="nodes healed" value={fmt(m.nodesHealed)} tone="ok" />
          <Row
            label="green on first measure (rare by design)"
            value={fmt(m.nodesBuiltCleanFirstPass)}
          />
          <Row label="unsuccessful fixes" value={fmt(m.fixesUnsuccessful)} />
          <Row label="unparseable fixes" value={fmt(m.fixesUnparseable)} />
          <Row label="heal rate" value={pct(healRate)} />
          <Row label="sick-red at freeze" value={fmt(m.sickRedCases)} />
          <Row label="freeze-gate fixes (0 tok)" value={fmt(m.testRhaSubstrateFixes)} tone="ok" />
          <Row label="freeze-gate redraws" value={fmt(m.testRhaDraws)} />
          <Row
            label="IR surgeries in RHA"
            value={fmt(m.irSurgeriesInRha)}
            tone={nz(m.irSurgeriesInRha) > 0 ? 'bad' : 'ok'}
          />
        </Group>
        <Group title="LAYERS">
          <Row label="L1 fixes (substrate)" value={fmt(m.fixesL1 ?? m.substrateFixes)} tone="ok" />
          <Row label="L2 fixes (schema-guided)" value={fmt(m.fixesL2)} tone={nz(m.fixesL2) > 0 ? 'ok' : undefined} />
          <Row label="L3 fixes (free permutation)" value={fmt(m.fixesL3)} />
          <Row label="L4 fixes (entropy reset)" value={fmt(m.fixesL4)} tone={nz(m.fixesL4) > 0 ? 'warn' : undefined} />
          <Row
            label="L2 draws / wins / bullseyes"
            value={`${fmt(m.l2Draws)} / ${fmt(m.l2Wins)} / ${fmt(m.l2Bullseyes)}`}
          />
          <Row
            label="corpse rejections (0 exec)"
            value={fmt(m.corpseRejections)}
            tone={nz(m.corpseRejections) > 0 ? 'friction' : undefined}
          />
          <Row
            label="classified %"
            value={pct(ratioOf(m.classifyCataloged, m.classifyAttempts))}
            tone={(() => {
              const r = ratioOf(m.classifyCataloged, m.classifyAttempts);
              // 95% categorization floor (binding doctrine).
              return r == null ? 'dim' : r >= 0.95 ? 'ok' : 'bad';
            })()}
          />
          <Row
            label="watcher (syntheses / readings / observations)"
            value={`${fmt(m.watcherSyntheses)} / ${fmt(m.watcherReadings)} / ${fmt(m.watcherObservations)}`}
          />
          <Row
            label="node-compliant repairs (attempted / accepted)"
            value={`${fmt(m.nodeRepairDraws)} / ${fmt(m.nodeRepairWins)}`}
            tone={nz(m.nodeRepairWins) > 0 ? 'ok' : undefined}
          />
          <Row
            label="observatory (polls / added / corroborated)"
            value={`${fmt(m.observationPolls)} / ${fmt(m.observationsNew)} / ${fmt(m.observationsCorroborated)}`}
            tone={nz(m.observationsCorroborated) > 0 ? 'ok' : undefined}
          />
          <Row
            label="watcher cache (hits / misses / stale-world skips)"
            value={`${fmt(m.watcherCacheHits)} / ${fmt(m.watcherCacheMisses)} / ${fmt(m.watcherWorldStaleSkips)}`}
          />
          <Row
            label="corpse rejection ceiling breaches"
            value={fmt(m.corpseRejectionCeilingBreaches)}
            tone={nz(m.corpseRejectionCeilingBreaches) > 0 ? 'warn' : undefined}
          />
          <Row
            label="judgment gates (operator/llm/dropped)"
            value={`${fmt(m.judgmentGates)} (${fmt(m.judgmentOperator)}/${fmt(m.judgmentLlm)}/${fmt(m.judgmentDropped)})`}
            tone={nz(m.judgmentLlm) > 0 ? 'warn' : nz(m.judgmentGates) > 0 ? 'ok' : undefined}
          />
        </Group>
      </div>
    </div>
  );
}
