/**
 * Rainmaker 1.0 — NCAA Tournament Contextual Intelligence Engine
 * 12 Layers of tournament-specific situational modeling
 *
 * Layers 1-11: Venue/Travel/Crowd/Arena/Referee/Fatigue/Experience/Seed/Coaching/Market/Player/Depth
 * Layer 12: Momentum & Form Modeling
 */

import pool from '../db';

// ════════════════════════════════════════════════════════════════
// TYPES
// ════════════════════════════════════════════════════════════════

export interface TravelReport {
  distance: number;       // miles
  travelTime: number;     // hours estimate
  classification: string; // same_city | same_state | neighboring | regional | long_distance
}

export interface MomentumProfile {
  offensiveMomentum: number;
  defensiveMomentum: number;
  shootingStability: number;
  playerStability: number;
  conferenceMomentum: number;
  momentumIndex: number;
  classification: string; // strong_positive | moderate_positive | neutral | declining | high_volatility
}

export interface TournamentContext {
  layer1_travel: {
    teamA: TravelReport;
    teamB: TravelReport;
    disparity: number;
    disparityScore: number;
  };
  layer2_crowd: {
    teamA: { score: number; level: string };
    teamB: { score: number; level: string };
    modifier: number;
  };
  layer3_arena: {
    venue: string;
    capacity: number;
    altitude: number;
    venueType: string;
    score: number;
  };
  layer4_referee: {
    available: boolean;
    score: number;
  };
  layer5_fatigue: {
    teamA: { restDays: number; score: number };
    teamB: { restDays: number; score: number };
  };
  layer6_experience: {
    teamA: { coachApps: number; coachWins: number; deepRuns: number; score: number };
    teamB: { coachApps: number; coachWins: number; deepRuns: number; score: number };
  };
  layer7_seedVolatility: {
    seedA: number;
    seedB: number;
    historicalUpsetRate: number;
    volatilityScore: number;
  };
  layer8_coachingMatchup: {
    tempoDiff: number;
    styleMismatch: string;
    score: number;
  };
  layer9_market: {
    lineMovement: number | null;
    sharpAction: string;
    score: number;
  };
  layer10_playerExploit: {
    teamA: { advantages: string[]; score: number };
    teamB: { advantages: string[]; score: number };
  };
  layer11_depth: {
    teamA: { benchPct: number; rotationSize: number; score: number };
    teamB: { benchPct: number; rotationSize: number; score: number };
  };
  layer12_momentum: {
    teamA: MomentumProfile;
    teamB: MomentumProfile;
  };
  finalAdjustment: number;
  adjustmentDirection: string;
  contextSummary: string;
}

// ════════════════════════════════════════════════════════════════
// CONSTANTS
// ════════════════════════════════════════════════════════════════

const LAYER_WEIGHTS = {
  travel: 0.10,
  crowd: 0.07,
  arena: 0.04,
  referee: 0.03,
  fatigue: 0.08,
  experience: 0.13,
  seedVolatility: 0.10,
  coachingMatchup: 0.09,
  market: 0.07,
  playerExploit: 0.09,
  depth: 0.06,
  momentum: 0.14,
};

const MAX_ADJUSTMENT = 0.12; // max ±12% shift on composite confidence

// Neighboring states map for travel classification
const NEIGHBORS: Record<string, string[]> = {
  AL: ['MS','TN','GA','FL'], AZ: ['NM','UT','NV','CA','CO'], AR: ['MO','TN','MS','LA','TX','OK'],
  CA: ['OR','NV','AZ'], CO: ['WY','NE','KS','OK','NM','UT'], CT: ['NY','MA','RI'],
  DE: ['MD','PA','NJ'], FL: ['GA','AL'], GA: ['FL','AL','TN','NC','SC'],
  ID: ['MT','WY','UT','NV','OR','WA'], IL: ['WI','IA','MO','KY','IN'], IN: ['IL','MI','OH','KY'],
  IA: ['MN','WI','IL','MO','NE','SD'], KS: ['NE','MO','OK','CO'], KY: ['IN','OH','WV','VA','TN','MO','IL'],
  LA: ['TX','AR','MS'], ME: ['NH'], MD: ['PA','DE','WV','VA','DC'], MA: ['NH','VT','NY','CT','RI'],
  MI: ['WI','IN','OH'], MN: ['WI','IA','SD','ND'], MS: ['TN','AL','LA','AR'],
  MO: ['IA','IL','KY','TN','AR','OK','KS','NE'], MT: ['ND','SD','WY','ID'],
  NE: ['SD','IA','MO','KS','CO','WY'], NV: ['OR','ID','UT','AZ','CA'], NH: ['ME','VT','MA'],
  NJ: ['NY','PA','DE'], NM: ['CO','OK','TX','AZ','UT'], NY: ['VT','MA','CT','NJ','PA'],
  NC: ['VA','TN','GA','SC'], ND: ['MN','SD','MT'], OH: ['MI','IN','KY','WV','PA'],
  OK: ['KS','MO','AR','TX','NM','CO'], OR: ['WA','ID','NV','CA'], PA: ['NY','NJ','DE','MD','WV','OH'],
  RI: ['MA','CT'], SC: ['NC','GA'], SD: ['ND','MN','IA','NE','WY','MT'],
  TN: ['KY','VA','NC','GA','AL','MS','AR','MO'], TX: ['OK','AR','LA','NM'],
  UT: ['ID','WY','CO','NM','AZ','NV'], VT: ['NH','MA','NY'], VA: ['MD','DC','WV','KY','TN','NC'],
  WA: ['ID','OR'], WV: ['OH','PA','MD','VA','KY'], WI: ['MN','IA','IL','MI'],
  WY: ['MT','SD','NE','CO','UT','ID'], DC: ['MD','VA'],
};

