'use strict';

/**
 * Grade-slate writer (Session 32).
 *
 * Closes the content pipeline. The grading engine (engine1 via
 * analyzeViaEngine1) grades props on demand, but nothing ever persisted a
 * sport's graded slate. `contentTemplateService.collectSlateData` reads a
 * `grades:{sport}` cache "when present" — without a writer it was always
 * empty, so slate/POTD content degraded to lines/schedule and never reached
 * `dataLevel: 'full'`.
 *
 * This service grades a sport's freshly-fetched props and writes the
 * `grades:{sport}` cache in the exact shape contentTemplateService expects.
 * It is wired fire-and-forget into `oddsService.recordDownstream` so it runs
 * on a cache MISS (≈hourly) WITHOUT holding the odds HTTP response — content
 * endpoints read the grades cache independently and asynchronously.
 *
 * The legacy grade shape (player_name?/player, stat_type, line, direction,
 * grade, confidence, edge_pct, reasoning.summary) is already what
 * `contentTemplateService.normalizeGrade` reads — so no field remapping is
 * needed at the write boundary.
 */

// Cost bounds: the odds slate carries one row per player+stat+line+book.
// We dedupe to unique player+stat+line and cap how many we grade, because
// each grade fans out to feature computation. Grading runs at most once per
// cache-miss per sport, but we still bound the herd.
const DEFAULT_LIMIT = 25;
const DEFAULT_CONCURRENCY = 5;
const DEFAULT_TTL = 7200; // 2 hours — matches the spec's grades-cache TTL.

// Collapse the multi-book prop rows to one entry per gradeable prop.
function dedupeProps(props, limit) {
  const seen = new Set();
  const out = [];
  for (const p of props || []) {
    if (!p || !p.player || !p.stat_type || p.line == null) continue;
    const key = `${p.player}::${p.stat_type}::${p.line}`;
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(p);
    if (out.length >= limit) break;
  }
  return out;
}

// engine1 is direction-aware, so a prop grades differently over vs under.
// Grade both sides and keep the higher-confidence verdict — that's the
// side the engine actually favors.
async function gradeBestSide(grade, prop, sport, opts = {}) {
  const base = {
    player: prop.player,
    stat_type: prop.stat_type,
    line: prop.line,
    sport,
    book: prop.book,
    // Session 62 (A1-S1) — real book odds ride into the engine so the
    // quarter-Kelly sizing can compute from actual prices (or not at all).
    over_odds: prop.over_odds ?? null,
    under_odds: prop.under_odds ?? null,
    // Session 64 (Order 1.6) — carry the BOUND game through to grading so
    // opponent/home-away features resolve against the RIGHT game. Without this
    // the grader fell back to ESPN's dateless "today", which at the late slots
    // is the previous day's card — binding yesterday's opponent into
    // opp_rank_stat and the home/away factor.
    game_date: prop.game_date ?? null,
    game_time: prop.game_time ?? null,
    home_team: prop.home_team ?? null,
    away_team: prop.away_team ?? null,
  };
  const sides = await Promise.all([
    Promise.resolve()
      .then(() => grade({ ...base, direction: 'over' }))
      .catch(() => null),
    Promise.resolve()
      .then(() => grade({ ...base, direction: 'under' }))
      .catch(() => null),
  ]);
  // Session 64 — RETENTION HOOK. Fires with BOTH sides, graded AND refused,
  // before any filtering. Refusals never reach the slate or the ledger, so this
  // is the only point where "the gate refused this prop" is observable — and a
  // gate that refuses winners is invisible without it. Best-effort: a retention
  // collector must never affect grading.
  if (typeof opts.onGraded === 'function') {
    try { opts.onGraded(base, sides); } catch { /* never breaks the slate */ }
  }

  // Session 58 (work-order 1.5) — a refused read (insufficient_data /
  // no grade) never enters the graded slate: no hollow rows in the grades
  // cache, the snapshot, or the ledger.
  const cands = sides.filter((s) => s && s.grade && !s.insufficient_data);
  if (cands.length === 0) return null;
  const winner = cands.reduce((a, b) => ((Number(b.confidence) || 0) > (Number(a.confidence) || 0) ? b : a));
  // Strip the internal retention fields so they never reach a cache or payload.
  delete winner._features;
  delete winner._grade_11;
  return winner;
}

// Run an async mapper over items with a bounded concurrency.
async function mapLimit(items, concurrency, fn) {
  const results = new Array(items.length);
  let cursor = 0;
  async function worker() {
    while (cursor < items.length) {
      const i = cursor++;
      results[i] = await fn(items[i], i);
    }
  }
  const pool = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
  await Promise.all(pool);
  return results;
}

/**
 * Grade a sport's props and write the `grades:{sport}` cache.
 *
 * @param {string} sport
 * @param {Array}  props        normalized odds props (oddsNormalizer shape)
 * @param {Object} [opts]
 * @param {Function} [opts.grade]    grader (default analyzeViaEngine1)
 * @param {Function} [opts.cacheSet] cache writer (default redis.cacheSet)
 * @param {string}   [opts.source]   provider tag ('propline' | 'odds-api')
 * @param {number}   [opts.limit]    max unique props graded
 * @param {number}   [opts.ttl]      cache TTL seconds
 * @param {Function} [opts.now]      timestamp source (testable)
 * @returns {Promise<{written:boolean,count:number,error?:string}>}
 */
async function gradeAndCacheSlate(sport, props, opts = {}) {
  const grade = opts.grade || require('./intelligence/analyzeViaEngine1').analyzeViaEngine1;
  const cacheSet = opts.cacheSet || require('../utils/redis').cacheSet;
  const source = opts.source || 'odds-api';
  const limit = opts.limit || DEFAULT_LIMIT;
  const ttl = opts.ttl || DEFAULT_TTL;
  const concurrency = opts.concurrency || DEFAULT_CONCURRENCY;
  const now = opts.now || (() => new Date().toISOString());

  if (!Array.isArray(props) || props.length === 0) {
    return { written: false, count: 0 };
  }

  try {
    const unique = dedupeProps(props, limit);
    if (unique.length === 0) return { written: false, count: 0 };

    const graded = (await mapLimit(unique, concurrency, (p) => gradeBestSide(grade, p, sport, opts)))
      .filter(Boolean)
      .sort((a, b) => (Number(b.confidence) || 0) - (Number(a.confidence) || 0));

    if (graded.length === 0) return { written: false, count: 0 };

    const envelope = { grades: graded, updated_at: now(), source };
    await cacheSet(`grades:${sport}`, envelope, ttl);
    return { written: true, count: graded.length };
  } catch (e) {
    // Best-effort — slate grading must never break odds delivery.
    console.warn('[gradeSlateService] grade slate failed:', e.message);
    return { written: false, count: 0, error: e.message };
  }
}

module.exports = {
  gradeAndCacheSlate,
  __internals: { dedupeProps, gradeBestSide, mapLimit, DEFAULT_LIMIT, DEFAULT_TTL, DEFAULT_CONCURRENCY },
};
