import pool from '../db';
import { SgoEvent, parseOdds } from './sgo';
import { generateForecast, ForecastResult } from './grok';
import { getCachedForecast, cacheForecast, updateCachedOdds } from '../models/forecast';
import { getPiffPropsForGame, loadPiffPropsForDate } from './piff';
import { getDigimonForGame, loadDigimonPicksForDate } from './digimon';
import { getDvpForMatchup } from './dvp';
import { calculateComposite, CompositeResult } from './composite-score';
import { getKenPomMatchup } from './kenpom';
import { getCornerScoutForGame, isSoccerLeague } from './corner-scout';
import { isTournamentActive, computeTournamentContext, TournamentContext } from '../tournament/context-engine';
import { runNarrativeEngine, NarrativeContext, NarrativeMetadata } from './narrative-engine';
import { deriveSpreadSignal } from './grok';
import { applyLegacyConfidenceAdjustment, buildIntelligence, formatStoredModelSignals, getLegacyCompositeView, mapToLegacyComposite } from './rie';
import { fetchInsightSourceData } from './insight-source';
import { getCurrentEtDateKey, getEtDateKey } from '../lib/league-windows';
import { buildLiveTeamPlusDecision, buildMlbBaselineOverride, type TeamPlusDecision } from './team-plus';
import {
  extractMlbDirectFeatures,
  MLB_DIRECT_MODEL_LIVE_CONFIG,
  MLB_DIRECT_MODEL_LIVE_POLICY,
  scoreMlbDirectModel,
} from './mlb-team-direct-model';
import type { SignalResult } from './rie/types';

const RIE_ENABLED = process.env.RIE_ENABLED === 'true';
const TEAM_PLUS_LOCK_THRESHOLD: Partial<Record<'mlb' | 'nba' | 'nhl', number>> = {
  nhl: 0.68,
};

type CuratedForecastRow = {
  event_id: string;
  league: string;
  home_team: string;
  away_team: string;
  home_short?: string | null;
  away_short?: string | null;
  starts_at: string;
  moneyline?: { home?: number | null; away?: number | null } | null;
  spread?: {
    home?: { line?: number | null; odds?: number | null } | null;
    away?: { line?: number | null; odds?: number | null } | null;
  } | null;
  total?: {
    over?: { line?: number | null; odds?: number | null } | null;
    under?: { line?: number | null; odds?: number | null } | null;
  } | null;
};

function buildSyntheticSgoOdds(curated: CuratedForecastRow): Record<string, any> {
  const odds: Record<string, any> = {};

  if (curated.moneyline?.home != null) {
    odds['points-home-game-ml-home'] = { bookOdds: String(curated.moneyline.home) };
  }
  if (curated.moneyline?.away != null) {
    odds['points-away-game-ml-away'] = { bookOdds: String(curated.moneyline.away) };
  }
  if (curated.spread?.home?.line != null || curated.spread?.home?.odds != null) {
    odds['points-home-game-sp-home'] = {
      bookSpread: curated.spread?.home?.line != null ? String(curated.spread.home.line) : undefined,
      bookOdds: curated.spread?.home?.odds != null ? String(curated.spread.home.odds) : undefined,
    };
  }
  if (curated.spread?.away?.line != null || curated.spread?.away?.odds != null) {
    odds['points-away-game-sp-away'] = {
      bookSpread: curated.spread?.away?.line != null ? String(curated.spread.away.line) : undefined,
      bookOdds: curated.spread?.away?.odds != null ? String(curated.spread.away.odds) : undefined,
    };
  }
  if (curated.total?.over?.line != null || curated.total?.over?.odds != null) {
    odds['points-all-game-ou-over'] = {
      bookOverUnder: curated.total?.over?.line != null ? String(curated.total.over.line) : undefined,
      bookOdds: curated.total?.over?.odds != null ? String(curated.total.over.odds) : undefined,
    };
  }
  if (curated.total?.under?.line != null || curated.total?.under?.odds != null) {
    odds['points-all-game-ou-under'] = {
      bookOverUnder: curated.total?.under?.line != null ? String(curated.total.under.line) : undefined,
      bookOdds: curated.total?.under?.odds != null ? String(curated.total.under.odds) : undefined,
    };
  }

  return odds;
}

export async function buildForecastFromCuratedEvent(
  curated: CuratedForecastRow,
  options?: { ignoreCache?: boolean },
) {
  const syntheticEvent: SgoEvent = {
    eventID: curated.event_id,
    teams: {
      home: { names: { long: curated.home_team, short: curated.home_short || '' } },
      away: { names: { long: curated.away_team, short: curated.away_short || '' } },
    },
    status: {
      startsAt: curated.starts_at,
      displayShort: 'Scheduled',
      oddsPresent: true,
    },
    odds: buildSyntheticSgoOdds(curated),
  };

  return buildForecast(syntheticEvent, curated.league, options);
}

/** Check if odds have real values (not all null/zero) */
function hasRealOdds(odds: { moneyline: any; spread: any; total: any }): boolean {
  return (
    (odds.moneyline.home !== null && odds.moneyline.home !== 0) ||
    (odds.moneyline.away !== null && odds.moneyline.away !== 0) ||
    (odds.spread.home?.line && odds.spread.home.line !== 0) ||
    (odds.total.over?.line && odds.total.over.line !== 0)
  );
}

function hasRealSideOdds(odds: { moneyline: any; spread: any; total: any }): boolean {
  return (
    (odds.moneyline.home !== null && odds.moneyline.home !== 0) ||
    (odds.moneyline.away !== null && odds.moneyline.away !== 0) ||
    (odds.spread.home?.line != null && odds.spread.home.line !== 0) ||
    (odds.spread.away?.line != null && odds.spread.away.line !== 0)
  );
}

type StandardGameOdds = {
  moneyline: { home: number | null; away: number | null };
  spread: { home: { line: number | null; odds: number | null } | null; away: { line: number | null; odds: number | null } | null };
  total: { over: { line: number | null; odds: number | null } | null; under: { line: number | null; odds: number | null } | null };
};

const MLB_GAME_ODDS_BOOK_PRIORITY = ['fanduel', 'draftkings', 'betmgm', 'betrivers', 'fanatics', 'williamhill_us', 'betonlineag', 'lowvig'];

function bookmakerPriority(bookmaker: string | null | undefined): number {
  const idx = MLB_GAME_ODDS_BOOK_PRIORITY.indexOf(String(bookmaker || '').trim().toLowerCase());
  return idx >= 0 ? idx : 999;
}

function mergeGameOdds(primary: any, fallback: any): StandardGameOdds {
  const base = primary && typeof primary === 'object' ? primary : {};
  const backup = fallback && typeof fallback === 'object' ? fallback : {};
  return {
    moneyline:
      base?.moneyline?.home != null && base?.moneyline?.away != null
        ? base.moneyline
        : (backup?.moneyline || { home: null, away: null }),
    spread:
      base?.spread?.home?.line != null && base?.spread?.away?.line != null
        ? base.spread
        : (backup?.spread || { home: null, away: null }),
    total:
      base?.total?.over?.line != null && base?.total?.under?.line != null
        ? base.total
        : (backup?.total || { over: null, under: null }),
  };
}

