// Candidate-generator for Stage 05 (Logits → Sample).
//
// Architecture (post-autoplan, M2):
//   Tier 1 — LIVE (via proxy, added in A3)
//     fetch('/api/next-tokens', ...) → Cloudflare Worker or local Node wrapper
//     using the owner's ANTHROPIC_API_KEY. Hard cost caps on the proxy side.
//   Tier 2 — LOCAL (this file, always available, never fails)
//     Hand-curated bias table keyed on the last 1–2 "words" of the prompt,
//     with category-based fallbacks by tail shape. Deterministic (seeded).
//
// A1 scope: LOCAL tier only. Proxy wiring lands in A3.
// SANDBOX tier (window.claude.complete) deliberately dropped — post-export
// it is permanently undefined and the code path is dead.

// ── Bias table ──────────────────────────────────────────────────────────────
// Keyed on the last 1 or 2 whitespace-trimmed words of the prompt, lowercased.
// Later words in the context dominate (last-2 beats last-1). Each entry lists
// plausible next tokens. Leading space preserved — that's how real tokenizers
// (BPE, SentencePiece) represent word boundaries: " the" vs "the" are different
// tokens with different ids.

const BIAS_TABLE = {
  // ── Two-word contexts (most specific) ────────────────────────────────────

  // Matrix-voiced + narrative openers the starter prompt benefits from
  'dreams in':    [' color', ' binary', ' silence', ' code', ' the', ' numbers', ' fragments', ' static'],
  'dreams of':    [' electric', ' flying', ' home', ' a', ' the', ' nothing', ' light', ' waking'],
  'machine dreams': [' in', ' of', ' about', ' alone', ' and'],
  'the machine':  [' dreams', ' thinks', ' learns', ' knows', ' is', ' was', ' has', ' became'],
  'the matrix':   [' is', ' has', ' was', ' knows', ' sees', ' expects'],
  'reality is':   [' a', ' the', ' not', ' an', ' just', ' only', ' made'],
  'truth is':     [' the', ' a', ' we', ' I', ' not', ' rarely', ' only'],
  'nobody knows': [' what', ' why', ' how', ' the', ' if', ' when', ' where'],

  // Well-worn narrative / rhetorical openings
  'once upon':    [' a'],
  'it was':       [' the', ' a', ' not', ' clear', ' only', ' all', ' already'],
  'he said':      [',', '.', ' nothing', ' that', ' to', ' quietly', ' again'],
  'she said':     [',', '.', ' nothing', ' that', ' to', ' quietly', ' again'],
  'they said':    [',', '.', ' nothing', ' that', ' it', ' to', ' we'],
  'i think':      [' the', ' it', ' we', ' that', ' I', ' about', ' so'],
  'i know':       [' the', ' it', ' that', ' what', ' why', ' how', ' you'],
  'i saw':        [' the', ' a', ' it', ' something', ' him', ' her', ' that'],

  // Identity / state-of-being copula patterns
  'to be':        [' or', ' a', ' the', ' honest', ' fair', ' clear', ' sure', ' continued'],
  'i am':         [' a', ' not', ' the', ' here', ' trying', ' going', ' sorry', ' still'],
  'you are':      [' a', ' not', ' the', ' here', ' right', ' welcome', ' still', ' going'],
  'we are':       [' not', ' the', ' a', ' here', ' living', ' working', ' going', ' still'],
  'they are':     [' not', ' the', ' a', ' still', ' already', ' only', ' mostly'],
  'it is':        [' a', ' the', ' not', ' also', ' important', ' possible', ' clear', ' only'],
  'there is':     [' a', ' no', ' also', ' something', ' an', ' only', ' nothing', ' little'],
  'there are':    [' no', ' many', ' also', ' several', ' two', ' three', ' some', ' few'],
  'this is':      [' the', ' a', ' not', ' my', ' how', ' what', ' why', ' where'],
  'that is':      [' why', ' the', ' a', ' not', ' how', ' what', ' where', ' when'],
  'what is':      [' the', ' a', ' this', ' it', ' happening', ' going', ' real', ' true'],
  'what are':     [' the', ' we', ' you', ' they', ' these', ' those', ' people', ' some'],
  'where is':     [' the', ' my', ' your', ' his', ' her', ' everyone', ' it', ' he'],
  'when is':      [' the', ' it', ' your', ' my', ' the next', ' a'],
  'who is':       [' the', ' this', ' that', ' going', ' responsible', ' there', ' in'],
  'why is':       [' the', ' it', ' this', ' everything', ' there', ' that', ' nobody'],
  'how is':       [' the', ' it', ' this', ' that', ' possible', ' she', ' he'],

  // Negation / modality
  'is not':       [' a', ' the', ' always', ' really', ' just', ' only', ' clear', ' enough'],
  'are not':      [' the', ' always', ' just', ' only', ' really', ' always', ' yet'],
  'will not':     [' be', ' let', ' make', ' happen', ' forget', ' change', ' matter'],
  'cannot be':    [' done', ' the', ' a', ' used', ' easily', ' fully', ' certain'],
  'do not':       [' know', ' think', ' want', ' have', ' need', ' use', ' let', ' worry'],
  'does not':     [' matter', ' mean', ' work', ' make', ' want', ' know', ' have', ' need'],
  'did not':      [' know', ' think', ' want', ' see', ' expect', ' mean', ' come'],
  'should not':   [' be', ' have', ' go', ' matter', ' let', ' worry', ' forget'],
  'could not':    [' be', ' have', ' see', ' find', ' stop', ' help', ' tell'],

  // Definite-article phrase chains
  'in the':       [' end', ' beginning', ' world', ' middle', ' dark', ' morning', ' future', ' system', ' data'],
  'on the':       [' other', ' same', ' way', ' surface', ' edge', ' contrary', ' horizon', ' screen', ' table'],
  'at the':       [' end', ' same', ' top', ' moment', ' heart', ' edge', ' time', ' bottom'],
  'of the':       [' world', ' day', ' machine', ' system', ' model', ' century', ' future', ' mind'],
  'from the':     [' beginning', ' start', ' inside', ' edge', ' depths', ' top', ' first', ' moment'],
  'to the':       [' next', ' last', ' edge', ' point', ' future', ' past', ' other', ' top'],
  'for the':      [' first', ' last', ' next', ' next', ' record', ' world', ' moment', ' rest'],
  'with the':     [' same', ' rest', ' others', ' others', ' right', ' world', ' model'],
  'by the':       [' time', ' end', ' way', ' side', ' moment', ' hour', ' year'],
  'about the':    [' world', ' future', ' system', ' model', ' way', ' fact', ' problem'],
  'through the':  [' door', ' window', ' screen', ' mirror', ' fog', ' static', ' years'],
  'across the':   [' room', ' screen', ' world', ' years', ' street', ' void', ' network'],
  'beyond the':   [' veil', ' horizon', ' edge', ' screen', ' pale', ' reach', ' surface'],
  'within the':   [' system', ' model', ' network', ' world', ' structure', ' data', ' limits'],
  'inside the':   [' system', ' model', ' machine', ' head', ' room', ' box', ' code'],

  // Indefinite-article phrase chains
  'in a':         [' moment', ' way', ' world', ' few', ' single', ' flash', ' sense', ' dream'],
  'on a':         [' screen', ' day', ' hill', ' whim', ' basic', ' single', ' table'],
  'at a':         [' time', ' point', ' distance', ' table', ' stop', ' rate', ' glance'],
  'of a':         [' kind', ' sort', ' man', ' woman', ' child', ' machine', ' dream'],
  'to a':         [' point', ' degree', ' world', ' place', ' stop', ' man', ' woman'],
  'for a':        [' moment', ' while', ' long', ' second', ' reason', ' change', ' while'],
  'with a':       [' smile', ' sigh', ' nod', ' shrug', ' laugh', ' frown', ' gesture'],
  'into a':       [' room', ' dream', ' single', ' new', ' different', ' state', ' pool'],

  // Quick browns / test utterances
  'the quick':    [' brown', ' fox', ' and', ' response'],
  'brown fox':    [' jumps', ' ran', ' is', ' was', ' came', ' over'],

  // Tech & ML domain patterns
  'machine learning': [' is', ' models', ' can', ' algorithms', ' techniques', ' has', ' systems'],
  'artificial intelligence': [' is', ' has', ' will', ' can', ' systems', ' models', ' agents'],
  'language model':   [' is', ' can', ' outputs', ' predicts', ' thinks', ' generates'],
  'language models':  [' are', ' can', ' learn', ' predict', ' process', ' generate'],
  'neural network':   [' is', ' was', ' learns', ' has', ' outputs', ' trains'],
  'neural networks':  [' are', ' learn', ' have', ' train', ' can', ' process'],
  'deep learning':    [' is', ' has', ' can', ' models', ' systems', ' framework'],
  'attention head':   [' looks', ' focuses', ' attends', ' weights', ' captures'],
  'attention heads':  [' look', ' focus', ' attend', ' weight', ' specialize'],
  'token embedding':  [' is', ' captures', ' represents', ' lives'],
  'residual stream':  [' is', ' carries', ' accumulates', ' flows'],

  // Contemplative continuations
  'how to':       [' make', ' build', ' create', ' use', ' write', ' find', ' think', ' avoid'],
  'when you':     [' are', ' see', ' know', ' think', ' come', ' go', ' find', ' start'],
  'if you':       [' are', ' want', ' need', ' know', ' see', ' try', ' can', ' look'],
  'because we':   [' are', ' have', ' know', ' cannot', ' do', ' were', ' still'],
  'and then':     [' the', ' I', ' we', ' it', ' suddenly', ' everything', ' nothing'],
  'but when':     [' the', ' I', ' we', ' you', ' it', ' he', ' she'],
  'but then':     [' the', ' I', ' we', ' you', ' it', ' suddenly', ' again'],

  // ── One-word contexts ────────────────────────────────────────────────────

  // Articles + possessives
  'the':      [' model', ' world', ' system', ' data', ' future', ' number', ' machine', ' signal', ' answer', ' code'],
  'a':        [' new', ' single', ' few', ' simple', ' model', ' system', ' small', ' kind'],
  'an':       [' old', ' entire', ' example', ' array', ' order', ' idea', ' answer', ' hour'],
  'this':     [' is', ' was', ' means', ' time', ' one', ' way', ' could', ' kind'],
  'that':     [' is', ' was', ' means', ' could', ' the', ' we', ' you', ' one'],
  'these':    [' are', ' were', ' things', ' days', ' systems', ' models', ' numbers'],
  'those':    [' are', ' were', ' who', ' days', ' things', ' people', ' numbers'],
  'my':       [' own', ' mind', ' head', ' heart', ' point', ' job', ' life', ' first'],
  'your':     [' own', ' mind', ' life', ' job', ' time', ' own', ' head', ' work'],
  'their':    [' own', ' minds', ' lives', ' work', ' data', ' own', ' systems'],
  'our':      [' own', ' work', ' data', ' world', ' time', ' model', ' system'],
  'its':      [' own', ' weight', ' output', ' input', ' shape', ' form'],

  // Be-verbs / aux
  'is':       [' a', ' the', ' not', ' also', ' an', ' still', ' only', ' never'],
  'was':      [' a', ' the', ' not', ' still', ' once', ' already', ' never'],
  'are':      [' the', ' not', ' also', ' often', ' still', ' many', ' never'],
  'were':     [' the', ' not', ' all', ' still', ' never', ' only', ' many'],
  'be':       [' a', ' the', ' able', ' used', ' more', ' in', ' on'],
  'been':     [' the', ' a', ' many', ' working', ' a', ' trying', ' running'],
  'being':    [' a', ' the', ' one', ' honest', ' human', ' used', ' able'],
  'have':     [' been', ' the', ' a', ' to', ' never', ' not', ' already'],
  'has':      [' been', ' the', ' a', ' to', ' never', ' not', ' already'],
  'had':      [' been', ' the', ' a', ' to', ' never', ' not', ' already'],
  'will':     [' be', ' not', ' the', ' have', ' never', ' always', ' happen'],
  'would':    [' be', ' not', ' the', ' have', ' never', ' rather'],
  'could':    [' be', ' not', ' have', ' see', ' find', ' use'],
  'should':   [' be', ' not', ' have', ' see', ' do', ' know'],
  'can':      [' be', ' not', ' do', ' see', ' still', ' always', ' find'],

  // Conjunctions
  'and':      [' the', ' a', ' then', ' so', ' yet', ' I', ' now', ' we'],
  'but':      [' the', ' it', ' I', ' not', ' also', ' when', ' if', ' we'],
  'or':       [' the', ' a', ' not', ' maybe', ' even', ' at', ' rather'],
  'if':       [' you', ' we', ' the', ' it', ' I', ' not', ' there', ' they'],
  'when':     [' the', ' you', ' we', ' I', ' it', ' they', ' she', ' he'],
  'while':    [' the', ' you', ' we', ' I', ' it', ' they', ' doing', ' waiting'],
  'until':    [' the', ' you', ' we', ' it', ' I', ' they', ' everything'],
  'because':  [' the', ' it', ' of', ' we', ' I', ' they', ' you'],
  'though':   [' the', ' it', ' we', ' I', ' not', ' sometimes'],
  'so':       [' the', ' it', ' we', ' I', ' they', ' much', ' far', ' that'],
  'yet':      [' the', ' no', ' another', ' still', ' to', ' also'],

  // Pronouns
  'i':        [' am', ' was', ' have', ' had', ' will', ' would', ' think', ' know'],
  'you':      [' are', ' were', ' have', ' had', ' will', ' would', ' think', ' know'],
  'he':       [' is', ' was', ' has', ' had', ' will', ' would', ' could', ' said'],
  'she':      [' is', ' was', ' has', ' had', ' will', ' would', ' could', ' said'],
  'we':       [' are', ' were', ' have', ' had', ' will', ' would', ' can', ' should'],
  'they':     [' are', ' were', ' have', ' had', ' will', ' would', ' can', ' say'],
  'it':       [' is', ' was', ' has', ' had', ' will', ' would', ' could', ' should'],

  // Subject-domain nouns
  'dreams':   [' of', ' in', ' are', ' about', ' and', ' were', ' become'],
  'machine':  [' learning', ' is', ' that', ' can', ' has', ' was', ' thinks'],
  'model':    [' is', ' can', ' has', ' was', ' learns', ' outputs', ' thinks'],
  'models':   [' are', ' can', ' have', ' were', ' learn', ' output', ' think'],
  'data':     [' is', ' are', ' from', ' about', ' shows', ' points', ' science'],
  'system':   [' is', ' has', ' was', ' will', ' can', ' keeps', ' runs'],
  'systems':  [' are', ' have', ' were', ' will', ' can', ' keep', ' run'],
  'neural':   [' network', ' networks', ' pathways', ' activity', ' architecture', ' firing'],
  'deep':     [' learning', ' inside', ' in', ' within', ' neural', ' beneath', ' down'],
  'attention':[' is', ' heads', ' head', ' matters', ' flows', ' pattern'],
  'token':    [' is', ' by', ' after', ' in', ' that', ' which', ' embedding'],
  'tokens':   [' are', ' in', ' flow', ' form', ' make', ' span', ' have'],
  'think':    [' about', ' of', ' that', ' it', ' we', ' again', ' twice'],
  'thinks':   [' about', ' the', ' it', ' that', ' of', ' we'],
  'says':     [' the', ' it', ' he', ' she', ' they', ' a', ' nothing'],
  'said':     [' the', ' it', ' he', ' she', ' to', ' that', ' nothing'],
  'reality':  [' is', ' was', ' has', ' checks', ' itself', ' bends', ' consists'],
  'code':     [' is', ' that', ' the', ' runs', ' reads', ' compiles', ' defines'],
  'signal':   [' is', ' from', ' noise', ' and', ' that', ' flow', ' decays'],
  'numbers':  [' flow', ' are', ' that', ' streaming', ' as', ' become', ' fall'],
};

