/**
 * Bill James Sabermetric Engine for Rainmaker MLB Forecasts
 *
 * Implements foundational concepts from:
 *   - "The New Bill James Historical Baseball Abstract" (Runs Created, Pythagorean W%, Log5, Secondary Average)
 *   - "Win Shares" (Park Factors, Component ERA)
 *
 * All formulas are computed from existing FanGraphs data already in the database.
 * Output is a formatted prompt section injected into the LLM alongside FanGraphs raw stats.
 */

import pool from '../db';

// ---------------------------------------------------------------------------
// 1. PARK FACTORS — from Win Shares methodology
//    Multipliers relative to league average (1.000 = neutral).
//    Source: FanGraphs / ESPN park factors, updated for 2024-2025 stadium configs.
//    Each park has: runs, hr, hits, doubles, triples, bb, so
// ---------------------------------------------------------------------------

export interface ParkFactor {
  name: string;          // Stadium name
  team: string;          // Team abbreviation
  runs: number;          // Run factor (1.0 = neutral)
  hr: number;            // Home run factor
  hits: number;          // Hit factor
  doubles: number;       // Doubles factor
  triples: number;       // Triples factor
  so: number;            // Strikeout factor (>1 = more K's)
}

export const PARK_FACTORS: Record<string, ParkFactor> = {
  // AL East
  BAL: { name: 'Camden Yards', team: 'BAL', runs: 1.03, hr: 1.11, hits: 1.01, doubles: 0.99, triples: 0.82, so: 0.98 },
  BOS: { name: 'Fenway Park', team: 'BOS', runs: 1.08, hr: 0.95, hits: 1.10, doubles: 1.40, triples: 0.55, so: 0.97 },
  NYY: { name: 'Yankee Stadium', team: 'NYY', runs: 1.05, hr: 1.18, hits: 0.99, doubles: 0.90, triples: 0.65, so: 1.01 },
  TB:  { name: 'Tropicana Field', team: 'TB', runs: 0.92, hr: 0.88, hits: 0.95, doubles: 0.97, triples: 0.78, so: 1.03 },
  TOR: { name: 'Rogers Centre', team: 'TOR', runs: 1.01, hr: 1.08, hits: 0.99, doubles: 1.00, triples: 0.72, so: 1.00 },
  // AL Central
  CLE: { name: 'Progressive Field', team: 'CLE', runs: 0.95, hr: 0.92, hits: 0.97, doubles: 1.03, triples: 0.90, so: 1.01 },
  CWS: { name: 'Guaranteed Rate', team: 'CWS', runs: 1.04, hr: 1.14, hits: 1.01, doubles: 0.97, triples: 0.60, so: 0.99 },
  DET: { name: 'Comerica Park', team: 'DET', runs: 0.94, hr: 0.85, hits: 0.97, doubles: 1.05, triples: 1.20, so: 1.02 },
  KC:  { name: 'Kauffman Stadium', team: 'KC', runs: 0.97, hr: 0.89, hits: 1.00, doubles: 1.05, triples: 1.30, so: 0.98 },
  MIN: { name: 'Target Field', team: 'MIN', runs: 1.00, hr: 1.04, hits: 0.99, doubles: 0.97, triples: 0.90, so: 1.00 },
  // AL West
  HOU: { name: 'Minute Maid Park', team: 'HOU', runs: 1.03, hr: 1.06, hits: 1.01, doubles: 0.98, triples: 0.70, so: 0.99 },
  LAA: { name: 'Angel Stadium', team: 'LAA', runs: 0.97, hr: 0.99, hits: 0.97, doubles: 0.95, triples: 0.85, so: 1.01 },
  OAK: { name: 'Oakland Coliseum', team: 'OAK', runs: 0.91, hr: 0.82, hits: 0.95, doubles: 0.95, triples: 1.10, so: 1.03 },
  SEA: { name: 'T-Mobile Park', team: 'SEA', runs: 0.93, hr: 0.88, hits: 0.96, doubles: 1.00, triples: 0.80, so: 1.04 },
  TEX: { name: 'Globe Life Field', team: 'TEX', runs: 0.98, hr: 1.02, hits: 0.98, doubles: 0.95, triples: 0.70, so: 1.01 },
  // NL East
  ATL: { name: 'Truist Park', team: 'ATL', runs: 1.01, hr: 1.04, hits: 1.00, doubles: 0.98, triples: 0.75, so: 1.00 },
  MIA: { name: 'LoanDepot Park', team: 'MIA', runs: 0.90, hr: 0.80, hits: 0.94, doubles: 0.95, triples: 0.85, so: 1.05 },
  NYM: { name: 'Citi Field', team: 'NYM', runs: 0.95, hr: 0.93, hits: 0.97, doubles: 1.00, triples: 0.80, so: 1.02 },
  PHI: { name: 'Citizens Bank Park', team: 'PHI', runs: 1.06, hr: 1.15, hits: 1.02, doubles: 1.00, triples: 0.70, so: 0.98 },
  WSH: { name: 'Nationals Park', team: 'WSH', runs: 1.00, hr: 1.03, hits: 0.99, doubles: 0.97, triples: 0.75, so: 1.00 },
  // NL Central
  CHC: { name: 'Wrigley Field', team: 'CHC', runs: 1.05, hr: 1.10, hits: 1.02, doubles: 1.00, triples: 0.65, so: 0.97 },
  CIN: { name: 'Great American', team: 'CIN', runs: 1.09, hr: 1.22, hits: 1.03, doubles: 0.98, triples: 0.65, so: 0.96 },
  MIL: { name: 'American Family', team: 'MIL', runs: 1.01, hr: 1.06, hits: 1.00, doubles: 0.98, triples: 0.70, so: 0.99 },
  PIT: { name: 'PNC Park', team: 'PIT', runs: 0.93, hr: 0.84, hits: 0.97, doubles: 1.05, triples: 1.10, so: 1.02 },
  STL: { name: 'Busch Stadium', team: 'STL', runs: 0.96, hr: 0.93, hits: 0.98, doubles: 0.98, triples: 0.80, so: 1.01 },
  // NL West
  ARI: { name: 'Chase Field', team: 'ARI', runs: 1.07, hr: 1.09, hits: 1.04, doubles: 1.08, triples: 1.20, so: 0.96 },
  COL: { name: 'Coors Field', team: 'COL', runs: 1.27, hr: 1.30, hits: 1.18, doubles: 1.25, triples: 1.80, so: 0.83 },
  LAD: { name: 'Dodger Stadium', team: 'LAD', runs: 0.95, hr: 0.96, hits: 0.97, doubles: 0.95, triples: 0.80, so: 1.02 },
  SD:  { name: 'Petco Park', team: 'SD', runs: 0.93, hr: 0.88, hits: 0.96, doubles: 0.98, triples: 0.90, so: 1.04 },
  SF:  { name: 'Oracle Park', team: 'SF', runs: 0.90, hr: 0.78, hits: 0.95, doubles: 1.02, triples: 1.15, so: 1.04 },
};