async function loadMlbDbOddsFallback(params: {
  startsAt: string;
  homeTeam: string;
  awayTeam: string;
}): Promise<StandardGameOdds | null> {
  const { rows } = await pool.query(
    `SELECT bookmaker, market, "lineValue", "homeOdds", "awayOdds", "overOdds", "underOdds", "fetchedAt"
       FROM "GameOdds"
      WHERE league = 'mlb'
        AND DATE("gameDate") IN (DATE($1::timestamptz), DATE($1::timestamptz - INTERVAL '1 day'))
        AND "homeTeam" = $2
        AND "awayTeam" = $3
      ORDER BY "fetchedAt" DESC NULLS LAST`,
    [params.startsAt, params.homeTeam, params.awayTeam],
  ).catch(() => ({ rows: [] as any[] }));

  if (!rows.length) return null;

  const latestByBucket = new Map<string, any>();
  for (const row of rows) {
    const bucketKey = `${String(row.market || '').trim().toLowerCase()}:${String(row.bookmaker || '').trim().toLowerCase()}`;
    const existing = latestByBucket.get(bucketKey);
    const nextTs = row.fetchedAt ? Date.parse(String(row.fetchedAt)) : 0;
    const existingTs = existing?.fetchedAt ? Date.parse(String(existing.fetchedAt)) : 0;
    if (!existing || nextTs >= existingTs) {
      latestByBucket.set(bucketKey, row);
    }
  }

  const pickBest = (marketKeys: string[]): any | null => {
    const candidates = [...latestByBucket.values()].filter((row) => marketKeys.includes(String(row.market || '').trim().toLowerCase()));
    if (!candidates.length) return null;
    candidates.sort((left, right) => {
      const priorityDelta = bookmakerPriority(left.bookmaker) - bookmakerPriority(right.bookmaker);
      if (priorityDelta !== 0) return priorityDelta;
      const leftTs = left.fetchedAt ? Date.parse(String(left.fetchedAt)) : 0;
      const rightTs = right.fetchedAt ? Date.parse(String(right.fetchedAt)) : 0;
      return rightTs - leftTs;
    });
    return candidates[0];
  };

  const moneyline = pickBest(['h2h', 'moneyline']);
  const spread = pickBest(['spreads', 'spread']);
  const total = pickBest(['totals', 'total']);
  const output: StandardGameOdds = {
    moneyline: { home: null, away: null },
    spread: { home: null, away: null },
    total: { over: null, under: null },
  };

  if (moneyline && (moneyline.homeOdds != null || moneyline.awayOdds != null)) {
    output.moneyline = {
      home: moneyline.homeOdds != null ? Number(moneyline.homeOdds) : null,
      away: moneyline.awayOdds != null ? Number(moneyline.awayOdds) : null,
    };
  }
  if (spread && spread.lineValue != null) {
    const homeLine = Number(spread.lineValue);
    output.spread = {
      home: {
        line: Number.isFinite(homeLine) ? homeLine : null,
        odds: spread.homeOdds != null ? Number(spread.homeOdds) : null,
      },
      away: {
        line: Number.isFinite(homeLine) ? -homeLine : null,
        odds: spread.awayOdds != null ? Number(spread.awayOdds) : null,
      },
    };
  }
  if (total && total.lineValue != null) {
    const totalLine = Number(total.lineValue);
    output.total = {
      over: {
        line: Number.isFinite(totalLine) ? totalLine : null,
        odds: total.overOdds != null ? Number(total.overOdds) : null,
      },
      under: {
        line: Number.isFinite(totalLine) ? totalLine : null,
        odds: total.underOdds != null ? Number(total.underOdds) : null,
      },
    };
  }

  return output;
}

/** Convert Grok's projected_lines into the standard odds format */
function projectedToOdds(projected: ForecastResult['projected_lines']): {
  moneyline: { home: number | null; away: number | null };
  spread: { home: { line: number; odds: number | null } | null; away: { line: number; odds: number | null } | null };
  total: { over: { line: number; odds: number | null } | null; under: { line: number; odds: number | null } | null };
} {
  if (!projected) {
    return {
      moneyline: { home: null, away: null },
      spread: { home: null, away: null },
      total: { over: null, under: null },
    };
  }
  return {
    moneyline: {
      home: projected.moneyline?.home ?? null,
      away: projected.moneyline?.away ?? null,
    },
    spread: {
      home: projected.spread?.home != null ? { line: projected.spread.home, odds: -110 } : null,
      away: projected.spread?.away != null ? { line: projected.spread.away, odds: -110 } : null,
    },
    total: {
      over: projected.total != null ? { line: projected.total, odds: -110 } : null,
      under: projected.total != null ? { line: projected.total, odds: -110 } : null,
    },
  };
}

function deriveDeterministicMlbMarginFromSignals(params: {
  signals: Array<{ signalId: string; score: number; available: boolean; rawData: any }>;
  fallbackMargin: number | null | undefined;
  marketHomeSpread?: number | null | undefined;
}): number | null {
  const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));
  let marginBase: number | null = null;
  let marginAdjustment = 0;

  const matchup = params.signals.find((signal) => signal.signalId === 'mlb_matchup' && signal.available);
  const phase = params.signals.find((signal) => signal.signalId === 'mlb_phase' && signal.available);
  const fangraphs = params.signals.find((signal) => signal.signalId === 'fangraphs' && signal.available);

  const impliedSpread = matchup?.rawData?.log5?.impliedSpread;
  if (typeof impliedSpread === 'number' && Number.isFinite(impliedSpread)) {
    marginBase = -impliedSpread;
  } else if (typeof params.marketHomeSpread === 'number' && Number.isFinite(params.marketHomeSpread)) {
    // When the matchup model cannot produce a real implied spread, fall back to
    // the live market number as the base and let the deterministic MLB signals
    // move around it. Using the compressed matchup score alone collapses whole
    // slates into coin-flip margins.
    marginBase = -params.marketHomeSpread;
  } else if (typeof matchup?.score === 'number') {
    marginBase = (matchup.score - 0.5) * 7.5;
  }

  if (typeof phase?.score === 'number') {
    marginAdjustment += (phase.score - 0.5) * 1.2;
  }

  if (typeof fangraphs?.score === 'number') {
    marginAdjustment += (fangraphs.score - 0.5) * 0.8;
  }

  const rcDiff =
    typeof matchup?.rawData?.runsCreated?.home === 'number' && typeof matchup?.rawData?.runsCreated?.away === 'number'
      ? matchup.rawData.runsCreated.home - matchup.rawData.runsCreated.away
      : null;
  if (typeof rcDiff === 'number' && Number.isFinite(rcDiff)) {
    marginAdjustment += clamp(rcDiff * 0.15, -0.6, 0.6);
  }

  const homeStarterFip = matchup?.rawData?.starters?.home?.fangraphs?.fip ?? fangraphs?.rawData?.starters?.home?.fip;
  const awayStarterFip = matchup?.rawData?.starters?.away?.fangraphs?.fip ?? fangraphs?.rawData?.starters?.away?.fip;
  if (typeof homeStarterFip === 'number' && typeof awayStarterFip === 'number') {
    marginAdjustment += clamp((awayStarterFip - homeStarterFip) * 0.2, -0.75, 0.75);
  }

  const homeProjWrcPlus = matchup?.rawData?.projections?.homeBat?.projWrcPlus;
  const awayProjWrcPlus = matchup?.rawData?.projections?.awayBat?.projWrcPlus;
  if (typeof homeProjWrcPlus === 'number' && typeof awayProjWrcPlus === 'number') {
    marginAdjustment += clamp((homeProjWrcPlus - awayProjWrcPlus) / 50, -0.75, 0.75);
  }

  const bullpenSideScore = phase?.rawData?.bullpen?.sideScore;
  if (typeof bullpenSideScore === 'number' && Number.isFinite(bullpenSideScore)) {
    marginAdjustment += (bullpenSideScore - 0.5) * 1.0;
  }

  const contextSideScore = phase?.rawData?.context?.sideScore;
  if (typeof contextSideScore === 'number' && Number.isFinite(contextSideScore)) {
    marginAdjustment += (contextSideScore - 0.5) * 0.4;
  }

  if (marginBase == null && Math.abs(marginAdjustment) < 0.05) {
    return params.fallbackMargin ?? null;
  }

  const calibratedValue = clamp((marginBase ?? 0) + marginAdjustment, -3.5, 3.5);
  return Math.round(calibratedValue * 10) / 10;
}

