'use strict';

/**
 * Content template engine (Session 29).
 *
 * Generates STRUCTURED DATA objects (not text, not images) from live VYNDR
 * data. A separate formatter (contentFormatter) renders text; a future
 * design layer renders images.
 *
 * Templates degrade gracefully by data level:
 *   full     → graded props exist → rich content (grade, edge, projection)
 *   lines    → Tank01 game lines exist → line-focused content
 *   schedule → only ESPN schedule → matchup/time content
 *   empty    → nothing today → minimal, never a crash
 *
 * The pure generators take a `data` bundle and never touch the network.
 * `collectSlateData` is the thin orchestrator that gathers the bundle from
 * the live services; its collectors are injectable so it stays testable.
 */

const GRADE_RANK = { 'A+': 0, A: 1, 'A-': 2, 'B+': 3, B: 4, 'B-': 5, 'C+': 6, C: 7, 'C-': 8, 'D+': 9, D: 10, 'D-': 11, F: 12 };
const SITE = 'vyndr.app';

// ---- field-alias normalizers (grades come from several shapes) --------
function gradeField(g, ...keys) {
  for (const k of keys) if (g && g[k] != null && g[k] !== '') return g[k];
  return null;
}
function normalizeGrade(g) {
  return {
    player: gradeField(g, 'player_name', 'player'),
    stat: gradeField(g, 'stat_type', 'stat'),
    line: gradeField(g, 'line'),
    side: gradeField(g, 'side', 'direction'),
    grade: gradeField(g, 'grade') || 'C',
    confidence: Number(gradeField(g, 'confidence')) || 0,
    edge: Number(gradeField(g, 'edge', 'edge_pct')) || 0,
    projection: gradeField(g, 'projection'),
    matchup: gradeField(g, 'matchup'),
    killConditions: g?.killConditions || g?.kill_conditions_triggered || [],
    analysis: gradeField(g, 'analysis', 'one_line_reason') || (g?.reasoning && g.reasoning.summary) || null,
    _raw: g,
  };
}

// ---- data level -------------------------------------------------------
function determineDataLevel(data) {
  const grades = data?.grades || [];
  const gameLines = data?.gameLines || {};
  const schedule = data?.schedule || [];
  if (grades.length > 0) return 'full';
  if (gameLines && Object.keys(gameLines).length > 0) return 'lines';
  if (schedule.length > 0) return 'schedule';
  return 'empty';
}

function prettyDate(now = Date.now()) {
  return new Date(now).toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' });
}

function sportLabel(sport) {
  return String(sport || '').toUpperCase();
}

// ---- game-line highlight extraction -----------------------------------
function numOdds(v) {
  const n = Number(v);
  return Number.isFinite(n) ? n : null;
}
function americanToDecimal(odds) {
  const n = Number(odds);
  if (!Number.isFinite(n) || n === 0) return null;
  return n > 0 ? 1 + n / 100 : 1 + 100 / Math.abs(n);
}

// books: array of [bookName, { homeML, awayML, total, homeSpread, ... }]
function bestOdds(books, field) {
  let best = null;
  for (const [book, line] of books) {
    const dec = americanToDecimal(line?.[field]);
    if (dec == null) continue;
    if (!best || dec > best.decimal) best = { book, odds: line[field], decimal: dec };
  }
  return best;
}
function consensus(books, field) {
  const vals = books.map(([, l]) => Number(l?.[field])).filter((n) => Number.isFinite(n));
  if (vals.length === 0) return null;
  return Math.round((vals.reduce((a, b) => a + b, 0) / vals.length) * 10) / 10;
}
// Disagreement = spread between best & worst home-ML decimal payout.
function bookDisagreement(books) {
  const decs = books.map(([, l]) => americanToDecimal(l?.homeML)).filter((n) => n != null);
  if (decs.length < 2) return 0;
  return Math.round((Math.max(...decs) - Math.min(...decs)) * 100) / 100;
}

function findGameLinesFor(game, gameLines) {
  if (!gameLines) return null;
  const h = (game.homeTeam?.abbreviation || '').toUpperCase();
  const a = (game.awayTeam?.abbreviation || '').toUpperCase();
  for (const entry of Object.values(gameLines)) {
    const eh = String(entry.homeTeam || '').toUpperCase();
    const ea = String(entry.awayTeam || '').toUpperCase();
    if ((eh === h && ea === a) || (eh === a && ea === h)) return entry;
  }
  return null;
}

