'use strict';

/**
 * DIRECTIONAL CLV — PER-READ signal (Session 64).
 *
 * "Did the market move TOWARD the side we graded, between our lock and the
 * close?" Toward = positive (a badge: we were early). Away = negative
 * (caution). This is a per-read signal ONLY — there is deliberately no
 * aggregate "our CLV is +X%" anywhere, and that claim stays held until the
 * model is backtest-proven.
 *
 * WHY IT IS COMPUTABLE NOW: grade-lock was verified live (0 true overwrites
 * across 698 identity+line groups), so the lock is a fixed reference rather
 * than something later cycles rewrite.
 *
 * BOTH ENDS ARE DE-VIGGED WITH THE SAME METHOD. Comparing a raw price to a
 * de-vigged one would manufacture movement out of vig, so both ends go through
 * `devig.devigTwoWay` (multiplicative). The lock end comes from
 * `model_snapshots`, which retains BOTH side prices and an already-de-vigged
 * `fair_prob` from that same function — `ledger_entries.locked_odds` is
 * single-side and CANNOT be de-vigged, so it is not the source.
 *
 * SIGN IS SIDE-BOUND. A move that helps an over HURTS an under on the same
 * prop, so the sign is computed from the fair probability OF OUR GRADED SIDE —
 * never from the raw direction of the line. This is the badge-inverting trap
 * and it is guarded by a test that asserts the two sides are exact mirrors.
 *
 * FLAT IS A PROBABILITY-SPACE THRESHOLD, ALWAYS. Price space lies: a 40-cent
 * move on a deep favourite is a tiny probability move, while 10 cents near
 * even money is large.
 *
 * MISSING/AMBIGUOUS CLOSE IS `unknown`, NEVER 0. Zero means "the market did not
 * move", which is a claim; absence of a close is not.
 */

const { devigTwoWay } = require('../utils/devig');

// Fair-probability points below which we call it flat. 1.5pp — smaller than
// this is inside the noise of two books' vig assumptions.
const FLAT_THRESHOLD = Number(process.env.DCLV_FLAT_THRESHOLD || 0.015);

const sideOf = (s) => (String(s || 'over').toLowerCase() === 'under' ? 'under' : 'over');

function num(v) {
  if (v == null || v === '') return null;
  const n = Number(v);
  return Number.isFinite(n) ? n : null;
}

/** Fair probability of ONE side, de-vigged from both raw prices. */
function fairProbOfSide(overOdds, underOdds, side) {
  const o = num(overOdds);
  const u = num(underOdds);
  if (o == null || u == null) return null;      // one-sided → not de-viggable
  const dv = devigTwoWay(o, u);
  if (!dv) return null;
  return sideOf(side) === 'under' ? dv.under.fair_prob : dv.over.fair_prob;
}

const UNKNOWN = (reason) => ({
  state: 'unknown', clv: null, fair_lock: null, fair_close: null, reason,
});

/**
 * Compute the per-read directional CLV.
 *
 * @param {Object} a
 *  - side                    our graded side ('over' | 'under')
 *  - lockOverOdds/lockUnderOdds   both raw prices at grade time, OR
 *  - lockFairProb            the already-de-vigged fair prob of our side
 *  - closeOverOdds/closeUnderOdds both raw prices at the close
 *  - missedReason            capture refusal (missed_window, doubleheader_…)
 *  - flatThreshold           probability-space flat band
 */
function computeDirectionalClv(a = {}) {
  const side = sideOf(a.side);
  const threshold = Number.isFinite(a.flatThreshold) ? a.flatThreshold : FLAT_THRESHOLD;

  // A capture that refused is not a close. Consistency with the capture layer:
  // what was unknowable then stays unknowable now.
  if (a.missedReason) return UNKNOWN(a.missedReason);

  const fairLock = num(a.lockFairProb) != null
    ? num(a.lockFairProb)
    : fairProbOfSide(a.lockOverOdds, a.lockUnderOdds, side);
  if (fairLock == null) return UNKNOWN('lock_not_devigable');

  const fairClose = fairProbOfSide(a.closeOverOdds, a.closeUnderOdds, side);
  if (fairClose == null) return UNKNOWN('no_usable_close');

  // SIDE-BOUND by construction: both probabilities are already "our side".
  const clv = Math.round((fairClose - fairLock) * 1e6) / 1e6;
  let state;
  if (Math.abs(clv) < threshold) state = 'flat';
  else state = clv > 0 ? 'positive' : 'negative';

  return { state, clv, fair_lock: fairLock, fair_close: fairClose, reason: null };
}

/** Copy for the badge. Never renders anything for `unknown`. */
function describe(result) {
  if (!result) return null;
  switch (result.state) {
    case 'positive': return { label: 'MOVED TOWARD US', tone: 'confirm' };
    case 'negative': return { label: 'MOVED AWAY', tone: 'caution' };
    case 'flat': return { label: 'LINE HELD', tone: 'neutral' };
    default: return null;                    // unknown → render NOTHING
  }
}

module.exports = { computeDirectionalClv, fairProbOfSide, describe, FLAT_THRESHOLD };
