/* ============================================================
   VYNDR — THE PRICE-LAYER LAW (Session 66).

   Plain CommonJS so .tsx components import it AND the Jest suite requires it
   directly (same pattern as colorContract.js / vyndrTokens.js / playerName.js).

   ONE function decides what a price row is allowed to SAY. The triplet renders
   whatever this returns and never re-derives a verdict of its own, so there is
   exactly one place where "is this value?" is answered.

   THE FIVE HONESTY STATES (design: `Vyndr Price Triplet.dc.html`, ACT 01):

     VALUE       green  — ev >= VALUE_EV_THRESHOLD *AND* the book price is
                          inside the takeable band. Both. Raw positive EV is
                          NOT value: +11.7% EV at -210 is a juiced price we
                          will not call a play.
     PRICED_OUT  blue   — real edge, untakeable price. We won't call a juiced
                          price a value, and we won't pretend the edge wasn't
                          there. This state exists so green never has to lie
                          in either direction.
     NO_EDGE     grey   — the honest number is already priced. Stated at full
                          voice, never hidden. That "no" is the instrument
                          working.
     QUARANTINE  amber  — the MODEL leg is withheld (graded against inputs we
                          no longer trust). Book and fair still render: we
                          never hide the honest fair number because a
                          different leg is poisoned. RARE — as of Jul 2026 the
                          only quarantined rows are one frozen historical
                          cohort, so this is not a routine state.
     REFUSAL     dim    — no fair price we'd defend. Nothing renders. No
                          fabricated price, no empty gauge.

   TRUTH LAW: the triplet never prints a number it can't stand behind. A leg
   with no value renders absent, never a zero and never a placeholder.
   ============================================================ */

/* ---- Backend mirror -----------------------------------------------------
   These MUST match src/config/valueEngine.js. The browser can't require the
   backend module, so the constants are duplicated here and a test
   (priceTriplet.test.js) reads BOTH files and fails if they drift — the same
   guard used for playerName.js. If you tune the band, tune it in both. */
const TAKEABLE_ODDS_CEILING = -160; // most-juiced favourite we'll promote
const TAKEABLE_ODDS_MAX = 200; // longest dog we'll promote
const VALUE_EV_THRESHOLD = 2; // a real edge, not rounding

const STATES = Object.freeze({
  VALUE: 'VALUE',
  PRICED_OUT: 'PRICED_OUT',
  NO_EDGE: 'NO_EDGE',
  QUARANTINE: 'QUARANTINE',
  REFUSAL: 'REFUSAL',
  // Not a verdict — a display state. The model leg exists and is honest, the
  // viewer just isn't entitled to it (free tier). Book + fair render normally.
  NO_VERDICT_LOCKED: 'NO_VERDICT_LOCKED',
});

/** Strict numeric parse — `Number(null) === 0` is the fabrication bug this
 *  whole layer exists to prevent, so nothing coerces. */
function num(v) {
  if (v == null || v === '') return null;
  const n = typeof v === 'number' ? v : Number(v);
  return Number.isFinite(n) ? n : null;
}

/** isTakeable(american) — inside the promotable band. Mirrors valueEngine. */
function isTakeable(american) {
  const a = num(american);
  if (a == null) return false;
  return a >= TAKEABLE_ODDS_CEILING && a <= TAKEABLE_ODDS_MAX;
}

/** isValue(american, evPct) — takeable AND clears the EV threshold. Both. */
function isValue(american, evPct) {
  const ev = num(evPct);
  return isTakeable(american) && ev != null && ev >= VALUE_EV_THRESHOLD;
}