function extractGameLineHighlights(schedule, gameLines) {
  const highlights = [];
  for (const game of schedule || []) {
    const lines = findGameLinesFor(game, gameLines);
    const books = lines && lines.books ? Object.entries(lines.books) : [];
    if (books.length === 0) continue;
    highlights.push({
      type: 'game_line_highlight',
      game: `${game.awayTeam?.abbreviation || '?'} @ ${game.homeTeam?.abbreviation || '?'}`,
      time: game.gameTime || null,
      venue: game.venue || null,
      bookCount: books.length,
      bestHomeML: bestOdds(books, 'homeML'),
      bestAwayML: bestOdds(books, 'awayML'),
      total: consensus(books, 'total'),
      spread: consensus(books, 'homeSpread'),
      disagreement: bookDisagreement(books),
    });
  }
  return highlights.sort((a, b) => (b.disagreement || 0) - (a.disagreement || 0));
}

// ---- hook + CTA -------------------------------------------------------
function generateHookText(sport, data) {
  const label = sportLabel(sport);
  const date = prettyDate();
  const games = (data.schedule || []).length;
  const bookCount = maxBookCount(data.gameLines);
  if (data.dataLevel === 'full') {
    return `🔥 VYNDR SLATE — ${date}. ${games} ${label} games graded${bookCount ? ` · lines from ${bookCount} books` : ''}. Top reads below. 🧵`;
  }
  if (data.dataLevel === 'lines') {
    return `🔥 VYNDR SLATE — ${date}. ${games} ${label} games · lines from ${bookCount} sportsbooks. Here's what stands out. 🧵`;
  }
  if (data.dataLevel === 'schedule') {
    return `🔥 VYNDR SLATE — ${date}. ${games} ${label} games on the board today. 🧵`;
  }
  return `VYNDR — ${date}. No ${label} games today. Back tomorrow.`;
}
function maxBookCount(gameLines) {
  if (!gameLines) return 0;
  let max = 0;
  for (const entry of Object.values(gameLines)) {
    const n = entry && entry.books ? Object.keys(entry.books).length : 0;
    if (n > max) max = n;
  }
  return max;
}
function generateCTAText(dataLevel) {
  if (dataLevel === 'empty') return `Full slate + intelligence at ${SITE}.`;
  return `Full slate live at ${SITE}. Free tier: reads on us. Founder pricing from $14.99/mo.`;
}
function generateAnalysisLine(grade) {
  if (grade.analysis) return grade.analysis;
  const edge = grade.edge ? `${grade.edge > 0 ? '+' : ''}${grade.edge}% edge` : null;
  const proj = grade.projection != null ? `projection ${grade.projection}` : null;
  return [proj, edge].filter(Boolean).join(' · ') || 'Model edge on this line.';
}

// ---- slate thread -----------------------------------------------------
function generateSlateThread(sport, data) {
  const dataLevel = data.dataLevel || determineDataLevel(data);
  const enriched = { ...data, dataLevel };
  const thread = { type: 'slate_thread', sport, date: prettyDate(), dataLevel, posts: [] };

  thread.posts.push({ role: 'hook', text: generateHookText(sport, enriched) });

  if (dataLevel === 'full') {
    const top = (data.grades || []).map(normalizeGrade)
      .sort((a, b) => b.confidence - a.confidence)
      .slice(0, 5);
    for (const g of top) {
      thread.posts.push({
        role: 'pick',
        player: g.player, stat: g.stat, line: g.line, side: g.side,
        grade: g.grade, confidence: g.confidence, edge: g.edge, projection: g.projection,
        analysis: generateAnalysisLine(g), cardData: g._raw,
      });
    }
  } else if (dataLevel === 'lines') {
    const highlights = extractGameLineHighlights(data.schedule, data.gameLines);
    for (const h of highlights.slice(0, 4)) thread.posts.push({ role: 'game_highlight', ...h });
    if ((data.movers || []).length > 0) {
      thread.posts.push({ role: 'movers', movers: data.movers.slice(0, 3) });
    }
  } else if (dataLevel === 'schedule') {
    const games = (data.schedule || []).slice(0, 8).map((g) => ({
      away: g.awayTeam?.name || g.awayTeam?.abbreviation || null,
      home: g.homeTeam?.name || g.homeTeam?.abbreviation || null,
      time: g.gameTime || null,
      venue: g.venue || null,
    }));
    thread.posts.push({ role: 'schedule', games });
  }

  thread.posts.push({ role: 'cta', text: generateCTAText(dataLevel) });
  return thread;
}