// Alternate abbreviation lookup
const PARK_ALIAS: Record<string, string> = {
  AZ: 'ARI', WSN: 'WSH', SFG: 'SF', SDP: 'SD', TBR: 'TB', KCR: 'KC', CHW: 'CWS', ATH: 'OAK',
};

export function getParkFactor(homeTeam: string): ParkFactor | null {
  const key = homeTeam.toUpperCase();
  return PARK_FACTORS[key] || PARK_FACTORS[PARK_ALIAS[key] || ''] || null;
}

// ---------------------------------------------------------------------------
// 2. RUNS CREATED — Bill James' foundational offensive metric
//    Basic RC = (H + BB) * TB / (AB + BB)
//    RC/27 = estimated runs per game (27 outs)
// ---------------------------------------------------------------------------

export interface RunsCreated {
  team: string;
  rc: number;            // Total runs created
  rcPerGame: number;     // RC/27 — runs per game equivalent
  rcParkAdj: number;     // Park-adjusted RC/27
  ab: number;
  h: number;
  bb: number;
  tb: number;            // Total bases (computed: 1B + 2*2B + 3*3B + 4*HR)
  playerCount: number;
}

export async function computeRunsCreated(team: string, season: number, parkTeam?: string): Promise<RunsCreated | null> {
  const { rows } = await pool.query(
    `SELECT team,
       SUM(ab) as ab, SUM(h) as h, SUM(bb) as bb, SUM(hbp) as hbp,
       SUM(singles) as singles, SUM(doubles) as doubles,
       SUM(triples) as triples, SUM(hr) as hr,
       SUM(sf) as sf, SUM(cs) as cs, SUM(so) as so,
       COUNT(*) as player_count
     FROM fg_batting_stats
     WHERE team = $1 AND season = $2 AND split = 'full'
       AND pull_date = (SELECT MAX(pull_date) FROM fg_batting_stats WHERE team = $1 AND season = $2 AND split = 'full')
     GROUP BY team`,
    [team, season],
  );

  if (rows.length === 0) return null;
  const r = rows[0];

  const ab = Number(r.ab);
  const h = Number(r.h);
  const bb = Number(r.bb);
  const hbp = Number(r.hbp) || 0;
  const sf = Number(r.sf) || 0;
  const singles = Number(r.singles);
  const doubles = Number(r.doubles);
  const triples = Number(r.triples);
  const hr = Number(r.hr);
  const cs = Number(r.cs) || 0;
  const so = Number(r.so);

  // Total bases
  const tb = singles + (2 * doubles) + (3 * triples) + (4 * hr);

  // Bill James "Technical" Runs Created (more accurate than basic)
  // A = H + BB + HBP - CS - GIDP (simplified: H + BB + HBP - CS)
  // B = TB + 0.26*(BB + HBP) + 0.52*(SH + SF + SB) — simplified without SH/SB
  // C = AB + BB + HBP + SF
  const a = h + bb + hbp - cs;
  const b = tb + 0.26 * (bb + hbp) + 0.52 * sf;
  const c = ab + bb + hbp + sf;

  const rc = c > 0 ? (a * b) / c : 0;

  // RC/27 = runs per game (27 outs per game)
  // Outs ≈ AB - H + CS + SF (simplified)
  const outs = ab - h + cs + sf;
  const rcPerGame = outs > 0 ? (rc * 27) / outs : 0;

  // Park-adjust RC/27
  const park = getParkFactor(parkTeam || team);
  const parkRunFactor = park?.runs || 1.0;
  // Normalize: if park inflates runs by 1.27 (Coors), divide by that to get true talent
  const rcParkAdj = parkRunFactor > 0 ? rcPerGame / parkRunFactor : rcPerGame;

  return {
    team, rc, rcPerGame, rcParkAdj, ab, h, bb, tb,
    playerCount: Number(r.player_count),
  };
}