// ════════════════════════════════════════════════════════════════
// UTILITY FUNCTIONS
// ════════════════════════════════════════════════════════════════

function clamp(val: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, val));
}

function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
  const R = 3959; // Earth radius in miles
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLon = (lon2 - lon1) * Math.PI / 180;
  const a = Math.sin(dLat / 2) ** 2 +
    Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
    Math.sin(dLon / 2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

function classifyTravel(distance: number, teamState: string, venueState: string): string {
  if (teamState === venueState) {
    return distance < 50 ? 'same_city' : 'same_state';
  }
  if (NEIGHBORS[teamState]?.includes(venueState)) return 'neighboring';
  if (distance < 600) return 'regional';
  return 'long_distance';
}

function estimateTravelTime(distance: number): number {
  // Flight threshold ~300 miles, otherwise driving
  if (distance < 300) return distance / 55; // ~55 mph driving
  return 2 + (distance / 500); // 2h airport overhead + flight time
}

// ════════════════════════════════════════════════════════════════
// LAYER COMPUTATIONS
// ════════════════════════════════════════════════════════════════

async function computeTravel(
  teamA: string, teamB: string, venueName: string, season: number
): Promise<TournamentContext['layer1_travel']> {
  const neutral = { distance: 0, travelTime: 0, classification: 'unknown' as string };
  const defaultResult = { teamA: neutral, teamB: neutral, disparity: 0, disparityScore: 0.5 };

  try {
    // Get venue coords
    const venueRes = await pool.query(
      `SELECT latitude, longitude, city, state FROM rm_tournament_venues WHERE venue_name = $1 AND season = $2 LIMIT 1`,
      [venueName, season]
    );
    if (venueRes.rows.length === 0) return defaultResult;
    const venue = venueRes.rows[0];

    // Get team coords
    const geoA = await pool.query(`SELECT latitude, longitude, state FROM rm_team_geo WHERE team_name = $1 LIMIT 1`, [teamA]);
    const geoB = await pool.query(`SELECT latitude, longitude, state FROM rm_team_geo WHERE team_name = $1 LIMIT 1`, [teamB]);

    if (geoA.rows.length === 0 || geoB.rows.length === 0) return defaultResult;

    const distA = haversineDistance(geoA.rows[0].latitude, geoA.rows[0].longitude, venue.latitude, venue.longitude);
    const distB = haversineDistance(geoB.rows[0].latitude, geoB.rows[0].longitude, venue.latitude, venue.longitude);

    const reportA: TravelReport = {
      distance: Math.round(distA),
      travelTime: Math.round(estimateTravelTime(distA) * 10) / 10,
      classification: classifyTravel(distA, geoA.rows[0].state, venue.state),
    };
    const reportB: TravelReport = {
      distance: Math.round(distB),
      travelTime: Math.round(estimateTravelTime(distB) * 10) / 10,
      classification: classifyTravel(distB, geoB.rows[0].state, venue.state),
    };

    const disparity = Math.abs(distA - distB);
    // Score: 0.5 = neutral, <0.5 favors B (A travels more), >0.5 favors A (B travels more)
    const maxDisparity = 2000;
    let disparityScore = 0.5;
    if (distA < distB) {
      disparityScore = 0.5 + clamp(disparity / maxDisparity, 0, 0.5);
    } else {
      disparityScore = 0.5 - clamp(disparity / maxDisparity, 0, 0.5);
    }

    return { teamA: reportA, teamB: reportB, disparity: Math.round(disparity), disparityScore };
  } catch {
    return defaultResult;
  }
}

function computeCrowd(
  travelA: TravelReport, travelB: TravelReport
): TournamentContext['layer2_crowd'] {
  // Crowd advantage correlates with proximity
  function crowdScore(t: TravelReport): { score: number; level: string } {
    if (t.classification === 'same_city') return { score: 0.9, level: 'strong' };
    if (t.classification === 'same_state') return { score: 0.75, level: 'strong' };
    if (t.classification === 'neighboring') return { score: 0.6, level: 'moderate' };
    if (t.classification === 'regional') return { score: 0.45, level: 'moderate' };
    return { score: 0.3, level: 'low' };
  }

  const a = crowdScore(travelA);
  const b = crowdScore(travelB);
  // modifier: positive favors A, negative favors B
  const modifier = (a.score - b.score) / 2;

  return { teamA: a, teamB: b, modifier };
}

async function computeArena(
  venueName: string, season: number
): Promise<TournamentContext['layer3_arena']> {
  const defaultArena = { venue: venueName, capacity: 18000, altitude: 500, venueType: 'arena', score: 0.5 };

  try {
    const res = await pool.query(
      `SELECT capacity, altitude_ft, venue_type FROM rm_tournament_venues WHERE venue_name = $1 AND season = $2 LIMIT 1`,
      [venueName, season]
    );
    if (res.rows.length === 0) return defaultArena;

    const { capacity, altitude_ft, venue_type } = res.rows[0];

    // Higher altitude = slightly harder shooting (above 4000ft meaningful)
    // Larger venue = different atmosphere (stadium vs arena)
    let score = 0.5;
    if (altitude_ft > 4000) score -= 0.05; // altitude drag
    if (venue_type === 'stadium') score -= 0.03; // stadium shooting difficulty (depth perception)
    if (capacity > 50000) score -= 0.02; // massive venue atmosphere change

    return { venue: venueName, capacity, altitude: altitude_ft, venueType: venue_type, score: clamp(score, 0, 1) };
  } catch {
    return defaultArena;
  }
}

async function computeExperience(
  teamA: string, teamB: string
): Promise<TournamentContext['layer6_experience']> {
  const defaultExp = { coachApps: 0, coachWins: 0, deepRuns: 0, score: 0.3 };

  async function getCoachExp(team: string) {
    try {
      const res = await pool.query(
        `SELECT tourney_appearances, tourney_wins, sweet16, elite8, final4, championship_games, titles
         FROM rm_coach_tourney_history WHERE current_team = $1 LIMIT 1`,
        [team]
      );
      if (res.rows.length === 0) return defaultExp;
      const c = res.rows[0];
      const deepRuns = (c.sweet16 || 0) + (c.elite8 || 0) * 2 + (c.final4 || 0) * 3 + (c.titles || 0) * 5;

      // Score: normalized experience (0-1)
      let score = 0.3; // baseline
      score += clamp((c.tourney_appearances || 0) / 25, 0, 0.25);
      score += clamp((c.tourney_wins || 0) / 50, 0, 0.25);
      score += clamp(deepRuns / 30, 0, 0.2);

      return {
        coachApps: c.tourney_appearances || 0,
        coachWins: c.tourney_wins || 0,
        deepRuns,
        score: clamp(score, 0, 1),
      };
    } catch {
      return defaultExp;
    }
  }

  const [expA, expB] = await Promise.all([getCoachExp(teamA), getCoachExp(teamB)]);
  return {
    teamA: expA,
    teamB: expB,
  };
}

async function computeSeedVolatility(
  seedA: number, seedB: number
): Promise<TournamentContext['layer7_seedVolatility']> {
  const highSeed = Math.min(seedA, seedB);
  const lowSeed = Math.max(seedA, seedB);

  try {
    const res = await pool.query(
      `SELECT upset_rate FROM rm_seed_upset_history WHERE high_seed = $1 AND low_seed = $2 AND round = 'first_round' LIMIT 1`,
      [highSeed, lowSeed]
    );
    const upsetRate = res.rows.length > 0 ? res.rows[0].upset_rate : estimateUpsetRate(highSeed, lowSeed);
    return {
      seedA, seedB,
      historicalUpsetRate: upsetRate,
      volatilityScore: upsetRate, // Higher = more volatile/upset-prone
    };
  } catch {
    return { seedA, seedB, historicalUpsetRate: 0.3, volatilityScore: 0.3 };
  }
}

function estimateUpsetRate(high: number, low: number): number {
  // Fallback estimation based on seed gap
  const gap = low - high;
  if (gap <= 1) return 0.48;
  if (gap <= 3) return 0.38;
  if (gap <= 5) return 0.28;
  if (gap <= 8) return 0.15;
  if (gap <= 12) return 0.06;
  return 0.02;
}

async function computeCoachingMatchup(
  teamA: string, teamB: string
): Promise<TournamentContext['layer8_coachingMatchup']> {
  try {
    // Pull KenPom tempo data
    const res = await pool.query(
      `SELECT team_name, adj_tempo, adj_oe, adj_de FROM kp_ratings WHERE team_name IN ($1, $2)`,
      [teamA, teamB]
    );

    if (res.rows.length < 2) return { tempoDiff: 0, styleMismatch: 'unknown', score: 0.5 };

    const a = res.rows.find((r: any) => r.team_name === teamA) || res.rows[0];
    const b = res.rows.find((r: any) => r.team_name === teamB) || res.rows[1];

    const tempoDiff = Math.abs(a.adj_tempo - b.adj_tempo);

    // Style mismatch: big tempo differences create volatility
    let mismatch = 'neutral';
    let score = 0.5;
    if (tempoDiff > 8) { mismatch = 'major_tempo_clash'; score = 0.7; }
    else if (tempoDiff > 5) { mismatch = 'moderate_tempo_diff'; score = 0.6; }
    else if (tempoDiff > 3) { mismatch = 'slight_tempo_diff'; score = 0.55; }

    // Offense-heavy vs defense-heavy
    const oeGap = Math.abs(a.adj_oe - b.adj_oe);
    const deGap = Math.abs(a.adj_de - b.adj_de);
    if (oeGap > 10 && deGap > 10) { mismatch = 'style_clash'; score = 0.75; }

    return { tempoDiff: Math.round(tempoDiff * 10) / 10, styleMismatch: mismatch, score };
  } catch {
    return { tempoDiff: 0, styleMismatch: 'unknown', score: 0.5 };
  }
}

async function computeMarketSentiment(
  eventId: string | null
): Promise<TournamentContext['layer9_market']> {
  if (!eventId) return { lineMovement: null, sharpAction: 'none', score: 0.5 };

  try {
    const res = await pool.query(
      `SELECT market_type, opening_line, current_line, movement_pct
       FROM "LineMovement" WHERE event_id = $1 ORDER BY updated_at DESC LIMIT 5`,
      [eventId]
    );
    if (res.rows.length === 0) return { lineMovement: null, sharpAction: 'none', score: 0.5 };

    // Average line movement magnitude
    const movements = res.rows.map((r: any) => Math.abs(r.movement_pct || 0));
    const avgMovement = movements.reduce((s: number, m: number) => s + m, 0) / movements.length;

    let sharpAction = 'none';
    let score = 0.5;
    if (avgMovement > 15) { sharpAction = 'strong_sharp_action'; score = 0.8; }
    else if (avgMovement > 8) { sharpAction = 'moderate_movement'; score = 0.65; }
    else if (avgMovement > 3) { sharpAction = 'slight_movement'; score = 0.55; }

    return { lineMovement: Math.round(avgMovement * 10) / 10, sharpAction, score };
  } catch {
    return { lineMovement: null, sharpAction: 'none', score: 0.5 };
  }
}

async function computePlayerExploitation(
  teamA: string, teamB: string, league: string
): Promise<TournamentContext['layer10_playerExploit']> {
  const defaultResult = { advantages: [] as string[], score: 0.5 };

  try {
    // Use KenPom height data for size mismatches
    const heightRes = await pool.query(
      `SELECT team_name, avg_hgt, experience FROM kp_height WHERE team_name IN ($1, $2)`,
      [teamA, teamB]
    );

    // Use DVP data for positional advantages
    const dvpRes = await pool.query(
      `SELECT team_abbr, position, stat_key, rank, tier
       FROM "DefenseVsPosition" WHERE league = $1 AND team_abbr IN (
         SELECT canonical_name FROM kp_team_map WHERE kp_name IN ($2, $3) AND canonical_name IS NOT NULL
       ) AND rank <= 5
       ORDER BY rank LIMIT 20`,
      [league, teamA, teamB]
    ).catch(() => ({ rows: [] }));

    const aAdvantages: string[] = [];
    const bAdvantages: string[] = [];

    if (heightRes.rows.length === 2) {
      const hA = heightRes.rows.find((r: any) => r.team_name === teamA);
      const hB = heightRes.rows.find((r: any) => r.team_name === teamB);
      if (hA && hB) {
        const heightDiff = (hA.avg_hgt || 0) - (hB.avg_hgt || 0);
        if (heightDiff > 1.5) aAdvantages.push(`Size advantage (+${heightDiff.toFixed(1)}" avg height)`);
        if (heightDiff < -1.5) bAdvantages.push(`Size advantage (+${Math.abs(heightDiff).toFixed(1)}" avg height)`);

        const expDiff = (hA.experience || 0) - (hB.experience || 0);
        if (expDiff > 0.5) aAdvantages.push(`Experience edge (+${expDiff.toFixed(1)} yrs)`);
        if (expDiff < -0.5) bAdvantages.push(`Experience edge (+${Math.abs(expDiff).toFixed(1)} yrs)`);
      }
    }

    // DVP advantages
    for (const row of dvpRes.rows) {
      if (row.tier === 'elite' || row.rank <= 3) {
        // If team B is weak at defending a position, team A has the advantage
        // Simplified: just note the elite DVP matchups
        aAdvantages.push(`DVP exploit: ${row.position} ${row.stat_key} (rank ${row.rank})`);
      }
    }

    const scoreA = clamp(0.5 + aAdvantages.length * 0.08, 0, 1);
    const scoreB = clamp(0.5 + bAdvantages.length * 0.08, 0, 1);

    return {
      teamA: { advantages: aAdvantages.slice(0, 5), score: scoreA },
      teamB: { advantages: bAdvantages.slice(0, 5), score: scoreB },
    };
  } catch {
    return { teamA: defaultResult, teamB: defaultResult };
  }
}

async function computeDepthVolatility(
  teamA: string, teamB: string
): Promise<TournamentContext['layer11_depth']> {
  const defaultDepth = { benchPct: 0, rotationSize: 8, score: 0.5 };

  try {
    // Use KenPom bench minutes and height/experience for depth proxy
    const res = await pool.query(
      `SELECT r.team_name, h.bench_minutes, h.experience, h.continuity
       FROM kp_ratings r
       LEFT JOIN kp_height h ON r.team_name = h.team_name AND r.season = h.season
       WHERE r.team_name IN ($1, $2)`,
      [teamA, teamB]
    );

    function depthFromRow(row: any) {
      if (!row) return defaultDepth;
      const bench = row.bench_minutes || 0;
      const continuity = row.continuity || 0;

      // Higher bench minutes = deeper team
      // Higher continuity = more stable rotation
      let score = 0.5;
      if (bench > 35) score += 0.15; // deep bench
      else if (bench < 20) score -= 0.15; // shallow rotation
      if (continuity > 0.5) score += 0.1;
      else if (continuity < 0.2) score -= 0.1;

      return { benchPct: Math.round(bench * 10) / 10, rotationSize: bench > 30 ? 9 : bench > 20 ? 8 : 7, score: clamp(score, 0, 1) };
    }

    const rowA = res.rows.find((r: any) => r.team_name === teamA);
    const rowB = res.rows.find((r: any) => r.team_name === teamB);

    return { teamA: depthFromRow(rowA), teamB: depthFromRow(rowB) };
  } catch {
    return { teamA: defaultDepth, teamB: defaultDepth };
  }
}

// ════════════════════════════════════════════════════════════════
// LAYER 12: MOMENTUM & FORM MODELING
// ════════════════════════════════════════════════════════════════

async function computeMomentum(teamName: string): Promise<MomentumProfile> {
  const neutral: MomentumProfile = {
    offensiveMomentum: 0.5, defensiveMomentum: 0.5, shootingStability: 0.5,
    playerStability: 0.5, conferenceMomentum: 0.5, momentumIndex: 0.5,
    classification: 'neutral',
  };

  try {
    // Pull all KenPom data in parallel
    const [kpRes, ffRes, miscRes, contRes] = await Promise.all([
      pool.query(`SELECT adj_oe, adj_de, adj_tempo, luck FROM kp_ratings WHERE team_name = $1 LIMIT 1`, [teamName]),
      pool.query(`SELECT efg_pct, to_pct, or_pct, ft_rate FROM kp_four_factors WHERE team_name = $1 LIMIT 1`, [teamName]),
      pool.query(`SELECT fg3_pct, fg2_pct, ft_pct, opp_fg3_pct, opp_fg2_pct FROM kp_misc_stats WHERE team_name = $1 LIMIT 1`, [teamName]),
      pool.query(`SELECT continuity, experience FROM kp_height WHERE team_name = $1 LIMIT 1`, [teamName]),
    ]);

    if (kpRes.rows.length === 0) return neutral;
    const kp = kpRes.rows[0];
    const ff = ffRes.rows[0] || {};
    const misc = miscRes.rows[0] || {};
    const cont = contRes.rows[0] || {};

    // Offensive Momentum: high AdjOE = strong offense
    const oeBaseline = 100;
    const oeMomentum = clamp(0.5 + (kp.adj_oe - oeBaseline) * 0.01, 0.1, 0.95);

    // Defensive Momentum: low AdjDE = strong defense
    const deBaseline = 100;
    const deMomentum = clamp(0.5 + (deBaseline - kp.adj_de) * 0.01, 0.1, 0.95);

    // Shooting Stability: eFG% and 3PT%
    const efgBaseline = 50;
    const shootStability = clamp(0.5 + ((ff.efg_pct || efgBaseline) - efgBaseline) * 0.02, 0.1, 0.95);

    // Player Stability: use continuity from height table (already fetched in parallel above)
    const playerStab = clamp(0.3 + (cont.continuity || 0.3) * 0.8 + (cont.experience || 1) * 0.1, 0.1, 0.95);

    // Conference Momentum: use luck as proxy for recent over/underperformance
    // Positive luck = winning more than expected (could indicate hot streak or regression risk)
    const luck = kp.luck || 0;
    const confMomentum = clamp(0.5 + luck * 2, 0.2, 0.8);

    // Composite Momentum Index
    const momentumIndex =
      oeMomentum * 0.25 +
      deMomentum * 0.25 +
      shootStability * 0.20 +
      playerStab * 0.15 +
      confMomentum * 0.15;

    // Classification
    let classification = 'neutral';
    if (momentumIndex >= 0.72) classification = 'strong_positive';
    else if (momentumIndex >= 0.60) classification = 'moderate_positive';
    else if (momentumIndex >= 0.45) classification = 'neutral';
    else if (momentumIndex >= 0.35) classification = 'declining';
    else classification = 'high_volatility';

    // Check for volatility: if luck is extreme, flag as volatile
    if (Math.abs(luck) > 0.06) {
      if (classification === 'strong_positive' || classification === 'moderate_positive') {
        classification = 'high_volatility'; // luck-inflated performance
      }
    }

    return {
      offensiveMomentum: Math.round(oeMomentum * 1000) / 1000,
      defensiveMomentum: Math.round(deMomentum * 1000) / 1000,
      shootingStability: Math.round(shootStability * 1000) / 1000,
      playerStability: Math.round(playerStab * 1000) / 1000,
      conferenceMomentum: Math.round(confMomentum * 1000) / 1000,
      momentumIndex: Math.round(momentumIndex * 1000) / 1000,
      classification,
    };
  } catch {
    return neutral;
  }
}

// ════════════════════════════════════════════════════════════════
// MAIN ENGINE
// ════════════════════════════════════════════════════════════════

/**
 * Check if tournament mode is active (bracket has been populated).
 */
export async function isTournamentActive(season: number = 2026): Promise<boolean> {
  try {
    const res = await pool.query(
      `SELECT count(*) as ct FROM rm_tournament_bracket WHERE season = $1`,
      [season]
    );
    return parseInt(res.rows[0].ct) > 0;
  } catch {
    return false;
  }
}

/**
 * Get venue for a matchup based on bracket assignments.
 */
export async function getMatchupVenue(
  teamA: string, teamB: string, season: number, round: string
): Promise<string | null> {
  try {
    const res = await pool.query(
      `SELECT first_round_venue FROM rm_tournament_bracket
       WHERE season = $1 AND team_name IN ($2, $3) AND first_round_venue IS NOT NULL LIMIT 1`,
      [season, teamA, teamB]
    );
    return res.rows.length > 0 ? res.rows[0].first_round_venue : null;
  } catch {
    return null;
  }
}

/**
 * Get seeds for teams from the bracket.
 */
async function getSeeds(teamA: string, teamB: string, season: number): Promise<{ seedA: number; seedB: number }> {
  try {
    const res = await pool.query(
      `SELECT team_name, seed FROM rm_tournament_bracket WHERE season = $1 AND team_name IN ($2, $3)`,
      [season, teamA, teamB]
    );
    let seedA = 8, seedB = 8; // default
    for (const row of res.rows) {
      if (row.team_name === teamA) seedA = row.seed;
      if (row.team_name === teamB) seedB = row.seed;
    }
    return { seedA, seedB };
  } catch {
    return { seedA: 8, seedB: 8 };
  }
}

/**
 * Compute all 12 contextual intelligence layers for a tournament matchup.
 */
export async function computeTournamentContext(params: {
  teamA: string;
  teamB: string;
  venue?: string;
  round?: string;
  eventId?: string;
  season?: number;
  league?: string;
}): Promise<TournamentContext> {
  const { teamA, teamB, round = 'first_round', eventId = null, league = 'ncaab' } = params;
  const season = params.season || 2026;

  // Determine venue
  let venue = params.venue || await getMatchupVenue(teamA, teamB, season, round) || 'Unknown Venue';
  const { seedA, seedB } = await getSeeds(teamA, teamB, season);

  // Compute all layers in parallel
  const [
    travel, arena, experience, seedVol, coachMatch,
    market, playerExploit, depth, momentumA, momentumB,
  ] = await Promise.all([
    computeTravel(teamA, teamB, venue, season),
    computeArena(venue, season),
    computeExperience(teamA, teamB),
    computeSeedVolatility(seedA, seedB),
    computeCoachingMatchup(teamA, teamB),
    computeMarketSentiment(eventId),
    computePlayerExploitation(teamA, teamB, league),
    computeDepthVolatility(teamA, teamB),
    computeMomentum(teamA),
    computeMomentum(teamB),
  ]);

  const crowd = computeCrowd(travel.teamA, travel.teamB);

  // Layer 4 (Referee) — neutral until assignments available
  const referee = { available: false, score: 0.5 };

  // Layer 5 (Fatigue) — estimated from round schedule
  const fatigue = {
    teamA: { restDays: round === 'first_round' ? 5 : round === 'second_round' ? 1 : 3, score: 0.5 },
    teamB: { restDays: round === 'first_round' ? 5 : round === 'second_round' ? 1 : 3, score: 0.5 },
  };
  // Shorter rest = higher fatigue risk
  if (fatigue.teamA.restDays <= 1) fatigue.teamA.score = 0.35;
  if (fatigue.teamB.restDays <= 1) fatigue.teamB.score = 0.35;

  // ── Final Contextual Adjustment ──
  // Each layer produces a directional signal. Positive = favors A, negative = favors B.
  const signals: number[] = [];

  // Travel: closer team gets advantage
  signals.push((travel.disparityScore - 0.5) * 2 * LAYER_WEIGHTS.travel);

  // Crowd
  signals.push(crowd.modifier * LAYER_WEIGHTS.crowd);

  // Arena: neutral modifier (same for both)
  signals.push(0);

  // Referee: neutral
  signals.push(0);

  // Fatigue: compare scores
  signals.push((fatigue.teamA.score - fatigue.teamB.score) * LAYER_WEIGHTS.fatigue);

  // Experience
  signals.push((experience.teamA.score - experience.teamB.score) * LAYER_WEIGHTS.experience);

  // Seed volatility: higher upset rate = slight underdog boost (compress toward 0)
  const seedSignal = seedA < seedB
    ? -seedVol.volatilityScore * 0.3 * LAYER_WEIGHTS.seedVolatility  // underdog potential
    : seedVol.volatilityScore * 0.3 * LAYER_WEIGHTS.seedVolatility;
  signals.push(seedSignal);

  // Coaching matchup: high volatility slightly favors underdog
  signals.push(0); // neutral without directional data

  // Market: neutral without directional data
  signals.push(0);

  // Player exploitation
  signals.push((playerExploit.teamA.score - playerExploit.teamB.score) * LAYER_WEIGHTS.playerExploit);

  // Depth
  signals.push((depth.teamA.score - depth.teamB.score) * LAYER_WEIGHTS.depth);

  // Momentum
  signals.push((momentumA.momentumIndex - momentumB.momentumIndex) * LAYER_WEIGHTS.momentum);

  const rawAdjustment = signals.reduce((s, v) => s + v, 0);
  const finalAdjustment = clamp(rawAdjustment, -MAX_ADJUSTMENT, MAX_ADJUSTMENT);

  let adjustmentDirection = 'neutral';
  if (finalAdjustment > 0.02) adjustmentDirection = 'favors_a';
  else if (finalAdjustment < -0.02) adjustmentDirection = 'favors_b';

  // Build summary
  const summaryParts: string[] = [];
  if (Math.abs(travel.disparity) > 500) {
    summaryParts.push(`Travel disparity: ${travel.disparity}mi`);
  }
  if (crowd.modifier > 0.1) summaryParts.push(`Crowd favors ${teamA}`);
  else if (crowd.modifier < -0.1) summaryParts.push(`Crowd favors ${teamB}`);
  if (experience.teamA.score - experience.teamB.score > 0.15) summaryParts.push(`${teamA} coach more experienced`);
  else if (experience.teamB.score - experience.teamA.score > 0.15) summaryParts.push(`${teamB} coach more experienced`);
  if (momentumA.classification === 'strong_positive') summaryParts.push(`${teamA}: strong momentum`);
  if (momentumB.classification === 'strong_positive') summaryParts.push(`${teamB}: strong momentum`);
  if (momentumA.classification === 'high_volatility') summaryParts.push(`${teamA}: volatile (luck-inflated)`);
  if (momentumB.classification === 'high_volatility') summaryParts.push(`${teamB}: volatile (luck-inflated)`);
  if (seedVol.volatilityScore > 0.35) summaryParts.push(`${seedA}v${seedB}: historically upset-prone`);

  const contextSummary = summaryParts.length > 0 ? summaryParts.join('; ') : 'No significant contextual factors';

  return {
    layer1_travel: travel,
    layer2_crowd: crowd,
    layer3_arena: arena,
    layer4_referee: referee,
    layer5_fatigue: fatigue,
    layer6_experience: experience,
    layer7_seedVolatility: { seedA, seedB, historicalUpsetRate: seedVol.historicalUpsetRate, volatilityScore: seedVol.volatilityScore },
    layer8_coachingMatchup: coachMatch,
    layer9_market: market,
    layer10_playerExploit: playerExploit,
    layer11_depth: depth,
    layer12_momentum: { teamA: momentumA, teamB: momentumB },
    finalAdjustment: Math.round(finalAdjustment * 10000) / 10000,
    adjustmentDirection,
    contextSummary,
  };
}

/**
 * Format tournament context for Grok prompt injection.
 */
export function formatTournamentContextForPrompt(ctx: TournamentContext, teamA: string, teamB: string): string {
  const lines: string[] = [
    `\n── NCAA TOURNAMENT CONTEXTUAL INTELLIGENCE ──`,
    `Venue: ${ctx.layer3_arena.venue} (${ctx.layer3_arena.venueType}, ${ctx.layer3_arena.capacity.toLocaleString()} cap, ${ctx.layer3_arena.altitude}ft altitude)`,
    ``,
    `TRAVEL ANALYSIS:`,
    `  ${teamA}: ${ctx.layer1_travel.teamA.distance}mi (${ctx.layer1_travel.teamA.classification}), ~${ctx.layer1_travel.teamA.travelTime}hr`,
    `  ${teamB}: ${ctx.layer1_travel.teamB.distance}mi (${ctx.layer1_travel.teamB.classification}), ~${ctx.layer1_travel.teamB.travelTime}hr`,
    `  Disparity: ${ctx.layer1_travel.disparity}mi`,
    ``,
    `CROWD ADVANTAGE:`,
    `  ${teamA}: ${ctx.layer2_crowd.teamA.level} (${(ctx.layer2_crowd.teamA.score * 100).toFixed(0)}%)`,
    `  ${teamB}: ${ctx.layer2_crowd.teamB.level} (${(ctx.layer2_crowd.teamB.score * 100).toFixed(0)}%)`,
    ``,
    `COACHING EXPERIENCE:`,
    `  ${teamA}: ${ctx.layer6_experience.teamA.coachApps} tournament apps, ${ctx.layer6_experience.teamA.coachWins} wins`,
    `  ${teamB}: ${ctx.layer6_experience.teamB.coachApps} tournament apps, ${ctx.layer6_experience.teamB.coachWins} wins`,
    ``,
    `SEED MATCHUP: ${ctx.layer7_seedVolatility.seedA} vs ${ctx.layer7_seedVolatility.seedB}`,
    `  Historical upset rate: ${(ctx.layer7_seedVolatility.historicalUpsetRate * 100).toFixed(1)}%`,
    ``,
    `COACHING STYLE:`,
    `  Tempo diff: ${ctx.layer8_coachingMatchup.tempoDiff} poss/game`,
    `  Mismatch: ${ctx.layer8_coachingMatchup.styleMismatch}`,
    ``,
    `MOMENTUM & FORM:`,
    `  ${teamA}: ${ctx.layer12_momentum.teamA.classification} (index: ${ctx.layer12_momentum.teamA.momentumIndex})`,
    `    Off: ${ctx.layer12_momentum.teamA.offensiveMomentum} | Def: ${ctx.layer12_momentum.teamA.defensiveMomentum} | Shoot: ${ctx.layer12_momentum.teamA.shootingStability}`,
    `  ${teamB}: ${ctx.layer12_momentum.teamB.classification} (index: ${ctx.layer12_momentum.teamB.momentumIndex})`,
    `    Off: ${ctx.layer12_momentum.teamB.offensiveMomentum} | Def: ${ctx.layer12_momentum.teamB.defensiveMomentum} | Shoot: ${ctx.layer12_momentum.teamB.shootingStability}`,
    ``,
    `DEPTH & ROSTER:`,
    `  ${teamA}: bench ${ctx.layer11_depth.teamA.benchPct}%, ~${ctx.layer11_depth.teamA.rotationSize}-man rotation`,
    `  ${teamB}: bench ${ctx.layer11_depth.teamB.benchPct}%, ~${ctx.layer11_depth.teamB.rotationSize}-man rotation`,
  ];

  if (ctx.layer10_playerExploit.teamA.advantages.length > 0) {
    lines.push(``, `MATCHUP ADVANTAGES (${teamA}):`);
    for (const adv of ctx.layer10_playerExploit.teamA.advantages) {
      lines.push(`  • ${adv}`);
    }
  }
  if (ctx.layer10_playerExploit.teamB.advantages.length > 0) {
    lines.push(``, `MATCHUP ADVANTAGES (${teamB}):`);
    for (const adv of ctx.layer10_playerExploit.teamB.advantages) {
      lines.push(`  • ${adv}`);
    }
  }

  lines.push(
    ``,
    `CONTEXTUAL ADJUSTMENT: ${ctx.finalAdjustment > 0 ? '+' : ''}${(ctx.finalAdjustment * 100).toFixed(1)}% (${ctx.adjustmentDirection})`,
    `CONTEXT SUMMARY: ${ctx.contextSummary}`,
  );

  return lines.join('\n');
}

/**
 * Store computed context in the database.
 */
export async function storeTournamentContext(
  ctx: TournamentContext, teamA: string, teamB: string, season: number, eventId?: string, round?: string
): Promise<void> {
  try {
    await pool.query(`
      INSERT INTO rm_tournament_context (
        season, event_id, team_a, team_b, seed_a, seed_b, venue, round,
        travel_distance_a, travel_distance_b, travel_time_a, travel_time_b,
        travel_class_a, travel_class_b, travel_disparity,
        crowd_score_a, crowd_score_b, crowd_modifier,
        arena_env_score, referee_score,
        fatigue_score_a, fatigue_score_b, experience_score_a, experience_score_b,
        seed_volatility, coaching_matchup, market_sentiment,
        player_exploit_a, player_exploit_b, depth_vol_a, depth_vol_b,
        contextual_adjustment, adjustment_direction, contextual_detail
      ) VALUES (
        $1, $2, $3, $4, $5, $6, $7, $8,
        $9, $10, $11, $12, $13, $14, $15,
        $16, $17, $18,
        $19, $20,
        $21, $22, $23, $24,
        $25, $26, $27,
        $28, $29, $30, $31,
        $32, $33, $34
      ) ON CONFLICT (season, team_a, team_b, round) DO UPDATE SET
        event_id = COALESCE(EXCLUDED.event_id, rm_tournament_context.event_id),
        contextual_adjustment = EXCLUDED.contextual_adjustment,
        adjustment_direction = EXCLUDED.adjustment_direction,
        contextual_detail = EXCLUDED.contextual_detail,
        computed_at = NOW()
    `, [
      season, eventId, teamA, teamB,
      ctx.layer7_seedVolatility.seedA, ctx.layer7_seedVolatility.seedB,
      ctx.layer3_arena.venue, round || 'first_round',
      ctx.layer1_travel.teamA.distance, ctx.layer1_travel.teamB.distance,
      ctx.layer1_travel.teamA.travelTime, ctx.layer1_travel.teamB.travelTime,
      ctx.layer1_travel.teamA.classification, ctx.layer1_travel.teamB.classification,
      ctx.layer1_travel.disparity,
      ctx.layer2_crowd.teamA.score, ctx.layer2_crowd.teamB.score, ctx.layer2_crowd.modifier,
      ctx.layer3_arena.score, ctx.layer4_referee.score,
      ctx.layer5_fatigue.teamA.score, ctx.layer5_fatigue.teamB.score,
      ctx.layer6_experience.teamA.score, ctx.layer6_experience.teamB.score,
      ctx.layer7_seedVolatility.volatilityScore,
      ctx.layer8_coachingMatchup.score, ctx.layer9_market.score,
      ctx.layer10_playerExploit.teamA.score, ctx.layer10_playerExploit.teamB.score,
      ctx.layer11_depth.teamA.score, ctx.layer11_depth.teamB.score,
      ctx.finalAdjustment, ctx.adjustmentDirection,
      JSON.stringify(ctx),
    ]);
  } catch (err) {
    console.error('[tournament-context] Failed to store context:', err);
  }
}