// ---- prop / game of the day -------------------------------------------
function generatePOTD(sport, data) {
  const dataLevel = data.dataLevel || determineDataLevel(data);
  if (dataLevel === 'full' && (data.grades || []).length > 0) {
    const best = (data.grades || []).map(normalizeGrade).reduce((a, b) => (b.confidence > a.confidence ? b : a));
    return {
      type: 'potd', dataLevel: 'full',
      player: best.player, stat: best.stat, line: best.line, side: best.side,
      grade: best.grade, confidence: best.confidence, edge: best.edge,
      projection: best.projection, matchup: best.matchup,
      killConditions: best.killConditions,
      analysis: generateAnalysisLine(best), cardData: best._raw,
    };
  }
  if (dataLevel === 'lines') {
    const top = extractGameLineHighlights(data.schedule, data.gameLines)[0];
    if (top) return { type: 'potd', dataLevel: 'lines', subtype: 'game_of_the_day', ...top };
  }
  return { type: 'potd', dataLevel: 'empty', available: false };
}

// ---- results recap (pure, takes resolved grades) ----------------------
function gradeTier(grade) {
  return String(grade || '').charAt(0).toUpperCase();
}
function tierRecord(grades, tier) {
  const inTier = grades.filter((g) => gradeTier(gradeField(g, 'grade')) === tier);
  const wins = inTier.filter((g) => g.result === 'win').length;
  const losses = inTier.filter((g) => g.result === 'loss').length;
  return { wins, losses, total: inTier.length };
}
function brierScore(grades) {
  // Brier = mean( (p - outcome)^2 ); p from confidence%, outcome 1 win / 0 loss.
  const scored = grades.filter((g) => g.result === 'win' || g.result === 'loss');
  if (scored.length === 0) return null;
  const sum = scored.reduce((acc, g) => {
    const p = (Number(gradeField(g, 'confidence')) || 50) / 100;
    const outcome = g.result === 'win' ? 1 : 0;
    return acc + (p - outcome) ** 2;
  }, 0);
  return Math.round((sum / scored.length) * 1000) / 1000;
}
function avgCLV(grades) {
  const vals = grades.map((g) => Number(gradeField(g, 'clv'))).filter((n) => Number.isFinite(n));
  if (vals.length === 0) return null;
  return Math.round((vals.reduce((a, b) => a + b, 0) / vals.length) * 100) / 100;
}

function generateResultsRecap(sport, resolvedGrades) {
  if (!Array.isArray(resolvedGrades) || resolvedGrades.length === 0) {
    return { type: 'recap', available: false };
  }
  const wins = resolvedGrades.filter((g) => g.result === 'win');
  const losses = resolvedGrades.filter((g) => g.result === 'loss');
  const pushes = resolvedGrades.filter((g) => g.result === 'push');
  const decided = wins.length + losses.length;

  const byConfidence = (arr) => [...arr].sort((a, b) => (Number(gradeField(b, 'confidence')) || 0) - (Number(gradeField(a, 'confidence')) || 0));

  return {
    type: 'recap',
    available: true,
    sport,
    date: prettyDate(),
    record: { wins: wins.length, losses: losses.length, pushes: pushes.length },
    winRate: decided > 0 ? Math.round((wins.length / decided) * 1000) / 1000 : null,
    topHits: byConfidence(wins).slice(0, 3).map((g) => ({
      player: gradeField(g, 'player_name', 'player'),
      stat: gradeField(g, 'stat_type', 'stat'),
      line: gradeField(g, 'line'),
      side: gradeField(g, 'side', 'direction'),
      grade: gradeField(g, 'grade'),
      actual: gradeField(g, 'actual'),
    })),
    biggestMiss: losses.length > 0 ? (() => {
      const m = byConfidence(losses)[0];
      return {
        player: gradeField(m, 'player_name', 'player'),
        stat: gradeField(m, 'stat_type', 'stat'),
        line: gradeField(m, 'line'),
        side: gradeField(m, 'side', 'direction'),
        grade: gradeField(m, 'grade'),
        actual: gradeField(m, 'actual'),
      };
    })() : null,
    byTier: { A: tierRecord(resolvedGrades, 'A'), B: tierRecord(resolvedGrades, 'B'), C: tierRecord(resolvedGrades, 'C') },
    metrics: { brierScore: brierScore(resolvedGrades), clv: avgCLV(resolvedGrades) },
  };
}

// ---- matchup preview --------------------------------------------------
function generateMatchupNarrative(game, lines, streaks) {
  const away = game.awayTeam?.name || game.awayTeam?.abbreviation || 'Away';
  const home = game.homeTeam?.name || game.homeTeam?.abbreviation || 'Home';
  const matched = (streaks || []).length;
  if (lines && lines.total != null) {
    return `${away} at ${home} — consensus total ${lines.total}${matched ? `, ${matched} active player streak${matched === 1 ? '' : 's'} in play` : ''}.`;
  }
  return `${away} at ${home}${matched ? ` — ${matched} player${matched === 1 ? '' : 's'} riding a streak` : ''}.`;
}