// ---------------------------------------------------------------------------
// 3. PYTHAGOREAN WIN% — predicts true team strength from run differential
//    W% = RS^exp / (RS^exp + RA^exp)
//    James' refined exponent: 1.83 (later Smyth/Patriot found RS+RA dependent)
// ---------------------------------------------------------------------------

export interface PythagoreanRecord {
  team: string;
  runsScored: number;
  runsAllowed: number;
  pythWinPct: number;    // Expected win% from run differential
  pythWins: number;      // Projected wins over 162 games
  pythLosses: number;
  runDifferential: number;
  strengthRating: string; // "Elite" / "Strong" / "Average" / "Weak" / "Rebuilding"
}

export async function computePythagorean(team: string, season: number): Promise<PythagoreanRecord | null> {
  // Get runs scored from batting
  const batResult = await pool.query(
    `SELECT SUM(r) as runs_scored FROM fg_batting_stats
     WHERE team = $1 AND season = $2 AND split = 'full'
       AND pull_date = (SELECT MAX(pull_date) FROM fg_batting_stats WHERE team = $1 AND season = $2 AND split = 'full')`,
    [team, season],
  );

  // Get runs allowed from pitching
  const pitResult = await pool.query(
    `SELECT SUM(r) as runs_allowed, SUM(ip) as innings FROM fg_pitching_stats
     WHERE team = $1 AND season = $2 AND split = 'full'
       AND pull_date = (SELECT MAX(pull_date) FROM fg_pitching_stats WHERE team = $1 AND season = $2 AND split = 'full')`,
    [team, season],
  );

  if (batResult.rows.length === 0 || pitResult.rows.length === 0) return null;

  const rs = Number(batResult.rows[0].runs_scored) || 0;
  const ra = Number(pitResult.rows[0].runs_allowed) || 0;

  if (rs === 0 && ra === 0) return null;

  // Pythagenpat exponent (David Smyth): exp = ((RS + RA) / G) ^ 0.287
  // Since we don't have exact G, estimate from IP: G ≈ IP / 9 (approximate)
  const ip = Number(pitResult.rows[0].innings) || 0;
  const estGames = ip > 0 ? ip / 9 : 1;

  // Use Pythagenpat for more accuracy when we have enough games
  const rpg = (rs + ra) / Math.max(estGames, 1);
  const exp = Math.pow(rpg, 0.287);

  const rsExp = Math.pow(rs, exp);
  const raExp = Math.pow(ra, exp);
  const pythWinPct = (rsExp + raExp) > 0 ? rsExp / (rsExp + raExp) : 0.500;

  const pythWins = Math.round(pythWinPct * 162);
  const pythLosses = 162 - pythWins;
  const runDiff = rs - ra;

  let strengthRating = 'Average';
  if (pythWinPct >= 0.580) strengthRating = 'Elite';
  else if (pythWinPct >= 0.540) strengthRating = 'Strong';
  else if (pythWinPct >= 0.480) strengthRating = 'Average';
  else if (pythWinPct >= 0.420) strengthRating = 'Weak';
  else strengthRating = 'Rebuilding';

  return {
    team, runsScored: rs, runsAllowed: ra,
    pythWinPct, pythWins, pythLosses, runDifferential: runDiff,
    strengthRating,
  };
}

