/**
 * KenPom Data Service for Rainmaker Forecasts
 * -------------------------------------------------
 * Provides NCAA team metrics from KenPom tables for:
 *  - Forecast prompt enrichment (injected into Grok context)
 *  - Composite score adjustments
 *  - Matchup profiling
 */

import pool from '../db';

export interface KenPomTeamProfile {
  teamName: string;
  record: string;
  kpRank: number;
  adjEM: number;
  adjOE: number;
  rankAdjOE: number;
  adjDE: number;
  rankAdjDE: number;
  adjTempo: number;
  luck: number;
  sosRank: number;
  // Four factors
  efgPct: number | null;
  toPct: number | null;
  orPct: number | null;
  ftRate: number | null;
  deFgPct: number | null;
  dToPct: number | null;
  dOrPct: number | null;
  dFtRate: number | null;
  // Height / Experience
  avgHgt: number | null;
  experience: number | null;
  bench: number | null;
  continuity: number | null;
  // Misc
  fg3Pct: number | null;
  fg2Pct: number | null;
  aRate: number | null;
  blockPct: number | null;
  oppFg3Pct: number | null;
  oppFg2Pct: number | null;
  // Point distribution
  offFg3: number | null;
  offFg2: number | null;
  defFg3: number | null;
  // Conference
  confShort: string | null;
  confRank: number | null;
  confRating: number | null;
}

export interface KenPomMatchup {
  home: KenPomTeamProfile | null;
  away: KenPomTeamProfile | null;
  fanmatch: {
    homeWP: number | null;
    homePred: number | null;
    visitorPred: number | null;
    predTempo: number | null;
    thrillScore: number | null;
  } | null;
  edgeSignals: {
    emGap: number | null;       // AdjEM difference
    tempoMismatch: boolean;     // >5 tempo gap
    offDefMismatch: string | null; // e.g. "high-O vs weak-D"
    experienceEdge: string | null;
    luckWarning: string | null;
    sosAdjustment: string | null;
  };
}

/**
 * Static map for SGO long names that don't match KenPom.
 * SGO sends "Auburn Tigers", KenPom has "Auburn".
 * This handles the non-obvious translations + mascot stripping failures.
 */