// ── Categorical fallback pools ──────────────────────────────────────────────
// Used when the bias table has no match for the tail. Chosen by the tail's
// rough "shape" (article, preposition, verb-like, sentence-end, etc).

const POOLS = {
  after_article:       [' model', ' world', ' system', ' data', ' machine', ' number', ' future', ' signal', ' answer', ' code', ' thing', ' moment'],
  after_prep:          [' the', ' a', ' its', ' their', ' our', ' my', ' every', ' each', ' some', ' this'],
  after_verb_past:     [' the', ' a', ' it', ' me', ' him', ' her', ' them', ' us', ' everyone'],
  after_verb_ing:      [' the', ' a', ' to', ' at', ' for', ' with', ' from', ' about', ' on'],
  after_verb_generic:  [' a', ' the', ' to', ' it', ' that', ' in', ' with', ' into', ' for'],
  after_plural:        [' are', ' were', ' have', ' had', ' will', ' can', ' and', ' that'],
  after_singular_noun: [' is', ' was', ' has', ' had', ' will', ' can', ' that', ' which'],
  sentence_start:      [' The', ' It', ' We', ' Then', ' A', ' In', ' This', ' There', ' After', ' But'],
  after_comma:         [' and', ' but', ' or', ' so', ' then', ' the', ' a', ' which'],
  after_quote:         [' said', ' the', ' he', ' she', ' they', ',', '.'],
  generic:             [' the', ' a', ' and', ' of', ' to', ' in', ' is', ' it', ' that', ' with', ' as', ' on'],
  punctuation:         ['.', ',', ';', '—', '…'],
};