// ---------------------------------------------------------------------------
// 4. LOG5 — head-to-head probability between two teams
//    P(A beats B) = (pA - pA*pB) / (pA + pB - 2*pA*pB)
//    Uses Pythagorean W% as true team strength
// ---------------------------------------------------------------------------

export interface Log5Matchup {
  homeTeam: string;
  awayTeam: string;
  homeWinPct: number;     // Pythagorean W%
  awayWinPct: number;
  homeWinProb: number;    // Log5 probability (before HFA)
  homeWinProbHFA: number; // Log5 with home field advantage (~54% baseline)
  impliedSpread: number;  // Rough spread from probability
  strengthDiff: string;   // "Dominant mismatch" / "Clear edge" / "Slight edge" / "Toss-up"
}

const MLB_HFA = 0.540; // Historical MLB home field advantage ~54%

export function computeLog5(
  homePyth: PythagoreanRecord,
  awayPyth: PythagoreanRecord,
): Log5Matchup {
  const pA = homePyth.pythWinPct;
  const pB = awayPyth.pythWinPct;

  // Raw Log5
  const denom = pA + pB - 2 * pA * pB;
  const homeWinProb = denom !== 0 ? (pA - pA * pB) / denom : 0.500;

  // Adjust for home field advantage
  // Blend: use HFA as the "average" opponent baseline
  // P(home) = (pA * (1 - pB) * MLB_HFA) / (pA * (1 - pB) * MLB_HFA + pB * (1 - pA) * (1 - MLB_HFA))
  const hfaNumerator = pA * (1 - pB) * MLB_HFA;
  const hfaDenominator = hfaNumerator + pB * (1 - pA) * (1 - MLB_HFA);
  const homeWinProbHFA = hfaDenominator > 0 ? hfaNumerator / hfaDenominator : 0.500;

  // Convert probability to approximate run line spread
  // Rough conversion: each 10% above 50% ≈ 1.5 runs
  const impliedSpread = -((homeWinProbHFA - 0.500) * 15);

  const diff = Math.abs(homeWinProbHFA - 0.500);
  let strengthDiff = 'Toss-up';
  if (diff >= 0.15) strengthDiff = 'Dominant mismatch';
  else if (diff >= 0.10) strengthDiff = 'Clear edge';
  else if (diff >= 0.05) strengthDiff = 'Slight edge';

  return {
    homeTeam: homePyth.team,
    awayTeam: awayPyth.team,
    homeWinPct: pA,
    awayWinPct: pB,
    homeWinProb,
    homeWinProbHFA,
    impliedSpread: Math.round(impliedSpread * 10) / 10,
    strengthDiff,
  };
}

// ---------------------------------------------------------------------------
// 5. SECONDARY AVERAGE — reveals hidden offensive value beyond batting average
//    SecA = (TB - H + BB + SB - CS) / AB
//    High SecA = power + patience + speed (total bases prop gold)
// ---------------------------------------------------------------------------

export interface SecondaryAvg {
  player: string;
  team: string;
  secAvg: number;
  avg: number;
  gap: number;          // SecA - AVG: positive = hidden value, negative = empty average
  interpretation: string;
}