const SGO_TO_KP: Record<string, string> = {
  // Abbreviation-based names (KenPom uses acronym, SGO uses full name)
  'Brigham Young Cougars': 'BYU',
  'Southern Methodist Mustangs': 'SMU',
  'Maryland Baltimore County Retrievers': 'UMBC',
  'California San Diego Tritons': 'UC San Diego',
  'Florida International Golden Panthers': 'FIU',
  'Texas Rio Grande Valley Vaqueros': 'UT Rio Grande Valley',
  'Texas A&M Corpus Christi Islanders': 'Texas A&M Corpus Chris',
  'Miami (OH) RedHawks': 'Miami OH',
  'UC Santa Barbara Gauchos': 'UC Santa Barbara',
  'UC Davis Aggies': 'UC Davis',
  'Loyola Chicago Ramblers': 'Loyola Chicago',
  'UMass Lowell River Hawks': 'UMass Lowell',
  'Cal Poly Mustangs': 'Cal Poly',
  'NJIT Highlanders': 'NJIT',
  'LSU Tigers': 'LSU',
  'USC Trojans': 'USC',
  'UNLV Runnin\' Rebels': 'UNLV',

  // SGO "St" without period → KenPom "St." with period
  'Penn St Nittany Lions': 'Penn St.',
  'Grambling St Tigers': 'Grambling St.',

  // SGO uses "State" but KenPom drops it entirely
  'McNeese State Cowboys': 'McNeese',
  'Nicholls State Colonels': 'Nicholls',
  'Towson State Tigers': 'Towson',
  'Troy State Trojans': 'Troy',

  // KenPom uses different base name
  'Detroit Titans': 'Detroit Mercy',
  'Detroit Mercy Titans': 'Detroit Mercy',
  'Loyola Maryland Greyhounds': 'Loyola MD',
  'Loyola MD Greyhounds': 'Loyola MD',
  'Massachusetts Minutemen': 'Massachusetts',
  'UMass Minutemen': 'Massachusetts',
  'Central Connecticut St Blue Devils': 'Central Connecticut',
  'Central Connecticut State': 'Central Connecticut',
  'East Tennessee St Buccaneers': 'East Tennessee St.',
  'East Tennessee State Bucs': 'East Tennessee St.',
  'East Tennessee State Buccaneers': 'East Tennessee St.',
  'East Texas A&M Lions': 'East Texas A&M',
  'Florida A and M': 'Florida A&M',
  'Florida A&M Rattlers': 'Florida A&M',
  'Miami FL Hurricanes': 'Miami FL',
  'Green Bay Phoenix': 'Green Bay',
  'Milwaukee Panthers': 'Milwaukee',
  'Mississippi Valley State Delta Devils': 'Mississippi Valley St.',
  'St Mary\'s Gaels': 'Saint Mary\'s',
  'Saint Mary\'s Gaels': 'Saint Mary\'s',
  'Houston Christian Huskies': 'Houston Christian',
  'Gardner Webb Runnin Bulldogs': 'Gardner Webb',

  // SGO sends full name, KenPom uses short form
  'Little Rock Trojans': 'Little Rock',
  'Louisiana Ragin Cajuns': 'Louisiana',
  'Louisiana Monroe Warhawks': 'Louisiana Monroe',
  'Southeastern Louisiana Lions': 'Southeastern Louisiana',
  'Southeast Missouri Redhawks': 'Southeast Missouri',
  'South Dakota Coyotes': 'South Dakota',
  'South Dakota State Jackrabbits': 'South Dakota St.',
  'North Carolina A&T Aggies': 'North Carolina A&T',
  'North Carolina Central Eagles': 'North Carolina Central',
  'Prairie View A&M Panthers': 'Prairie View A&M',
  'Tennessee Martin Skyhawks': 'Tennessee Martin',
  'Tennessee State Tigers': 'Tennessee St.',
  'Tennessee Tech Golden Eagles': 'Tennessee Tech',
  'Texas State Bobcats': 'Texas St.',
  'UT Arlington Mavericks': 'UT Arlington',
  'Sacramento State Hornets': 'Sacramento St.',
  'Portland State Vikings': 'Portland St.',
  'Montana State Bobcats': 'Montana St.',
  'Weber State Wildcats': 'Weber St.',
  'Wright State Raiders': 'Wright St.',
  'Morehead State Eagles': 'Morehead St.',
  'Morgan State Bears': 'Morgan St.',
  'Murray State Racers': 'Murray St.',
  'Norfolk State Spartans': 'Norfolk St.',
  'Youngstown State Penguins': 'Youngstown St.',
  'Wichita State Shockers': 'Wichita St.',
  'San Diego State Aztecs': 'San Diego St.',
  'San Jose State Spartans': 'San Jose St.',
  'Boise State Broncos': 'Boise St.',
  'Colorado State Rams': 'Colorado St.',
  'Fresno State Bulldogs': 'Fresno St.',
  'Oklahoma State Cowboys': 'Oklahoma St.',
  'Oregon State Beavers': 'Oregon St.',
  'Jacksonville State Gamecocks': 'Jacksonville St.',
  'Kennesaw State Owls': 'Kennesaw St.',
  'Appalachian State Mountaineers': 'Appalachian St.',
  'Jackson State Tigers': 'Jackson St.',
  'Coppin State Eagles': 'Coppin St.',
  'Cleveland State Vikings': 'Cleveland St.',
  'Denver Pioneers': 'Denver',

  // Queens / St. Thomas / other newer D1
  'Queens Royals': 'Queens',
  'St Thomas Tommies': 'St. Thomas',
  'Merrimack Warriors': 'Merrimack',
  'New Haven Chargers': 'New Haven',
  'Stonehill Skyhawks': 'Stonehill',
  'Lindenwood Lions': 'Lindenwood',
  'Bellarmine Knights': 'Bellarmine',

  // "N.C. State" variant
  'NC State Wolfpack': 'N.C. State',
  'N.C. State Wolfpack': 'N.C. State',
};