function buildStoredMlbMatchupContextFromSignals(
  signals: Array<{ signalId: string; score: number; available: boolean; rawData: any }>,
): Record<string, any> | null {
  const matchup = signals.find((signal) => signal.signalId === 'mlb_matchup' && signal.available);
  const fangraphs = signals.find((signal) => signal.signalId === 'fangraphs' && signal.available);
  if (!matchup?.rawData && !fangraphs?.rawData) return null;

  const context = {
    log5_home_win_prob: matchup?.rawData?.log5?.homeWinProbHFA ?? null,
    implied_spread: matchup?.rawData?.log5?.impliedSpread ?? null,
    strength_diff: matchup?.rawData?.log5?.strengthDiff ?? null,
    park_runs_factor: matchup?.rawData?.park?.runs ?? null,
    park_hr_factor: matchup?.rawData?.park?.hr ?? null,
    home_runs_created: matchup?.rawData?.runsCreated?.home ?? null,
    away_runs_created: matchup?.rawData?.runsCreated?.away ?? null,
    home_pythag_win_pct: matchup?.rawData?.pythagorean?.home?.winPct ?? null,
    away_pythag_win_pct: matchup?.rawData?.pythagorean?.away?.winPct ?? null,
    home_projected_wrc_plus: matchup?.rawData?.projections?.homeBat?.projWrcPlus ?? null,
    away_projected_wrc_plus: matchup?.rawData?.projections?.awayBat?.projWrcPlus ?? null,
    home_team_wrc_plus: fangraphs?.rawData?.homeBat?.wrcPlus ?? null,
    away_team_wrc_plus: fangraphs?.rawData?.awayBat?.wrcPlus ?? null,
    home_starter_fip:
      matchup?.rawData?.starters?.home?.fangraphs?.fip
      ?? fangraphs?.rawData?.homePit?.fip
      ?? null,
    away_starter_fip:
      matchup?.rawData?.starters?.away?.fangraphs?.fip
      ?? fangraphs?.rawData?.awayPit?.fip
      ?? null,
  };

  return Object.values(context).some((value) => value != null) ? context : null;
}

function formatMarginForCopy(value: number): string {
  const rounded = Math.round(value * 10) / 10;
  return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
}

function deriveProjectedWinnerFromMargin(
  forecast: ForecastResult,
  homeTeam: string,
  awayTeam: string,
): string {
  if (typeof forecast.projected_margin === 'number' && Number.isFinite(forecast.projected_margin)) {
    if (forecast.projected_margin > 0) return homeTeam;
    if (forecast.projected_margin < 0) return awayTeam;
  }
  return forecast.projected_winner || forecast.winner_pick || homeTeam;
}

function syncProjectedScoresFromMarginAndTotal(forecast: ForecastResult): void {
  const margin = typeof forecast.projected_margin === 'number' && Number.isFinite(forecast.projected_margin)
    ? forecast.projected_margin
    : null;
  const total = typeof forecast.projected_total_points === 'number' && Number.isFinite(forecast.projected_total_points)
    ? forecast.projected_total_points
    : null;

  if (margin == null || total == null) return;

  forecast.projected_team_score_home = Math.round(((total + margin) / 2) * 10) / 10;
  forecast.projected_team_score_away = Math.round(((total - margin) / 2) * 10) / 10;
}

