/**
 * Engine 1 (new) → legacy grader shape adapter.
 *
 * Engine 1 emits an 11-step grade (F..A+) + 0-1 confidence + a labelled
 * factors array. The frontend (`DemoScan.tsx`, `GradeCard.tsx`) and the
 * `/api/analyze`, `/api/scan`, `/api/bets` routes were built against the
 * legacy `grader.js` shape:
 *
 *   {
 *     player, stat_type, line, direction, book,
 *     grade,        // 'A' | 'B' | 'C' | 'D' (4-letter)
 *     confidence,   // 0-100 integer
 *     edge_pct,     // signed percentage
 *     kill_conditions_triggered: [{ code, ... }],
 *     reasoning: { summary: string, steps: {...} }
 *   }
 *
 * Future route rewires that swap `analyzeProp` for the orchestrator/
 * engine1 path will pipe the result through `toLegacyShape()` so the
 * frontend sees no change.
 *
 * NOTE — Session 7e ESCAPE HATCH: the adapter is built but not yet
 * applied to a live route. Engine 1's input is a pre-computed feature
 * vector; the legacy analyzer takes a raw prop and fetches its own
 * data. Wiring those two together requires an orchestrator-lite
 * preprocessor that doesn't exist yet. This file exists so the next
 * session can drop it in once the preprocessor is in place. See
 * docs/SYSTEM-MANIFEST.md §8 ARCH-1.
 */

const FOUR_LETTER_MAP = Object.freeze({
  'A+': 'A', 'A': 'A', 'A-': 'A',
  'B+': 'B', 'B': 'B', 'B-': 'B',
  'C+': 'C', 'C': 'C', 'C-': 'C',
  'D':  'D',
  'F':  'F',  // DemoScan's ACCURACY map yields '—' for F, which is fine.
});

// Factors that, when present, suggest a kill condition in the legacy
// sense. Each maps to a stable code + human reason so the UI keeps
// rendering the same chip set it always has. Unknown factors fall
// through to a generic "signal" entry rather than disappearing.
const FACTOR_TO_KILL_CONDITION = Object.freeze({
  trap_composite_high:   { code: 'TRAP',        reason: 'Multiple trap signals firing.' },
  l5_cold_vs_line:       { code: 'COLD_L5',     reason: 'Last-5 average significantly below the line.' },
  l5_hot_vs_under:       { code: 'COLD_L5',     reason: 'Hot streak conflicts with UNDER side.' },
  consistency_boom_bust: { code: 'BOOM_BUST',   reason: 'Player is boom-or-bust on this stat.' },
  top_opponent_defense:  { code: 'TOP_DEFENSE', reason: 'Opponent ranks top-five defending this stat.' },
  back_to_back:          { code: 'B2B',         reason: 'Back-to-back game.' },
  heavy_workload_7d:     { code: 'FATIGUE',     reason: '4+ games in the last 7 days.' },
  away_vs_top5_defense:  { code: 'TOP_DEFENSE', reason: 'Away game vs a top-five defense.' },
  rookie_in_playoffs:    { code: 'NEW_CONTEXT', reason: 'No prior playoff experience.' },
});

function fourLetterGrade(elevenStep) {
  if (typeof elevenStep !== 'string') return null;
  return FOUR_LETTER_MAP[elevenStep.trim()] ?? null;
}

function legacyConfidence(unitProb) {
  // Guard nullish first — Number(null) === 0 is finite, which would
  // silently produce a 0% confidence instead of "unknown".
  if (unitProb == null) return null;
  const n = Number(unitProb);
  if (!Number.isFinite(n)) return null;
  return Math.max(0, Math.min(100, Math.round(n * 100)));
}

// Build kill_conditions_triggered from engine1 factors. Only factors
// that signal something the user should worry about become chips —
// positive factors (e.g. l5_hot_vs_line) don't.
function killConditionsFromFactors(factors) {
  if (!Array.isArray(factors)) return [];
  const out = [];
  const seen = new Set();
  for (const f of factors) {
    const entry = FACTOR_TO_KILL_CONDITION[f];
    if (entry && !seen.has(entry.code)) {
      out.push({ ...entry });
      seen.add(entry.code);
    }
  }
  return out;
}