function generateMatchupPreview(game, gameLines, streaks) {
  const books = gameLines && gameLines.books ? Object.entries(gameLines.books) : [];
  const lines = books.length > 0 ? {
    spread: consensus(books, 'homeSpread'),
    total: consensus(books, 'total'),
    homeFavorite: (() => {
      const hs = consensus(books, 'homeSpread');
      return hs != null ? hs < 0 : null;
    })(),
    bookCount: books.length,
  } : null;

  const h = (game.homeTeam?.abbreviation || '').toUpperCase();
  const a = (game.awayTeam?.abbreviation || '').toUpperCase();
  const playerStreaks = (streaks || []).filter((s) => {
    const t = (s.team || '').toUpperCase();
    return t && (t === h || t === a);
  });

  return {
    type: 'matchup_preview',
    away: game.awayTeam || null,
    home: game.homeTeam || null,
    time: game.gameTime || null,
    venue: game.venue || null,
    lines,
    playerStreaks,
    narrative: generateMatchupNarrative(game, lines, playerStreaks),
  };
}

// ---- orchestrator (injectable collectors for testability) -------------
function defaultCollectors() {
  const scheduleService = require('./scheduleService');
  const lineSnapshotService = require('./lineSnapshotService');
  const bookComparisonService = require('./bookComparisonService');
  const streaksService = require('./streaksService');
  const { loadRosterLogs } = require('./rosterLogs');
  const { cacheGet } = require('../utils/redis');
  const nbaAdapter = require('./adapters/tank01NbaAdapter');
  const mlbAdapter = require('./adapters/tank01MlbAdapter');
  const { __internals: glInternals } = require('../routes/gameLines');

  return {
    getSchedule: (sport, date) => scheduleService.getSchedule(sport, date),
    getGameLines: async (sport, date) => {
      const fetcher = sport === 'nba' ? nbaAdapter.getNBABettingOdds : sport === 'mlb' ? mlbAdapter.getMLBBettingOdds : null;
      if (!fetcher) return {};
      const body = await fetcher(date).catch(() => null);
      return body ? glInternals.normalizeGameLines(body) : {};
    },
    getGrades: async (sport) => {
      // odds-api grades cache (when present); degrade to [] otherwise.
      const utc = new Date().toISOString().split('T')[0];
      const cache = (await cacheGet(`grades:${sport}:${utc}`)) ?? (await cacheGet(`grades:${sport}`));
      return Array.isArray(cache) ? cache : Array.isArray(cache?.grades) ? cache.grades : [];
    },
    getStreaks: async (sport) => {
      const roster = await loadRosterLogs(sport);
      return streaksService.computeStreaks(roster, sport, {});
    },
    getMovers: (sport) => lineSnapshotService.getBiggestMovers(sport, { limit: 10 }),
    getBestLines: async (sport) => {
      const utc = new Date().toISOString().split('T')[0];
      const cache = (await cacheGet(`odds:${sport}:${utc}`)) ?? (await cacheGet(`odds:${sport}`));
      const props = Array.isArray(cache?.props) ? cache.props : [];
      return bookComparisonService.bestLines(props, { limit: 10 });
    },
  };
}

async function collectSlateData(sport, deps) {
  const c = deps || defaultCollectors();
  const date = (() => {
    try { return require('./scheduleService').todayET(); } catch { return undefined; }
  })();

  const [schedule, gameLines, grades, streaks, movers, bestLines] = await Promise.allSettled([
    c.getSchedule(sport, date),
    c.getGameLines(sport, date),
    c.getGrades(sport),
    c.getStreaks(sport),
    c.getMovers(sport),
    c.getBestLines(sport),
  ]);

  const val = (r, fallback) => (r.status === 'fulfilled' && r.value != null ? r.value : fallback);
  const data = {
    sport,
    schedule: val(schedule, []),
    gameLines: val(gameLines, {}),
    grades: val(grades, []),
    streaks: val(streaks, []),
    movers: val(movers, []),
    bestLines: val(bestLines, []),
  };
  data.dataLevel = determineDataLevel(data);
  return data;
}

module.exports = {
  collectSlateData,
  determineDataLevel,
  generateSlateThread,
  generatePOTD,
  generateResultsRecap,
  generateMatchupPreview,
  __internals: {
    normalizeGrade, extractGameLineHighlights, generateHookText, generateCTAText,
    brierScore, avgCLV, tierRecord, bestOdds, consensus, bookDisagreement, findGameLinesFor,
  },
};