export async function getTeamSecondaryAverages(team: string, season: number): Promise<SecondaryAvg[]> {
  const { rows } = await pool.query(
    `SELECT player_name, team, ab, h, bb, sb, cs, hbp, avg,
       singles, doubles, triples, hr
     FROM fg_batting_stats
     WHERE team = $1 AND season = $2 AND split = 'full' AND ab >= 50
       AND pull_date = (SELECT MAX(pull_date) FROM fg_batting_stats WHERE team = $1 AND season = $2 AND split = 'full')
     ORDER BY ab DESC`,
    [team, season],
  );

  return rows.map((r: any) => {
    const ab = Number(r.ab);
    const h = Number(r.h);
    const bb = Number(r.bb);
    const sb = Number(r.sb) || 0;
    const cs = Number(r.cs) || 0;
    const singles = Number(r.singles);
    const doubles = Number(r.doubles);
    const triples = Number(r.triples);
    const hr = Number(r.hr);
    const tb = singles + 2 * doubles + 3 * triples + 4 * hr;

    const secAvg = ab > 0 ? (tb - h + bb + sb - cs) / ab : 0;
    const avg = Number(r.avg);
    const gap = secAvg - avg;

    let interpretation = 'Balanced';
    if (gap > 0.080) interpretation = 'Hidden power (TB/SB prop value)';
    else if (gap > 0.040) interpretation = 'Above-avg secondary skills';
    else if (gap < -0.040) interpretation = 'Contact-dependent (risky props)';
    else if (gap < -0.080) interpretation = 'Empty average (fade props)';

    return {
      player: r.player_name,
      team: r.team,
      secAvg: Math.round(secAvg * 1000) / 1000,
      avg,
      gap: Math.round(gap * 1000) / 1000,
      interpretation,
    };
  });
}

// ---------------------------------------------------------------------------
// 6. COMPONENT ERA — Bill James' regression detector
//    ERC = different weighting of H, HR, BB, K than FIP
//    When ERA << ERC → pitcher has been lucky → expect regression
//    When ERA >> ERC → pitcher has been unlucky → expect improvement
// ---------------------------------------------------------------------------

export interface ComponentERA {
  pitcher: string;
  team: string;
  era: number;
  componentERA: number;
  regression: number;     // ERA - ERC: negative = lucky, positive = unlucky
  flag: string;           // "Lucky (due for regression)" / "Unlucky (due for improvement)" / "Stable"
  ip: number;
}

export async function getStarterComponentERA(name: string, team: string, season: number): Promise<ComponentERA | null> {
  const { rows } = await pool.query(
    `SELECT player_name, team, era, ip, h, hr, bb, so, hbp, er,
       fip, xfip, babip, lob_pct
     FROM fg_pitching_stats
     WHERE team = $1 AND season = $2 AND split = 'full' AND gs > 0
       AND pull_date = (SELECT MAX(pull_date) FROM fg_pitching_stats WHERE team = $1 AND season = $2 AND split = 'full')
       AND (LOWER(player_name) = LOWER($3) OR player_name ILIKE '%' || $3 || '%')
     ORDER BY gs DESC LIMIT 1`,
    [team, season, name],
  );

  if (rows.length === 0) return null;
  const p = rows[0];

  const ip = Number(p.ip);
  const h = Number(p.h);
  const hr = Number(p.hr);
  const bb = Number(p.bb);
  const so = Number(p.so);
  const hbp = Number(p.hbp) || 0;
  const era = Number(p.era);

  if (ip <= 0) return null;

  // Bill James Component ERA:
  // PTB (pitcher total bases) = 0.89*(1.255*(H-HR) + 4*HR) + 0.56*(BB + HBP)
  // Then: ERC = 9 * ((H + BB + HBP) * PTB) / (BFP * IP) - 0.56
  // Simplified version closer to modern DIPS:
  const bfp = ip * 3 + h + bb + hbp; // approximate batters faced
  const ptb = 0.89 * (1.255 * (h - hr) + 4 * hr) + 0.56 * (bb + hbp);
  const componentERA = bfp > 0 && ip > 0
    ? (9 * ((h + bb + hbp) * ptb) / (bfp * ip)) - 0.56
    : era;

  const regression = era - componentERA;

  let flag = 'Stable';
  if (regression < -0.50) flag = 'Lucky (due for regression UP)';
  else if (regression < -0.25) flag = 'Slightly lucky';
  else if (regression > 0.50) flag = 'Unlucky (due for improvement)';
  else if (regression > 0.25) flag = 'Slightly unlucky';

  return {
    pitcher: p.player_name,
    team: p.team,
    era,
    componentERA: Math.round(componentERA * 100) / 100,
    regression: Math.round(regression * 100) / 100,
    flag,
    ip,
  };
}