/**
 * Common NCAAB mascot suffixes to strip when matching SGO → KenPom.
 * Applied after the static map lookup fails.
 */
const MASCOT_SUFFIXES = [
  // Multi-word mascots (must come first for longest-match-first sort)
  'Runnin\' Rebels', 'Fighting Camels', 'Fighting Illini', 'Fighting Irish',
  'Crimson Tide', 'Golden Bears', 'Golden Eagles', 'Golden Gophers',
  'Golden Panthers', 'Blue Demons', 'Blue Devils', 'Blue Raiders',
  'Green Wave', 'Mean Green', 'Scarlet Knights', 'Red Raiders', 'Red Storm',
  'Nittany Lions', 'Mountain Hawks', 'River Hawks', 'Sun Devils',
  'Demon Deacons', 'Tar Heels', 'Yellow Jackets', 'Wolf Pack',
  'Delta Devils', 'Ragin Cajuns', 'Runnin Bulldogs',

  // Standard single-word mascots
  'Wildcats', 'Bulldogs', 'Tigers', 'Bears', 'Eagles', 'Falcons', 'Cougars',
  'Mustangs', 'Terriers', 'Panthers', 'Hawks', 'Owls', 'Huskies', 'Rams',
  'Ducks', 'Trojans', 'Utes', 'Bearcats', 'Buffaloes', 'Cowboys', 'Hornets',
  'Tritons', 'Hoyas', 'Pride', 'Vandals', 'Gamecocks', 'Ramblers', 'Retrievers',
  'Terrapins', 'Minutemen', 'Warriors', 'RedHawks', 'Redhawks', 'Rebels',
  'Musketeers', 'Islanders', 'Longhorns', 'Vaqueros', 'Rockets', 'Gauchos',
  'Catamounts', 'Hokies', 'Raiders', 'Sooners', 'Cardinals', 'Cardinal', 'Friars',
  'Spiders', 'Spartans', 'Broncos', 'Saints', 'Bonnies', 'Texans', 'Explorers',
  'Colonials', 'Seminoles', 'Aggies', 'Highlanders', 'Lumberjacks', 'Orange',
  'Mountaineers', 'Jayhawks', 'Volunteers', 'Boilermakers', 'Hawkeyes', 'Badgers',
  'Wolverines', 'Cavaliers', 'Razorbacks', 'Commodores', 'Gators', 'Hurricanes',
  'Wolfpack', 'Hoosiers', 'Buckeyes', 'Cornhuskers', 'Bruins',

  // Mascots that were missing (caused 56% KenPom miss rate)
  'Grizzlies', 'Paladins', 'Beavers', 'Vikings', 'Norse', 'Colonels',
  'Privateers', 'Buccaneers', 'Bucs', 'Gaels', 'Camels', 'Ospreys',
  'Bobcats', 'Bison', 'Bisons', 'Blazers', 'Bluejays', 'Bengals', 'Bulls',
  'Braves', 'Chippewas', 'Coyotes', 'Cyclones', 'Dolphins', 'Dons', 'Dragons',
  'Dukes', 'Flames', 'Flyers', 'Governors', 'Greyhounds', 'Hatters',
  'Hilltoppers', 'Jaspers', 'Knights', 'Lancers', 'Leopards', 'Lions',
  'Lobos', 'Mavericks', 'Miners', 'Mocs', 'Monarchs', 'Penguins', 'Phoenix',
  'Pilots', 'Pioneers', 'Pirates', 'Racers', 'Rattlers', 'Roadrunners',
  'Salukis', 'Seahawks', 'Seawolves', 'Shockers', 'Skyhawks', 'Stags',
  'Sycamores', 'Thunderbirds', 'Toreros', 'Zips', 'Chanticleers', 'Leathernecks',
  'Jackrabbits', 'Kangaroos', 'Peacocks', 'Royals', 'Billikens', 'Anteaters',
  'Aztecs', 'Mastodons', 'Tommies', 'Titans', 'Warhawks',
];

