/**
 * SET 37i / 42g / 45a — SINGLE source of truth: event-kind → tone map.
 *
 * GREEN    = verified success only
 * YELLOW   = operational work in flight / neutral machinery
 * FRICTION = setback absorbed, fight continues (dark orange)
 * RED      = terminal crash, drop, park, stall, abandonment, fault
 * DIM      = notify, provenance, evidence, honest catalog notes
 *
 * UI maps these to CSS: green→ok, yellow→warn, friction→friction, red→bad, dim→dim.
 */

export type EventTone = 'green' | 'yellow' | 'friction' | 'red' | 'dim';

/** CSS class suffix used by App.tsx log rows (`tone-${css}`). */
export type ToneCss = 'ok' | 'warn' | 'friction' | 'bad' | 'dim' | 'neutral';

export function toneToCss(tone: EventTone): ToneCss {
  switch (tone) {
    case 'green': return 'ok';
    case 'yellow': return 'warn';
    case 'friction': return 'friction';
    case 'red': return 'bad';
    case 'dim': return 'dim';
  }
}

/**
 * Canonical event kinds the classifier knows. Every kind has a tone.
 * Message-prefix / phase heuristics below map free-form pipeline strings
 * onto these kinds before lookup.
 */
export const EVENT_KIND_TONES: Record<string, EventTone> = {
  // GREEN — verified success only
  'filled': 'green',
  'frozen': 'green',
  'converged': 'green',
  'healed': 'green',
  'rebound': 'green',
  'surgery-refrozen': 'green',
  'unparked': 'green',
  'vet-accepted': 'green',
  'clean-run': 'green',
  'auth-all-passing': 'green',
  'gate-clean': 'green',
  'stubs-ready': 'green',
  'consistency-pass': 'green',
  'fresh-take-kept': 'green',
  'built-files': 'green',
  // SET 60 — a losing candidate's validated case composed into the suite.
  'salvaged': 'green',
  // SET 66 — test-RHA at freeze.
  'test-rha-fix': 'green',
  'test-rha-redraw': 'yellow',
  'test-rha-sick': 'friction',
  'test-rha-drop': 'dim',
  // SET 70 — end-of-run case-relay.
  'case-reenqueued': 'friction',
  'case-recovered': 'green',
  'case-relay-op': 'yellow',
  'case-final-drop': 'dim',
  // SET 64 — L2 schema-guided rung + corpse detection + metaSb hygiene.
  'l2-win': 'green',
  'l2-loop': 'yellow',
  'l2-miss': 'friction',
  'corpse-rejected': 'friction',
  'metasb-refused': 'friction',

  // YELLOW — operational
  'llm-call': 'yellow',
  'llm-usage': 'yellow',
  'fill-batch': 'yellow',
  'superbuffer': 'yellow',
  'page-assembly': 'yellow',
  'permutation-select': 'yellow',
  'build': 'yellow',
  'building': 'yellow',
  'quarantine-diagnosis': 'yellow',
  'relay': 'yellow',
  'failure-stack': 'yellow',
  'entropy': 'yellow',
  'entropy-reset': 'yellow',
  'entropy-escalate': 'yellow',
  'desync': 'yellow',
  'replan': 'yellow',
  'encoding': 'yellow',
  'phase': 'yellow',
  'metaloop': 'yellow',
  'plan': 'yellow',
  'templated': 'yellow',
  'concept-draw': 'yellow',
  'repair-pass': 'yellow',
  'rha-sweep': 'yellow',
  'rha-episode': 'yellow',
  'rha-supervisor': 'yellow',
  'stub-repair': 'yellow',
  'ir-repair': 'yellow',
  'encoding-retry': 'yellow',
  'fixStub': 'yellow',
  'recompute': 'yellow',
  'resume': 'yellow',
  'autopilot': 'yellow',
  'gate-override': 'yellow',
  'prompt-gate-enum': 'yellow',
  'feature-encoded': 'yellow',
  'ir-generated': 'yellow',
  'ir-feature': 'yellow',
  'build-feature': 'yellow',

  // FRICTION — setback absorbed, fight continues (SET 42g)
  'rejected-draw': 'friction',
  'program-parse-rejection': 'friction',
  'vet-rejection': 'friction',
  'partial-passing': 'friction',
  'incumbent-kept': 'friction',
  'skip-no-track': 'friction',
  // SET 64b — corpus fetch failed at run start; run fights substrate-starved.
  'corpus-starved': 'friction',

  // RED — terminal failures / faults
  'crash': 'red',
  'dropped': 'red',
  'parked': 'red',
  'stalled': 'red',
  'abandonment': 'red',
  'abandoned': 'red',
  'artifact-fault': 'red',
  'provider-error': 'red',
  'catalog-drift': 'red',
  'sev1': 'red',
  'no-coverage': 'red',
  'dependency-uncovered': 'red',
  'consistency-fail': 'red',
  'auth-failing': 'red',
  'ir-escalated': 'red',
  'build-rejected': 'red',
  'build-failed': 'red',
  'provider-degraded': 'red',
  'persist-failed': 'red',

  // DIM — evidence / notify / honest substrate notes (not friction)
  'deferral': 'dim',
  'deferred': 'dim',
  'excluded': 'dim',
  'notify': 'dim',
  'provenance': 'dim',
  'spec-hash': 'dim',
  'evidence': 'dim',
  'substrate-refused': 'dim',
  'substrate-no-handler': 'dim',
  'cataloged-honest': 'dim',
  'partially-deferred': 'dim',
  'partially-excluded': 'dim',
  'clade-candidate': 'dim',
  'handler-candidate': 'dim',
  'planner-bypass': 'dim',
  'seam-dump': 'dim',
  'attribution-miss': 'dim',
  'prompt-gate-probe-fail': 'dim',
  'prompt-gate-insufficient': 'dim',
  'parse-gate-unavailable': 'dim',
  'born-red': 'dim',
  'hang': 'dim',
  'stage-transition': 'dim',
};