// ---------------------------------------------------------------------------
// 7. POWER/SPEED NUMBER — identifies dual-threat prop candidates
//    PSN = (2 * HR * SB) / (HR + SB)
//    High PSN = player can beat both HR and SB lines
// ---------------------------------------------------------------------------

export interface PowerSpeed {
  player: string;
  team: string;
  hr: number;
  sb: number;
  psn: number;
  tier: string;  // "Elite dual-threat" / "Power-speed" / "One-dimensional" / "Low impact"
}

export async function getTeamPowerSpeed(team: string, season: number): Promise<PowerSpeed[]> {
  const { rows } = await pool.query(
    `SELECT player_name, team, hr, sb
     FROM fg_batting_stats
     WHERE team = $1 AND season = $2 AND split = 'full' AND ab >= 50
       AND pull_date = (SELECT MAX(pull_date) FROM fg_batting_stats WHERE team = $1 AND season = $2 AND split = 'full')
       AND (hr > 0 OR sb > 0)
     ORDER BY (hr + sb) DESC`,
    [team, season],
  );

  return rows.map((r: any) => {
    const hr = Number(r.hr);
    const sb = Number(r.sb);
    const psn = (hr + sb) > 0 ? (2 * hr * sb) / (hr + sb) : 0;

    let tier = 'Low impact';
    if (psn >= 20) tier = 'Elite dual-threat';
    else if (psn >= 12) tier = 'Power-speed';
    else if (psn >= 5) tier = 'Moderate';
    else if (hr >= 15 || sb >= 20) tier = 'One-dimensional';

    return {
      player: r.player_name,
      team: r.team,
      hr, sb,
      psn: Math.round(psn * 10) / 10,
      tier,
    };
  });
}

// ---------------------------------------------------------------------------
// 8. MASTER ORCHESTRATOR — fetches all Bill James metrics for a matchup
// ---------------------------------------------------------------------------

export interface BillJamesMatchup {
  // Park context
  park: ParkFactor | null;
  // Runs Created
  homeRC: RunsCreated | null;
  awayRC: RunsCreated | null;
  // Pythagorean records
  homePyth: PythagoreanRecord | null;
  awayPyth: PythagoreanRecord | null;
  // Log5 head-to-head
  log5: Log5Matchup | null;
  // Secondary averages (top players)
  homeSecAvg: SecondaryAvg[];
  awaySecAvg: SecondaryAvg[];
  // Component ERA for starters
  homeStarterERC: ComponentERA | null;
  awayStarterERC: ComponentERA | null;
  // Power/Speed
  homePSN: PowerSpeed[];
  awayPSN: PowerSpeed[];
}

export async function getBillJamesMatchup(
  homeTeam: string,
  awayTeam: string,
  season: number,
  homeStarter?: string,
  awayStarter?: string,
): Promise<BillJamesMatchup> {
  const ht = homeTeam.toUpperCase();
  const at = awayTeam.toUpperCase();

  // Fetch everything in parallel for speed
  const [homeRC, awayRC, homePyth, awayPyth, homeSecAvg, awaySecAvg, homeStarterERC, awayStarterERC, homePSN, awayPSN] =
    await Promise.all([
      computeRunsCreated(ht, season, ht),
      computeRunsCreated(at, season, ht), // away team adjusted to HOME park
      computePythagorean(ht, season),
      computePythagorean(at, season),
      getTeamSecondaryAverages(ht, season),
      getTeamSecondaryAverages(at, season),
      homeStarter ? getStarterComponentERA(homeStarter, ht, season) : null,
      awayStarter ? getStarterComponentERA(awayStarter, at, season) : null,
      getTeamPowerSpeed(ht, season),
      getTeamPowerSpeed(at, season),
    ]);

  const park = getParkFactor(ht);
  const log5 = (homePyth && awayPyth) ? computeLog5(homePyth, awayPyth) : null;

  return {
    park, homeRC, awayRC, homePyth, awayPyth, log5,
    homeSecAvg, awaySecAvg, homeStarterERC, awayStarterERC,
    homePSN, awayPSN,
  };
}