/**
 * deriveValueState(row) — the single verdict function.
 *
 * row: { book_odds, fair_odds, model_odds, ev_pct, quarantine_reason,
 *        refused, model_price_locked }
 *
 * Order matters and encodes the honesty ordering:
 *  1. REFUSAL first — with no fair price there is nothing honest to show.
 *  2. QUARANTINE next — the model leg is untrustworthy, so no verdict may be
 *     computed from it, but book+fair still stand.
 *  3. Then the three real verdicts.
 * A locked (free-tier) model leg is NOT quarantine and NOT refusal: the data
 * exists and is honest, the viewer just isn't entitled to it — so the verdict
 * is withheld rather than asserted, and book+fair render normally.
 */
function deriveValueState(row = {}) {
  const fair = num(row.fair_odds);
  const book = num(row.book_odds);

  // 1. Refusal — no fair price we'd defend (or no market to price against).
  if (row.refused === true || fair == null || book == null) return STATES.REFUSAL;

  // 2. Quarantine — model leg withheld; book + fair still render.
  if (row.quarantine_reason) return STATES.QUARANTINE;

  // Free-tier lock: entitled data withheld, not absent. No verdict is claimed
  // (the verdict IS the model leg), but this is not quarantine — the row is
  // honest, it's just gated.
  if (row.model_price_locked === true) return STATES.NO_VERDICT_LOCKED;

  const model = num(row.model_odds);
  const ev = num(row.ev_pct);
  // Without a model price or an EV there is no verdict to render — treat as a
  // withheld model leg rather than asserting "no edge" we didn't compute.
  if (model == null || ev == null) return STATES.QUARANTINE;

  // 3. The three real verdicts.
  if (isValue(book, ev)) return STATES.VALUE;
  if (ev >= VALUE_EV_THRESHOLD) return STATES.PRICED_OUT; // edge, untakeable price
  return STATES.NO_EDGE;
}

/** valueStateColor(state) — the ONE mapping from verdict to token.
 *  Green appears here exactly once, on VALUE, and nowhere else. */
function valueStateColor(state) {
  switch (state) {
    case STATES.VALUE:
      return 'var(--g-a)';
    case STATES.PRICED_OUT:
      return 'var(--priced-out)';
    case STATES.QUARANTINE:
      return 'var(--amber)';
    case STATES.NO_EDGE:
    case STATES.REFUSAL:
    case STATES.NO_VERDICT_LOCKED:
    default:
      return 'var(--text-2)';
  }
}

function impliedProb(american) {
  const a = num(american);
  if (a == null) return null;
  return a > 0 ? 100 / (a + 100) : -a / (-a + 100);
}

/**
 * modelVsFair(modelOdds, fairOdds) — the "+2.9% VS FAIR" figure on the verdict
 * line. It is the gap between OUR price and the honest de-vigged price, in
 * implied-probability PERCENTAGE POINTS (not a ratio, and NOT book-vs-fair).
 *
 * Derived from the design file's own two worked examples, both of which this
 * reproduces exactly:
 *   STATE 1  book +125 · fair +110 · model  +98 → "+2.9% VS FAIR"
 *   STATE 3  book +118 · fair +104 · model +112 → "-1.8% VS FAIR"
 * Positive = the model prices it SHORTER than fair (we think it's likelier
 * than the honest number says). Null unless both legs are real.
 */
function modelVsFair(modelOdds, fairOdds) {
  const pm = impliedProb(modelOdds);
  const pf = impliedProb(fairOdds);
  if (pm == null || pf == null) return null;
  return Math.round((pm - pf) * 1000) / 10;
}

/** fmtOddsAmerican(v) — display an American price, or null for honest absence.
 *  Never returns '0', never returns a placeholder number. */
function fmtOddsAmerican(v) {
  const n = num(v);
  if (n == null) return null;
  const r = Math.round(n);
  return r > 0 ? `+${r}` : String(r);
}

module.exports = {
  STATES,
  TAKEABLE_ODDS_CEILING,
  TAKEABLE_ODDS_MAX,
  VALUE_EV_THRESHOLD,
  isTakeable,
  isValue,
  deriveValueState,
  valueStateColor,
  modelVsFair,
  impliedProb,
  fmtOddsAmerican,
};