function syncCalibratedMlbNarrative(forecast: ForecastResult, homeTeam: string, awayTeam: string): void {
  if (forecast.projected_margin == null) return;

  const projectedWinner = deriveProjectedWinnerFromMargin(forecast, homeTeam, awayTeam);
  const projectedLoser = projectedWinner === homeTeam ? awayTeam : homeTeam;
  const marginLabel = formatMarginForCopy(Math.abs(forecast.projected_margin));
  const projectedTotal = typeof forecast.projected_total_points === 'number' && Number.isFinite(forecast.projected_total_points)
    ? formatMarginForCopy(forecast.projected_total_points)
    : null;

  forecast.projected_winner = projectedWinner;
  forecast.winner_pick = projectedWinner;
  syncProjectedScoresFromMarginAndTotal(forecast);

  if (typeof forecast.summary === 'string' && forecast.summary.trim()) {
    const summaryBody = forecast.summary
      .replace(/^Rain Man forecasts[\s\S]*?Here's how Rain Man got there:\s*/i, '')
      .trim();
    const distinction = forecast.forecast_side && forecast.forecast_side !== projectedWinner
      ? `Outcome lean: ${projectedWinner}. Best value: ${forecast.forecast_side} against the spread.`
      : null;
    const line1 = `Rain Man forecasts ${projectedWinner} to outscore ${projectedLoser} by ~${marginLabel} runs.`;
    const line2 = projectedTotal
      ? `Projected combined score: ~${projectedTotal} runs.`
      : 'Projected combined score: market-aligned MLB run environment.';
    const body = [distinction, summaryBody].filter(Boolean).join(' ');
    forecast.summary = `${line1}\n${line2}\nHere's how Rain Man got there:${body ? ` ${body}` : ''}`;
  }

  if (Array.isArray((forecast as any).clip_metadata)) {
    const spreadEdge = forecast.spread_edge ?? 0;
    const projWinner = forecast.projected_winner || forecast.winner_pick;
    (forecast as any).clip_metadata = (forecast as any).clip_metadata.map((clip: any) => {
      if (!clip || typeof clip !== 'object') return clip;

      if (clip.clip_type === 'GAME_FORECAST') {
        return {
          ...clip,
          display_text: `${awayTeam} @ ${homeTeam} | Value: ${forecast.forecast_side} | Edge +${spreadEdge}pts | Winner: ${projWinner}`,
          clip_data: {
            ...(clip.clip_data || {}),
            winner: projWinner,
            forecast_side: forecast.forecast_side,
            edge_pct: spreadEdge,
            forecasted_value: forecast.projected_margin,
          },
        };
      }

      if (clip.clip_type === 'SPREAD') {
        return {
          ...clip,
          clip_data: {
            ...(clip.clip_data || {}),
            forecasted_value: forecast.projected_margin,
            forecast_side: forecast.forecast_side,
            edge_pct: spreadEdge,
          },
        };
      }

      return clip;
    });
  }
}

function applyMlbAlgorithmicOverride(params: {
  forecast: ForecastResult;
  rawTeamPlusDecision: Awaited<ReturnType<typeof buildLiveTeamPlusDecision>> | null;
  homeTeam: string;
  awayTeam: string;
}): void {
  const baselineWinner = deriveProjectedWinnerFromMargin(params.forecast, params.homeTeam, params.awayTeam);
  const override = buildMlbBaselineOverride({
    decision: params.rawTeamPlusDecision,
    baselineWinner,
    homeTeam: params.homeTeam,
    awayTeam: params.awayTeam,
  });

  if (!override?.shouldOverride) {
    if (override) {
      (params.forecast as any).mlb_algo_override = {
        applied: false,
        flip_probability: override.flipProbability,
        baseline_winner: baselineWinner,
        final_winner: baselineWinner,
        reason: override.reason,
        features: override.features,
      };
    }
    return;
  }

  params.forecast.winner_pick = override.winnerPick;
  params.forecast.projected_winner = override.winnerPick;

  const marginMagnitude = Math.max(
    Math.abs(Number(params.forecast.projected_margin || 0)),
    Math.abs(Number(params.rawTeamPlusDecision?.lockedMargin || 0.9)),
  );
  params.forecast.projected_margin = override.winnerPick === params.homeTeam
    ? Math.abs(marginMagnitude)
    : -Math.abs(marginMagnitude);

  (params.forecast as any).mlb_algo_override = {
    applied: true,
    flip_probability: override.flipProbability,
    baseline_winner: baselineWinner,
    final_winner: override.winnerPick,
    reason: override.reason,
    features: override.features,
  };

  syncCalibratedMlbNarrative(params.forecast, params.homeTeam, params.awayTeam);
}

function buildMlbDirectSignalsFromTeamPlusDecision(rawTeamPlusDecision: Awaited<ReturnType<typeof buildLiveTeamPlusDecision>> | null): SignalResult[] {
  if (!rawTeamPlusDecision?.components?.length) return [];
  return rawTeamPlusDecision.components
    .filter((component) => ['mlb_matchup', 'mlb_phase', 'fangraphs', 'dvp_team_plus'].includes(component.signalId))
    .map((component) => ({
      signalId: component.signalId === 'dvp_team_plus' ? 'dvp' : component.signalId,
      score: Number(component.homeScore ?? 0.5),
      weight: Number(component.weight || 0),
      available: Boolean(component.available),
      rawData: component.rawData || {},
      metadata: {
        latencyMs: 0,
        source: 'cache' as const,
        freshness: new Date().toISOString(),
      },
    }));
}

function deriveMlbDirectLockedMargin(params: {
  oddsPayload: { spread?: any } | null | undefined;
  winnerPick: string;
  homeTeam: string;
}): number {
  const homeSpread = Number(params.oddsPayload?.spread?.home?.line ?? null);
  if (Number.isFinite(homeSpread) && homeSpread !== 0) {
    return params.winnerPick === params.homeTeam
      ? Math.abs(homeSpread)
      : -Math.abs(homeSpread);
  }
  return params.winnerPick === params.homeTeam ? 0.9 : -0.9;
}

function buildMlbDirectLockedDecision(params: {
  homeTeam: string;
  awayTeam: string;
  oddsPayload: { moneyline?: any; spread?: any; total?: any };
  rawTeamPlusDecision: Awaited<ReturnType<typeof buildLiveTeamPlusDecision>> | null;
}): TeamPlusDecision | null {
  const signals = buildMlbDirectSignalsFromTeamPlusDecision(params.rawTeamPlusDecision);
  if (signals.length === 0) return null;

  const featureVector = extractMlbDirectFeatures({
    signals,
    oddsData: params.oddsPayload,
  });
  const decision = scoreMlbDirectModel({
    homeTeam: params.homeTeam,
    awayTeam: params.awayTeam,
    featureVector,
    config: MLB_DIRECT_MODEL_LIVE_CONFIG,
    policy: MLB_DIRECT_MODEL_LIVE_POLICY,
  });
  if (!decision.available || !decision.policy.shouldBet || decision.policy.recommendedMarket !== 'moneyline') {
    return null;
  }

  const directMetrics = {
    supportCount: decision.policy.supportCount,
    conflictCount: decision.policy.conflictCount,
    featureCoverageScore: decision.policy.featureCoverageScore,
    marketEdge: featureVector.marketEdge,
    structuralEdge: decision.policy.structuralEdgeAbs,
    offenseContactEdge: featureVector.offenseContactEdge,
    starterOffenseInteractionEdge: featureVector.starterOffenseInteractionEdge,
    lineupPressureEdge: featureVector.lineupPressureEdge,
    decompositionEdge: featureVector.decompositionEdge,
    calibratedConfidence: decision.calibratedConfidence,
    confidencePenalty: decision.confidencePenalty,
  };

  return {
    league: 'mlb',
    available: true,
    homeScore: decision.homeProbability,
    awayScore: decision.awayProbability,
    winnerPick: decision.winnerPick,
    confidence: decision.calibratedConfidence,
    lockedMargin: deriveMlbDirectLockedMargin({
      oddsPayload: params.oddsPayload,
      winnerPick: decision.winnerPick,
      homeTeam: params.homeTeam,
    }),
    weights: {
      mlb_direct_market_anchor: 1,
      mlb_direct_signal_scale: Number(MLB_DIRECT_MODEL_LIVE_CONFIG.signalScale ?? 0),
    },
    components: params.rawTeamPlusDecision?.components || [],
    explanation: [
      `mlb_direct_home_prob=${decision.homeProbability}`,
      `mlb_direct_market_edge=${featureVector.marketEdge}`,
      `mlb_direct_policy=${decision.policy.recommendedMarket}`,
      ...decision.explanation,
    ],
    directMetrics,
  } as TeamPlusDecision & { directMetrics: typeof directMetrics };
}

function enforceMlbDirectLockedForecast(params: {
  forecast: ForecastResult;
  lockedDecision: TeamPlusDecision;
  homeTeam: string;
  awayTeam: string;
}): void {
  params.forecast.winner_pick = params.lockedDecision.winnerPick;
  params.forecast.projected_winner = params.lockedDecision.winnerPick;
  params.forecast.projected_margin = params.lockedDecision.lockedMargin;
  if (params.forecast.projected_lines?.spread) {
    params.forecast.projected_lines.spread = {
      home: Math.round((-params.lockedDecision.lockedMargin) * 10) / 10,
      away: Math.round(params.lockedDecision.lockedMargin * 10) / 10,
    };
  }
  syncCalibratedMlbNarrative(params.forecast, params.homeTeam, params.awayTeam);
}

export async function buildForecast(
  event: SgoEvent,
  league: string,
  options?: { ignoreCache?: boolean },
): Promise<{
  forecast: ForecastResult;
  forecastId: string;
  confidenceScore: number;
  rawConfidenceScore?: number;
  cached: boolean;
  odds: { moneyline: any; spread: any; total: any };
  composite?: CompositeResult;
}> {
  // Always parse current live odds from the event
  const liveOdds = parseOdds(event);
  let livePayload: StandardGameOdds = {
    moneyline: liveOdds.moneyline,
    spread: liveOdds.spread,
    total: liveOdds.total,
  };

  // Check cache first
  const existingCached = await getCachedForecast(event.eventID);
  const cached = options?.ignoreCache ? null : existingCached;
  if (cached) {
    // Determine best odds: live feed > cached odds_data > Grok projected_lines
    let bestOdds = livePayload;
    if (!hasRealOdds(bestOdds)) {
      if (cached.odds_data && hasRealOdds(cached.odds_data)) {
        bestOdds = cached.odds_data;
      } else if (cached.forecast_data?.projected_lines) {
        bestOdds = projectedToOdds(cached.forecast_data.projected_lines);
      }
    }
    // Update stored odds
    await updateCachedOdds(event.eventID, bestOdds);

    // Build composite from cached data if available
    let composite: CompositeResult | undefined;
    if (cached.composite_confidence && cached.model_signals) {
      const legacyComposite = getLegacyCompositeView(
        cached.model_signals,
        cached.composite_confidence || cached.confidence_score || 0.5,
        cached.forecast_data?.value_rating || 5,
      );
      composite = {
        compositeConfidence: cached.composite_confidence,
        stormCategory: legacyComposite?.stormCategory || 2,
        modelSignals: legacyComposite?.modelSignals || { grok: null, piff: null, digimon: null, dvp: null },
        edgeBreakdown: legacyComposite?.edgeBreakdown || { grokScore: 0, piffScore: 0, digimonScore: null, weights: { grok: 0.5, piff: 0.5, digimon: 0 } },
        compositeVersion: cached.composite_version || legacyComposite?.compositeVersion || 'v1',
      };
    }

    return {
      forecast: cached.forecast_data,
      forecastId: cached.id,
      confidenceScore: cached.composite_confidence || cached.confidence_score,
      rawConfidenceScore: cached.confidence_score || cached.composite_confidence || undefined,
      cached: true,
      odds: bestOdds,
      composite,
    };
  }

  const homeTeam = event.teams.home.names.long;
  const awayTeam = event.teams.away.names.long;

  if (league === 'mlb') {
    if (existingCached?.odds_data) {
      livePayload = mergeGameOdds(livePayload, existingCached.odds_data);
    }
    if (!hasRealSideOdds(livePayload)) {
      const dbOddsFallback = await loadMlbDbOddsFallback({
        startsAt: event.status.startsAt || new Date().toISOString(),
        homeTeam,
        awayTeam,
      });
      if (dbOddsFallback) {
        livePayload = mergeGameOdds(livePayload, dbOddsFallback);
      }
    }
  }

  // Query DB analytics — pass short names for injury/lineup/transaction lookups
  const dbAnalytics = await fetchDbAnalytics(homeTeam, awayTeam, league, event.teams.home.names.short, event.teams.away.names.short);

  // Pre-compute tournament context for NCAAB (used in both prompt injection and composite adjustment)
  let tournamentCtx: TournamentContext | null = null;
  if (league === 'ncaab') {
    try {
      const active = await isTournamentActive();
      if (active) {
        tournamentCtx = await computeTournamentContext({
          teamA: homeTeam,
          teamB: awayTeam,
          eventId: event.eventID,
          league: 'ncaab',
        });
        // Store for audit trail
        const { storeTournamentContext } = require('../tournament/context-engine');
        await storeTournamentContext(tournamentCtx, homeTeam, awayTeam, 2026, event.eventID, 'first_round');
      }
    } catch (err: any) {
      console.warn(`[forecast-builder] Tournament context pre-compute error: ${err.message}`);
    }
  }

  const rawTeamPlusDecision = (
    (league === 'mlb' || league === 'nba' || league === 'nhl')
      && event.teams.home.names.short
      && event.teams.away.names.short
  )
    ? await buildLiveTeamPlusDecision({
        league: league as 'mlb' | 'nba' | 'nhl',
        homeTeam,
        awayTeam,
        homeShort: event.teams.home.names.short,
        awayShort: event.teams.away.names.short,
        startsAt: event.status.startsAt || new Date().toISOString(),
        eventId: event.eventID,
        event,
      }).catch((err: any) => {
        console.warn(`[team-plus] build failed for ${event.eventID}: ${err.message}`);
        return null;
      })
    : null;

  const teamPlusLockThreshold = TEAM_PLUS_LOCK_THRESHOLD[league as 'mlb' | 'nba' | 'nhl'];
  const teamPlusDecision = rawTeamPlusDecision && teamPlusLockThreshold != null
    ? (rawTeamPlusDecision.confidence >= teamPlusLockThreshold ? rawTeamPlusDecision : null)
    : null;
  const mlbDirectLockedDecision = league === 'mlb'
    ? buildMlbDirectLockedDecision({
        homeTeam,
        awayTeam,
        oddsPayload: livePayload,
        rawTeamPlusDecision,
      })
    : null;

  // Generate with Grok — pass current live odds + team shorts for PIFF into the prompt
  const forecast = await generateForecast({
    homeTeam,
    awayTeam,
    homeShort: event.teams.home.names.short,
    awayShort: event.teams.away.names.short,
    league,
    startsAt: event.status.startsAt,
    moneyline: liveOdds.moneyline,
    spread: liveOdds.spread,
    total: liveOdds.total,
    dbAnalytics,
    eventId: event.eventID,
    tournamentContext: tournamentCtx,
    lockedTeamDecision: mlbDirectLockedDecision || teamPlusDecision,
  });

  if (league === 'mlb' && mlbDirectLockedDecision) {
    (forecast as any).mlb_direct_model = {
      applied: true,
      winner_pick: mlbDirectLockedDecision.winnerPick,
      home_score: mlbDirectLockedDecision.homeScore,
      away_score: mlbDirectLockedDecision.awayScore,
      confidence: mlbDirectLockedDecision.confidence,
      locked_margin: mlbDirectLockedDecision.lockedMargin,
      explanation: mlbDirectLockedDecision.explanation,
      metrics: (mlbDirectLockedDecision as any).directMetrics || null,
      config: MLB_DIRECT_MODEL_LIVE_CONFIG,
      policy: MLB_DIRECT_MODEL_LIVE_POLICY,
    };
    enforceMlbDirectLockedForecast({
      forecast,
      lockedDecision: mlbDirectLockedDecision,
      homeTeam,
      awayTeam,
    });
  }

  if (league === 'mlb' && !mlbDirectLockedDecision) {
    applyMlbAlgorithmicOverride({
      forecast,
      rawTeamPlusDecision,
      homeTeam,
      awayTeam,
    });
  }

  // Determine odds: live feed first, then Grok projected lines
  let oddsPayload = livePayload;
  if (!hasRealOdds(oddsPayload) && forecast.projected_lines) {
    oddsPayload = projectedToOdds(forecast.projected_lines);
  }

  // Calculate composite score from all model signals
  const homeShort = event.teams.home.names.short || '';
  const awayShort = event.teams.away.names.short || '';

  let composite: CompositeResult;
  let storedModelSignals: any;
  let inputQuality: InputQuality;

  if (RIE_ENABLED) {
    // ── RIE pipeline: unified signal collection + composite ──
    const intel = await buildIntelligence({
      event,
      league,
      homeTeam,
      awayTeam,
      homeShort,
      awayShort,
      startsAt: event.status.startsAt || new Date().toISOString(),
      eventId: event.eventID,
      grokConfidence: forecast.confidence,
      grokValueRating: forecast.value_rating || 5,
    });
    composite = mapToLegacyComposite(intel, forecast.confidence, forecast.value_rating || 5);
    storedModelSignals = formatStoredModelSignals(intel, composite as any);

    if (league === 'mlb') {
      const calibratedMargin = deriveDeterministicMlbMarginFromSignals({
        signals: intel.signals,
        fallbackMargin: forecast.projected_margin,
        marketHomeSpread: oddsPayload.spread?.home?.line ?? null,
      });
      if (calibratedMargin != null && !mlbDirectLockedDecision) {
        forecast.projected_margin = calibratedMargin;
        if (forecast.projected_lines?.spread) {
          forecast.projected_lines.spread = {
            home: Math.round((-calibratedMargin) * 10) / 10,
            away: Math.round(calibratedMargin * 10) / 10,
          };
        }
        const spreadSignal = deriveSpreadSignal({
          league,
          projectedMargin: calibratedMargin,
          marketHomeSpread: oddsPayload.spread?.home?.line ?? null,
          homeTeam,
          awayTeam,
          winnerPick: forecast.winner_pick,
        });
        forecast.forecast_side = spreadSignal.forecastSide;
        forecast.spread_edge = spreadSignal.spreadEdge;
        syncCalibratedMlbNarrative(forecast, homeTeam, awayTeam);
      }
    }

    if (league === 'mlb') {
      const mlbPhaseSignal = intel.signals.find((signal) => signal.signalId === 'mlb_phase');
      if (mlbPhaseSignal?.rawData) {
        (forecast as any).mlb_phase_context = mlbPhaseSignal.rawData;
      }

      const mlbMatchupContext = buildStoredMlbMatchupContextFromSignals(intel.signals);
      if (mlbMatchupContext) {
        (forecast as any).mlb_matchup_context = mlbMatchupContext;
      }
    }

    // Map RIE input quality to a stable UI payload with real counts.
    const iq = intel.inputQuality;
    const piffSignal = intel.signals.find((signal) => signal.signalId === 'piff');
    const dvpSignal = intel.signals.find((signal) => signal.signalId === 'dvp');
    const digimonSignal = intel.signals.find((signal) => signal.signalId === 'digimon');
    const ragSignal = intel.signals.find((signal) => signal.signalId === 'rag');
    inputQuality = {
      piff: {
        grade: iq.piff,
        legs: piffSignal?.rawData?.legCount || 0,
        avgEdge: Math.round((((piffSignal?.rawData?.avgEdge) || 0) as number) * 10) / 10,
      },
      dvp: {
        grade: iq.dvp,
        stats: countDvpStatsFromSignal(dvpSignal?.rawData),
        freshness: dvpSignal?.metadata?.freshness?.slice(0, 10) || new Date().toISOString().split('T')[0],
      },
      digimon: digimonSignal?.available
        ? { grade: iq.digimon, lockCount: digimonSignal?.rawData?.lockCount || 0 }
        : { grade: iq.digimon, reason: league === 'nba' ? 'No DIGIMON locks available' : `${league.toUpperCase()} — not available` },
      odds: { grade: iq.odds, markets: countAvailableMarkets(oddsPayload) },
      rag: { grade: iq.rag, chunks: ragSignal?.rawData?.chunkCount || 0 },
      overall: iq.overall,
    };

    console.log(`[RIE] ${homeTeam} vs ${awayTeam} — composite ${(composite.compositeConfidence * 100).toFixed(1)}% Cat${composite.stormCategory} (${intel.strategyProfile.league} strategy, ${intel.signals.filter(s => s.available).length} signals, ${intel.ragInsights.length} RAG insights)`);
  } else {
    // ── Legacy pipeline: manual signal loading + calculateComposite ──
    const eventDateKey = getEtDateKey(event.status?.startsAt || '') || getCurrentEtDateKey();
    const piffProps = homeShort && awayShort && league !== 'ncaab'
      ? getPiffPropsForGame(homeShort, awayShort, loadPiffPropsForDate(eventDateKey), league)
      : [];
    const digimonPicks = homeShort && awayShort && league === 'nba'
      ? getDigimonForGame(homeShort, awayShort, loadDigimonPicksForDate(eventDateKey))
      : [];
    const dvpData = await getDvpForMatchup(homeShort, awayShort, league);

    let cornerScoutData = null;
    if (isSoccerLeague(league)) {
      try {
        cornerScoutData = getCornerScoutForGame(event.teams.home.names.long, event.teams.away.names.long, undefined, eventDateKey);
        if (cornerScoutData) {
          console.log(`[corner-scout] Composite: proj=${cornerScoutData.projection?.toFixed(1)} edge=${cornerScoutData.edge?.toFixed(1)} rating=${cornerScoutData.rating}`);
        }
      } catch (err: any) {
        console.warn(`[corner-scout] Composite load error: ${err.message}`);
      }
    }

    let kenpomMatchup = null;
    if (league === 'ncaab') {
      try {
        const gameDate = event.status.startsAt ? event.status.startsAt.slice(0, 10) : new Date().toISOString().slice(0, 10);
        kenpomMatchup = await getKenPomMatchup(homeTeam, awayTeam, 'ncaab', gameDate);
      } catch (err: any) {
        console.warn(`[forecast-builder] KenPom matchup error: ${err.message}`);
      }
    }

    composite = calculateComposite({
      grokConfidence: forecast.confidence,
      grokValueRating: forecast.value_rating || 5,
      piffProps,
      digimonPicks,
      dvp: dvpData,
      league,
      kenpomMatchup,
      cornerScoutData,
    });
    storedModelSignals = composite;

    inputQuality = computeInputQuality(piffProps, dvpData, digimonPicks, oddsPayload, league);
  }

  // Apply tournament contextual adjustment to composite confidence (capped at ±12%)
  if (tournamentCtx) {
    if (RIE_ENABLED) {
      applyLegacyConfidenceAdjustment(composite as any, tournamentCtx.finalAdjustment);
    } else {
      composite.compositeConfidence = Math.max(0, Math.min(1,
        composite.compositeConfidence + tournamentCtx.finalAdjustment
      ));
    }
    console.log(`[tournament] Applied contextual adjustment: ${(tournamentCtx.finalAdjustment * 100).toFixed(1)}% → composite: ${(composite.compositeConfidence * 100).toFixed(1)}%`);
  }

  const rawConfidenceScore = forecast.confidence;
  forecast.confidence = composite.compositeConfidence;

  if (league === 'mlb' && mlbDirectLockedDecision) {
    enforceMlbDirectLockedForecast({
      forecast,
      lockedDecision: mlbDirectLockedDecision,
      homeTeam,
      awayTeam,
    });
  }

  // Cache the result with odds + composite
  const startsAt = event.status.startsAt || new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
  const saved = await cacheForecast({
    eventId: event.eventID,
    league,
    homeTeam,
    awayTeam,
    forecastData: forecast,
    confidenceScore: rawConfidenceScore,
    startsAt,
    expiresAt: startsAt,
    oddsData: oddsPayload,
  });

  // ── NARRATIVE ENGINE: Process all narrative fields ──
  let narrativeMetadata: NarrativeMetadata | null = null;
  try {
    const hasQuestionableInjuries = (dbAnalytics?.injuries || []).some(
      (i: any) => ['questionable', 'gtd', 'day-to-day', 'dtd', 'doubtful'].includes(i.status?.toLowerCase())
    );
    const hasRecentRosterMoves = (dbAnalytics?.transactions || []).length > 0;
    const hasSharpMoves = (dbAnalytics?.sharpMoves || []).length > 0;

    const narrativeCtx: NarrativeContext = {
      homeTeam,
      awayTeam,
      league,
      marketSpread: liveOdds.spread?.home?.line ?? null,
      projectedMargin: forecast.projected_margin,
      marketTotal: liveOdds.total?.over?.line ?? null,
      projectedTotal: forecast.projected_total_points,
      confidence: composite.compositeConfidence,
      valueRating: forecast.value_rating || 5,
      stormCategory: composite.stormCategory,
      forecastSide: forecast.forecast_side || homeTeam,
      spreadEdge: forecast.spread_edge || 0,
      totalDirection: forecast.total_direction || 'NONE',
      totalEdge: forecast.total_edge || 0,
      injuryUncertainty: hasQuestionableInjuries,
      recentFormInstability: false,
      rosterChanges: hasRecentRosterMoves,
      sharpMovesDetected: hasSharpMoves,
      marketMovedSinceOpen: false,
      marketMoveDirection: null,
    };

    const { processedFields, metadata } = runNarrativeEngine(forecast, narrativeCtx);
    narrativeMetadata = metadata;

    // Merge processed narrative fields back into forecast object
    Object.assign(forecast, processedFields);
    console.log(`[narrative-engine] Processed — tier=${metadata.confidenceTier} variance=${metadata.varianceLevel} spread=${metadata.spreadScenario} market=${metadata.marketDirection}${metadata.totalsAuditFlag ? ' TOTALS-AUDIT' : ''}`);
  } catch (err: any) {
    console.error(`[narrative-engine] Processing failed (non-fatal): ${err.message}`);
  }

  // Store composite data + input quality + narrative metadata in rm_forecast_cache
  try {
    await pool.query(
      `UPDATE rm_forecast_cache SET composite_confidence = $1, model_signals = $2, composite_version = $3, input_quality = $5, narrative_metadata = $6 WHERE id = $4`,
      [composite.compositeConfidence, JSON.stringify(storedModelSignals || composite), composite.compositeVersion, saved.id, JSON.stringify(inputQuality), narrativeMetadata ? JSON.stringify(narrativeMetadata) : null]
    );
  } catch (err) {
    console.error('Failed to store composite data:', err);
  }

  return {
    forecast,
    forecastId: saved.id,
    confidenceScore: composite.compositeConfidence,
    rawConfidenceScore,
    cached: false,
    odds: oddsPayload,
    composite,
  };
}

interface InputQuality {
  piff: { grade: string; legs: number; avgEdge: number };
  dvp: { grade: string; stats: number; freshness: string };
  digimon: { grade: string; lockCount?: number; reason?: string };
  odds: { grade: string; markets: number };
  rag?: { grade: string; chunks: number };
  overall: string;
}

function countAvailableMarkets(odds: { moneyline: any; spread: any; total: any }): number {
  let markets = 0;
  if (odds.moneyline?.home || odds.moneyline?.away) markets++;
  if (odds.spread?.home?.line || odds.spread?.away?.line) markets++;
  if (odds.total?.over?.line || odds.total?.under?.line) markets++;
  return markets;
}

function countDvpStatsFromSignal(rawData: any): number {
  const countSide = (side: Record<string, any> | null | undefined): number => {
    if (!side || typeof side !== 'object') return 0;
    return Object.values(side).reduce((sum, entry) => {
      if (Array.isArray(entry)) return sum + entry.length;
      if (entry && typeof entry === 'object') return sum + 1;
      return sum;
    }, 0);
  };

  return countSide(rawData?.home) + countSide(rawData?.away);
}

function computeInputQuality(
  piffProps: any[],
  dvpData: { home: any; away: any },
  digimonPicks: any[],
  odds: { moneyline: any; spread: any; total: any },
  league: string
): InputQuality {
  // PIFF grade
  let piffGrade = 'D';
  let avgEdge = 0;
  if (piffProps.length > 0) {
    avgEdge = piffProps.reduce((s, p) => s + (p.edge || 0), 0) / piffProps.length;
    if (piffProps.length > 5 && avgEdge > 0.10) piffGrade = 'A';
    else if (piffProps.length > 2) piffGrade = 'B';
    else piffGrade = 'C';
  }

  // DVP grade
  const dvpHome = dvpData.home?.ranks?.length || 0;
  const dvpAway = dvpData.away?.ranks?.length || 0;
  const dvpStats = dvpHome + dvpAway;
  let dvpGrade = 'D';
  if (dvpHome > 0 && dvpAway > 0) dvpGrade = 'A';
  else if (dvpHome > 0 || dvpAway > 0) dvpGrade = 'B';

  // DIGIMON grade (NBA only)
  let digimonGrade = 'N/A';
  let digimonReason: string | undefined;
  if (league === 'nba') {
    if (digimonPicks.length > 0) {
      const locks = digimonPicks.filter(p => p.verdict === 'LOCK').length;
      digimonGrade = locks >= 3 ? 'A' : locks >= 1 ? 'B' : 'C';
    } else {
      digimonGrade = 'D';
    }
  } else {
    digimonReason = `${league.toUpperCase()} — not available`;
  }

  // Odds grade
  let markets = 0;
  if (odds.moneyline?.home || odds.moneyline?.away) markets++;
  if (odds.spread?.home?.line || odds.spread?.away?.line) markets++;
  if (odds.total?.over?.line || odds.total?.under?.line) markets++;
  let oddsGrade = 'D';
  if (markets >= 3) oddsGrade = 'A';
  else if (markets >= 2) oddsGrade = 'B';
  else if (markets >= 1) oddsGrade = 'C';

  // Overall: weighted average of applicable grades
  const gradeMap: Record<string, number> = { 'A': 4, 'B': 3, 'C': 2, 'D': 1 };
  const grades = [piffGrade, dvpGrade, oddsGrade];
  if (digimonGrade !== 'N/A') grades.push(digimonGrade);
  const avg = grades.reduce((s, g) => s + (gradeMap[g] || 0), 0) / grades.length;
  let overall = 'C';
  if (avg >= 3.5) overall = 'A';
  else if (avg >= 2.8) overall = 'B+';
  else if (avg >= 2.3) overall = 'B';
  else if (avg >= 1.8) overall = 'C+';

  return {
    piff: { grade: piffGrade, legs: piffProps.length, avgEdge: Math.round(avgEdge * 1000) / 10 },
    dvp: { grade: dvpGrade, stats: dvpStats, freshness: new Date().toISOString().split('T')[0] },
    digimon: digimonReason ? { grade: digimonGrade, reason: digimonReason } : { grade: digimonGrade },
    odds: { grade: oddsGrade, markets },
    overall,
  };
}

async function fetchDbAnalytics(homeTeam: string, awayTeam: string, league: string, homeShort?: string, awayShort?: string): Promise<any> {
  try {
    const predictions = await pool.query(
      `SELECT prediction_type, predicted_value, confidence, created_at
       FROM "PredictionHistory"
       WHERE league = $1
         AND (home_team ILIKE $2 OR away_team ILIKE $2 OR home_team ILIKE $3 OR away_team ILIKE $3)
       ORDER BY created_at DESC LIMIT 10`,
      [league, `%${homeTeam}%`, `%${awayTeam}%`]
    ).catch(() => ({ rows: [] }));

    const { sharpMoves, lineMovements } = await fetchInsightSourceData({
      homeTeam,
      awayTeam,
      league,
    });

    // Fetch injuries from both PlayerInjury (Prisma) and sportsclaw.injuries (comprehensive)
    // Match by abbreviation (LAL, MIN) OR full name (Los Angeles Lakers)
    const injuries = await pool.query(
      `SELECT "playerName", team, position, status, "injuryType", description, source, "reportedAt"
       FROM "PlayerInjury"
       WHERE league = $1
         AND (team ILIKE $2 OR team ILIKE $3 OR team ILIKE $4 OR team ILIKE $5)
         AND LOWER(status) IN ('out', 'ir', 'suspension', 'doubtful', 'questionable', 'day-to-day', 'gtd', 'dtd')
       ORDER BY CASE LOWER(status)
         WHEN 'out' THEN 1 WHEN 'ir' THEN 2 WHEN 'suspension' THEN 3
         WHEN 'doubtful' THEN 4 WHEN 'gtd' THEN 4 WHEN 'questionable' THEN 5
         WHEN 'day-to-day' THEN 5 WHEN 'dtd' THEN 5 ELSE 6 END,
         "reportedAt" DESC
       LIMIT 30`,
      [league, `%${homeTeam}%`, `%${awayTeam}%`, homeShort || '', awayShort || '']
    ).catch(() => ({ rows: [] }));

    // Supplement with sportsclaw.injuries (more comprehensive, freshly scraped)
    const scInjuries = await pool.query(
      `SELECT player_name AS "playerName", team, position, status, injury_type AS "injuryType",
              injury_desc AS description, source, reported_at AS "reportedAt"
       FROM sportsclaw.injuries
       WHERE league = $1
         AND (team ILIKE $2 OR team ILIKE $3 OR team ILIKE $4 OR team ILIKE $5)
         AND LOWER(status) IN ('out', 'ir', 'suspension', 'doubtful', 'questionable', 'day-to-day', 'gtd', 'dtd')
         AND is_active = true
       ORDER BY CASE LOWER(status)
         WHEN 'out' THEN 1 WHEN 'ir' THEN 2 WHEN 'suspension' THEN 3
         WHEN 'doubtful' THEN 4 WHEN 'gtd' THEN 4 WHEN 'questionable' THEN 5
         WHEN 'day-to-day' THEN 5 WHEN 'dtd' THEN 5 ELSE 6 END,
         reported_at DESC
       LIMIT 30`,
      [league, `%${homeTeam}%`, `%${awayTeam}%`, homeShort || '', awayShort || '']
    ).catch(() => ({ rows: [] }));

    // Build set of players marked Active in PlayerInjury (returned to play)
    const activePlayers = await pool.query(
      `SELECT "playerName", team FROM "PlayerInjury"
       WHERE league = $1
         AND (team ILIKE $2 OR team ILIKE $3 OR team ILIKE $4 OR team ILIKE $5)
         AND LOWER(status) = 'active'`,
      [league, `%${homeTeam}%`, `%${awayTeam}%`, homeShort || '', awayShort || '']
    ).catch(() => ({ rows: [] }));
    const activeSet = new Set(activePlayers.rows.map((r: any) => `${r.playerName?.toLowerCase()}-${r.team}`));

    // Merge and deduplicate — exclude players confirmed Active in PlayerInjury
    const seenPlayers = new Set<string>();
    const mergedInjuries: any[] = [];
    for (const row of [...scInjuries.rows, ...injuries.rows]) {
      const key = `${row.playerName?.toLowerCase()}-${row.team}`;
      if (!seenPlayers.has(key) && !activeSet.has(key)) {
        seenPlayers.add(key);
        mergedInjuries.push(row);
      }
    }

    // Fetch recent transactions (trades, signings) for both teams
    // Match by abbreviation or full name
    const transactions = await pool.query(
      `SELECT "playerName", "fromTeam", "toTeam", "transactionType", "transactionDate"
       FROM "PlayerTransaction"
       WHERE league = $1
         AND ("fromTeam" ILIKE $2 OR "toTeam" ILIKE $2 OR "fromTeam" ILIKE $3 OR "toTeam" ILIKE $3
           OR "fromTeam" ILIKE $4 OR "toTeam" ILIKE $4 OR "fromTeam" ILIKE $5 OR "toTeam" ILIKE $5)
         AND "transactionDate" > NOW() - INTERVAL '14 days'
       ORDER BY "transactionDate" DESC
       LIMIT 10`,
      [league, `%${homeTeam}%`, `%${awayTeam}%`, homeShort || '', awayShort || '']
    ).catch(() => ({ rows: [] }));

    // Fetch confirmed/expected lineups for today's matchup
    // Match by abbreviation (LAL, MIN) or full name
    const lineups = await pool.query(
      `SELECT "homeTeam", "awayTeam", "homeStatus", "awayStatus",
              "homePlayers", "awayPlayers", "homeProjMinutes", "awayProjMinutes",
              "updatedAt"
       FROM "GameLineup"
       WHERE league = $1 AND "gameDate" = CURRENT_DATE
         AND (("homeTeam" ILIKE $2 AND "awayTeam" ILIKE $3)
           OR ("homeTeam" ILIKE $3 AND "awayTeam" ILIKE $2)
           OR ("homeTeam" ILIKE $4 AND "awayTeam" ILIKE $5)
           OR ("homeTeam" ILIKE $5 AND "awayTeam" ILIKE $4))
       ORDER BY "updatedAt" DESC LIMIT 1`,
      [league, `%${homeTeam}%`, `%${awayTeam}%`, homeShort || '', awayShort || '']
    ).catch(() => ({ rows: [] }));

    return {
      predictions: predictions.rows,
      sharpMoves,
      lineMovements,
      injuries: mergedInjuries,
      transactions: transactions.rows,
      lineups: lineups.rows,
    };
  } catch (err) {
    console.error('DB analytics fetch error:', err);
    return null;
  }
}