// ---------------------------------------------------------------------------
// 9. PROMPT FORMATTER — produces LLM-ready text
// ---------------------------------------------------------------------------

function f(v: number | null | undefined, d: number = 2): string {
  if (v === null || v === undefined || isNaN(v)) return 'N/A';
  return v.toFixed(d);
}

export function formatBillJamesForPrompt(data: BillJamesMatchup, homeTeam: string, awayTeam: string): string {
  if (!data.homeRC && !data.awayRC && !data.homePyth && !data.awayPyth) return '';

  let s = '\n--- BILL JAMES SABERMETRIC MODEL (MLB) ---\n';

  // Park Factors
  if (data.park) {
    const p = data.park;
    const parkLabel = p.runs >= 1.05 ? 'HITTER-FRIENDLY'
      : p.runs <= 0.95 ? 'PITCHER-FRIENDLY'
      : 'NEUTRAL';
    s += `\nPARK: ${p.name} (${parkLabel})\n`;
    s += `  Run Factor: ${f(p.runs, 2)} | HR Factor: ${f(p.hr, 2)} | Hit Factor: ${f(p.hits, 2)} | K Factor: ${f(p.so, 2)}\n`;

    if (p.runs >= 1.05) {
      s += `  ⚠ PARK BOOST: Expect +${((p.runs - 1) * 100).toFixed(0)}% more runs, +${((p.hr - 1) * 100).toFixed(0)}% more HR than average park\n`;
    } else if (p.runs <= 0.95) {
      s += `  ⚠ PARK SUPPRESSION: Expect ${((1 - p.runs) * 100).toFixed(0)}% fewer runs, ${((1 - p.hr) * 100).toFixed(0)}% fewer HR than average park\n`;
    }
    s += `  ADJUST your total and HR props accordingly. A 0.5 HR line here is ${p.hr >= 1.10 ? 'MORE' : p.hr <= 0.90 ? 'LESS' : 'ROUGHLY AS'} likely to hit than at an average park.\n`;
  }

  // Runs Created
  if (data.homeRC || data.awayRC) {
    s += `\nRUNS CREATED MODEL (Bill James):\n`;
    if (data.homeRC) {
      s += `  ${homeTeam}: ${f(data.homeRC.rcPerGame, 1)} RC/game (park-adj: ${f(data.homeRC.rcParkAdj, 1)})\n`;
    }
    if (data.awayRC) {
      s += `  ${awayTeam}: ${f(data.awayRC.rcPerGame, 1)} RC/game (park-adj: ${f(data.awayRC.rcParkAdj, 1)})\n`;
    }
    if (data.homeRC && data.awayRC) {
      // Projected game total using park-adjusted RC
      const projTotal = (data.homeRC.rcPerGame + data.awayRC.rcPerGame) *
        (data.park?.runs || 1.0) * 0.5; // Rough: average of both teams' run production at this park
      // More accurate: home team plays in their park, away team adjusted
      const adjTotal = data.homeRC.rcPerGame + (data.awayRC.rcParkAdj * (data.park?.runs || 1.0));
      s += `  RC-Projected Game Total: ${f(adjTotal, 1)} runs (use to evaluate over/under)\n`;
    }
  }

  // Pythagorean Record + Log5
  if (data.homePyth || data.awayPyth) {
    s += `\nPYTHAGOREAN STRENGTH (true talent from run differential):\n`;
    if (data.homePyth) {
      s += `  ${homeTeam}: ${f(data.homePyth.pythWinPct * 100, 1)}% (${data.homePyth.pythWins}-${data.homePyth.pythLosses} pace) | Run diff: ${data.homePyth.runDifferential > 0 ? '+' : ''}${data.homePyth.runDifferential} | ${data.homePyth.strengthRating}\n`;
    }
    if (data.awayPyth) {
      s += `  ${awayTeam}: ${f(data.awayPyth.pythWinPct * 100, 1)}% (${data.awayPyth.pythWins}-${data.awayPyth.pythLosses} pace) | Run diff: ${data.awayPyth.runDifferential > 0 ? '+' : ''}${data.awayPyth.runDifferential} | ${data.awayPyth.strengthRating}\n`;
    }
  }

  if (data.log5) {
    s += `\nLOG5 HEAD-TO-HEAD (Bill James probability model):\n`;
    s += `  ${homeTeam} win probability: ${f(data.log5.homeWinProbHFA * 100, 1)}% (with HFA)\n`;
    s += `  ${awayTeam} win probability: ${f((1 - data.log5.homeWinProbHFA) * 100, 1)}%\n`;
    s += `  Model spread: ${homeTeam} ${data.log5.impliedSpread > 0 ? '+' : ''}${f(data.log5.impliedSpread, 1)}\n`;
    s += `  Matchup: ${data.log5.strengthDiff}\n`;
    s += `  USE THIS as your statistical anchor for moneyline and spread picks.\n`;
  }

  // Component ERA regression flags
  if (data.homeStarterERC || data.awayStarterERC) {
    s += `\nPITCHER REGRESSION ANALYSIS (Component ERA vs actual ERA):\n`;
    if (data.homeStarterERC) {
      const e = data.homeStarterERC;
      s += `  ${e.pitcher} (${homeTeam}): ERA ${f(e.era)} vs Component ERA ${f(e.componentERA)} → ${e.flag}\n`;
      if (Math.abs(e.regression) > 0.25) {
        s += `    ${e.regression < 0 ? '⚠ This pitcher has been LUCKY — his stats suggest he should be giving up more runs. Expect regression.' : '✓ This pitcher has been UNLUCKY — his underlying skills are better than results show. Expect improvement.'}\n`;
      }
    }
    if (data.awayStarterERC) {
      const e = data.awayStarterERC;
      s += `  ${e.pitcher} (${awayTeam}): ERA ${f(e.era)} vs Component ERA ${f(e.componentERA)} → ${e.flag}\n`;
      if (Math.abs(e.regression) > 0.25) {
        s += `    ${e.regression < 0 ? '⚠ This pitcher has been LUCKY — his stats suggest he should be giving up more runs. Expect regression.' : '✓ This pitcher has been UNLUCKY — his underlying skills are better than results show. Expect improvement.'}\n`;
      }
    }
  }

  // Secondary Averages (top 3 per team — the most interesting for props)
  const formatSecAvg = (players: SecondaryAvg[], teamLabel: string): string => {
    if (players.length === 0) return '';
    // Sort by gap (highest hidden value first) and show top 3
    const top = [...players].sort((a, b) => b.gap - a.gap).slice(0, 3);
    let out = `  ${teamLabel} — best prop value (by Secondary Average):\n`;
    for (const p of top) {
      out += `    ${p.player}: AVG ${f(p.avg, 3)} | SecA ${f(p.secAvg, 3)} | Gap ${p.gap > 0 ? '+' : ''}${f(p.gap, 3)} — ${p.interpretation}\n`;
    }
    return out;
  };

  if (data.homeSecAvg.length > 0 || data.awaySecAvg.length > 0) {
    s += `\nSECONDARY AVERAGE (hidden value beyond batting average — Bill James):\n`;
    s += `  SecA = (TB - H + BB + SB - CS) / AB. High SecA + low AVG = power/patience/speed hidden by batting avg.\n`;
    s += formatSecAvg(data.homeSecAvg, homeTeam);
    s += formatSecAvg(data.awaySecAvg, awayTeam);
    s += `  Players with high SecA gap are UNDERVALUED for total bases, HR, and RBI props.\n`;
  }

  // Power/Speed (only mention elite/notable)
  const formatPSN = (players: PowerSpeed[], teamLabel: string): string => {
    const notable = players.filter(p => p.psn >= 5 || p.hr >= 10 || p.sb >= 10);
    if (notable.length === 0) return '';
    const top = notable.sort((a, b) => b.psn - a.psn).slice(0, 3);
    let out = `  ${teamLabel}: `;
    out += top.map(p => `${p.player} PSN=${f(p.psn, 1)} (${p.hr}HR/${p.sb}SB) [${p.tier}]`).join(', ');
    return out + '\n';
  };

  if (data.homePSN.length > 0 || data.awayPSN.length > 0) {
    s += `\nPOWER/SPEED NUMBER (dual-threat prop candidates):\n`;
    s += formatPSN(data.homePSN, homeTeam);
    s += formatPSN(data.awayPSN, awayTeam);
  }

  s += '--- END BILL JAMES MODEL ---\n';
  return s;
}