/** Every registered kind (for exhaustive tests). */
export function allEventKinds(): string[] {
  return Object.keys(EVENT_KIND_TONES);
}

export function toneForKind(kind: string): EventTone {
  const t = EVENT_KIND_TONES[kind];
  if (!t) throw new Error(`eventTone: unmapped kind "${kind}"`);
  return t;
}

/** Success-predicate kinds — the ONLY ones allowed to be green. */
export const GREEN_SUCCESS_KINDS = new Set(
  Object.entries(EVENT_KIND_TONES)
    .filter(([, t]) => t === 'green')
    .map(([k]) => k),
);

export function isSuccessKind(kind: string): boolean {
  return GREEN_SUCCESS_KINDS.has(kind);
}

/**
 * Classify a free-form pipeline message (+ optional phase / job-event kind)
 * into a canonical event kind, then return its tone.
 */
export function classifyEventKind(input: {
  message?: string;
  phase?: string;
  kind?: string;
}): string {
  const m = (input.message ?? '').trim();
  const kind = input.kind ?? '';

  // Explicit JobEvent kinds (engine audit log).
  if (kind === 'llm-usage') return 'llm-usage';
  if (kind === 'sev1') return 'sev1';
  if (kind === 'stubs-ready') return 'stubs-ready';
  if (kind === 'rha-fresh-take' || kind === 'ir-fresh-take') return 'entropy-reset';
  if (kind === 'ir-fresh-take-kept') return 'fresh-take-kept';
  if (kind === 'ir-escalated' || kind === 'ir-escalations') return 'ir-escalated';
  if (kind === 'encoding-retry') return 'encoding-retry';
  if (kind === 'stub-repair') return 'stub-repair';
  if (kind === 'fixStub') return 'fixStub';
  if (kind === 'ir-repair') return 'ir-repair';
  if (kind === 'feature-encoded') return 'feature-encoded';
  if (kind === 'ir-generated') return 'ir-generated';
  if (kind === 'ir-feature') return 'ir-feature';
  if (kind === 'build-feature') return 'build-feature';
  if (kind === 'rha-sweep') return 'rha-sweep';
  if (kind === 'rha-episode') return 'rha-episode';
  if (kind === 'rha-supervisor') return 'rha-supervisor';
  if (kind === 'recompute') return 'recompute';
  if (kind === 'resume') return 'resume';
  if (kind === 'autopilot' || kind === 'gate-override') return kind === 'autopilot' ? 'autopilot' : 'gate-override';
  if (kind === 'prompt-gate-enum') return 'prompt-gate-enum';
  if (kind === 'prompt-gate-probe') {
    return /probe ✓|clear/.test(m) || /\u2713/.test(m) ? 'gate-clean' : 'prompt-gate-probe-fail';
  }
  if (kind === 'prompt-gate') {
    return /demarcated|sufficient/.test(m) && !/ambiguous|insufficient/.test(m)
      ? 'gate-clean'
      : 'prompt-gate-insufficient';
  }
  if (kind === 'consistency-gate') {
    return /✓|pass/.test(m) && !/✗|fail/.test(m) ? 'consistency-pass' : 'consistency-fail';
  }
  if (kind === 'auth-result') {
    return /passed ✓|0 failed/.test(m) || /\/\d+ passed ✓/.test(m) ? 'auth-all-passing' : 'auth-failing';
  }
  if (kind === 'rha-repair') {
    return /healed/.test(m) ? 'healed' : 'rejected-draw';
  }
  if (kind === 'complete') {
    return /failing|escalated/.test(m) ? 'auth-failing' : 'clean-run';
  }
  if (kind === 'phase') return 'phase';

  // Message-prefix heuristics (Tier1 log).
  if (/^superbuffer:/.test(m) || /^pages:/.test(m)) return 'superbuffer';
  if (/^relay:/.test(m)) return 'relay';
  // SET 73 — a pick that lands NOTHING is a setback absorbed, not neutral
  // operational news (Andy: "not BAD but not neutral → dark yellow/orange").
  // Must precede the generic permutation-select matcher.
  if (/^permutation-select: .*picked .*\(0\/\d+ (?:passing|healed|redrawn)\)/.test(m)) return 'partial-passing';
  if (/^permutation-select:/.test(m)) return 'permutation-select';
  if (/^fill batch|^fallback batch|^batch \d+:/i.test(m) || /fill targets:/.test(m)) return 'fill-batch';
  if (/^repair pass\b/i.test(m)) return 'repair-pass';
  if (/^plan:/.test(m)) return 'plan';
  if (/^templated /.test(m)) return 'templated';
  if (/^replan \(substrate-anchored\)/.test(m)) return 'replan';
  if (/^metaloop /.test(m)) return 'metaloop';
  if (/^concept /.test(m) || /concept page|concept draw/i.test(m)) return 'concept-draw';
  if (/^LLM |^llm /i.test(m) || /still generating/.test(m)) return 'llm-call';
  if (/escalating entropy|entropy reset|fresh draw/i.test(m)) return 'entropy';
  if (/desync/i.test(m)) return 'desync';
  if (/crash quarantine diagnosis/i.test(m)) return 'quarantine-diagnosis';
  if (/provider degraded|provider unreachable|provider connection lost/i.test(m)) {
    return /recovered/i.test(m) ? 'llm-call' : 'provider-error';
  }
  if (/catalog.?drift/i.test(m)) return 'catalog-drift';
  // SET 42g — vet-gate / program-parse rejections are friction (fight continues).
  if (/program-parse rejection/i.test(m)) return 'program-parse-rejection';
  if (/artifact vet rejected|vet rejected/i.test(m)) return 'vet-rejection';
  if (/artifact fault/i.test(m)) return 'artifact-fault';
  if (/rejected:|draw rejected|build draw rejected|repair draw rejected|reset draw rejected/i.test(m)) {
    return 'rejected-draw';
  }
  if (/no-coverage|abandoned after metaloop/i.test(m)) return 'abandonment';
  if (/dependency uncovered|uses .+ — dependency uncovered/i.test(m)) return 'dependency-uncovered';
  if (/blocked-crash: parked|parked \(crash/i.test(m)) return 'parked';
  if (/\bstalled\b/i.test(m)) return 'stalled';
  // GREEN success predicates first — filled must not be stolen by dropped's case ".+" …
  if (/case ".+" filled by model|filled by model/.test(m)) return 'filled';
  if (/frozen suite |suite re-frozen|IR frozen|frozen \(/.test(m)) return 'frozen';
  if (/converged in \d+ passes|converged/.test(m)) return 'converged';
  if (/healed via |→ fixed \(0 tok\)|outcome.?healed|: healed/.test(m)) return 'healed';
  if (/rebound via clade/.test(m)) return 'rebound';
  if (/salvaged from candidate/.test(m)) return 'salvaged';
  if (/^test-rha: .*suite-side fix/.test(m)) return 'test-rha-fix';
  if (/^test-rha: case ".*" redrawn at freeze-gate/.test(m)) return 'test-rha-redraw';
  if (/^test-rha: case ".*" dropped at freeze-gate/.test(m)) return 'test-rha-drop';
  if (/^test-rha: \d+ sick-red/.test(m)) return 'test-rha-sick';
  if (/re-enqueued to end-of-run stack/.test(m)) return 'case-reenqueued';
  if (/rescue exhausted for/.test(m)) return 'case-reenqueued';
  if (/^case-relay: case ".*" recovered at end-of-run/.test(m)) return 'case-recovered';
  if (/^case-relay: case ".*" final drop/.test(m)) return 'case-final-drop';
  if (/^case-relay: /.test(m)) return 'case-relay-op';
  if (/^born-red: \d+ throw-expecting case\(s\) retained/.test(m)) return 'templated';
  // SET 81 — every FAILURE reads dark orange (Andy). A substrate miss, a
  // no-handler, and a cataloged-but-unfixed class are all "we did not repair
  // this", which is friction, not neutral operational news.
  if (/^substrate: .* → (cataloged|no-handler|catalog-drift)/.test(m)) return 'partial-passing';
  if (/^catalog-drift: /.test(m)) return 'partial-passing';
  if (/^epoch closed: .*\b0\/\d+ cases passing/.test(m)) return 'dropped';
  if (/^epoch closed: /.test(m)) {
    const f = /(\d+)\/(\d+) cases passing/.exec(m);
    return f && f[1] === f[2] ? 'converged' : 'partial-passing';
  }
  // SET 74/75 — ledger + watcher: bookkeeping, never a claim about health.
  if (/^supergit: \d+ ledger nodes/.test(m)) return 'evidence';
  if (/^watcher (synthesis )?@ /.test(m)) return 'evidence';
  if (/^fill accepted at try /.test(m)) return 'filled';
  // SET 68 — judgment gates: the gate is friction, the ruling is operational.
  if (/^judgment gate: /.test(m)) return 'incumbent-kept';
  if (/^judgment: case ".*" dropped/.test(m)) return 'test-rha-drop';
  if (/^judgment: /.test(m)) return 'l2-win';
  if (/^judgment (resolver failed|amend|drop produced)/.test(m)) return 'l2-miss';
  if (/^gate: judgment required/.test(m)) return 'l2-loop';
  // SET 65 — synthesis grammar + shell-lock (flagged).
  if (/^grammar: case ".*" conformed to /.test(m)) return 'healed';
  if (/^grammar fallback: /.test(m)) return 'incumbent-kept';
  if (/^shell-lock: .* re-emitted through deterministic frame/.test(m)) return 'healed';
  if (/^shell-lock: .* — draw missing bodies/.test(m)) return 'incumbent-kept';
  // SET 64 — L2 rung + corpse detection + metaSb hygiene.
  if (/^L2 (bullseye|improvement kept)/.test(m)) return 'l2-win';
  if (/^L2 procedure loop /.test(m)) return 'l2-loop';
  if (/^L2 (loop .* no improvement|draw failed)/.test(m)) return 'l2-miss';
  if (/^corpse detected /.test(m)) return 'corpse-rejected';
  if (/^metaSb write refused/.test(m)) return 'metasb-refused';
  if (/suite re-frozen after (quarantine|surgery)/i.test(m)) return 'surgery-refrozen';
  // hyphenated "un-parked" and compact "unparked"
  if (/^un-?parked —/.test(m)) return 'unparked';
  if (/vet[- ]accepted|artifact vet (accepted|clean)/i.test(m)) return 'vet-accepted';
  if (/gate.*clean|cross-suite consistency gate ✓/i.test(m)) return 'gate-clean';
  // SET 42h — built N/N = green win; built 0/N or partial K/N = friction.
  {
    const built = /^built (\d+)\/(\d+) owned files/.exec(m);
    if (built) {
      const k = Number(built[1]);
      const n = Number(built[2]);
      if (n > 0 && k === n) return 'built-files';
      return 'rejected-draw'; // friction — incomplete / rejected build family
    }
  }
  if (/\d+\/\d+ passing/.test(m)) {
    const frac = /(\d+)\/(\d+) passing/.exec(m);
    if (frac && frac[1] === frac[2]) return 'auth-all-passing';
    if (frac && frac[1] === '0') return 'auth-failing';
    return 'partial-passing'; // SET 42g — friction
  }
  if (/dropped —|case ".+" dropped|unfilled after retries/i.test(m)) return 'dropped';
  if (/^crash:|crash while |crashed/i.test(m)) return 'crash';
  if (/SEV1|algorithm breakdown/i.test(m)) return 'sev1';
  if (/run paused/i.test(m)) return 'provider-error';
  if (/^persist failed:/i.test(m)) return 'persist-failed';

  // FRICTION (before dim)
  if (/corpus unavailable — substrate-starved/.test(m)) return 'corpus-starved';
  if (/no improvement — incumbent kept/.test(m)) return 'incumbent-kept';
  if (/retry skipped: no new track/.test(m)) return 'skip-no-track';

  // DIM / excluded disclosure
  if (/feature deferred|deferred to end-of-phase/i.test(m)) return 'deferral';
  if (/partially excluded:|partially deferred:/i.test(m)) return 'partially-excluded';
  if (/^excluded:|deferred \(permanent until|excluded — requires/i.test(m)) return 'excluded';
  if (/^deferred:/i.test(m)) return 'excluded';
  if (/substrate:.*→ refused|refused\(/i.test(m)) return 'substrate-refused';
  if (/substrate:.*→ no-handler|no-handler \(0 tok\)/i.test(m)) return 'substrate-no-handler';
  if (/cataloged|honest \(no-handler\)/i.test(m)) return 'cataloged-honest';
  if (/^clade-candidate:/.test(m)) return 'clade-candidate';
  if (/^handler-candidate:/.test(m)) return 'handler-candidate';
  if (/^planner bypass:/.test(m)) return 'planner-bypass';
  if (/^spec: .+ of encode output/.test(m)) return 'spec-hash';
  if (/^seam-dump:|^seam-dump-program:/.test(m)) return 'seam-dump';
  if (/^attribution-miss:/.test(m)) return 'attribution-miss';
  if (/^buffer had templates:|^parse-poison incidents:|^solution-strata/i.test(m)) return 'evidence';
  if (/^hang:/i.test(m)) return 'hang';
  if (/stage-transition spacing applied/i.test(m)) return 'stage-transition';
  if (/parse-gate unavailable|module-probe unavailable/i.test(m)) return 'parse-gate-unavailable';
  if (/^born-red check:/i.test(m)) return 'born-red';
  if (/build phase failed:|build-failed/i.test(m)) return 'build-failed';
  // SET 42h — build-phase notify lines stay dim (not green).
  if (/provenance|notify/i.test(m)) return /notify/i.test(m) ? 'notify' : 'provenance';
  if (/boundary violations dropped/i.test(m)) return 'deferral';

  // Phase fallbacks (never green without success predicate)
  switch (input.phase) {
    case 'encode': return 'encoding';
    case 'gate': return 'phase';
    case 'ir': return 'plan';
    case 'build': return 'build'; // yellow operational
    case 'auth': return 'build';
    case 'rha': return 'rha-sweep';
    case 'done': return 'phase';
    default: return 'phase';
  }
}

export function eventTone(input: {
  message?: string;
  phase?: string;
  kind?: string;
}): EventTone {
  const k = classifyEventKind(input);
  return toneForKind(k);
}

/**
 * SET 45a — orthogonal substrate flag (independent of outcome tone).
 * True for deterministic 0-token machinery: classifications, mechanical fixes,
 * parser-guided heals, clade rebinds, vet rejections, oracle passes, ddmin,
 * born-red checks, program-parse gates, etc.
 */
export function isSubstrateEvent(input: {
  message?: string;
  phase?: string;
  kind?: string;
}): boolean {
  const m = (input.message ?? '').trim();
  // Pin: every message carrying `(0 tok…)` is substrate work.
  if (/\(0\s*tok\b/i.test(m)) return true;
  if (/^ddmin\b|^minimal failing set:|mechanical fix|program-parse/i.test(m)) return true;
  const k = classifyEventKind(input);
  return (
    k === 'substrate-refused'
    || k === 'substrate-no-handler'
    || k === 'healed'
    || k === 'rebound'
    || k === 'born-red'
    || k === 'program-parse-rejection'
    || k === 'vet-rejection'
    || k === 'vet-accepted'
    || k === 'clade-candidate'
    || k === 'handler-candidate'
    || k === 'parse-gate-unavailable'
    || k === 'cataloged-honest'
  );
}

export function eventFlags(input: {
  message?: string;
  phase?: string;
  kind?: string;
}): { substrate: boolean } {
  return { substrate: isSubstrateEvent(input) };
}

/** App.tsx helper — CSS tone class for Tier1 log rows. */
export function t1EventToneCss(e: { phase: string; message: string }): ToneCss {
  return toneToCss(eventTone({ phase: e.phase, message: e.message }));
}

/** App.tsx helper — CSS tone for engine JobEvent audit log. */
export function jobEventToneCss(ev: { kind: string; data?: Record<string, unknown> }): ToneCss {
  const d = ev.data ?? {};
  // Synthesize a short message for kinds that encode outcome in data.
  let message = '';
  if (ev.kind === 'prompt-gate-probe') {
    message = d.clear ? 'probe ✓' : 'probe ✗';
  } else if (ev.kind === 'prompt-gate') {
    message = d.sufficient ? 'demarcated' : 'ambiguous';
  } else if (ev.kind === 'consistency-gate') {
    message = d.pass ? 'pass ✓' : 'fail ✗';
  } else if (ev.kind === 'auth-result') {
    message = Number(d.failed) === 0 ? 'passed ✓' : 'failed';
  } else if (ev.kind === 'rha-repair') {
    message = String(d.outcome ?? '');
  } else if (ev.kind === 'complete') {
    const r = d.report as { failed?: number } | undefined;
    message = r && r.failed && r.failed > 0 ? 'failing escalated' : 'complete clean';
  } else if (ev.kind === 'rha-supervisor') {
    message = d.error ? 'error' : 'attempt';
  }
  return toneToCss(eventTone({ kind: ev.kind, message }));
}