/**
 * Teams where KenPom drops "State" / "St." entirely.
 * e.g. "McNeese State" → "McNeese", "Towson State" → "Towson"
 */
const KP_DROP_STATE = new Set([
  'mcneese', 'nicholls', 'towson', 'troy', 'seattle', 'liberty',
  'merrimack', 'jacksonville', 'maine', 'denver', 'oakland',
  'campbell', 'furman', 'radford', 'belmont', 'lipscomb',
]);

function normalizeForKP(name: string): string {
  if (!name) return '';
  // Step 1: Static map (handles BYU, SMU, UMBC, FIU, etc.)
  if (SGO_TO_KP[name]) return SGO_TO_KP[name];

  let s = name.trim();

  // Step 2: Strip mascot suffix (longest match first)
  const sorted = MASCOT_SUFFIXES.slice().sort((a, b) => b.length - a.length);
  for (const mascot of sorted) {
    if (s.endsWith(' ' + mascot)) {
      s = s.slice(0, -(mascot.length + 1)).trim();
      break;
    }
  }

  // Step 3: KenPom uses "St." for "State" — but some teams drop it entirely
  if (s.endsWith(' State')) {
    const base = s.replace(/\s+State$/, '');
    if (KP_DROP_STATE.has(base.toLowerCase())) {
      s = base;
    } else {
      s = base + ' St.';
    }
  }

  return s;
}

/**
 * Lowercased normalization for fuzzy DB matching
 */