// Engine 1 factors are abstract labels. To produce a reasoning.summary
// the legacy UI renders, we string the top-three factors together with a
// human-readable verb. Callers that have richer context (e.g. the
// orchestrator with featurePayload) should pass their own sentence in
// `summaryOverride`.
function buildReasoningSummary(engine1Result, prop, summaryOverride) {
  if (summaryOverride && typeof summaryOverride === 'string') return summaryOverride;
  const top = Array.isArray(engine1Result?.top_factors) ? engine1Result.top_factors.slice(0, 3) : [];
  if (top.length === 0) {
    return `Grade ${engine1Result?.grade ?? '—'} from Engine 1 (no surfaced factors).`;
  }
  const verb = engine1Result?.grade?.startsWith('A') ? 'favoring the play'
    : engine1Result?.grade?.startsWith('B') ? 'leaning the play'
    : engine1Result?.grade?.startsWith('C') ? 'split'
    : 'against the play';
  return `Engine 1 graded ${engine1Result?.grade ?? '—'} ${verb} — top factors: ${top.join(', ')}.`;
}

// edge_pct lives in the legacy response. Engine 1 doesn't compute it.
// If the caller passes a computed value (e.g. l5_avg − line), use it;
// otherwise return 0 so the field exists and the UI doesn't blow up
// on missing data.
function legacyEdgePct(edgePctOverride) {
  const n = Number(edgePctOverride);
  return Number.isFinite(n) ? n : 0;
}

/**
 * toLegacyShape — transform an engine1 grading result into the shape
 * `/api/analyze` and downstream callers historically returned.
 *
 * @param {Object} engine1Result      — { grade, confidence, top_factors, all_factors }
 * @param {Object} prop               — { player, stat_type, line, direction, book, sport }
 * @param {Object} [opts]
 * @param {string} [opts.summaryOverride]  — supply a fuller human sentence if available
 * @param {number} [opts.edgePct]          — pre-computed edge percentage
 * @returns {Object} legacy-shaped result, including the `_cache: 'MISS'` field
 *                   the PERF-1 wrapper expects callers to receive.
 */
function toLegacyShape(engine1Result, prop = {}, opts = {}) {
  if (!engine1Result || typeof engine1Result !== 'object') return null;
  const grade = fourLetterGrade(engine1Result.grade);
  const confidence = legacyConfidence(engine1Result.confidence);
  const kill = killConditionsFromFactors(engine1Result.all_factors || engine1Result.top_factors);
  return {
    player: prop.player ?? null,
    stat_type: prop.stat_type ?? null,
    line: prop.line ?? null,
    direction: prop.direction ?? null,
    book: prop.book ?? null,
    grade,
    confidence,
    // Session 63 — TRUTH LABEL. `confidence` is NOT a probability: engine1
    // derives it by looking up the midpoint of the band belonging to the letter
    // it already chose, so it carries ZERO information beyond the letter and can
    // never disagree with it. (That is also why mlb-grade-degradation.md's
    // "25/25 grade<->confidence agreement" was a tautology, not a validation.)
    // The real, independent probability is `p_win` — the quantile estimate over
    // actual game logs — which is attached by analyzeViaEngine1 and is what any
    // surface showing a percentage should render.
    confidence_basis: 'grade_band',
    edge_pct: legacyEdgePct(opts.edgePct),
    kill_conditions_triggered: kill,
    reasoning: {
      summary: buildReasoningSummary(engine1Result, prop, opts.summaryOverride),
      // The legacy `steps` block carried season_avg / recent_form /
      // situational / line_comparison / kill_conditions / final_grade.
      // Engine 1 doesn't surface those individually, so the adapter
      // emits an `engine1_factors` block as the modern equivalent.
      // The frontend reads `.summary` only; this is documentation.
      steps: {
        engine1_factors: engine1Result.all_factors || engine1Result.top_factors || [],
        final_grade: grade,
      },
    },
  };
}

module.exports = {
  toLegacyShape,
  __internals: {
    FOUR_LETTER_MAP,
    FACTOR_TO_KILL_CONDITION,
    fourLetterGrade,
    legacyConfidence,
    killConditionsFromFactors,
    buildReasoningSummary,
    legacyEdgePct,
  },
};