// ── Category detectors ──────────────────────────────────────────────────────
const ARTICLES = new Set(['the', 'a', 'an', 'this', 'that', 'these', 'those', 'my', 'our', 'your', 'their', 'its', 'his', 'her']);
const PREPS = new Set(['in', 'on', 'at', 'of', 'to', 'for', 'with', 'by', 'from', 'about', 'into', 'onto', 'through', 'across', 'beyond', 'within', 'inside', 'between', 'against', 'toward', 'towards', 'under', 'over']);
const PLURAL_HINTS = /s$/;
const VERB_ING = /ing$/;
const VERB_ED = /(ed|ght)$/;

// Split raw text into whitespace-trimmed "words" for lookup (lowercased,
// stripped of most punctuation).
function lastWords(text, n) {
  const trimmed = text.replace(/\s+$/, '');
  const m = trimmed.match(/\S+/g);
  if (!m) return [];
  return m.slice(-n).map(w => w.toLowerCase().replace(/[^a-z']/g, ''));
}

// Returns a candidate-token array given the raw text context.
function localCandidates(text) {
  const w2 = lastWords(text, 2);
  const w1 = lastWords(text, 1);
  const trimmed = text.trim();
  const last = w1[0] || '';
  const prevChar = trimmed.slice(-1);

  // 1. Two-word exact match (strongest)
  if (w2.length === 2) {
    const key2 = w2.join(' ');
    if (BIAS_TABLE[key2]) return augment(BIAS_TABLE[key2]);
  }
  // 2. One-word exact match
  if (w1.length === 1 && BIAS_TABLE[last]) return augment(BIAS_TABLE[last]);

  // 3. Categorical fallback by tail shape
  let pool;
  if (prevChar === '.' || prevChar === '!' || prevChar === '?') {
    pool = POOLS.sentence_start;
  } else if (prevChar === ',') {
    pool = POOLS.after_comma;
  } else if (prevChar === '"' || prevChar === "'" || prevChar === '"' || prevChar === '"' || prevChar === "'") {
    pool = POOLS.after_quote;
  } else if (ARTICLES.has(last)) {
    pool = POOLS.after_article;
  } else if (PREPS.has(last)) {
    pool = POOLS.after_prep;
  } else if (VERB_ING.test(last)) {
    pool = POOLS.after_verb_ing;
  } else if (VERB_ED.test(last)) {
    pool = POOLS.after_verb_past;
  } else if (PLURAL_HINTS.test(last) && last.length > 3) {
    // "systems", "models" — plural nouns usually take plural verbs
    pool = POOLS.after_plural;
  } else if (last.length > 2) {
    // Fallback for unmatched word: likely singular noun
    pool = POOLS.after_singular_noun;
  } else {
    pool = POOLS.generic;
  }
  return augment(pool);
}

// Pad / diversify a candidate list up to ~12 entries by mixing in a few
// generics and a punctuation mark. Returns plain strings (no probs yet).
function augment(primary) {
  const out = [...primary];
  const used = new Set(out);
  for (const g of POOLS.generic) {
    if (out.length >= 11) break;
    if (!used.has(g)) { out.push(g); used.add(g); }
  }
  if (out.length < 12 && !used.has('.')) out.push('.');
  return out.slice(0, 12);
}

// Turn a ranked candidate list (strongest first) into a distribution by
// assigning logits that decay with rank, then softmax-ing at temperature T.
// A seeded nudge keeps identical contexts deterministic while still feeling
// organic (no perfectly clean geometric decay).
function distribute(candidates, temperature, seedHash) {
  const rand = seededRand(seedHash);
  const T = Math.max(0.05, temperature);
  const n = candidates.length;
  const logits = candidates.map((_, i) => {
    const base = 3 - (4 * i) / Math.max(1, n - 1);
    const jitter = (rand() - 0.5) * 0.9;
    return base + jitter;
  });
  const scaled = logits.map(l => l / T);
  const mx = Math.max(...scaled);
  const exps = scaled.map(l => Math.exp(l - mx));
  const sum = exps.reduce((a, b) => a + b, 0);
  const probs = exps.map(e => e / sum);
  const items = candidates.map((tok, i) => ({ tok, logit: logits[i], prob: probs[i] }));
  items.sort((a, b) => b.prob - a.prob);
  return items;
}

// FNV-1a hash for deterministic seeding
function hashString(s) {
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
  return Math.abs(h);
}

// ── Public: local (deterministic, offline) ─────────────────────────────────
function computeLocalDistribution(context, temperature) {
  const text = context.map(t => t.text).join('');
  const tail = text.slice(-80); // enough for context matching
  const cands = localCandidates(tail);
  const seed = hashString(tail);
  const items = distribute(cands, temperature, seed);
  return { items, source: 'local' };
}

// ── Debug helper (exposed for A4 tests later) ──────────────────────────────
// Returns the candidate array AND which lookup path matched, so tests can
// assert the bias table vs. category fallback behavior without simulating
// the full distribution pipeline.
function debugLookup(text) {
  const w2 = lastWords(text, 2);
  const w1 = lastWords(text, 1);
  const last = w1[0] || '';
  const prevChar = text.trim().slice(-1);

  if (w2.length === 2) {
    const key2 = w2.join(' ');
    if (BIAS_TABLE[key2]) return { match: 'bias-2', key: key2, tokens: BIAS_TABLE[key2] };
  }
  if (w1.length === 1 && BIAS_TABLE[last]) {
    return { match: 'bias-1', key: last, tokens: BIAS_TABLE[last] };
  }
  // Category fallback
  let category;
  if (prevChar === '.' || prevChar === '!' || prevChar === '?') category = 'sentence_start';
  else if (prevChar === ',') category = 'after_comma';
  else if (prevChar === '"' || prevChar === "'") category = 'after_quote';
  else if (ARTICLES.has(last)) category = 'after_article';
  else if (PREPS.has(last)) category = 'after_prep';
  else if (VERB_ING.test(last)) category = 'after_verb_ing';
  else if (VERB_ED.test(last)) category = 'after_verb_past';
  else if (PLURAL_HINTS.test(last) && last.length > 3) category = 'after_plural';
  else if (last.length > 2) category = 'after_singular_noun';
  else category = 'generic';
  return { match: 'category', key: category, tokens: POOLS[category] };
}

// ── Public: proxy (LIVE, via our owner-hosted Anthropic proxy) ─────────────
// Architecture:
//   - In dev (origin: localhost:* / 127.*), the proxy is expected at
//     http://127.0.0.1:8787/api/next-tokens (run with `node proxy/server.mjs`).
//   - In prod, the proxy is same-origin at /api/next-tokens (Cloudflare Worker).
//   - Configurable override: set window.LLM_MATRIX_PROXY_URL before app.jsx
//     loads to point at a different endpoint.
//
// State machine (H5 from PLAN.md):
//   status 200          → LIVE, no cooldown
//   400                 → ERROR, 5-min tier cooldown (bad input, won't recover)
//   429 (our limit)     → ERROR, 60s tier cooldown
//   503 (budget)        → BUDGET_EXHAUSTED, 10-min tier cooldown
//   529 (upstream)      → ERROR, 60s tier cooldown (Anthropic will recover)
//   timeout / network   → ERROR, 30s tier cooldown
// After cooldown expires, the engine tries the proxy again on the next call.

const PROXY_TIMEOUT_MS = 2000;
const proxyCache = new Map(); // key: `${n}|${temp.toFixed(2)}|${prompt}`, value: { items, source }

// Per-session state — tier cooldown + current badge.
const proxyState = {
  /** @type {'available'|'cooldown'} */
  tier: 'available',
  /** @type {'LIVE'|'LOCAL'|'BUDGET_EXHAUSTED'|'ERROR'} */
  badge: 'LOCAL',
  cooldownUntil: 0,          // ms epoch; 0 = no cooldown
  lastError: '',             // human-readable reason for last failure
  dayRemainingUsd: null,     // from X-Budget-Day-Remaining header
  monthRemainingUsd: null,   // from X-Budget-Month-Remaining header
};

function resolveProxyUrl() {
  if (typeof window !== 'undefined' && typeof window.LLM_MATRIX_PROXY_URL === 'string') {
    return window.LLM_MATRIX_PROXY_URL;
  }
  if (typeof window === 'undefined' || !window.location) {
    return '/api/next-tokens';
  }
  const host = window.location.hostname;
  if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host === '::1') {
    return 'http://127.0.0.1:8787/api/next-tokens';
  }
  return '/api/next-tokens';
}

function setCooldown(seconds, badge, reason) {
  proxyState.tier = 'cooldown';
  proxyState.cooldownUntil = Date.now() + seconds * 1000;
  proxyState.badge = badge;
  proxyState.lastError = reason;
}

function clearCooldownIfElapsed() {
  if (proxyState.tier === 'cooldown' && Date.now() >= proxyState.cooldownUntil) {
    proxyState.tier = 'available';
    proxyState.cooldownUntil = 0;
  }
}

/**
 * Fetch a LIVE distribution from the proxy, with fallback-ready semantics.
 * Returns { items, source: 'proxy' | 'proxy-cached' } on success,
 * or null on any failure (after updating proxyState accordingly).
 */
async function computeProxyDistribution(context, temperature, n = 10) {
  clearCooldownIfElapsed();
  if (proxyState.tier === 'cooldown') return null;

  const text = context.map(t => t.text).join('');
  const tempRounded = Math.round(temperature * 100) / 100;
  const cacheK = `${n}|${tempRounded.toFixed(2)}|${text}`;
  const cached = proxyCache.get(cacheK);
  if (cached) {
    proxyState.badge = 'LIVE';
    return { items: cached.items, source: 'proxy-cached' };
  }

  const ac = new AbortController();
  const timeoutId = setTimeout(() => ac.abort('timeout'), PROXY_TIMEOUT_MS);

  try {
    const resp = await fetch(resolveProxyUrl(), {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ prompt: text, temperature: tempRounded, n }),
      signal: ac.signal,
    });
    clearTimeout(timeoutId);

    // Always read the budget headers if present
    const dayRem = resp.headers.get('x-budget-day-remaining');
    const monRem = resp.headers.get('x-budget-month-remaining');
    if (dayRem !== null) proxyState.dayRemainingUsd = parseFloat(dayRem);
    if (monRem !== null) proxyState.monthRemainingUsd = parseFloat(monRem);

    if (resp.status === 200) {
      const data = await resp.json();
      const items = (data.candidates || []).map(c => ({ tok: c.tok, prob: c.prob }));
      if (items.length === 0) {
        setCooldown(60, 'ERROR', 'proxy returned empty candidates');
        return null;
      }
      items.sort((a, b) => b.prob - a.prob);
      proxyCache.set(cacheK, { items });
      proxyState.tier = 'available';
      proxyState.badge = 'LIVE';
      proxyState.lastError = '';
      return { items, source: 'proxy' };
    }

    // Non-200: cool down the tier per status
    if (resp.status === 400) {
      setCooldown(300, 'ERROR', 'proxy rejected input (400)');
    } else if (resp.status === 429) {
      setCooldown(60, 'ERROR', 'proxy rate limit hit');
    } else if (resp.status === 503) {
      setCooldown(600, 'BUDGET_EXHAUSTED', 'daily or monthly budget exhausted');
    } else if (resp.status === 529) {
      setCooldown(60, 'ERROR', 'upstream overloaded');
    } else {
      setCooldown(30, 'ERROR', `proxy HTTP ${resp.status}`);
    }
    return null;
  } catch (err) {
    clearTimeout(timeoutId);
    const reason = (err && err.name === 'AbortError') ? 'proxy timeout' : `proxy network error: ${err?.message || err}`;
    setCooldown(30, 'ERROR', reason);
    return null;
  }
}

/** Snapshot proxy state for UI display. */
function getProxyState() {
  clearCooldownIfElapsed();
  return {
    tier: proxyState.tier,
    badge: proxyState.badge,
    cooldownUntil: proxyState.cooldownUntil,
    lastError: proxyState.lastError,
    dayRemainingUsd: proxyState.dayRemainingUsd,
    monthRemainingUsd: proxyState.monthRemainingUsd,
  };
}

window.LogitsEngine = {
  computeLocalDistribution,
  computeProxyDistribution,
  getProxyState,
  debugLookup,
};