function normalizeForKPLower(name: string): string {
  let s = normalizeForKP(name);
  s = s.toLowerCase();
  s = s.replace(/['\.\-,;:!?()]/g, '');
  s = s.replace(/\s+/g, ' ').trim();
  return s;
}

/**
 * Get KenPom team profile for a given team name.
 * Tries: exact KenPom name → kp_team_map canonical → normalized fuzzy match
 */
export async function getKenPomProfile(teamName: string, season = 2026): Promise<KenPomTeamProfile | null> {
  const client = await pool.connect();
  try {
    // Normalize SGO long name → KenPom-style name (strips mascot, handles BYU/SMU/etc)
    const kpNormalized = normalizeForKP(teamName);
    const kpLower = normalizeForKPLower(teamName);

    // Try direct match on kp_ratings (both original name and normalized)
    let result = await client.query(`
      SELECT r.*, ff.efg_pct, ff.to_pct, ff.or_pct, ff.ft_rate,
             ff.defg_pct, ff.dto_pct, ff.dor_pct, ff.dft_rate,
             h.avg_hgt, h.experience, h.bench, h.continuity,
             ms.fg3_pct, ms.fg2_pct, ms.a_rate, ms.block_pct,
             ms.opp_fg3_pct, ms.opp_fg2_pct,
             pd.off_fg3, pd.off_fg2, pd.def_fg3,
             cr.rank as conf_rank, cr.rating as conf_rating
      FROM kp_ratings r
      LEFT JOIN kp_four_factors ff ON ff.team_name = r.team_name AND ff.season = r.season
          AND ff.pull_date = (SELECT MAX(pull_date) FROM kp_four_factors WHERE season = $2) AND ff.conf_only = false
      LEFT JOIN kp_height h ON h.team_name = r.team_name AND h.season = r.season
          AND h.pull_date = (SELECT MAX(pull_date) FROM kp_height WHERE season = $2)
      LEFT JOIN kp_misc_stats ms ON ms.team_name = r.team_name AND ms.season = r.season
          AND ms.pull_date = (SELECT MAX(pull_date) FROM kp_misc_stats WHERE season = $2) AND ms.conf_only = false
      LEFT JOIN kp_point_dist pd ON pd.team_name = r.team_name AND pd.season = r.season
          AND pd.pull_date = (SELECT MAX(pull_date) FROM kp_point_dist WHERE season = $2) AND pd.conf_only = false
      LEFT JOIN kp_conf_ratings cr ON cr.conf_short = r.conf_short AND cr.season = r.season
          AND cr.pull_date = (SELECT MAX(pull_date) FROM kp_conf_ratings WHERE season = $2)
      WHERE r.season = $2 AND r.pull_date = (SELECT MAX(pull_date) FROM kp_ratings WHERE season = $2)
        AND (r.team_name = $1 OR r.team_name = $3 OR r.team_name_canonical = $4)
      LIMIT 1
    `, [teamName, season, kpNormalized, kpLower]);

    // If no direct match, try kp_team_map (with both original and normalized names)
    if (result.rows.length === 0) {
      const mapResult = await client.query(`
        SELECT kp_name FROM kp_team_map
        WHERE canonical_name = $1 OR kp_name = $1 OR canonical_name = $2 OR kp_name = $2
        LIMIT 1
      `, [teamName, kpNormalized]);

      if (mapResult.rows.length > 0) {
        const kpName = mapResult.rows[0].kp_name;
        result = await client.query(`
          SELECT r.*, ff.efg_pct, ff.to_pct, ff.or_pct, ff.ft_rate,
                 ff.defg_pct, ff.dto_pct, ff.dor_pct, ff.dft_rate,
                 h.avg_hgt, h.experience, h.bench, h.continuity,
                 ms.fg3_pct, ms.fg2_pct, ms.a_rate, ms.block_pct,
                 ms.opp_fg3_pct, ms.opp_fg2_pct,
                 pd.off_fg3, pd.off_fg2, pd.def_fg3,
                 cr.rank as conf_rank, cr.rating as conf_rating
          FROM kp_ratings r
          LEFT JOIN kp_four_factors ff ON ff.team_name = r.team_name AND ff.season = r.season
              AND ff.pull_date = (SELECT MAX(pull_date) FROM kp_four_factors WHERE season = $2) AND ff.conf_only = false
          LEFT JOIN kp_height h ON h.team_name = r.team_name AND h.season = r.season
              AND h.pull_date = (SELECT MAX(pull_date) FROM kp_height WHERE season = $2)
          LEFT JOIN kp_misc_stats ms ON ms.team_name = r.team_name AND ms.season = r.season
              AND ms.pull_date = (SELECT MAX(pull_date) FROM kp_misc_stats WHERE season = $2) AND ms.conf_only = false
          LEFT JOIN kp_point_dist pd ON pd.team_name = r.team_name AND pd.season = r.season
              AND pd.pull_date = (SELECT MAX(pull_date) FROM kp_point_dist WHERE season = $2) AND pd.conf_only = false
          LEFT JOIN kp_conf_ratings cr ON cr.conf_short = r.conf_short AND cr.season = r.season
              AND cr.pull_date = (SELECT MAX(pull_date) FROM kp_conf_ratings WHERE season = $2)
          WHERE r.season = $2 AND r.pull_date = (SELECT MAX(pull_date) FROM kp_ratings WHERE season = $2)
            AND r.team_name = $1
          LIMIT 1
        `, [kpName, season]);
      }
    }

    if (result.rows.length === 0) return null;

    const r = result.rows[0];
    return {
      teamName: r.team_name,
      record: `${r.wins}-${r.losses}`,
      kpRank: r.rank_adj_em,
      adjEM: r.adj_em,
      adjOE: r.adj_oe,
      rankAdjOE: r.rank_adj_oe,
      adjDE: r.adj_de,
      rankAdjDE: r.rank_adj_de,
      adjTempo: r.adj_tempo,
      luck: r.luck,
      sosRank: r.rank_sos,
      efgPct: r.efg_pct,
      toPct: r.to_pct,
      orPct: r.or_pct,
      ftRate: r.ft_rate,
      deFgPct: r.defg_pct,
      dToPct: r.dto_pct,
      dOrPct: r.dor_pct,
      dFtRate: r.dft_rate,
      avgHgt: r.avg_hgt,
      experience: r.experience,
      bench: r.bench,
      continuity: r.continuity,
      fg3Pct: r.fg3_pct,
      fg2Pct: r.fg2_pct,
      aRate: r.a_rate,
      blockPct: r.block_pct,
      oppFg3Pct: r.opp_fg3_pct,
      oppFg2Pct: r.opp_fg2_pct,
      offFg3: r.off_fg3,
      offFg2: r.off_fg2,
      defFg3: r.def_fg3,
      confShort: r.conf_short,
      confRank: r.conf_rank,
      confRating: r.conf_rating,
    };
  } finally {
    client.release();
  }
}

/**
 * Get KenPom fanmatch prediction for a game on a specific date.
 */
async function getFanmatch(homeTeam: string, awayTeam: string, gameDate: string): Promise<any | null> {
  const result = await pool.query(`
    SELECT * FROM kp_fanmatch
    WHERE game_date = $1
      AND (home = $2 OR home_canonical = $4)
      AND (visitor = $3 OR visitor_canonical = $5)
    ORDER BY pull_date DESC LIMIT 1
  `, [gameDate, homeTeam, awayTeam, normalizeForKP(homeTeam), normalizeForKP(awayTeam)]);

  return result.rows[0] || null;
}

/**
 * Build a full KenPom matchup profile for two teams.
 */
export async function getKenPomMatchup(
  homeTeam: string,
  awayTeam: string,
  league: string,
  gameDate?: string,
): Promise<KenPomMatchup | null> {
  // Only NCAAB
  if (league !== 'ncaab') return null;

  const [home, away] = await Promise.all([
    getKenPomProfile(homeTeam),
    getKenPomProfile(awayTeam),
  ]);

  if (!home && !away) return null;

  // Get fanmatch if date provided
  let fanmatch = null;
  if (gameDate && home && away) {
    const fm = await getFanmatch(home.teamName, away.teamName, gameDate);
    if (fm) {
      fanmatch = {
        homeWP: fm.home_wp,
        homePred: fm.home_pred,
        visitorPred: fm.visitor_pred,
        predTempo: fm.pred_tempo,
        thrillScore: fm.thrill_score,
      };
    }
  }

  // Compute edge signals
  const edgeSignals: KenPomMatchup['edgeSignals'] = {
    emGap: null,
    tempoMismatch: false,
    offDefMismatch: null,
    experienceEdge: null,
    luckWarning: null,
    sosAdjustment: null,
  };

  if (home && away) {
    edgeSignals.emGap = home.adjEM - away.adjEM;
    edgeSignals.tempoMismatch = Math.abs(home.adjTempo - away.adjTempo) > 5;

    // Offensive strength vs defensive weakness detection
    if (home.rankAdjOE <= 30 && away.rankAdjDE >= 200) {
      edgeSignals.offDefMismatch = `${home.teamName} elite offense vs ${away.teamName} weak defense`;
    } else if (away.rankAdjOE <= 30 && home.rankAdjDE >= 200) {
      edgeSignals.offDefMismatch = `${away.teamName} elite offense vs ${home.teamName} weak defense`;
    }

    // Experience edge
    if (home.experience && away.experience) {
      const expGap = home.experience - away.experience;
      if (Math.abs(expGap) > 0.5) {
        edgeSignals.experienceEdge = expGap > 0
          ? `${home.teamName} more experienced (+${expGap.toFixed(2)})`
          : `${away.teamName} more experienced (+${(-expGap).toFixed(2)})`;
      }
    }

    // Luck warning: teams with high luck may regress
    if (Math.abs(home.luck) > 0.05 || Math.abs(away.luck) > 0.05) {
      const warnings: string[] = [];
      if (home.luck > 0.05) warnings.push(`${home.teamName} running lucky (+${home.luck.toFixed(3)})`);
      if (home.luck < -0.05) warnings.push(`${home.teamName} running unlucky (${home.luck.toFixed(3)})`);
      if (away.luck > 0.05) warnings.push(`${away.teamName} running lucky (+${away.luck.toFixed(3)})`);
      if (away.luck < -0.05) warnings.push(`${away.teamName} running unlucky (${away.luck.toFixed(3)})`);
      edgeSignals.luckWarning = warnings.join('; ');
    }

    // SOS adjustment
    if (Math.abs(home.sosRank - away.sosRank) > 100) {
      const harder = home.sosRank < away.sosRank ? home : away;
      const easier = home.sosRank < away.sosRank ? away : home;
      edgeSignals.sosAdjustment = `${harder.teamName} (SOS #${harder.sosRank}) played much tougher schedule than ${easier.teamName} (SOS #${easier.sosRank})`;
    }
  }

  return { home, away, fanmatch, edgeSignals };
}

/**
 * Compute KenPom-predicted Margin of Victory using AdjEM differential.
 * Formula: predicted MOV = (home AdjEM - away AdjEM) + home court advantage
 * Home court advantage in college basketball averages ~3.3 points.
 * For neutral sites (tournament), home court = 0.
 * Returns a calibrated 0-1 confidence score based on how strongly KenPom predicts the winner.
 */
export function computeKenPomMOVScore(matchup: KenPomMatchup, neutralSite = false): {
  predictedMOV: number;     // positive = home favored
  confidence: number;       // 0-1 calibrated score for composite blending
  gamesPlayed: number;      // avg games played by both teams (for weighting)
} {
  const HOME_COURT_ADV = neutralSite ? 0 : 3.3;
  const home = matchup.home;
  const away = matchup.away;

  if (!home || !away) {
    return { predictedMOV: 0, confidence: 0.5, gamesPlayed: 0 };
  }

  // AdjEM-based MOV: this is the core power rating formula from Mathletics/KenPom theory
  const predictedMOV = (home.adjEM - away.adjEM) + HOME_COURT_ADV;

  // Convert MOV to confidence: larger predicted margin = higher confidence
  // Scale: 0pt MOV = 0.50, ±10pt MOV = ~0.85, ±20pt MOV = ~0.95
  // Using logistic-like mapping: conf = 0.5 + 0.5 * tanh(MOV / 12)
  const absMOV = Math.abs(predictedMOV);
  const confidence = Math.min(0.98, Math.max(0.50, 0.5 + 0.5 * Math.tanh(absMOV / 12)));

  // Estimate games played from record (format "W-L")
  let gamesPlayed = 30; // default late-season
  try {
    const homeGP = home.record.split('-').reduce((s, n) => s + parseInt(n), 0);
    const awayGP = away.record.split('-').reduce((s, n) => s + parseInt(n), 0);
    gamesPlayed = (homeGP + awayGP) / 2;
  } catch { /* use default */ }

  return { predictedMOV, confidence, gamesPlayed };
}

/**
 * Format KenPom data for injection into the Grok forecast prompt.
 */
export function formatKenPomForPrompt(matchup: KenPomMatchup): string {
  if (!matchup) return '';

  const sections: string[] = ['--- KENPOM ANALYTICS ---'];

  for (const [label, team] of [['HOME', matchup.home], ['AWAY', matchup.away]] as const) {
    if (!team) continue;
    sections.push(`\n${label}: ${team.teamName} (${team.record}) — KenPom #${team.kpRank}`);
    sections.push(`  AdjEM: ${team.adjEM?.toFixed(1)} | AdjOE: ${team.adjOE?.toFixed(1)} (#${team.rankAdjOE}) | AdjDE: ${team.adjDE?.toFixed(1)} (#${team.rankAdjDE}) | Tempo: ${team.adjTempo?.toFixed(1)}`);
    sections.push(`  SOS: #${team.sosRank} | Luck: ${team.luck?.toFixed(3)} | Conf: ${team.confShort} (#${team.confRank})`);

    if (team.efgPct != null) {
      sections.push(`  Four Factors (O): eFG=${team.efgPct?.toFixed(1)}% | TO%=${team.toPct?.toFixed(1)}% | OREB%=${team.orPct?.toFixed(1)}% | FTR=${team.ftRate?.toFixed(1)}`);
      sections.push(`  Four Factors (D): eFG=${team.deFgPct?.toFixed(1)}% | TO%=${team.dToPct?.toFixed(1)}% | DREB%=${team.dOrPct?.toFixed(1)}% | FTR=${team.dFtRate?.toFixed(1)}`);
    }
    if (team.experience != null) {
      sections.push(`  Experience: ${team.experience?.toFixed(2)} | Bench: ${team.bench?.toFixed(1)} | Continuity: ${team.continuity?.toFixed(2)} | Height: ${team.avgHgt?.toFixed(1)}"`);
    }
    if (team.fg3Pct != null) {
      sections.push(`  Shooting: 3P%=${team.fg3Pct?.toFixed(1)} | 2P%=${team.fg2Pct?.toFixed(1)} | Blk%=${team.blockPct?.toFixed(1)} | Ast%=${team.aRate?.toFixed(1)}`);
      sections.push(`  Opp Shooting: 3P%=${team.oppFg3Pct?.toFixed(1)} | 2P%=${team.oppFg2Pct?.toFixed(1)}`);
    }
  }

  if (matchup.fanmatch) {
    const fm = matchup.fanmatch;
    // homeWP may be stored as percentage (69) or decimal (0.69) — normalize for display
    const wpDisplay = fm.homeWP! > 1 ? fm.homeWP!.toFixed(0) : (fm.homeWP! * 100).toFixed(0);
    sections.push(`\nKENPOM PREDICTION: Home ${fm.homePred?.toFixed(0)}-${fm.visitorPred?.toFixed(0)} Away | Home Win Prob: ${wpDisplay}% | PredTempo: ${fm.predTempo?.toFixed(1)} | Thrill: ${fm.thrillScore?.toFixed(1)}`);
  }

  // Add KenPom-predicted MOV (AdjEM + home court)
  if (matchup.home && matchup.away) {
    const movResult = computeKenPomMOVScore(matchup);
    const favored = movResult.predictedMOV > 0 ? 'Home' : 'Away';
    sections.push(`\nKENPOM PREDICTED MOV: ${favored} by ${Math.abs(movResult.predictedMOV).toFixed(1)} points (includes ~3.3pt home court advantage)`);
    sections.push(`  Use this as a secondary anchor alongside market spread. If KenPom and market agree, confidence should be higher.`);
  }

  if (matchup.edgeSignals.emGap != null) {
    sections.push(`\nEDGE SIGNALS:`);
    sections.push(`  EM Gap: ${matchup.edgeSignals.emGap > 0 ? 'Home' : 'Away'} +${Math.abs(matchup.edgeSignals.emGap).toFixed(1)}`);
    if (matchup.edgeSignals.tempoMismatch) sections.push(`  TEMPO MISMATCH detected (>5pt gap)`);
    if (matchup.edgeSignals.offDefMismatch) sections.push(`  OFF/DEF MISMATCH: ${matchup.edgeSignals.offDefMismatch}`);
    if (matchup.edgeSignals.experienceEdge) sections.push(`  EXP EDGE: ${matchup.edgeSignals.experienceEdge}`);
    if (matchup.edgeSignals.luckWarning) sections.push(`  LUCK WARNING: ${matchup.edgeSignals.luckWarning}`);
    if (matchup.edgeSignals.sosAdjustment) sections.push(`  SOS: ${matchup.edgeSignals.sosAdjustment}`);
  }

  sections.push('--- END KENPOM ---');
  return sections.join('\n');
}
