import { Router, Request, Response } from 'express';
import { authMiddleware, optionalAuth } from '../middleware/auth';
import { auditAdminAccess, requireAdminAccess } from '../middleware/admin';
import { findUserById, deductPick, getPickBalance, ensureDailyGrant, isInGracePeriod } from '../models/user';
import { deletePick, hasUserPurchasedPick, recordPick } from '../models/pick';
import { getCachedForecast, getCachedForecastByTeams, cacheForecast, updateCachedOdds } from '../models/forecast';
import { recordLedgerEntry, LedgerReason } from '../models/ledger';
import { fetchEvents, parseOdds, sanitizeGameOddsForLeague, sanitizeMoneylinePair } from '../services/sgo';
import { buildMlbFallbackProps, buildSourceBackedFallbackProps, generateForecast, generateTeamProps, generateSteamInsight, generateSharpInsight } from '../services/grok';
import { buildForecastFromCuratedEvent } from '../services/forecast-builder';
import { generateDvpInsight, hasDvpData } from '../services/dvp';
import { generateHcwInsight, hasHcwData } from '../services/home-field-scout';
import { LEAGUE_MAP } from '../services/sgo';
import pool from '../db';
import { deduplicateProps } from '../services/prop-dedup';
import { getLegacyCompositeView, getStoredPayloadExtensions } from '../services/rie';
import { describeForecastAsset, getMlbPlayerRole } from '../services/forecast-asset-taxonomy';
import { buildTopPropsOfDay } from '../services/top-props';
import { buildTopPickEntries } from '../services/top-picks';
import { assessPublicGameFit } from '../services/top-game-profile';
import { isPlayerOnTeam, resolveCanonicalName } from '../services/canonical-names';
import {
  getPlayerPropLabelForLeague,
  getSupportedPlayerPropQueryValues,
  getTheOddsPlayerPropMarketKeyForLeague,
  resolvePlayerPropStatIdentity,
} from '../services/player-prop-market-registry';
import { buildPlayerPropMetadata, buildPlayerPropSignal } from '../services/player-prop-signals';
import {
  buildStoredPlayerPropPayload,
  shouldPersistStandalonePlayerPropMetadata,
  shouldRenderTeamPropBundleEntry,
} from '../services/player-prop-storage';
import { buildFeaturedPlayersFromRows, type PlayerRadarLineupSnapshot } from '../services/player-prop-radar';
import { fetchTeamPropMarketCandidates, type TeamPropMarketCandidate } from '../services/team-prop-market-candidates';
import { fetchDirectMlbPropCandidates, fetchMlbPropCandidates, getMlbPropFeedSummary, type MlbPropCandidate } from '../services/mlb-prop-markets';
import { getPiffPropsForGame, loadPiffPropsForDate, type PiffLeg } from '../services/piff';
import { getDigimonForGame, type DigimonPick } from '../services/digimon';
import { recordClvPick } from '../services/clv-picks';
import { fetchInsightSourceData } from '../services/insight-source';
import { normalizePublicForecastNarrative, reconcilePublicForecastProjection } from '../services/public-forecast-normalizer';
import { trackTikTokWebEvent } from '../services/tiktok';
import { teamMatchScore } from '../services/the-odds';
import {
  digilanderModuleAvailable,
  digilanderModuleEmpty,
  digilanderModuleFailed,
  digilanderModuleFromError,
  digilanderModuleLocked,
  digilanderModuleOk,
  digilanderSafeArrayLength,
  digilanderSummarizeModule,
  type DigilanderModuleResponse,
} from '../services/digilander-contract';
import { getTeamAbbr, normalizeTeamNameKey, teamNameKeySql } from '../lib/team-abbreviations';
import { getPublicMarketSourceMeta, hasNamedMarketSource } from '../lib/market-source';
import { usesAssetBackedPropHighlights } from '../lib/prop-highlights';
import {
  EU_EVENT_LOOKAHEAD_DAYS,
  getEtDateKey,
  getLeagueLookaheadDays,
  isLeagueDateKeyInWindow,
  isLeagueEventInWindow,
} from '../lib/league-windows';
import {
  buildPublicGroupedPlayers,
  buildForecastBalanceFields,
  buildPublicDigiviewEvidence,
  camelizeObjectKeys,
  FORECAST_PUBLIC_CONTRACT_VERSION,
  maybeBuildMlbMatchupContext,
  maybeBuildMlbPhaseContext,
  serializeMlbPropContext,
  serializePublicForecastBlob,
  serializePublicPlayerProp,
  type PublicTopGameCard,
} from '../contracts/forecast-public-contract';

const router = Router();

const SOCCER_LEAGUES = new Set([
  'epl',
  'la_liga',
  'serie_a',
  'bundesliga',
  'ligue_1',
  'champions_league',
  'mls',
]);

const CLUB_PREFIX_TOKENS = new Set([
  '1',
  '1fc',
  'ac',
  'afc',
  'as',
  'athletic',
  'bsc',
  'cf',
  'club',
  'de',
  'fc',
  'fk',
  'if',
  'ofc',
  'rc',
  'sc',
  'scl',
  'sd',
  'sk',
  'sporting',
  'ss',
  'stade',
  'sv',
  'tsg',
  'ud',
  'united',
  'vfb',
  'vfl',
]);

const GENERIC_TEAM_SEARCH_TOKENS = new Set([
  ...CLUB_PREFIX_TOKENS,
  'city',
  'club',
  'county',
  'real',
  'town',
  'united',
]);

const SOCCER_NEWS_TEAM_ALIASES: Record<string, string[]> = {
  hamburgersv: ['hamburg'],
  svwerderbremen: ['werder'],
  bayerleverkusen: ['leverkusen'],
  bayernmunich: ['bayern'],
  borussiamonchengladbach: ['gladbach'],
  westhamunited: ['west ham'],
  tottenhamhotspur: ['spurs'],
  wolverhamptonwanderers: ['wolves'],
  manchesterunited: ['man utd', 'man united'],
  manchestercity: ['man city'],
  brightonhovealbion: ['brighton'],
  newcastleunited: ['newcastle'],
  crystalpalace: ['palace'],
};

const SOCCER_NEWS_SHORT_ALIASES: Record<string, string[]> = {
  hsv: ['hamburg'],
  svw: ['werder'],
  bay: ['bayern'],
  bmg: ['gladbach'],
  whu: ['west ham'],
  tot: ['spurs'],
  wol: ['wolves'],
  mun: ['man utd', 'man united'],
  mci: ['man city'],
  bha: ['brighton'],
  new: ['newcastle'],
  cry: ['palace'],
};

function isSoccerLeague(league: string | null | undefined): boolean {
  return SOCCER_LEAGUES.has(String(league || '').trim().toLowerCase());
}

function normalizeSearchText(value: string | null | undefined): string {
  return String(value || '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function stripClubPrefixes(value: string | null | undefined): string {
  const words = normalizeSearchText(value).split(' ').filter(Boolean);
  while (words.length > 1 && CLUB_PREFIX_TOKENS.has(words[0])) {
    words.shift();
  }
  return words.join(' ').trim();
}

function addPhraseVariants(terms: Set<string>, words: string[]): void {
  if (words.length < 2) return;
  const maxSpan = Math.min(3, words.length);
  for (let span = 2; span <= maxSpan; span += 1) {
    for (let index = 0; index <= words.length - span; index += 1) {
      terms.add(words.slice(index, index + span).join(' '));
    }
  }
}

function addTeamSearchVariant(terms: Set<string>, value: string | null | undefined): void {
  const normalized = normalizeSearchText(value);
  if (!normalized) return;

  terms.add(normalized);

  const stripped = stripClubPrefixes(normalized);
  if (stripped) terms.add(stripped);

  const words = stripped ? stripped.split(' ').filter(Boolean) : normalized.split(' ').filter(Boolean);
  addPhraseVariants(terms, words);

  const wordsWithoutNumbers = words.filter((word) => !/^\d+$/.test(word));
  if (wordsWithoutNumbers.length !== words.length) {
    addPhraseVariants(terms, wordsWithoutNumbers);
    if (wordsWithoutNumbers.length >= 2) {
      terms.add(wordsWithoutNumbers.join(' '));
    }
  }

  if (words.length >= 2) {
    terms.add(words.slice(-2).join(' '));
  }
  if (words.length >= 1 && words[words.length - 1].length >= 4) {
    terms.add(words[words.length - 1]);
  }
  for (const word of words) {
    if (word.length >= 4) terms.add(word);
  }
}

function addSoccerNewsAliasVariants(
  terms: Set<string>,
  teamName: string | null | undefined,
  teamShort: string | null | undefined,
): void {
  const normalizedKey = normalizeTeamNameKey(teamName);
  const shortKey = normalizeSearchText(teamShort);

  for (const alias of [
    ...(SOCCER_NEWS_TEAM_ALIASES[normalizedKey] || []),
    ...(SOCCER_NEWS_SHORT_ALIASES[shortKey] || []),
  ]) {
    addTeamSearchVariant(terms, alias);
  }
}

function buildTeamSearchTerms(params: {
  teamName: string | null | undefined;
  teamShort?: string | null | undefined;
  league?: string | null | undefined;
}): string[] {
  const terms = new Set<string>();
  addTeamSearchVariant(terms, params.teamName);
  if (params.teamShort) addTeamSearchVariant(terms, params.teamShort);
  if (isSoccerLeague(params.league)) {
    addSoccerNewsAliasVariants(terms, params.teamName, params.teamShort);
  }
  return [...terms]
    .map((term) => normalizeSearchText(term))
    .filter((term) => {
      if (!term || term.length < 4) return false;
      if (!term.includes(' ') && GENERIC_TEAM_SEARCH_TOKENS.has(term)) return false;
      return true;
    });
}

function buildMarketHistoryLikePatterns(params: {
  teamName: string | null | undefined;
  teamShort?: string | null | undefined;
  league?: string | null | undefined;
}): string[] {
  return buildMarketHistoryExactKeys(params).map((term) => `%${term}%`);
}

function buildMarketHistoryExactKeys(params: {
  teamName: string | null | undefined;
  teamShort?: string | null | undefined;
  league?: string | null | undefined;
}): string[] {
  const keys = new Set<string>();

  for (const value of [
    params.teamName,
    params.teamShort,
    ...buildTeamSearchTerms(params),
  ]) {
    const normalizedKey = normalizeTeamNameKey(value);
    if (normalizedKey.length >= 3) keys.add(normalizedKey);
  }

  return [...keys];
}

type TeamNamedRow = {
  homeTeam?: string | null;
  awayTeam?: string | null;
  home_team?: string | null;
  away_team?: string | null;
};

function getRowTeamNames(row: TeamNamedRow): { homeTeam: string; awayTeam: string } {
  return {
    homeTeam: String(row.homeTeam ?? row.home_team ?? '').trim(),
    awayTeam: String(row.awayTeam ?? row.away_team ?? '').trim(),
  };
}

function scoreTeamNamedRow(
  row: TeamNamedRow,
  targetHomeTeam: string,
  targetAwayTeam: string,
): number {
  const { homeTeam, awayTeam } = getRowTeamNames(row);
  if (!homeTeam || !awayTeam) return 0;

  const directHome = teamMatchScore(homeTeam, targetHomeTeam);
  const directAway = teamMatchScore(awayTeam, targetAwayTeam);
  const direct = directHome > 0 && directAway > 0 ? directHome + directAway : 0;

  const swappedHome = teamMatchScore(homeTeam, targetAwayTeam);
  const swappedAway = teamMatchScore(awayTeam, targetHomeTeam);
  const swapped = swappedHome > 0 && swappedAway > 0 ? swappedHome + swappedAway : 0;

  return Math.max(direct, swapped);
}

function selectBestMatchedTeamRows<T extends TeamNamedRow>(
  rows: T[],
  targetHomeTeam: string,
  targetAwayTeam: string,
): T[] {
  if (!rows.length || !targetHomeTeam || !targetAwayTeam) return [];

  const grouped = new Map<string, { score: number; rows: T[] }>();

  for (const row of rows) {
    const { homeTeam, awayTeam } = getRowTeamNames(row);
    const score = scoreTeamNamedRow(row, targetHomeTeam, targetAwayTeam);
    if (score <= 0) continue;
    const key = `${normalizeTeamNameKey(homeTeam)}::${normalizeTeamNameKey(awayTeam)}`;
    const existing = grouped.get(key);
    if (!existing) {
      grouped.set(key, { score, rows: [row] });
      continue;
    }
    existing.score = Math.max(existing.score, score);
    existing.rows.push(row);
  }

  let bestKey: string | null = null;
  let bestScore = 0;
  let bestCount = 0;

  for (const [key, value] of grouped.entries()) {
    if (
      value.score > bestScore
      || (value.score === bestScore && value.rows.length > bestCount)
    ) {
      bestKey = key;
      bestScore = value.score;
      bestCount = value.rows.length;
    }
  }

  return bestKey ? grouped.get(bestKey)?.rows || [] : [];
}

function normalizeDigiviewText(value: string | null | undefined): string {
  return String(value || '')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, ' ')
    .trim();
}

function normalizeDigiviewStatKey(value: string | null | undefined): string {
  const normalized = normalizeDigiviewText(value);
  if (!normalized) return '';
  if (normalized.includes('point')) return 'pts';
  if (normalized.includes('rebound')) return 'reb';
  if (normalized.includes('assist')) return 'ast';
  if (normalized.includes('three') || normalized.includes('3pt') || normalized.includes('3 pointer')) return '3pm';
  if (normalized.includes('steal')) return 'stl';
  if (normalized.includes('block')) return 'blk';
  if (normalized.includes('pra') || (normalized.includes('points') && normalized.includes('rebounds') && normalized.includes('assists'))) return 'pra';
  return normalized.split(' ')[0] || normalized;
}

function findMatchingDigimonPick(params: {
  picks: DigimonPick[];
  player: string | null | undefined;
  team: string | null | undefined;
  statHint: string | null | undefined;
  line: number | null | undefined;
}): DigimonPick | null {
  const playerKey = normalizeDigiviewText(params.player);
  const teamKey = String(params.team || '').trim().toUpperCase();
  const statKey = normalizeDigiviewStatKey(params.statHint);
  const lineValue = params.line == null ? null : Number(params.line);

  if (!playerKey || !teamKey || !statKey) return null;

  const matches = params.picks.filter((pick) => {
    if (String(pick.team || '').trim().toUpperCase() !== teamKey) return false;
    if (normalizeDigiviewText(pick.player) !== playerKey) return false;
    if (normalizeDigiviewStatKey(pick.prop) !== statKey) return false;
    if (lineValue == null || pick.line == null) return true;
    return Math.abs(Number(pick.line) - lineValue) <= 1.5;
  });

  return matches[0] || null;
}

function findMatchingPiffLeg(params: {
  legs: PiffLeg[];
  player: string | null | undefined;
  team: string | null | undefined;
  statHint: string | null | undefined;
  line: number | null | undefined;
}): PiffLeg | null {
  const playerKey = normalizeDigiviewText(params.player);
  const teamKey = String(params.team || '').trim().toUpperCase();
  const statKey = normalizeDigiviewStatKey(params.statHint);
  const lineValue = params.line == null ? null : Number(params.line);

  if (!playerKey || !teamKey || !statKey) return null;

  const matches = params.legs.filter((leg) => {
    if (String(leg.team || '').trim().toUpperCase() !== teamKey) return false;
    if (normalizeDigiviewText(leg.name) !== playerKey) return false;
    if (normalizeDigiviewStatKey(leg.stat) !== statKey) return false;
    if (lineValue == null || leg.line == null) return true;
    return Math.abs(Number(leg.line) - lineValue) <= 1.5;
  });

  return matches[0] || null;
}

function getHistoricalStatComponents(league: string | null | undefined, statHint: string | null | undefined): string[][] {
  const lg = String(league || '').toLowerCase();
  const text = normalizeDigiviewText(statHint);
  const compact = text.replace(/\s+/g, '');
  if (!text) return [];

  const hasAny = (...needles: string[]): boolean =>
    needles.some((needle) => text.includes(needle) || compact.includes(needle.replace(/\s+/g, '')));

  if (lg === 'nba' || lg === 'wnba' || lg === 'ncaab') {
    if (hasAny('points rebounds assists', 'pointsreboundsassists', 'pra')) return [['points', 'espn_pts'], ['rebounds', 'espn_reb'], ['assists', 'espn_ast']];
    if (hasAny('points rebounds', 'pointsrebounds', 'pr')) return [['points', 'espn_pts'], ['rebounds', 'espn_reb']];
    if (hasAny('points assists', 'pointsassists', 'pa')) return [['points', 'espn_pts'], ['assists', 'espn_ast']];
    if (hasAny('rebounds assists', 'reboundsassists', 'ra')) return [['rebounds', 'espn_reb'], ['assists', 'espn_ast']];
    if (hasAny('offensive rebounds', 'oreb')) return [['espn_oreb']];
    if (hasAny('defensive rebounds', 'dreb')) return [['espn_dreb']];
    if (hasAny('turnovers', 'turnover', 'tov')) return [['espn_to']];
    if (hasAny('point', 'pts')) return [['points', 'espn_pts']];
    if (hasAny('rebound', 'reb')) return [['rebounds', 'espn_reb']];
    if (hasAny('assist', 'ast')) return [['assists', 'espn_ast']];
    if (hasAny('three pointers made', 'three pointer', 'three', '3pt', '3pm')) return [['threes', 'threePointersMade', 'espn_3pt']];
    if (hasAny('steal', 'stl')) return [['steals', 'espn_stl']];
    if (hasAny('block', 'blk')) return [['blocks', 'espn_blk']];
  }

  if (lg === 'nhl') {
    if (hasAny('goalie saves', 'goaliesaves', 'save')) return [['saves', 'nhl_g_saves']];
    if (hasAny('shots on goal', 'shotsongoal', 'sog', 'shot')) return [['shots']];
    if (hasAny('blocked shots', 'blockedshots', 'blocked')) return [['blockedShots']];
    if (hasAny('points')) return [['points']];
    if (hasAny('assist')) return [['assists']];
    if (hasAny('goal')) return [['goals']];
    if (hasAny('hits', 'hit')) return [['hits']];
  }

  if (lg === 'mlb') {
    if (hasAny('hits runs rbis', 'hits runs rbi', 'hitsrunsrbi', 'batting hits runs rbi', 'battinghitsrunsrbi', 'hrr')) return [['mlb_hits', 'hits'], ['mlb_runs'], ['mlb_rbi', 'rbi', 'batting_RBI']];
    if (hasAny('home runs allowed', 'homerunsallowed')) return [['mlb_home_runs_allowed']];
    if (hasAny('runs allowed', 'runsallowed')) return [['mlb_runs_allowed']];
    if (hasAny('hits allowed', 'hitsallowed')) return [['mlb_hits_allowed', 'pitching_hits']];
    if (hasAny('walks allowed', 'walksallowed')) return [['mlb_walks_allowed', 'pitching_basesOnBalls']];
    if (hasAny('earned run', 'earnedruns')) return [['mlb_earned_runs', 'pitching_earnedRuns']];
    if (hasAny('pitcher strikeout', 'pitching strikeout', 'pitchingstrikeouts', 'pitchingstrikeout', 'strikeouts allowed', 'pitcher_so')) return [['mlb_pitcher_strikeouts', 'pitcher_so', 'pitching_strikeouts']];
    if (hasAny('innings pitched', 'inningspitched')) return [['mlb_innings_pitched']];
    if (hasAny('total bases', 'total base', 'totalbases', 'battingtotalbases')) return [['mlb_total_bases', 'total_bases']];
    if (hasAny('batting home runs', 'battinghomeruns', 'home run', 'homeruns')) return [['mlb_home_runs', 'home_runs', 'batting_homeRuns']];
    if (hasAny('batting rbi', 'battingrbi', 'rbi')) return [['mlb_rbi', 'rbi', 'batting_RBI']];
    if (hasAny('batting doubles', 'battingdoubles', 'doubles')) return [['mlb_doubles']];
    if (hasAny('batting triples', 'battingtriples', 'triples')) return [['mlb_triples']];
    if (hasAny('batting stolen bases', 'battingstolenbases', 'stolen bases', 'stolenbases', 'stolen')) return [['mlb_stolen_bases']];
    if (hasAny('batting bases on balls', 'battingbasesonballs', 'bases on balls', 'walk')) return [['mlb_walks', 'batting_basesOnBalls']];
    if (hasAny('batting strikeouts', 'battingstrikeouts', 'hitter strikeouts', 'hitterstrikeouts', 'strikeout')) return [['mlb_strikeouts']];
    if (hasAny('batting hits', 'battinghits', 'hit')) return [['mlb_hits', 'hits']];
    if (hasAny('run', 'runs')) return [['mlb_runs']];
    if (hasAny('outs')) return [['pitching_outs']];
  }

  return [];
}

type HistoricalGameSample = {
  gameDate: string | null;
  opponent: string | null;
  value: number | null;
  hit: boolean | null;
};

type PlayerDigiviewLineSnapshot = {
  marketType: string | null;
  bookmaker: string | null;
  propLine: number | null;
  overPrice: number | null;
  underPrice: number | null;
  capturedAt: string | null;
};

async function fetchHistoricalDigiviewSamples(params: {
  league: string;
  curated: any;
  rows: Array<{ id: string; player_name: string | null; team_id: string | null; team_side: 'home' | 'away' | null; forecast_payload?: any }>;
}): Promise<Map<string, { last5Samples: HistoricalGameSample[]; last10Samples: HistoricalGameSample[]; h2hSamples: HistoricalGameSample[] }>> {
  const requests = params.rows
    .map((row) => {
      const components = getHistoricalStatComponents(
        params.league,
        row.forecast_payload?.normalized_stat_type || row.forecast_payload?.stat_type || row.forecast_payload?.prop || null,
      );
      if (!row.id || !row.player_name || !row.team_id || components.length === 0) return null;
      return {
        rowId: row.id,
        playerName: String(row.player_name).trim(),
        team: String(row.team_id).trim(),
        opponent: row.team_side === 'home'
          ? String(params.curated?.away_short || '').trim()
          : String(params.curated?.home_short || '').trim(),
        direction: String(row.forecast_payload?.forecast_direction || row.forecast_payload?.recommendation || '').trim().toLowerCase(),
        line: Number(row.forecast_payload?.market_line_value ?? row.forecast_payload?.line),
        components,
      };
    })
    .filter((entry): entry is NonNullable<typeof entry> => Boolean(entry));

  if (requests.length === 0) return new Map();

  const players = Array.from(new Set(requests.map((item) => item.playerName)));
  const teams = Array.from(new Set(requests.map((item) => item.team)));
  const statKeys = Array.from(new Set(requests.flatMap((item) => item.components.flat())));

  const { rows } = await pool.query(
    `SELECT "playerName", team, opponent, "gameDate", "canonicalGameId", "statKey", value
       FROM "PlayerGameMetric"
      WHERE league = $1
        AND "playerName" = ANY($2::text[])
        AND team = ANY($3::text[])
        AND "statKey" = ANY($4::text[])
      ORDER BY "gameDate" DESC`,
    [params.league, players, teams, statKeys],
  ).catch(() => ({ rows: [] as any[] }));

  const byPlayerTeam = new Map<string, any[]>();
  for (const row of rows) {
    const key = `${row.playerName}||${row.team}`;
    const bucket = byPlayerTeam.get(key);
    if (bucket) bucket.push(row);
    else byPlayerTeam.set(key, [row]);
  }

  const result = new Map<string, { last5Samples: HistoricalGameSample[]; last10Samples: HistoricalGameSample[]; h2hSamples: HistoricalGameSample[] }>();

  for (const request of requests) {
    const sourceRows = byPlayerTeam.get(`${request.playerName}||${request.team}`) || [];
    const games = new Map<string, { gameDate: string | null; opponent: string | null; stats: Map<string, number> }>();

    for (const row of sourceRows) {
      const gameKey = String(row.canonicalGameId || row.gameDate);
      if (!games.has(gameKey)) {
        games.set(gameKey, {
          gameDate: row.gameDate instanceof Date ? row.gameDate.toISOString() : row.gameDate ? new Date(row.gameDate).toISOString() : null,
          opponent: row.opponent || null,
          stats: new Map<string, number>(),
        });
      }
      games.get(gameKey)!.stats.set(String(row.statKey), Number(row.value));
    }

    const computed: HistoricalGameSample[] = [];
    for (const game of games.values()) {
      let total = 0;
      let hasAllComponents = true;
      for (const aliases of request.components) {
        const found = aliases.find((alias) => game.stats.has(alias));
        if (!found) {
          hasAllComponents = false;
          break;
        }
        total += Number(game.stats.get(found) || 0);
      }
      if (!hasAllComponents) continue;

      const line = Number.isFinite(request.line) ? request.line : null;
      const hit = line == null
        ? null
        : total === line
          ? null
          : request.direction === 'under'
            ? total < line
            : total > line;

      computed.push({
        gameDate: game.gameDate,
        opponent: game.opponent,
        value: Math.round(total * 10) / 10,
        hit,
      });
    }

    computed.sort((a, b) => new Date(b.gameDate || 0).getTime() - new Date(a.gameDate || 0).getTime());

    result.set(request.rowId, {
      last5Samples: computed.slice(0, 5),
      last10Samples: computed.slice(0, 10),
      h2hSamples: computed.filter((sample) => String(sample.opponent || '').trim().toUpperCase() === request.opponent.toUpperCase()).slice(0, 10),
    });
  }

  return result;
}

type SyntheticPlayerPropRow = {
  id: string;
  player_name: string | null;
  team_id: string | null;
  team_side: 'home' | 'away' | null;
  league: string | null;
  confidence_score?: number | null;
  forecast_payload?: Record<string, any> | null;
};

const MLB_FALLBACK_ROW_THRESHOLD = 12;

function buildSyntheticPlayerPropRowKey(row: {
  player_name: string | null | undefined;
  team_id: string | null | undefined;
  forecast_payload?: Record<string, any> | null;
}): string {
  const payload = row.forecast_payload || {};
  return [
    normalizeDigiviewText(row.player_name),
    String(row.team_id || '').trim().toUpperCase(),
    normalizeDigiviewText(payload.normalized_stat_type || payload.stat_type || payload.prop || ''),
    normalizeMarketLineLookup(payload.market_line_value ?? payload.line),
    String(payload.forecast_direction || payload.recommendation || '').trim().toUpperCase(),
  ].join('|');
}

function buildMlbFallbackAssetId(params: {
  eventId: string;
  teamShort: string;
  player: string;
  statType: string;
  line: number | null | undefined;
  side: 'over' | 'under';
}): string {
  return [
    'mlb-live',
    params.eventId,
    normalizeDigiviewText(params.player).replace(/\s+/g, '-'),
    String(params.teamShort || '').trim().toUpperCase(),
    String(params.statType || '').trim().toLowerCase(),
    normalizeMarketLineLookup(params.line),
    params.side,
  ].join(':');
}

function buildMlbFallbackPayload(params: {
  candidate: Awaited<ReturnType<typeof fetchMlbPropCandidates>>[number];
  side: 'over' | 'under';
  teamSide: 'home' | 'away';
  lineupSnapshot: PlayerRadarLineupSnapshot | null;
}): Record<string, any> {
  const price = params.side === 'over' ? params.candidate.overOdds : params.candidate.underOdds;
  const propLabel = getPlayerPropLabelForLeague('mlb', params.candidate.normalizedStatType || params.candidate.statType || null)
    || params.candidate.statType
    || 'Prop';
  const lineupStatus = params.teamSide === 'home'
    ? params.lineupSnapshot?.homeStatus
    : params.lineupSnapshot?.awayStatus;
  const sourceMap = Array.isArray(params.candidate.sourceMap) ? params.candidate.sourceMap : [];
  const availableBooks = countUniqueMarketBooks(sourceMap);
  const primarySource = sourceMap[0]?.source || null;

  return {
    prop: propLabel,
    stat_type: params.candidate.statType || null,
    normalized_stat_type: params.candidate.normalizedStatType || params.candidate.statType || null,
    market_line_value: params.candidate.marketLineValue ?? null,
    line: params.candidate.marketLineValue ?? null,
    odds: price ?? null,
    recommendation: params.side,
    forecast_direction: params.side.toUpperCase(),
    player_role: params.candidate.playerRole || null,
    market_completeness_status: params.candidate.completenessStatus || null,
    market_source: primarySource,
    source_backed: true,
    model_context: {
      lineup_certainty: lineupStatus || null,
      lineup_source: lineupStatus ? 'GameLineup' : null,
      available_books: availableBooks > 0 ? availableBooks : null,
      context_summary: [
        'Live MLB market',
        params.candidate.completenessStatus ? params.candidate.completenessStatus.replace(/_/g, ' ') : null,
        params.candidate.isGapFilled ? 'gap filled' : null,
      ].filter(Boolean).join(' • ') || null,
    },
    metadata: {
      live_api_fallback: true,
      source: params.candidate.source,
      completion_method: params.candidate.completionMethod || null,
      source_map: sourceMap,
    },
  };
}

async function fetchMlbLiveFallbackRows(params: {
  eventId: string;
  curated: any;
  lineupSnapshot: PlayerRadarLineupSnapshot | null;
}): Promise<SyntheticPlayerPropRow[]> {
  try {
    const homeShort = String(params.curated?.home_short || '').trim().toUpperCase();
    const awayShort = String(params.curated?.away_short || '').trim().toUpperCase();
    const startsAtRaw = params.curated?.starts_at;
    const startsAt = startsAtRaw instanceof Date
      ? startsAtRaw.toISOString()
      : startsAtRaw
        ? new Date(startsAtRaw).toISOString()
        : '';
    if (!homeShort || !awayShort || !startsAt) return [];

    const [homeCandidates, awayCandidates] = await Promise.all([
      fetchMlbPropCandidates({
        teamShort: homeShort,
        teamName: params.curated?.home_team || homeShort,
        opponentShort: awayShort,
        startsAt,
      }).catch((err) => {
        console.warn(`[mlb-fallback] local candidate fetch failed for ${params.eventId} home ${homeShort}: ${err instanceof Error ? err.message : String(err)}`);
        return [];
      }),
      fetchMlbPropCandidates({
        teamShort: awayShort,
        teamName: params.curated?.away_team || awayShort,
        opponentShort: homeShort,
        startsAt,
      }).catch((err) => {
        console.warn(`[mlb-fallback] local candidate fetch failed for ${params.eventId} away ${awayShort}: ${err instanceof Error ? err.message : String(err)}`);
        return [];
      }),
    ]);

    const [homeApiCandidates, awayApiCandidates] = homeCandidates.length + awayCandidates.length === 0
      ? await Promise.all([
          fetchDirectMlbPropCandidates({
            teamShort: homeShort,
            teamName: params.curated?.home_team || homeShort,
            opponentShort: awayShort,
            startsAt,
          }).catch((err) => {
            console.warn(`[mlb-fallback] direct api fetch failed for ${params.eventId} home ${homeShort}: ${err instanceof Error ? err.message : String(err)}`);
            return [];
          }),
          fetchDirectMlbPropCandidates({
            teamShort: awayShort,
            teamName: params.curated?.away_team || awayShort,
            opponentShort: homeShort,
            startsAt,
          }).catch((err) => {
            console.warn(`[mlb-fallback] direct api fetch failed for ${params.eventId} away ${awayShort}: ${err instanceof Error ? err.message : String(err)}`);
            return [];
          }),
        ])
      : [[], []];

    const finalHomeCandidates = homeCandidates.length > 0 ? homeCandidates : homeApiCandidates;
    const finalAwayCandidates = awayCandidates.length > 0 ? awayCandidates : awayApiCandidates;

    const out: SyntheticPlayerPropRow[] = [];
    const pushCandidateRows = (
      candidates: Awaited<ReturnType<typeof fetchMlbPropCandidates>>,
      teamSide: 'home' | 'away',
      teamShort: string,
    ) => {
      for (const candidate of candidates) {
        const sides = Array.isArray(candidate.availableSides) ? candidate.availableSides : [];
        for (const side of sides) {
          const price = side === 'over' ? candidate.overOdds : candidate.underOdds;
          if (price == null) continue;
          out.push({
            id: buildMlbFallbackAssetId({
              eventId: params.eventId,
              teamShort,
              player: candidate.player,
              statType: candidate.normalizedStatType || candidate.statType || 'prop',
              line: candidate.marketLineValue,
              side,
            }),
            player_name: candidate.player,
            team_id: teamShort,
            team_side: teamSide,
            league: 'mlb',
            confidence_score: null,
            forecast_payload: buildMlbFallbackPayload({
              candidate,
              side,
              teamSide,
              lineupSnapshot: params.lineupSnapshot,
            }),
          });
        }
      }
    };

    pushCandidateRows(finalHomeCandidates, 'home', homeShort);
    pushCandidateRows(finalAwayCandidates, 'away', awayShort);
    return out;
  } catch (err) {
    console.error('[mlb-fallback-error]', params.eventId, err);
    return [];
  }
}

function mergePlayerPropRowsWithFallback(
  primaryRows: Array<{ id: string; player_name: string | null; team_id: string | null; team_side: 'home' | 'away' | null; league: string | null; confidence_score?: number | null; forecast_payload?: Record<string, any> | null }>,
  fallbackRows: SyntheticPlayerPropRow[],
): Array<{ id: string; player_name: string | null; team_id: string | null; team_side: 'home' | 'away' | null; league: string | null; confidence_score?: number | null; forecast_payload?: Record<string, any> | null }> {
  if (fallbackRows.length === 0) return primaryRows;
  const merged = [...primaryRows];
  const seen = new Set(primaryRows.map((row) => buildSyntheticPlayerPropRowKey(row)));
  for (const row of fallbackRows) {
    const key = buildSyntheticPlayerPropRowKey(row);
    if (seen.has(key)) continue;
    seen.add(key);
    merged.push(row);
  }
  return merged;
}

function buildPlayerDigiviewSnapshotMarketKeys(league: string | null | undefined, statHint: string | null | undefined): string[] {
  const canonical = getTheOddsPlayerPropMarketKeyForLeague(league, statHint);
  const keys = new Set<string>();
  if (canonical) keys.add(canonical);

  if (canonical === 'player_threes') keys.add('player_threePointersMade');
  if (canonical === 'player_total_saves') keys.add('goalie_saves');
  if (canonical === 'player_shots_on_goal') keys.add('shots_onGoal');

  return Array.from(keys);
}

async function fetchPlayerDigiviewLineSnapshots(params: {
  league: string;
  startsAt: string | null | undefined;
  homeTeam: string | null | undefined;
  awayTeam: string | null | undefined;
  playerName: string;
  marketTypes: string[];
}): Promise<PlayerDigiviewLineSnapshot[]> {
  if (!params.startsAt || !params.playerName || params.marketTypes.length === 0) return [];

  const { rows } = await pool.query(
    `SELECT "marketType", "playerName", "propLine", "overPrice", "underPrice", "capturedAt", bookmaker
       FROM "PropSnapshot"
      WHERE league = $1
        AND DATE("gameDate") = DATE($2::timestamptz)
        AND LOWER("playerName") = LOWER($3)
        AND (
          (LOWER("homeTeam") = LOWER($4) AND LOWER("awayTeam") = LOWER($5))
          OR (LOWER("homeTeam") = LOWER($5) AND LOWER("awayTeam") = LOWER($4))
        )
        AND "marketType" = ANY($6::text[])
      ORDER BY "capturedAt" ASC, bookmaker ASC`,
    [params.league, params.startsAt, params.playerName, params.homeTeam || '', params.awayTeam || '', params.marketTypes],
  ).catch(() => ({ rows: [] as any[] }));

  const merged = new Map<string, PlayerDigiviewLineSnapshot>();
  for (const row of rows) {
    const capturedAt = row.capturedAt instanceof Date
      ? row.capturedAt.toISOString()
      : row.capturedAt
        ? new Date(row.capturedAt).toISOString()
        : null;
    const key = [
      String(row.marketType || ''),
      String(row.bookmaker || ''),
      String(row.propLine ?? ''),
      capturedAt || '',
    ].join('||');
    const existing = merged.get(key);
    if (existing) {
      if (existing.overPrice == null && row.overPrice != null) existing.overPrice = Number(row.overPrice);
      if (existing.underPrice == null && row.underPrice != null) existing.underPrice = Number(row.underPrice);
      continue;
    }
    merged.set(key, {
      marketType: row.marketType || null,
      bookmaker: row.bookmaker || null,
      propLine: row.propLine != null ? Number(row.propLine) : null,
      overPrice: row.overPrice != null ? Number(row.overPrice) : null,
      underPrice: row.underPrice != null ? Number(row.underPrice) : null,
      capturedAt,
    });
  }

  return Array.from(merged.values());
}

type GameMarketHistoryRow = {
  marketType: string | null;
  openLine: number | null;
  currentLine: number | null;
  closingLine: number | null;
  lineMovement: number | null;
  movementDirection: string | null;
  openOdds: number | null;
  currentOdds: number | null;
  closingOdds: number | null;
  steamMove: boolean | null;
  reverseLineMove: boolean | null;
  recordedAt: string | null;
};

type GameBookSnapshotRow = {
  bookmaker: string | null;
  market: string | null;
  lineValue: number | null;
  homeOdds: number | null;
  awayOdds: number | null;
  overOdds: number | null;
  underOdds: number | null;
  fetchedAt: string | null;
  isBestLine: boolean | null;
  source: string | null;
};

type NormalizedMarketHistoryKey = 'moneyline' | 'spread' | 'total' | 'unknown';

type GameMarketSummaryEntry = {
  marketType: NormalizedMarketHistoryKey;
  label: string;
  bookmakerCount: number;
  snapshotCount: number;
  lastSnapshotAt: string | null;
  freshness: 'fresh' | 'stale' | 'none';
  bestBookNow: GameBookSnapshotRow | null;
  lineChangedFrom: {
    openLine: number | null;
    currentLine: number | null;
    delta: number | null;
    movementDirection: string | null;
    recordedAt: string | null;
    steamMove: boolean | null;
    reverseLineMove: boolean | null;
  } | null;
};

type GameMarketSummary = {
  bookmakerCount: number;
  bookSnapshotCount: number;
  lineMovementCount: number;
  lastSnapshotAt: string | null;
  lastMovementAt: string | null;
  lastObservedAt: string | null;
  freshness: 'fresh' | 'stale' | 'none';
  bestBookNow: (GameBookSnapshotRow & { marketType: NormalizedMarketHistoryKey; label: string }) | null;
  lineChangedFrom: ({
    marketType: NormalizedMarketHistoryKey;
    label: string;
    openLine: number | null;
    currentLine: number | null;
    delta: number | null;
    movementDirection: string | null;
    recordedAt: string | null;
    steamMove: boolean | null;
    reverseLineMove: boolean | null;
  }) | null;
  cards: GameMarketSummaryEntry[];
};

const MARKET_HISTORY_QUERY_TIMEOUT_MS = process.env.NODE_ENV === 'test' ? 50 : 1200;
const SOCCER_LINE_MOVEMENT_QUERY_TIMEOUT_MS = process.env.NODE_ENV === 'test' ? 50 : 400;
const MARKET_HISTORY_STALE_THRESHOLD_MS = 36 * 60 * 60 * 1000;

function normalizeMarketHistoryKey(value: string | null | undefined): NormalizedMarketHistoryKey {
  const key = String(value || '').trim().toLowerCase();
  if (key === 'h2h' || key === 'moneyline') return 'moneyline';
  if (key === 'spreads' || key === 'spread') return 'spread';
  if (key === 'totals' || key === 'total') return 'total';
  return 'unknown';
}

function getMarketHistoryLabel(key: NormalizedMarketHistoryKey): string {
  if (key === 'moneyline') return 'Moneyline';
  if (key === 'spread') return 'Spread';
  if (key === 'total') return 'Total';
  return 'Market';
}

function getIsoTimestampMs(value: string | null | undefined): number | null {
  if (!value) return null;
  const timestampMs = new Date(value).getTime();
  return Number.isFinite(timestampMs) ? timestampMs : null;
}

function compareBookSnapshotRows(left: GameBookSnapshotRow, right: GameBookSnapshotRow): number {
  const bestLineDelta = Number(Boolean(right.isBestLine)) - Number(Boolean(left.isBestLine));
  if (bestLineDelta !== 0) return bestLineDelta;
  const fetchedAtDelta = (getIsoTimestampMs(right.fetchedAt) || 0) - (getIsoTimestampMs(left.fetchedAt) || 0);
  if (fetchedAtDelta !== 0) return fetchedAtDelta;
  const marketPriority = ['moneyline', 'spread', 'total', 'unknown'];
  const marketPriorityDelta =
    marketPriority.indexOf(normalizeMarketHistoryKey(left.market))
    - marketPriority.indexOf(normalizeMarketHistoryKey(right.market));
  if (marketPriorityDelta !== 0) return marketPriorityDelta;
  return String(left.bookmaker || '').localeCompare(String(right.bookmaker || ''));
}

function buildGameMarketSummary(params: {
  startsAt: string | null | undefined;
  lineMovement: GameMarketHistoryRow[];
  bookSnapshots: GameBookSnapshotRow[];
}): GameMarketSummary {
  const bookSnapshots = Array.isArray(params.bookSnapshots) ? params.bookSnapshots : [];
  const lineMovement = Array.isArray(params.lineMovement) ? params.lineMovement : [];
  const bookmakerCount = new Set(bookSnapshots.map((row) => String(row.bookmaker || '').trim()).filter(Boolean)).size;
  const lastSnapshotAtMs = bookSnapshots
    .map((row) => getIsoTimestampMs(row.fetchedAt))
    .filter((value): value is number => value != null)
    .sort((left, right) => right - left)[0] || null;
  const lastMovementAtMs = lineMovement
    .map((row) => getIsoTimestampMs(row.recordedAt))
    .filter((value): value is number => value != null)
    .sort((left, right) => right - left)[0] || null;
  const lastObservedAtMs = [lastSnapshotAtMs, lastMovementAtMs]
    .filter((value): value is number => value != null)
    .sort((left, right) => right - left)[0] || null;
  const freshness: 'fresh' | 'stale' | 'none' = lastObservedAtMs == null
    ? 'none'
    : (Date.now() - lastObservedAtMs) > MARKET_HISTORY_STALE_THRESHOLD_MS
      ? 'stale'
      : 'fresh';

  const buckets = new Map<NormalizedMarketHistoryKey, { snapshots: GameBookSnapshotRow[]; movement: GameMarketHistoryRow[] }>();
  for (const row of bookSnapshots) {
    const key = normalizeMarketHistoryKey(row.market);
    const bucket = buckets.get(key) || { snapshots: [], movement: [] };
    bucket.snapshots.push(row);
    buckets.set(key, bucket);
  }
  for (const row of lineMovement) {
    const key = normalizeMarketHistoryKey(row.marketType);
    const bucket = buckets.get(key) || { snapshots: [], movement: [] };
    bucket.movement.push(row);
    buckets.set(key, bucket);
  }

  const cards = [...buckets.entries()]
    .map(([marketType, bucket]) => {
      const orderedSnapshots = bucket.snapshots.slice().sort(compareBookSnapshotRows);
      const bestBookNow = orderedSnapshots[0] || null;
      const latestMovement = bucket.movement
        .slice()
        .sort((left, right) => (getIsoTimestampMs(right.recordedAt) || 0) - (getIsoTimestampMs(left.recordedAt) || 0))[0] || null;
      const cardLastSnapshotAtMs = orderedSnapshots
        .map((row) => getIsoTimestampMs(row.fetchedAt))
        .filter((value): value is number => value != null)
        .sort((left, right) => right - left)[0] || null;
      return {
        marketType,
        label: getMarketHistoryLabel(marketType),
        bookmakerCount: new Set(orderedSnapshots.map((row) => String(row.bookmaker || '').trim()).filter(Boolean)).size,
        snapshotCount: orderedSnapshots.length,
        lastSnapshotAt: cardLastSnapshotAtMs != null ? new Date(cardLastSnapshotAtMs).toISOString() : null,
        freshness: cardLastSnapshotAtMs == null
          ? (latestMovement ? freshness : 'none')
          : ((Date.now() - cardLastSnapshotAtMs) > MARKET_HISTORY_STALE_THRESHOLD_MS ? 'stale' : 'fresh'),
        bestBookNow,
        lineChangedFrom: latestMovement ? {
          openLine: latestMovement.openLine,
          currentLine: latestMovement.currentLine,
          delta: latestMovement.lineMovement,
          movementDirection: latestMovement.movementDirection,
          recordedAt: latestMovement.recordedAt,
          steamMove: latestMovement.steamMove,
          reverseLineMove: latestMovement.reverseLineMove,
        } : null,
      };
    })
    .sort((left, right) => {
      const priority = ['moneyline', 'spread', 'total', 'unknown'];
      return priority.indexOf(left.marketType) - priority.indexOf(right.marketType);
    });

  const bestBookNowCard = cards.find((card) => card.bestBookNow) || null;
  const strongestMoveCard = cards
    .filter((card) => card.lineChangedFrom && card.lineChangedFrom.delta != null)
    .sort((left, right) => Math.abs(right.lineChangedFrom?.delta || 0) - Math.abs(left.lineChangedFrom?.delta || 0))[0] || null;

  return {
    bookmakerCount,
    bookSnapshotCount: bookSnapshots.length,
    lineMovementCount: lineMovement.length,
    lastSnapshotAt: lastSnapshotAtMs != null ? new Date(lastSnapshotAtMs).toISOString() : null,
    lastMovementAt: lastMovementAtMs != null ? new Date(lastMovementAtMs).toISOString() : null,
    lastObservedAt: lastObservedAtMs != null ? new Date(lastObservedAtMs).toISOString() : null,
    freshness,
    bestBookNow: bestBookNowCard?.bestBookNow
      ? {
          ...bestBookNowCard.bestBookNow,
          marketType: bestBookNowCard.marketType,
          label: bestBookNowCard.label,
        }
      : null,
    lineChangedFrom: strongestMoveCard?.lineChangedFrom
      ? {
          marketType: strongestMoveCard.marketType,
          label: strongestMoveCard.label,
          ...strongestMoveCard.lineChangedFrom,
        }
      : null,
    cards,
  };
}

async function fetchGameMarketHistory(params: {
  league: string;
  startsAt: string | null | undefined;
  homeTeam: string | null | undefined;
  awayTeam: string | null | undefined;
  homeShort?: string | null | undefined;
  awayShort?: string | null | undefined;
}): Promise<{
  lineMovement: GameMarketHistoryRow[];
  bookSnapshots: GameBookSnapshotRow[];
  summary: GameMarketSummary;
  diagnostics: {
    fallbackUsage: {
      lineMovementSource: string;
      bookSnapshotSource: string;
      fallbackUsed: boolean;
      recovered: boolean;
    };
  };
}> {
  if (!params.startsAt || !params.homeTeam || !params.awayTeam) {
    return {
      lineMovement: [],
      bookSnapshots: [],
      summary: buildGameMarketSummary({
        startsAt: params.startsAt,
        lineMovement: [],
        bookSnapshots: [],
      }),
      diagnostics: {
        fallbackUsage: buildDigilanderMarketFallbackUsage(),
      },
    };
  }

  const homeTeamLikePatterns = buildMarketHistoryLikePatterns({
    teamName: params.homeTeam,
    league: params.league,
  });
  const awayTeamLikePatterns = buildMarketHistoryLikePatterns({
    teamName: params.awayTeam,
    league: params.league,
  });
  const homeTeamExactKeys = buildMarketHistoryExactKeys({
    teamName: params.homeTeam,
    teamShort: params.homeShort || getTeamAbbr(params.homeTeam || null) || null,
    league: params.league,
  });
  const awayTeamExactKeys = buildMarketHistoryExactKeys({
    teamName: params.awayTeam,
    teamShort: params.awayShort || getTeamAbbr(params.awayTeam || null) || null,
    league: params.league,
  });

  const queryRows = (sql: string, values: any[], timeoutMs = MARKET_HISTORY_QUERY_TIMEOUT_MS) => withTimeout(
    pool.query(sql, values).catch(() => ({ rows: [] as any[] })),
    timeoutMs,
    { rows: [] as any[] },
  );
  const lineMovementTimeoutMs = isSoccerLeague(params.league)
    ? SOCCER_LINE_MOVEMENT_QUERY_TIMEOUT_MS
    : MARKET_HISTORY_QUERY_TIMEOUT_MS;

  const [exactLineMovementRows, exactBookSnapshotRows] = await Promise.all([
    queryRows(
      `SELECT "marketType", "openLine", "currentLine", "closingLine", "lineMovement",
              "movementDirection", "openOdds", "currentOdds", "closingOdds",
              "steamMove", "reverseLineMove", "recordedAt"
         FROM "LineMovement"
        WHERE league = $1
          AND "gameDate" = DATE($2::timestamptz)
          AND (
            (${teamNameKeySql('"homeTeam"')} = ANY($3::text[]) AND ${teamNameKeySql('"awayTeam"')} = ANY($4::text[]))
            OR (${teamNameKeySql('"homeTeam"')} = ANY($4::text[]) AND ${teamNameKeySql('"awayTeam"')} = ANY($3::text[]))
          )
        ORDER BY "recordedAt" ASC`,
      [params.league, params.startsAt, homeTeamExactKeys, awayTeamExactKeys],
      lineMovementTimeoutMs,
    ),
    queryRows(
      `SELECT DISTINCT ON (bookmaker, market)
              bookmaker, market, "lineValue", "homeOdds", "awayOdds", "overOdds", "underOdds",
              "fetchedAt", "isBestLine", source
         FROM "GameOdds"
        WHERE league = $1
          AND DATE("gameDate") = DATE($2::timestamptz)
          AND (
            (${teamNameKeySql('"homeTeam"')} = ANY($3::text[]) AND ${teamNameKeySql('"awayTeam"')} = ANY($4::text[]))
            OR (${teamNameKeySql('"homeTeam"')} = ANY($4::text[]) AND ${teamNameKeySql('"awayTeam"')} = ANY($3::text[]))
          )
        ORDER BY bookmaker, market, "fetchedAt" DESC NULLS LAST`,
      [params.league, params.startsAt, homeTeamExactKeys, awayTeamExactKeys],
    ),
  ]);

  let lineMovementRows = exactLineMovementRows.rows;
  let bookSnapshotRows = exactBookSnapshotRows.rows;
  let lineMovementSource = lineMovementRows.length > 0 ? 'exact' : 'none';
  let bookSnapshotSource = bookSnapshotRows.length > 0 ? 'exact' : 'none';

  if (isSoccerLeague(params.league) && (lineMovementRows.length === 0 || bookSnapshotRows.length === 0)) {
    const [fallbackLineMovementRows, fallbackBookSnapshotRows] = await Promise.all([
      lineMovementRows.length === 0
        ? queryRows(
            `SELECT "homeTeam", "awayTeam", "marketType", "openLine", "currentLine", "closingLine", "lineMovement",
                    "movementDirection", "openOdds", "currentOdds", "closingOdds",
                    "steamMove", "reverseLineMove", "recordedAt"
               FROM "LineMovement"
              WHERE league = $1
                AND "gameDate" = DATE($2::timestamptz)
                AND (
                  (${teamNameKeySql('"homeTeam"')} LIKE ANY($3::text[]) OR ${teamNameKeySql('"awayTeam"')} LIKE ANY($3::text[]))
                  AND (${teamNameKeySql('"homeTeam"')} LIKE ANY($4::text[]) OR ${teamNameKeySql('"awayTeam"')} LIKE ANY($4::text[]))
                )
              ORDER BY "recordedAt" ASC`,
            [params.league, params.startsAt, homeTeamLikePatterns, awayTeamLikePatterns],
            lineMovementTimeoutMs,
          )
        : Promise.resolve({ rows: lineMovementRows }),
      bookSnapshotRows.length === 0
        ? queryRows(
            `SELECT "homeTeam", "awayTeam", bookmaker, market, "lineValue", "homeOdds", "awayOdds", "overOdds", "underOdds",
                    "fetchedAt", "isBestLine", source
               FROM "GameOdds"
              WHERE league = $1
                AND DATE("gameDate") = DATE($2::timestamptz)
                AND (
                  (${teamNameKeySql('"homeTeam"')} LIKE ANY($3::text[]) OR ${teamNameKeySql('"awayTeam"')} LIKE ANY($3::text[]))
                  AND (${teamNameKeySql('"homeTeam"')} LIKE ANY($4::text[]) OR ${teamNameKeySql('"awayTeam"')} LIKE ANY($4::text[]))
                )
              ORDER BY "fetchedAt" DESC NULLS LAST`,
            [params.league, params.startsAt, homeTeamLikePatterns, awayTeamLikePatterns],
          )
        : Promise.resolve({ rows: bookSnapshotRows }),
    ]);

    if (lineMovementRows.length === 0) {
      lineMovementRows = selectBestMatchedTeamRows(
        fallbackLineMovementRows.rows,
        params.homeTeam,
        params.awayTeam,
      );
      lineMovementSource = lineMovementRows.length > 0 ? 'fallback' : 'none';
    }

    if (bookSnapshotRows.length === 0) {
      const matchedRows = selectBestMatchedTeamRows(
        fallbackBookSnapshotRows.rows,
        params.homeTeam,
        params.awayTeam,
      );
      const latestByBookAndMarket = new Map<string, any>();
      for (const row of matchedRows) {
        const key = `${String(row.bookmaker || '').toLowerCase()}::${String(row.market || '').toLowerCase()}`;
        if (!latestByBookAndMarket.has(key)) latestByBookAndMarket.set(key, row);
      }
      bookSnapshotRows = [...latestByBookAndMarket.values()];
      bookSnapshotSource = bookSnapshotRows.length > 0 ? 'fallback' : 'none';
    }
  }

  const normalizedLineMovement = lineMovementRows.map((row: any) => ({
      marketType: row.marketType || null,
      openLine: row.openLine != null ? Number(row.openLine) : null,
      currentLine: row.currentLine != null ? Number(row.currentLine) : null,
      closingLine: row.closingLine != null ? Number(row.closingLine) : null,
      lineMovement: row.lineMovement != null ? Number(row.lineMovement) : null,
      movementDirection: row.movementDirection || null,
      openOdds: row.openOdds != null ? Number(row.openOdds) : null,
      currentOdds: row.currentOdds != null ? Number(row.currentOdds) : null,
      closingOdds: row.closingOdds != null ? Number(row.closingOdds) : null,
      steamMove: row.steamMove == null ? null : Boolean(row.steamMove),
      reverseLineMove: row.reverseLineMove == null ? null : Boolean(row.reverseLineMove),
      recordedAt: row.recordedAt instanceof Date ? row.recordedAt.toISOString() : row.recordedAt ? new Date(row.recordedAt).toISOString() : null,
    }));
  const normalizedBookSnapshots = bookSnapshotRows.map((row: any) => ({
      bookmaker: row.bookmaker || null,
      market: row.market || null,
      lineValue: row.lineValue != null ? Number(row.lineValue) : null,
      homeOdds: row.homeOdds != null ? Number(row.homeOdds) : null,
      awayOdds: row.awayOdds != null ? Number(row.awayOdds) : null,
      overOdds: row.overOdds != null ? Number(row.overOdds) : null,
      underOdds: row.underOdds != null ? Number(row.underOdds) : null,
      fetchedAt: row.fetchedAt instanceof Date ? row.fetchedAt.toISOString() : row.fetchedAt ? new Date(row.fetchedAt).toISOString() : null,
      isBestLine: row.isBestLine == null ? null : Boolean(row.isBestLine),
      source: row.source || null,
    }));

  return {
    lineMovement: normalizedLineMovement,
    bookSnapshots: normalizedBookSnapshots,
    summary: buildGameMarketSummary({
      startsAt: params.startsAt,
      lineMovement: normalizedLineMovement,
      bookSnapshots: normalizedBookSnapshots,
    }),
    diagnostics: {
      fallbackUsage: buildDigilanderMarketFallbackUsage({
        lineMovementSource,
        bookSnapshotSource,
      }),
    },
  };
}

async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, fallback: T): Promise<T> {
  return new Promise((resolve) => {
    let settled = false;
    const timer = setTimeout(() => {
      if (!settled) {
        settled = true;
        resolve(fallback);
      }
    }, timeoutMs);

    promise
      .then((value) => {
        if (!settled) {
          settled = true;
          clearTimeout(timer);
          resolve(value);
        }
      })
      .catch(() => {
        if (!settled) {
          settled = true;
          clearTimeout(timer);
          resolve(fallback);
        }
      });
  });
}

async function rejectOnTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number,
  buildError: () => Error,
): Promise<T> {
  return new Promise((resolve, reject) => {
    let settled = false;
    const timer = setTimeout(() => {
      if (!settled) {
        settled = true;
        reject(buildError());
      }
    }, timeoutMs);

    promise
      .then((value) => {
        if (!settled) {
          settled = true;
          clearTimeout(timer);
          resolve(value);
        }
      })
      .catch((error) => {
        if (!settled) {
          settled = true;
          clearTimeout(timer);
          reject(error);
        }
      });
  });
}

function createDigilanderStepTimeoutError(step: DigilanderTimedStepKey): Error {
  return createRouteError(504, `${DIGILANDER_STEP_LABELS[step]} timed out`);
}

async function runTimedDigilanderStep<T>(
  timingsMs: DigilanderTimingMap,
  step: DigilanderTimedStepKey,
  work: () => Promise<T>,
): Promise<T> {
  const startedAt = Date.now();
  try {
    return await rejectOnTimeout(
      Promise.resolve().then(work),
      DIGILANDER_MODULE_TIMEOUTS_MS[step],
      () => createDigilanderStepTimeoutError(step),
    );
  } finally {
    timingsMs[step] = Date.now() - startedAt;
  }
}

function getClientIp(req: Request): string | null {
  const forwarded = req.headers['x-forwarded-for'];
  if (Array.isArray(forwarded) && forwarded.length > 0) {
    return forwarded[0].split(',')[0].trim();
  }
  if (typeof forwarded === 'string' && forwarded.trim()) {
    return forwarded.split(',')[0].trim();
  }
  return req.socket?.remoteAddress || null;
}

function getCookieValue(req: Request, key: string): string | null {
  const value = req.cookies?.[key];
  return typeof value === 'string' && value.trim() ? value.trim() : null;
}

function trackTikTokForecastView(params: {
  req: Request;
  user: { id: string; email: string };
  eventId: string;
  league?: string | null;
  homeTeam: string;
  awayTeam: string;
}) {
  const matchup = `${params.homeTeam} vs ${params.awayTeam}`.trim();
  const leagueLabel = params.league ? String(params.league).toUpperCase() : null;
  return trackTikTokWebEvent({
    email: params.user.email,
    event: 'ViewContent',
    eventId: `forecast_${params.eventId}_${Date.now()}`,
    externalId: params.user.id,
    ip: getClientIp(params.req),
    referrer: typeof params.req.headers.referer === 'string' ? params.req.headers.referer : null,
    ttclid: getCookieValue(params.req, 'rm_ttclid'),
    ttp: getCookieValue(params.req, '_ttp'),
    url: typeof params.req.headers.referer === 'string' ? params.req.headers.referer : null,
    userAgent: typeof params.req.headers['user-agent'] === 'string' ? params.req.headers['user-agent'] : null,
    properties: {
      content_name: leagueLabel ? `${matchup} ${leagueLabel}` : matchup,
      content_type: 'product',
      contents: [
        {
          content_id: params.eventId,
          content_type: 'product',
          content_name: leagueLabel ? `${matchup} ${leagueLabel}` : matchup,
        },
      ],
      description: 'forecast_view',
      status: 'completed',
    },
  });
}

/** Get confirmed-OUT player names (2+ source) for serve-time injury filtering */
async function getOutPlayerNames(league: string): Promise<Set<string>> {
  const { rows } = await pool.query(`
    SELECT LOWER("playerName") AS name
    FROM "PlayerInjury"
    WHERE LOWER(league) = LOWER($1)
      AND status IN ('Out', 'IR', 'Suspension')
    GROUP BY LOWER("playerName")
    HAVING COUNT(DISTINCT source) >= 2
  `, [league]);
  return new Set(rows.map((r: any) => r.name));
}

/**
 * PUBLISHING INTEGRITY GATE: Filter prop_highlights at serve time.
 * Reads pi_prop_eligibility to suppress player props that failed validation.
 * Falls back gracefully if PI tables don't exist yet or have no data.
 */
async function filterPropsByIntegrity(forecastData: any, eventId: string): Promise<any> {
  if (!forecastData?.prop_highlights?.length) return forecastData;
  try {
    const { rows } = await pool.query(
      `SELECT player_id, player_name, publish_allowed, suppress_reason, validation_state
       FROM pi_prop_eligibility
       WHERE game_id = $1 AND publish_allowed = false`,
      [eventId]
    ).catch(() => ({ rows: [] }));
    if (rows.length === 0) return forecastData;

    const suppressedNames = new Set(
      rows.map((r: any) => (r.player_name || '').toLowerCase().trim())
    );
    const filtered = forecastData.prop_highlights.filter(
      (p: any) => !suppressedNames.has((p.player || '').toLowerCase().trim())
    );
    return { ...forecastData, prop_highlights: filtered };
  } catch {
    return forecastData; // graceful fallback — never block a forecast for PI table issues
  }
}

function getTeamSide(pick: string | null | undefined, homeTeam: string | null | undefined, awayTeam: string | null | undefined): 'home' | 'away' | null {
  const normalizedPick = (pick || '').trim().toLowerCase();
  if (!normalizedPick) return null;
  if (normalizedPick === (homeTeam || '').trim().toLowerCase()) return 'home';
  if (normalizedPick === (awayTeam || '').trim().toLowerCase()) return 'away';
  return null;
}

/** Capture game-level CLV picks from a forecast response */
async function captureGameClv(
  eventId: string,
  league: string,
  forecastData: any,
  odds: any,
  homeTeam: string,
  awayTeam: string,
  compositeConfidence?: number | null,
  stormTier?: number | null
): Promise<void> {
  if (!odds) return;

  const spreadSide = getTeamSide(forecastData?.forecast_side, homeTeam, awayTeam);
  if (spreadSide && odds.spread?.[spreadSide]?.line != null) {
    await recordClvPick({
      eventId,
      league,
      pickType: 'game_spread',
      direction: spreadSide,
      recLine: odds.spread[spreadSide].line,
      recOdds: odds.spread[spreadSide].odds,
      modelConfidence: compositeConfidence,
      stormTier,
    });
  }

  const winnerSide = getTeamSide(forecastData?.winner_pick, homeTeam, awayTeam);
  const moneylineOdds = odds.moneyline?.[winnerSide || ''];
  if (winnerSide && moneylineOdds != null) {
    await recordClvPick({
      eventId,
      league,
      pickType: 'game_moneyline',
      direction: winnerSide,
      recLine: moneylineOdds,
      recOdds: moneylineOdds,
      modelConfidence: compositeConfidence,
      stormTier,
    });
  }

  const totalDirection = (forecastData?.total_direction || '').toLowerCase();
  if ((totalDirection === 'over' || totalDirection === 'under') && odds.total?.[totalDirection]?.line != null) {
    await recordClvPick({
      eventId,
      league,
      pickType: 'game_total',
      direction: totalDirection,
      recLine: odds.total[totalDirection].line,
      recOdds: odds.total[totalDirection].odds,
      modelConfidence: compositeConfidence,
      stormTier,
    });
  }
}

// Feature flags for insight unlocks
const INSIGHT_UNLOCKS_ENABLED = () => process.env.ENABLE_INSIGHT_UNLOCKS !== 'false';
const STEAM_UNLOCK_ENABLED = () => INSIGHT_UNLOCKS_ENABLED() && process.env.ENABLE_STEAM_UNLOCK !== 'false';
const SHARP_UNLOCK_ENABLED = () => INSIGHT_UNLOCKS_ENABLED() && process.env.ENABLE_SHARP_UNLOCK !== 'false';
const DVP_UNLOCK_ENABLED = () => INSIGHT_UNLOCKS_ENABLED() && process.env.ENABLE_DVP_UNLOCK !== 'false';
const HCW_UNLOCK_ENABLED = () => INSIGHT_UNLOCKS_ENABLED() && process.env.ENABLE_HCW_UNLOCK !== 'false';
const PER_PLAYER_UNLOCK_ENABLED = () => process.env.FEATURE_PLAYER_PROPS_PER_PLAYER_UNLOCK === 'true';
const DUAL_READ_ENABLED = () => process.env.FEATURE_PLAYER_PROPS_DUAL_READ === 'true';
const PROP_DEDUP_ENABLED = () => process.env.PROP_DEDUP_ENABLED === 'true';
const PI_SIBLING_SUPPRESSION_ENABLED = () => process.env.PI_SIBLING_SUPPRESSION_ENABLED === 'true';
const MLB_MARKETS_ENABLED = () => process.env.MLB_MARKETS_ENABLED !== 'false';
const MLB_PROP_CONTEXT_V2 = () => process.env.MLB_PROP_CONTEXT_V2 !== 'false';
const PLAYER_PROP_MIN_CONFIDENCE = () => {
  const raw = Number(process.env.PLAYER_PROP_MIN_CONFIDENCE || 0.5);
  return Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0.5;
};
const VALID_UNLOCK_TYPES = ['TEAM_PROPS', 'STEAM_INSIGHT', 'SHARP_INSIGHT', 'DVP_INSIGHT', 'HCW_INSIGHT'];

const BRAND_MENTION_REGEX = /\bRainmaker\b|\bRain\s?Man\b|\bRainman\b/gi;
const PUBLIC_BANNED_TERMS: Array<{ pattern: RegExp; replacement: string }> = [
  { pattern: /\bmodel stack\b/gi, replacement: 'forecast analysis' },
  { pattern: /\bdata quality\b/gi, replacement: 'forecast context' },
  { pattern: /\bPIFF\b/gi, replacement: 'the analysis' },
  { pattern: /\bDIGIMON\b/gi, replacement: 'the analysis' },
  { pattern: /\bGrok\b/gi, replacement: 'the analysis' },
  { pattern: /\bKenPom\b/gi, replacement: 'the analysis' },
  { pattern: /\bDVP\b/gi, replacement: 'matchup context' },
  { pattern: /\bRIE\b/gi, replacement: 'the analysis' },
  { pattern: /\bRAG\b/gi, replacement: 'context' },
  { pattern: /\bHome Field Scout\b/gi, replacement: 'venue context' },
];

type PublicContentValidationState = {
  totalBrandMentions: number;
  warnings: string[];
};

function sanitizePublicTextBlock(
  text: string,
  state: PublicContentValidationState,
  context: string,
): string {
  let sanitized = text;

  for (const term of PUBLIC_BANNED_TERMS) {
    if (term.pattern.test(sanitized)) {
      state.warnings.push(`${context}: banned internal terminology removed`);
      sanitized = sanitized.replace(term.pattern, term.replacement);
    }
  }

  let blockMentions = 0;
  sanitized = sanitized.replace(BRAND_MENTION_REGEX, (match) => {
    if (blockMentions >= 1 || state.totalBrandMentions >= 2) {
      state.warnings.push(`${context}: excessive Rainmaker/Rainman branding reduced`);
      return 'the forecast';
    }
    blockMentions += 1;
    state.totalBrandMentions += 1;
    return match;
  });

  return sanitized;
}

function sanitizePublicForecastValue(
  value: any,
  state: PublicContentValidationState,
  context: string,
): any {
  if (typeof value === 'string') {
    return sanitizePublicTextBlock(value, state, context);
  }
  if (Array.isArray(value)) {
    return value.map((entry, index) => sanitizePublicForecastValue(entry, state, `${context}[${index}]`));
  }
  if (value && typeof value === 'object') {
    const clone: Record<string, any> = {};
    for (const [key, entry] of Object.entries(value)) {
      clone[key] = sanitizePublicForecastValue(entry, state, `${context}.${key}`);
    }
    return clone;
  }
  return value;
}

function sanitizeOddsBundle(odds: any, league?: string | null): any {
  if (!odds || typeof odds !== 'object') return odds;
  const sanitized = sanitizeGameOddsForLeague(league, {
    moneyline: sanitizeMoneylinePair(odds.moneyline),
    spread: odds.spread || { home: null, away: null },
    total: odds.total || { over: null, under: null },
  });
  return {
    ...odds,
    moneyline: sanitized.moneyline,
    spread: odds.spread == null ? odds.spread : sanitized.spread,
    total: odds.total == null ? odds.total : sanitized.total,
  };
}

function buildPublicForecastContent(
  forecastData: any,
  context: string,
  options: {
    homeTeam?: string | null;
    awayTeam?: string | null;
    homeShort?: string | null;
    awayShort?: string | null;
    league?: string | null;
    odds?: any;
  } = {},
): any {
  const state: PublicContentValidationState = {
    totalBrandMentions: 0,
    warnings: [],
  };

  const sanitized = sanitizePublicForecastValue(forecastData || {}, state, context);
  const normalized = normalizePublicForecastNarrative(sanitized, options);

  if (state.warnings.length > 0) {
    console.warn(`[forecast] Public content sanitized for ${context}: ${state.warnings.join(' | ')}`);
  }

  return serializePublicForecastBlob(normalized);
}

function buildStoredMlbMatchupContextFromSignals(rieSignals: any[] | null | undefined): Record<string, any> | null {
  if (!Array.isArray(rieSignals) || rieSignals.length === 0) return null;

  const matchup = rieSignals.find((signal: any) => signal?.signalId === 'mlb_matchup' && signal?.available);
  const fangraphs = rieSignals.find((signal: any) => 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 buildForecastPayloadV2Fields(cached: any, league: string | null | undefined): Record<string, any> {
  const extensions = getStoredPayloadExtensions(cached?.model_signals);
  const mlbMatchupContext =
    cached?.forecast_data?.mlb_matchup_context
    || buildStoredMlbMatchupContextFromSignals(extensions.rieSignals);
  return {
    ...(extensions.rieSignals ? { rieSignals: extensions.rieSignals } : {}),
    ...(extensions.ragInsights ? { ragInsights: extensions.ragInsights } : {}),
    ...(extensions.strategyId ? { strategyId: extensions.strategyId } : {}),
    ...maybeBuildMlbMatchupContext(mlbMatchupContext, league),
    ...maybeBuildMlbPhaseContext(cached?.forecast_data?.mlb_phase_context, league, MLB_MARKETS_ENABLED(), mlbMatchupContext),
  };
}

function readLegacyModelSignals(cached: any): any {
  return getLegacyCompositeView(
    cached?.model_signals,
    cached?.composite_confidence || cached?.confidence_score || 0.5,
    cached?.forecast_data?.value_rating || 5,
  )?.modelSignals || null;
}

function readPublicInputQuality(cached: any): any {
  if (cached?.input_quality && typeof cached.input_quality === 'object') {
    return camelizeObjectKeys(cached.input_quality);
  }

  if (cached?.model_signals?.inputQuality && typeof cached.model_signals.inputQuality === 'object') {
    return camelizeObjectKeys(cached.model_signals.inputQuality);
  }

  return null;
}

function readForecastVersion(cached: any): string | null {
  return (
    cached?.composite_version
    || getLegacyCompositeView(
      cached?.model_signals,
      cached?.composite_confidence || cached?.confidence_score || 0.5,
      cached?.forecast_data?.value_rating || 5,
    )?.compositeVersion
    || '1.0'
  );
}

function buildTeamForecastEngineField(cached: any, league: string | null | undefined): Record<string, any> {
  const normalizedLeague = String(league || cached?.league || '').toLowerCase();
  const forecastData = typeof cached?.forecast_data === 'string'
    ? JSON.parse(cached.forecast_data)
    : (cached?.forecast_data || {});

  if (normalizedLeague !== 'mlb') {
    return {};
  }

  const directApplied = Boolean(forecastData?.mlb_direct_model?.applied);
  if (directApplied) {
    return {
      teamForecastEngine: {
        code: 'mlb_direct_moneyline',
        label: 'MLB Direct Model',
        detail: 'Direct full-game moneyline lock passed the live abstain gate.',
        applied: true,
      },
    };
  }

  return {
    teamForecastEngine: {
      code: 'mlb_baseline_fallback',
      label: 'MLB Baseline Fallback',
      detail: 'Direct model abstained or shaped as F5-only, so the live forecast fell back to the baseline team engine.',
      applied: false,
    },
  };
}

function buildForecastTransparencyFields(cached: any, league: string | null | undefined): Record<string, any> {
  const modelSignals = readLegacyModelSignals(cached);
  const inputQuality = readPublicInputQuality(cached);

  return {
    contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
    compositeConfidence: cached?.composite_confidence ?? cached?.confidence_score ?? null,
    forecastVersion: readForecastVersion(cached),
    staticCommit: true,
    generatedAt: cached?.created_at || null,
    lastRefreshAt: cached?.last_refresh_at || null,
    lastRefreshType: cached?.last_refresh_type || null,
    materialChange: cached?.material_change || null,
    ...(modelSignals ? { modelSignals } : {}),
    ...(inputQuality ? { inputQuality } : {}),
    ...buildTeamForecastEngineField(cached, league),
    ...buildForecastPayloadV2Fields(cached, league),
  };
}

function buildPlayerPropTaxonomy(propPayload: any): Record<string, any> {
  const descriptor = describeForecastAsset('PLAYER_PROP', propPayload);
  return {
    marketType: descriptor.assetType,
    marketFamily: descriptor.marketFamily,
    marketOrigin: descriptor.marketOrigin,
    sourceBacked: descriptor.sourceBacked,
    playerRole: descriptor.playerRole,
  };
}

function buildForecastAssetMetadata(forecastType: string, payload: any = {}): Record<string, any> {
  const descriptor = describeForecastAsset(forecastType, payload);
  return {
    marketType: descriptor.assetType,
    marketFamily: descriptor.marketFamily,
    marketOrigin: descriptor.marketOrigin,
    sourceBacked: descriptor.sourceBacked,
    legacyBundle: descriptor.legacyBundle,
    ...(descriptor.playerRole ? { playerRole: descriptor.playerRole } : {}),
  };
}

function normalizePlayerLookupName(value: string | null | undefined): string {
  return String(value || '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/[^a-z0-9 ]/g, '')
    .replace(/\s+/g, ' ')
    .trim();
}

function normalizeMarketLineLookup(value: any): string {
  if (value == null || value === '') return '';
  const parsed = Number(value);
  return Number.isFinite(parsed) ? String(parsed) : '';
}

function buildMlbMarketLookupKey(player: string | null | undefined, statType: string | null | undefined, line: any): string {
  return `${normalizePlayerLookupName(player)}|${String(statType || '').toLowerCase()}|${normalizeMarketLineLookup(line)}`;
}

function readNormalizedMarketOdds(value: any): number | null {
  if (!value || typeof value !== 'object') return null;
  const candidate = value.odds ?? value.oddsAmerican ?? null;
  const parsed = Number(candidate);
  return Number.isFinite(parsed) ? parsed : null;
}

function buildPublishableTeamPropsForStorage(params: {
  propsResult: any;
  league: string;
  teamName: string;
  teamShort: string | null | undefined;
  teamSide: 'home' | 'away';
  mlbOddsLookup?: Map<string, {
    over: number | null;
    under: number | null;
    primarySource: string | null;
    completenessStatus: 'source_complete' | 'multi_source_complete' | 'incomplete' | null;
  }>;
}): any[] {
  const rawProps = Array.isArray(params.propsResult?.props) ? params.propsResult.props : [];

  return rawProps
    .map((prop: any) => {
      const playerRole = getMlbPlayerRole(prop.stat_type ?? null);
      const marketLookup = params.mlbOddsLookup?.get(
        buildMlbMarketLookupKey(prop.player, prop.stat_type ?? null, prop.market_line_value ?? null),
      );
      const recommendation = String(prop.recommendation || '').trim().toLowerCase();
      const resolvedOdds = prop.odds
        ?? (recommendation === 'under' ? marketLookup?.under : recommendation === 'over' ? marketLookup?.over : null)
        ?? null;
      const metadata = buildPlayerPropMetadata({
        player: prop.player,
        team: params.teamShort || params.teamName,
        teamSide: params.teamSide,
        league: params.league,
        prop: prop.prop,
        statType: prop.stat_type ?? null,
        normalizedStatType: prop.stat_type ?? null,
        marketLine: prop.market_line_value ?? null,
        odds: resolvedOdds,
        projectedProbability: prop.prob ?? null,
        projectedOutcome: prop.projected_stat_value ?? null,
        edgePct: prop.edge_pct ?? prop.edge ?? null,
        recommendation: prop.recommendation ?? null,
        playerRole,
        modelContext: prop.model_context ?? null,
        marketSource: marketLookup?.primarySource ?? null,
        marketCompletenessStatus: marketLookup?.completenessStatus ?? null,
        sourceBacked: true,
      });

      if (!metadata) return null;

      return {
        ...prop,
        odds: resolvedOdds,
        player_role: playerRole,
        signal_tier: metadata.signalTier,
        signal_label: metadata.signalLabel,
        forecast_direction: metadata.forecastDirection,
        market_implied_probability: metadata.marketImpliedProbability,
        projected_probability: metadata.projectedProbability,
        edge_pct: metadata.edgePct,
        agreement_score: metadata.agreementScore,
        agreement_label: metadata.agreementLabel,
        agreement_sources: metadata.agreementSources,
        market_source: marketLookup?.primarySource ?? null,
        market_quality_score: metadata.marketQualityScore,
        market_quality_label: metadata.marketQualityLabel,
        market_completeness_status: marketLookup?.completenessStatus ?? null,
        source_backed: true,
        signal_table_row: metadata.tableRow,
      };
    })
    .filter(Boolean);
}

async function upsertGeneratedTeamPropsAssets(params: {
  eventId: string;
  league: string;
  teamName: string;
  teamShort: string | null | undefined;
  teamSide: 'home' | 'away';
  startsAt: string;
  propsResult: any;
}): Promise<void> {
  if ((params.league || '').toLowerCase() !== 'mlb') return;

  const dateET = toEasternDateString(params.startsAt) || new Date().toLocaleDateString('en-CA', { timeZone: 'America/New_York' });
  const modelVersion = process.env.GROK_MODEL || 'grok-4-1-fast-reasoning';
  const teamId = params.teamShort || params.teamName;
  const { rows: normalizedMarketRows } = await pool.query(
    `SELECT player_name, stat_type, line, over_payload, under_payload, primary_source, completeness_status
     FROM rm_mlb_normalized_player_prop_markets
     WHERE event_id = $1`,
    [params.eventId],
  ).catch(() => ({ rows: [] as any[] }));
  const mlbOddsLookup = new Map<string, {
    over: number | null;
    under: number | null;
    primarySource: string | null;
    completenessStatus: 'source_complete' | 'multi_source_complete' | 'incomplete' | null;
  }>();
  for (const row of normalizedMarketRows) {
    mlbOddsLookup.set(
      buildMlbMarketLookupKey(row.player_name, row.stat_type, row.line),
      {
        over: readNormalizedMarketOdds(row.over_payload),
        under: readNormalizedMarketOdds(row.under_payload),
        primarySource: row.primary_source ?? null,
        completenessStatus: row.completeness_status ?? null,
      },
    );
  }
  const filteredProps = buildPublishableTeamPropsForStorage({
    propsResult: params.propsResult,
    league: params.league,
    teamName: params.teamName,
    teamShort: params.teamShort,
    teamSide: params.teamSide,
    mlbOddsLookup,
  });
  const renderableBundleProps = filteredProps.filter((prop) => shouldRenderTeamPropBundleEntry(prop));

  const teamPropsPayload = JSON.stringify({
    ...params.propsResult,
    props: renderableBundleProps,
    ...buildForecastAssetMetadata('TEAM_PROPS'),
  });
  const teamPropsValues = [
    dateET,
    params.league,
    params.eventId,
    teamId,
    params.teamSide,
    modelVersion,
    JSON.stringify({ source: 'ROUTE_REGEN', market_origin: 'bundle' }),
    teamPropsPayload,
    params.startsAt,
    'TEAM_PROPS',
    '',
  ];

  // Production cannot depend on a partial-expression index existing just to unlock paid props.
  const updatedTeamProps = await pool.query(
    `UPDATE rm_forecast_precomputed
     SET league = $2,
         team_id = $4,
         team_side = $5,
         player_name = NULL,
         model_id = 'grok-claw',
         model_version = $6,
         vendor_inputs_summary = $7,
         forecast_payload = $8,
         confidence_score = NULL,
         expires_at = $9,
         status = 'ACTIVE',
         run_id = NULL,
         generated_at = NOW()
     WHERE date_et = $1::date
       AND event_id = $3
       AND forecast_type = $10
       AND COALESCE(team_id, '') = COALESCE($4, '')
       AND COALESCE(player_name, '') = $11
     RETURNING id`,
    teamPropsValues,
  );

  if ((updatedTeamProps.rowCount || 0) === 0) {
    await pool.query(
      `INSERT INTO rm_forecast_precomputed
         (date_et, league, event_id, forecast_type, team_id, team_side, player_name, model_id, model_version,
          vendor_inputs_summary, forecast_payload, confidence_score, expires_at, status, run_id)
       SELECT *
       FROM (
         VALUES ($1::date,$2,$3,'TEAM_PROPS',$4,$5,NULL,'grok-claw',$6,$7,$8,NULL,$9,'ACTIVE',NULL)
       ) AS candidate(
         date_et, league, event_id, forecast_type, team_id, team_side, player_name, model_id, model_version,
         vendor_inputs_summary, forecast_payload, confidence_score, expires_at, status, run_id
       )
       WHERE NOT EXISTS (
         SELECT 1
         FROM rm_forecast_precomputed existing
         WHERE existing.date_et = $1::date
           AND existing.event_id = $3
           AND existing.forecast_type = $10
           AND COALESCE(existing.team_id, '') = COALESCE($4, '')
           AND COALESCE(existing.player_name, '') = $11
       )`,
      teamPropsValues,
    );
  }

  const playerPropRows = renderableBundleProps
    .filter((prop) => hasPersistablePlayerPropPricing(prop.odds ?? null))
    .filter((prop) => shouldPersistStandalonePlayerPropMetadata(prop))
    .map((prop) => {
      const canonicalPlayer = resolveCanonicalName(prop.player, params.league);
      const statIdentity = resolvePlayerPropStatIdentity({
        league: params.league,
        statType: prop.stat_type ?? null,
        normalizedStatType: prop.normalized_stat_type ?? null,
        propText: prop.prop ?? null,
      });

      return {
        date_et: dateET,
        league: params.league,
        event_id: params.eventId,
        forecast_type: 'PLAYER_PROP',
        team_id: teamId,
        team_side: params.teamSide,
        player_name: canonicalPlayer,
        model_version: modelVersion,
        vendor_inputs_summary: { source: 'TEAM_PROPS_ROUTE_EXTRACT' },
        forecast_payload: buildStoredPlayerPropPayload({
          assetMetadata: buildForecastAssetMetadata('PLAYER_PROP', { stat_type: statIdentity.statType }),
          league: params.league,
          playerName: canonicalPlayer,
          statType: statIdentity.statType,
          normalizedStatType: statIdentity.normalizedStatType,
          prop,
        }),
        confidence_score: prop.prob ? prop.prob / 100 : null,
        expires_at: params.startsAt,
        stat_type_key: String(statIdentity.statType ?? ''),
        market_line_key: prop.market_line_value == null ? '' : String(prop.market_line_value),
      };
    });

  if (playerPropRows.length > 0) {
    const playerPropRowsJson = JSON.stringify(playerPropRows);
    const incomingRecordset = `jsonb_to_recordset($1::jsonb) AS incoming(
      date_et date,
      league text,
      event_id text,
      forecast_type text,
      team_id text,
      team_side text,
      player_name text,
      model_version text,
      vendor_inputs_summary jsonb,
      forecast_payload jsonb,
      confidence_score double precision,
      expires_at timestamptz,
      stat_type_key text,
      market_line_key text
    )`;

    await pool.query(
      `WITH incoming AS (
         SELECT *
         FROM ${incomingRecordset}
       )
       UPDATE rm_forecast_precomputed existing
          SET league = incoming.league,
              team_id = incoming.team_id,
              team_side = incoming.team_side,
              player_name = incoming.player_name,
              model_id = 'grok-claw',
              model_version = incoming.model_version,
              vendor_inputs_summary = incoming.vendor_inputs_summary,
              forecast_payload = incoming.forecast_payload,
              confidence_score = incoming.confidence_score,
              expires_at = incoming.expires_at,
              status = 'ACTIVE',
              run_id = NULL,
              generated_at = NOW()
         FROM incoming
        WHERE existing.date_et = incoming.date_et
          AND existing.event_id = incoming.event_id
          AND existing.forecast_type = incoming.forecast_type
          AND COALESCE(existing.team_id, '') = COALESCE(incoming.team_id, '')
          AND COALESCE(existing.player_name, '') = COALESCE(incoming.player_name, '')
          AND COALESCE((existing.forecast_payload->>'stat_type'), (existing.forecast_payload->>'normalized_stat_type'), '') = incoming.stat_type_key
          AND COALESCE((existing.forecast_payload->>'market_line_value'), (existing.forecast_payload->>'line'), '') = incoming.market_line_key`,
      [playerPropRowsJson],
    );

    await pool.query(
      `WITH incoming AS (
         SELECT *
         FROM ${incomingRecordset}
       )
       INSERT INTO rm_forecast_precomputed
         (date_et, league, event_id, forecast_type, team_id, team_side, player_name, model_id, model_version,
          vendor_inputs_summary, forecast_payload, confidence_score, expires_at, status, run_id)
       SELECT incoming.date_et,
              incoming.league,
              incoming.event_id,
              incoming.forecast_type,
              incoming.team_id,
              incoming.team_side,
              incoming.player_name,
              'grok-claw',
              incoming.model_version,
              incoming.vendor_inputs_summary,
              incoming.forecast_payload,
              incoming.confidence_score,
              incoming.expires_at,
              'ACTIVE',
              NULL
         FROM incoming
        WHERE NOT EXISTS (
          SELECT 1
          FROM rm_forecast_precomputed existing
          WHERE existing.date_et = incoming.date_et
            AND existing.event_id = incoming.event_id
            AND existing.forecast_type = incoming.forecast_type
            AND COALESCE(existing.team_id, '') = COALESCE(incoming.team_id, '')
            AND COALESCE(existing.player_name, '') = COALESCE(incoming.player_name, '')
            AND COALESCE((existing.forecast_payload->>'stat_type'), (existing.forecast_payload->>'normalized_stat_type'), '') = incoming.stat_type_key
            AND COALESCE((existing.forecast_payload->>'market_line_value'), (existing.forecast_payload->>'line'), '') = incoming.market_line_key
        )`,
      [playerPropRowsJson],
    );
  }
}

async function hydratePropHighlightsFromAssets(forecastData: any, eventId: string, league?: string | null): Promise<any> {
  const existingHighlights = Array.isArray(forecastData?.prop_highlights)
    ? forecastData.prop_highlights
    : [];

  try {
    const { rows: playerPropRows } = await pool.query(
      `SELECT id, player_name, forecast_payload
       FROM rm_forecast_precomputed
       WHERE event_id = $1
         AND forecast_type = 'PLAYER_PROP'
         AND status = 'ACTIVE'
       ORDER BY confidence_score DESC NULLS LAST
       LIMIT 6`,
      [eventId],
    );

    if (playerPropRows.length > 0) {
      return {
        ...forecastData,
        prop_highlights: playerPropRows.map((row: any) => ({
          player: row.player_name,
          prop: row.forecast_payload?.prop || null,
          recommendation: row.forecast_payload?.recommendation || null,
          reasoning: row.forecast_payload?.reasoning || null,
        })),
      };
    }

    const { rows: teamPropRows } = await pool.query(
      `SELECT forecast_payload
       FROM rm_forecast_precomputed
       WHERE event_id = $1
         AND forecast_type = 'TEAM_PROPS'
       ORDER BY CASE WHEN status = 'ACTIVE' THEN 0 ELSE 1 END, generated_at DESC NULLS LAST
       LIMIT 2`,
      [eventId],
    );

    const fallbackProps = teamPropRows.flatMap((row: any) => row.forecast_payload?.props || []).slice(0, 6);
    if (fallbackProps.length > 0) {
      return {
        ...forecastData,
        prop_highlights: fallbackProps.map((prop: any) => ({
          player: prop.player || null,
          prop: prop.prop || null,
          recommendation: prop.recommendation || null,
          reasoning: prop.reasoning || null,
        })),
      };
    }
  } catch (err) {
    console.error('Prop highlight hydration error:', err);
  }

  if (existingHighlights.length > 0 && usesAssetBackedPropHighlights(league)) {
    return {
      ...forecastData,
      prop_highlights: [],
    };
  }

  return forecastData;
}

function hasPropHighlights(value: any): boolean {
  return Array.isArray(value?.prop_highlights) && value.prop_highlights.length > 0;
}

async function persistHydratedPropHighlightsIfMissing(cached: { id?: string; forecast_data?: any; league?: string } | null | undefined, hydratedForecastData: any): Promise<void> {
  if (!cached?.id || hasPropHighlights(cached.forecast_data) || !hasPropHighlights(hydratedForecastData)) {
    return;
  }

  try {
    await pool.query(
      `UPDATE rm_forecast_cache
       SET forecast_data = $1::jsonb
       WHERE id = $2`,
      [JSON.stringify(hydratedForecastData), cached.id],
    );
    cached.forecast_data = hydratedForecastData;
  } catch (err) {
    console.error('Prop highlight persistence error:', err);
  }
}

async function hydrateForecastPropHighlightsForResponse(cached: { id?: string; forecast_data?: any; league?: string } | null | undefined, eventId: string, basemonSummary: any): Promise<any> {
  const withAssets = await hydratePropHighlightsFromAssets(cached?.forecast_data, eventId, cached?.league);
  const hydrated = hydratePropHighlightsFromBasemon(withAssets, basemonSummary);
  await persistHydratedPropHighlightsIfMissing(cached, hydrated);
  return hydrated;
}

function formatBasemonMarket(market: string | null | undefined): string {
  const normalized = String(market || '').trim().toLowerCase();
  switch (normalized) {
    case 'pitcher_strikeouts':
      return 'Pitcher Ks';
    case 'batter_hits':
      return 'Batter Hits';
    default:
      return normalized
        .split('_')
        .filter(Boolean)
        .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
        .join(' ') || 'BASEMON';
  }
}

function toEasternDateString(value: string | Date | null | undefined): string | null {
  if (!value) return null;
  const parsed = new Date(value);
  if (Number.isNaN(parsed.getTime())) return null;
  const parts = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'America/New_York',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).formatToParts(parsed);
  const year = parts.find((part) => part.type === 'year')?.value;
  const month = parts.find((part) => part.type === 'month')?.value;
  const day = parts.find((part) => part.type === 'day')?.value;
  if (!year || !month || !day) return null;
  return `${year}-${month}-${day}`;
}

async function resolveEventShortNames(params: {
  eventId?: string | null;
  homeShort?: string | null;
  awayShort?: string | null;
  homeTeam?: string | null;
  awayTeam?: string | null;
  startsAt?: string | null;
}): Promise<{ homeShort: string | null; awayShort: string | null }> {
  const providedHome = params.homeShort?.trim() || null;
  const providedAway = params.awayShort?.trim() || null;
  if (providedHome && providedAway) {
    return { homeShort: providedHome, awayShort: providedAway };
  }

  try {
    if (params.eventId) {
      const { rows } = await pool.query(
        `SELECT home_short, away_short
         FROM rm_events
         WHERE event_id = $1
         LIMIT 1`,
        [params.eventId],
      ).catch(() => ({ rows: [] }));
      if (rows[0]?.home_short && rows[0]?.away_short) {
        return { homeShort: rows[0].home_short, awayShort: rows[0].away_short };
      }
    }

    if (params.homeTeam && params.awayTeam && params.startsAt) {
      const { rows } = await pool.query(
        `SELECT home_short, away_short
         FROM rm_events
         WHERE home_team = $1
           AND away_team = $2
           AND starts_at = $3
         LIMIT 1`,
        [params.homeTeam, params.awayTeam, params.startsAt],
      ).catch(() => ({ rows: [] }));
      if (rows[0]?.home_short && rows[0]?.away_short) {
        return { homeShort: rows[0].home_short, awayShort: rows[0].away_short };
      }
    }
  } catch (err) {
    console.error('BASEMON short-name resolution error:', err);
  }

  return { homeShort: providedHome, awayShort: providedAway };
}

async function fetchBasemonSummary(params: {
  eventId?: string | null;
  league?: string | null;
  startsAt?: string | null;
  homeShort?: string | null;
  awayShort?: string | null;
  homeTeam?: string | null;
  awayTeam?: string | null;
}): Promise<any | null> {
  if ((params.league || '').toLowerCase() !== 'mlb') return null;

  const { homeShort, awayShort } = await resolveEventShortNames(params);
  const gameDate = toEasternDateString(params.startsAt);
  if (!homeShort || !awayShort || !gameDate) {
    return null;
  }

  try {
    const { rows } = await pool.query(
      `SELECT market, player_name, line, direction, verdict, edge, model_prob, implied_prob, note
       FROM sportsclaw.basemon_picks
       WHERE game_date = $1::date
         AND league = 'mlb'
         AND (
           (home_team = $2 AND away_team = $3)
           OR (home_team = $3 AND away_team = $2)
         )
       ORDER BY
         CASE verdict
           WHEN 'LOCK' THEN 0
           WHEN 'PLAY' THEN 1
           WHEN 'LEAN' THEN 2
           ELSE 3
         END,
         COALESCE(edge, 0) DESC,
         player_name ASC`,
      [gameDate, homeShort.toUpperCase(), awayShort.toUpperCase()],
    ).catch(() => ({ rows: [] }));

    if (rows.length === 0) {
      return null;
    }

    const marketCounts = new Map<string, { market: string; lockCount: number; playCount: number; leanCount: number; skipCount: number }>();
    let totalLockCount = 0;
    let totalPlayCount = 0;

    for (const row of rows) {
      const label = formatBasemonMarket(row.market);
      if (!marketCounts.has(label)) {
        marketCounts.set(label, { market: label, lockCount: 0, playCount: 0, leanCount: 0, skipCount: 0 });
      }
      const bucket = marketCounts.get(label)!;
      switch (String(row.verdict || '').toUpperCase()) {
        case 'LOCK':
          bucket.lockCount += 1;
          totalLockCount += 1;
          break;
        case 'PLAY':
          bucket.playCount += 1;
          totalPlayCount += 1;
          break;
        case 'LEAN':
          bucket.leanCount += 1;
          break;
        default:
          bucket.skipCount += 1;
          break;
      }
    }

    const topPicks = rows
      .filter((row: any) => ['LOCK', 'PLAY'].includes(String(row.verdict || '').toUpperCase()))
      .slice(0, 6)
      .map((row: any) => ({
        market: formatBasemonMarket(row.market),
        player: row.player_name,
        line: row.line != null ? Number(row.line) : null,
        direction: row.direction,
        verdict: String(row.verdict || '').toUpperCase(),
        edge: row.edge != null ? Number(row.edge) * 100 : null,
        modelProb: row.model_prob != null ? Number(row.model_prob) : null,
        impliedProb: row.implied_prob != null ? Number(row.implied_prob) : null,
        note: row.note || null,
      }));

    return {
      available: topPicks.length > 0 || totalLockCount > 0 || totalPlayCount > 0,
      totalLockCount,
      totalPlayCount,
      markets: Array.from(marketCounts.values()),
      topPicks,
    };
  } catch (err) {
    console.error('BASEMON summary fetch error:', err);
    return null;
  }
}

function hydratePropHighlightsFromBasemon(forecastData: any, basemonSummary: any): any {
  if (Array.isArray(forecastData?.prop_highlights) && forecastData.prop_highlights.length > 0) {
    return forecastData;
  }
  if (!basemonSummary?.available || !Array.isArray(basemonSummary.topPicks) || basemonSummary.topPicks.length === 0) {
    return forecastData;
  }

  return {
    ...forecastData,
    prop_highlights: basemonSummary.topPicks.slice(0, 4).map((pick: any) => ({
      player: pick.player || null,
      prop: `${pick.market || 'Prop'} ${String(pick.direction || '').toUpperCase()} ${pick.line ?? ''}`.trim(),
      recommendation: String(pick.verdict || '').toUpperCase() || null,
      reasoning: pick.note || null,
    })),
  };
}

type CachedTopPropsResponse = {
  fingerprint: string;
  expiresAt: number;
  body: any;
};

const TOP_PROPS_CACHE = new Map<string, CachedTopPropsResponse>();
const TOP_PROPS_CACHE_TTL_MS = 60 * 1000;
const TOP_PICKS_CACHE = new Map<string, CachedTopPropsResponse>();
const TOP_PICKS_CACHE_TTL_MS = 60 * 1000;
const DVP_AVAILABILITY_CACHE = new Map<string, { available: boolean; expiresAt: number }>();
const DVP_AVAILABILITY_CACHE_TTL_MS = 5 * 60 * 1000;
const DIGILANDER_BUNDLE_CACHE = new Map<string, { expiresAt: number; body: any }>();
const DIGILANDER_BUNDLE_IN_FLIGHT = new Map<string, Promise<any>>();
const DIGILANDER_SUMMARY_CACHE_TTL_MS = 30 * 1000;
const DIGILANDER_FULL_CACHE_TTL_MS = 15 * 1000;
const DIGILANDER_NEWS_CACHE = new Map<string, { expiresAt: number; items: any[] }>();
const DIGILANDER_NEWS_CACHE_TTL_MS = 60 * 1000;
const DIGILANDER_NEWS_TIMEOUT_MS = process.env.NODE_ENV === 'test' ? 50 : 1500;
const DIGILANDER_MODULE_TIMEOUTS_MS = {
  preview: process.env.NODE_ENV === 'test' ? 250 : 2500,
  topPicks: process.env.NODE_ENV === 'test' ? 250 : 2500,
  news: process.env.NODE_ENV === 'test' ? 250 : 2200,
  // Must exceed buildForecastMarketHistoryBody's own 3000ms graceful fallback budget,
  // otherwise Digilander turns a degraded market payload into a hard module failure.
  market: process.env.NODE_ENV === 'test' ? 250 : 3200,
  summarySupport: process.env.NODE_ENV === 'test' ? 300 : 3500,
  injuries: process.env.NODE_ENV === 'test' ? 250 : 2500,
  gameData: process.env.NODE_ENV === 'test' ? 350 : 4500,
  playerProps: process.env.NODE_ENV === 'test' ? 350 : 4500,
  teamBreakdown: process.env.NODE_ENV === 'test' ? 350 : 4000,
} as const;

type DigilanderTimedStepKey = keyof typeof DIGILANDER_MODULE_TIMEOUTS_MS;
type DigilanderTimingMap = Partial<Record<DigilanderTimedStepKey | 'total', number>>;

const DIGILANDER_STEP_LABELS: Record<DigilanderTimedStepKey, string> = {
  preview: 'Preview',
  topPicks: 'Best plays',
  news: 'News',
  market: 'Market',
  summarySupport: 'Summary support',
  injuries: 'Injuries',
  gameData: 'Game data',
  playerProps: 'Player props',
  teamBreakdown: 'Team breakdown',
};

function getDigilanderBundleCacheKey(params: {
  eventId: string;
  signedIn?: boolean;
  summaryOnly?: boolean;
}): string | null {
  if (params.signedIn) return null;
  return `${params.eventId}|${params.summaryOnly ? 'summary' : 'full'}|public`;
}

function getDigilanderBundleRequestKey(params: {
  eventId: string;
  signedIn?: boolean;
  summaryOnly?: boolean;
}): string {
  return `${params.eventId}|${params.summaryOnly ? 'summary' : 'full'}|${params.signedIn ? 'signed-in' : 'public'}`;
}

function readDigilanderBundleCache(cacheKey: string | null): any | null {
  if (!cacheKey) return null;
  const cached = DIGILANDER_BUNDLE_CACHE.get(cacheKey);
  if (!cached) return null;
  if (cached.expiresAt <= Date.now()) {
    DIGILANDER_BUNDLE_CACHE.delete(cacheKey);
    return null;
  }
  return cached.body;
}

function writeDigilanderBundleCache(cacheKey: string | null, summaryOnly: boolean, body: any): void {
  if (!cacheKey) return;
  DIGILANDER_BUNDLE_CACHE.set(cacheKey, {
    expiresAt: Date.now() + (summaryOnly ? DIGILANDER_SUMMARY_CACHE_TTL_MS : DIGILANDER_FULL_CACHE_TTL_MS),
    body,
  });
}

function attachDigilanderRequestDiagnostics(
  body: any,
  meta: { cacheStatus: 'hit' | 'miss'; coalesced: boolean },
): any {
  return {
    ...body,
    diagnostics: {
      ...(body?.diagnostics || {}),
      request: {
        cacheStatus: meta.cacheStatus,
        coalesced: meta.coalesced,
      },
    },
  };
}

function buildDigilanderNewsFallbackUsage(source: string, itemCount = 0) {
  return {
    source,
    fallbackUsed: source === 'fresh_cache' || source === 'stale_cache',
    itemCount,
  };
}

function buildDigilanderMarketFallbackUsage(params?: {
  lineMovementSource?: string | null;
  bookSnapshotSource?: string | null;
}) {
  const lineMovementSource = String(params?.lineMovementSource || 'none');
  const bookSnapshotSource = String(params?.bookSnapshotSource || 'none');
  return {
    lineMovementSource,
    bookSnapshotSource,
    fallbackUsed: lineMovementSource === 'fallback' || bookSnapshotSource === 'fallback',
    recovered: lineMovementSource === 'fallback' || bookSnapshotSource === 'fallback',
  };
}

function buildDigilanderSummarySupportFallbackUsage(params?: {
  source?: string | null;
  playerCount?: number;
  linePropsCount?: number;
  marketPropsCount?: number;
}) {
  const source = String(params?.source || 'empty');
  return {
    source,
    fallbackUsed: [
      'cached_team_props',
      'precomputed_team_props',
      'mlb_candidate_markets',
      'team_prop_candidates',
    ].includes(source),
    playerCount: Number(params?.playerCount || 0),
    linePropsCount: Number(params?.linePropsCount || 0),
    marketPropsCount: Number(params?.marketPropsCount || 0),
  };
}

function getDigilanderNewsCacheKey(curated: any): string | null {
  const eventId = String(curated?.event_id || '').trim();
  return eventId || null;
}

function readDigilanderNewsCache(
  cacheKey: string | null,
  options: { allowStale?: boolean } = {},
): any[] | null {
  if (!cacheKey) return null;
  const cached = DIGILANDER_NEWS_CACHE.get(cacheKey);
  if (!cached) return null;
  if (cached.expiresAt <= Date.now() && !options.allowStale) {
    return null;
  }
  return Array.isArray(cached.items) ? cached.items : [];
}

function writeDigilanderNewsCache(cacheKey: string | null, items: any[]): void {
  if (!cacheKey) return;
  DIGILANDER_NEWS_CACHE.set(cacheKey, {
    expiresAt: Date.now() + DIGILANDER_NEWS_CACHE_TTL_MS,
    items,
  });
}

const TOP_BOARD_SPORT_LEAGUE_MAP: Record<string, string[]> = {
  basketball: ['nba', 'ncaab', 'ncaaw', 'wncaab', 'wnba'],
  baseball: ['mlb'],
  hockey: ['nhl'],
  soccer: ['epl', 'la_liga', 'bundesliga', 'serie_a', 'ligue_1', 'champions_league', 'mls'],
  football: ['nfl', 'ncaaf'],
  mma: ['ufc', 'mma'],
};

type TopBoardFilters = {
  league: string | null;
  sport: string | null;
  leagues: string[] | null;
  maxLookaheadDays: number;
};

function normalizeTopBoardFilters(league: string | null, sport: string | null): TopBoardFilters {
  const normalizedLeague = league || null;
  const normalizedSport = !normalizedLeague && sport && TOP_BOARD_SPORT_LEAGUE_MAP[sport]
    ? sport
    : null;
  const leagues = normalizedLeague
    ? [normalizedLeague]
    : normalizedSport
      ? TOP_BOARD_SPORT_LEAGUE_MAP[normalizedSport]
      : null;
  const maxLookaheadDays = normalizedLeague
    ? getLeagueLookaheadDays(normalizedLeague, EU_EVENT_LOOKAHEAD_DAYS, 0)
    : normalizedSport === 'soccer'
      ? EU_EVENT_LOOKAHEAD_DAYS
      : 0;

  return {
    league: normalizedLeague,
    sport: normalizedSport,
    leagues,
    maxLookaheadDays,
  };
}

function getTopBoardScopeKey(filters: TopBoardFilters, limit: number): string {
  const scope = filters.league || (filters.sport ? `sport:${filters.sport}` : 'all');
  return `${scope}|${limit}`;
}

function getTopBoardScopeLabel(filters: TopBoardFilters): string {
  return filters.league || filters.sport || 'all';
}

function buildTopBoardLeagueClause(column: string, leagues: string[] | null, params: any[]): string {
  if (!leagues?.length) return '';
  params.push(leagues);
  return `AND ${column} = ANY($${params.length})`;
}

function buildTopBoardDateClause(dateExpr: string, maxLookaheadDays: number, params: any[]): string {
  if (maxLookaheadDays > 0) {
    params.push(maxLookaheadDays);
    return `
      AND ${dateExpr} >= (NOW() AT TIME ZONE 'America/New_York')::date
      AND ${dateExpr} <= ((NOW() AT TIME ZONE 'America/New_York')::date + ($${params.length}::int))
    `;
  }

  return `AND ${dateExpr} = (NOW() AT TIME ZONE 'America/New_York')::date`;
}

function filterTopBoardRowsToWindow(
  rows: any[],
  filters: TopBoardFilters,
  options: { dateKeyField?: string } = {},
): any[] {
  if (filters.maxLookaheadDays === 0) return rows;

  const dateKeyField = options.dateKeyField || 'date_et';
  return rows.filter((row: any) => {
    const league = String(row.league || filters.league || '').toLowerCase();
    if (!league) return false;

    if (row.starts_at) {
      return isLeagueEventInWindow(league, row.starts_at, EU_EVENT_LOOKAHEAD_DAYS);
    }

    const dateKey = typeof row[dateKeyField] === 'string' ? row[dateKeyField] : null;
    return dateKey ? isLeagueDateKeyInWindow(league, dateKey, EU_EVENT_LOOKAHEAD_DAYS) : false;
  });
}

const TOP_PICKS_ODDS_FILTER = `AND (
  ((e.moneyline->>'home')::text IS NOT NULL AND (e.moneyline->>'home')::text != 'null')
  OR ((e.spread->>'home')::text IS NOT NULL AND (e.spread->>'home')::text != 'null')
  OR ((e.total->>'over')::text IS NOT NULL AND (e.total->>'over')::text != 'null')
)`;

const TOP_PICKS_FORECAST_CACHE_SELECT = `fc.event_id AS matched_event_id,
    COALESCE(NULLIF(fc.composite_confidence, 'NaN'::float8), fc.confidence_score) AS public_confidence,
    fc.forecast_data`;

const TOP_PICKS_FORECAST_LATERAL = `LEFT JOIN LATERAL (
  SELECT
    ${TOP_PICKS_FORECAST_CACHE_SELECT}
  FROM rm_forecast_cache fc
  WHERE fc.event_id = e.event_id
  ORDER BY fc.created_at DESC
  LIMIT 1
) fc_exact ON true
LEFT JOIN LATERAL (
  SELECT
    ${TOP_PICKS_FORECAST_CACHE_SELECT}
  FROM rm_forecast_cache fc
  WHERE fc_exact.matched_event_id IS NULL
    AND ${teamNameKeySql('e.home_team')} = ${teamNameKeySql('fc.home_team')}
    AND ${teamNameKeySql('e.away_team')} = ${teamNameKeySql('fc.away_team')}
    AND e.starts_at = fc.starts_at
  ORDER BY fc.created_at DESC
  LIMIT 1
) fc_fallback ON true`;

function hasPublicPlayerPropPricing(payload: any): boolean {
  const directOdds = payload?.odds;
  const tableOdds = payload?.signal_table_row?.odds;
  const hasFiniteOdds = (value: any) => value !== null && value !== undefined && value !== '' && Number.isFinite(Number(value));
  return hasFiniteOdds(directOdds) || hasFiniteOdds(tableOdds);
}

function parsePlayerPropConfidenceValue(value: any): number | null {
  if (value === null || value === undefined || value === '') return null;
  const numeric = Number(value);
  if (!Number.isFinite(numeric)) return null;
  if (numeric > 1) return numeric / 100;
  if (numeric < 0) return null;
  return numeric;
}

function extractPlayerPropConfidence(row: any): number | null {
  return parsePlayerPropConfidenceValue(
    row?.confidence_score
      ?? row?.forecast_payload?.projected_probability
      ?? row?.forecast_payload?.prob
      ?? null,
  );
}

function passesPlayerPropConfidenceGate(row: any): boolean {
  const confidence = extractPlayerPropConfidence(row);
  return confidence == null || confidence >= PLAYER_PROP_MIN_CONFIDENCE();
}

async function queryServeablePlayerPropRows(eventId: string, team: 'home' | 'away' | null): Promise<any[]> {
  let query = `SELECT id, player_name, team_id, team_side, forecast_payload, confidence_score, league, status, expires_at
               FROM rm_forecast_precomputed
               WHERE event_id = $1
                 AND forecast_type = 'PLAYER_PROP'
                 AND (
                   status = 'ACTIVE'
                   OR (status = 'STALE' AND expires_at > NOW())
                 )`;
  const params: any[] = [eventId];

  if (team) {
    query += ' AND team_side = $2';
    params.push(team);
  }

  query += ` ORDER BY
               CASE WHEN status = 'ACTIVE' THEN 0 ELSE 1 END,
               confidence_score DESC NULLS LAST`;

  const { rows } = await pool.query(query, params);
  return rows;
}

async function fetchEventLineupSnapshot(curated: any | null): Promise<PlayerRadarLineupSnapshot | null> {
  if (!curated?.league || !curated?.starts_at) return null;

  const league = String(curated.league || '').toLowerCase();
  const startsAt = curated.starts_at;
  const homeShort = String(curated.home_short || '').trim();
  const awayShort = String(curated.away_short || '').trim();
  const homeTeam = String(curated.home_team || '').trim();
  const awayTeam = String(curated.away_team || '').trim();

  const shortExact = [homeShort, awayShort].filter(Boolean);
  const teamLike = [`%${homeTeam}%`, `%${awayTeam}%`];
  const [homeCandidatePlayers, awayCandidatePlayers] = await Promise.all([
    fetchCandidateValidatedLineupPlayers({
      league,
      startsAt,
      teamShort: homeShort,
      opponentShort: awayShort,
      teamName: homeTeam,
      opponentName: awayTeam,
      homeTeam,
      awayTeam,
    }),
    fetchCandidateValidatedLineupPlayers({
      league,
      startsAt,
      teamShort: awayShort,
      opponentShort: homeShort,
      teamName: awayTeam,
      opponentName: homeTeam,
      homeTeam,
      awayTeam,
    }),
  ]);
  const { rows } = await pool.query(
    `SELECT "homeTeam", "awayTeam", "homeStatus", "awayStatus",
            source,
            "homePlayers", "awayPlayers", "homeProjMinutes", "awayProjMinutes"
     FROM "GameLineup"
     WHERE LOWER(league) = LOWER($1)
       AND "gameDate" = (($2::timestamptz) AT TIME ZONE 'America/New_York')::date
       AND (
         ("homeTeam" = ANY($3::text[]) AND "awayTeam" = ANY($3::text[]))
         OR ("homeTeam" ILIKE $4 AND "awayTeam" ILIKE $5)
         OR ("homeTeam" ILIKE $5 AND "awayTeam" ILIKE $4)
       )
     ORDER BY
       CASE LOWER(COALESCE(source, ''))
         WHEN 'rotowire' THEN 1
         WHEN 'underdog_nba' THEN 2
         WHEN 'mlb_statsapi' THEN 3
         ELSE 9
       END,
       "updatedAt" DESC
     LIMIT 1`,
    [league, startsAt, shortExact, teamLike[0], teamLike[1]],
  ).catch(() => ({ rows: [] as any[] }));

  const row = rows[0];
  if (!row) return null;

  const homePlayers = sanitizeLineupSnapshotPlayers(
    Array.isArray(row.homePlayers) ? row.homePlayers : [],
    homeShort,
    league,
    homeCandidatePlayers,
  );
  const awayPlayers = sanitizeLineupSnapshotPlayers(
    Array.isArray(row.awayPlayers) ? row.awayPlayers : [],
    awayShort,
    league,
    awayCandidatePlayers,
  );

  return {
    homeStatus: row.homeStatus ?? null,
    awayStatus: row.awayStatus ?? null,
    homePlayers,
    awayPlayers,
    homeProjMinutes: row.homeProjMinutes ?? null,
    awayProjMinutes: row.awayProjMinutes ?? null,
  };
}

async function fetchCandidateValidatedLineupPlayers(params: {
  league: string;
  startsAt: string;
  teamShort: string;
  opponentShort: string;
  teamName: string;
  opponentName: string;
  homeTeam: string;
  awayTeam: string;
}): Promise<Set<string>> {
  if (!params.league || !params.startsAt || !params.teamShort || !params.opponentShort) {
    return new Set<string>();
  }

  const candidates = (params.league === 'mlb'
    ? await fetchMlbPropCandidates({
        teamShort: params.teamShort,
        teamName: params.teamName,
        opponentShort: params.opponentShort,
        startsAt: params.startsAt,
      }).catch(() => [] as any[])
    : await fetchTeamPropMarketCandidates({
        league: params.league,
        teamShort: params.teamShort,
        opponentShort: params.opponentShort,
        teamName: params.teamName,
        opponentName: params.opponentName,
        homeTeam: params.homeTeam,
        awayTeam: params.awayTeam,
        startsAt: params.startsAt,
      }).catch(() => [] as any[]));

  return new Set(
    candidates
      .map((candidate: any) => normalizeRadarPlayerName(resolveCanonicalName(candidate.player, params.league)))
      .filter(Boolean),
  );
}

function normalizeRadarPlayerName(value: string | null | undefined): string {
  return String(value || '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function getLineupSnapshotPlayerName(player: any): string {
  return String(player?.name || player?.playerName || player?.fullName || '').trim();
}

function sanitizeLineupSnapshotPlayers(
  players: any[],
  teamShort: string,
  league: string,
  candidatePlayers: Set<string> | null = null,
): any[] {
  if (!Array.isArray(players) || !teamShort || !league) return Array.isArray(players) ? players : [];

  return players.reduce((acc: any[], player: any) => {
    const rawName = getLineupSnapshotPlayerName(player);
    if (!rawName) return acc;

    const canonicalName = resolveCanonicalName(rawName, league) || rawName;
    const normalizedPlayer = normalizeRadarPlayerName(canonicalName);
    const hasCandidateValidation = Boolean(candidatePlayers && candidatePlayers.size > 0);
    if (hasCandidateValidation && !candidatePlayers!.has(normalizedPlayer)) return acc;
    if (!hasCandidateValidation && !isPlayerOnTeam(canonicalName, teamShort, league)) return acc;

    acc.push({
      ...player,
      name: canonicalName,
    });
    return acc;
  }, []);
}

function getLineupPlayerNames(
  lineups: PlayerRadarLineupSnapshot | null,
  side: 'home' | 'away',
): string[] {
  const players = side === 'home' ? lineups?.homePlayers : lineups?.awayPlayers;
  if (!Array.isArray(players)) return [];
  return players
    .map((player: any) => getLineupSnapshotPlayerName(player))
    .filter(Boolean);
}

function getLineupProjectedMinutesMap(
  lineups: PlayerRadarLineupSnapshot | null,
  side: 'home' | 'away',
): Map<string, number> {
  const source = side === 'home' ? lineups?.homeProjMinutes : lineups?.awayProjMinutes;
  const map = new Map<string, number>();
  if (!source || typeof source !== 'object') return map;
  for (const [name, rawValue] of Object.entries(source)) {
    const minutes = Number(
      rawValue && typeof rawValue === 'object'
        ? (rawValue as any).minutes ?? (rawValue as any).projectedMinutes ?? (rawValue as any).projMinutes
        : rawValue,
    );
    if (!Number.isFinite(minutes)) continue;
    map.set(normalizeRadarPlayerName(name), minutes);
  }
  return map;
}

function buildExistingFeaturedPropKey(row: any): string | null {
  const payload = row?.forecast_payload || {};
  const playerName = String(row?.player_name || '').trim();
  const statType = String(payload.normalized_stat_type || payload.stat_type || '').trim();
  const line = Number(payload.market_line_value ?? payload.line);
  if (!playerName || !statType || !Number.isFinite(line)) return null;
  return `${normalizeRadarPlayerName(playerName)}|${String(statType).toLowerCase()}|${Math.round(line * 1000) / 1000}`;
}

function buildSupplementalFeaturedRow(params: {
  eventId: string;
  league: string;
  teamId: string | null;
  teamSide: 'home' | 'away';
  playerRole?: string | null;
  prop: any;
}): any {
  const payload = params.prop || {};
  const player = String(payload.player || '').trim();
  const statType = String(payload.stat_type || '').trim() || null;
  const line = Number(payload.market_line_value);
  const safeLine = Number.isFinite(line) ? Math.round(line * 1000) / 1000 : null;
  const assetId = [
    'radar',
    params.eventId,
    params.teamSide,
    normalizeRadarPlayerName(player).replace(/\s+/g, '-'),
    String(statType || 'prop').toLowerCase(),
    safeLine != null ? String(safeLine).replace(/\./g, '_') : 'na',
  ].join(':');

  return {
    id: assetId,
    player_name: player,
    team_id: params.teamId,
    team_side: params.teamSide,
    league: params.league,
    confidence_score: null,
    locked: false,
    playerRole: params.playerRole || null,
    forecast_payload: {
      prop: payload.prop ?? null,
      recommendation: payload.recommendation ?? null,
      reasoning: payload.reasoning ?? null,
      edge: payload.edge ?? null,
      prob: payload.prob ?? null,
      odds: payload.odds ?? null,
      line: safeLine,
      market_line_value: safeLine,
      projected_stat_value: payload.projected_stat_value ?? null,
      stat_type: statType,
      normalized_stat_type: statType,
      source_backed: true,
      model_context: payload.model_context ?? null,
    },
  };
}

function selectFeaturedFallbackPropsByPlayer(params: {
  lineupPlayers: string[];
  projectedMinutes: Map<string, number>;
  fallbackProps: any[];
  maxPlayersWithProps: number;
  maxPropsPerPlayer: number;
}): any[] {
  if (!Array.isArray(params.fallbackProps) || params.fallbackProps.length === 0) return [];

  const playerPriority = new Map<string, { index: number; minutes: number }>();
  params.lineupPlayers.forEach((player, index) => {
    const normalized = normalizeRadarPlayerName(player);
    if (!normalized) return;
    playerPriority.set(normalized, {
      index,
      minutes: params.projectedMinutes.get(normalized) ?? 0,
    });
  });

  const ranked = params.fallbackProps.slice().sort((a, b) => {
    const aPlayer = playerPriority.get(normalizeRadarPlayerName(a.player));
    const bPlayer = playerPriority.get(normalizeRadarPlayerName(b.player));
    const aMinutes = aPlayer?.minutes ?? 0;
    const bMinutes = bPlayer?.minutes ?? 0;
    if (bMinutes !== aMinutes) return bMinutes - aMinutes;
    const aIndex = aPlayer?.index ?? Number.MAX_SAFE_INTEGER;
    const bIndex = bPlayer?.index ?? Number.MAX_SAFE_INTEGER;
    if (aIndex !== bIndex) return aIndex - bIndex;
    const aEdge = Number(a.edge ?? -999);
    const bEdge = Number(b.edge ?? -999);
    if (bEdge !== aEdge) return bEdge - aEdge;
    return String(a.player || '').localeCompare(String(b.player || ''));
  });

  const selected: any[] = [];
  const playersWithProps = new Set<string>();
  const propsPerPlayer = new Map<string, number>();

  for (const prop of ranked) {
    const normalizedPlayer = normalizeRadarPlayerName(prop.player);
    if (!normalizedPlayer || !playerPriority.has(normalizedPlayer)) continue;
    if (playersWithProps.has(normalizedPlayer)) continue;
    playersWithProps.add(normalizedPlayer);
    propsPerPlayer.set(normalizedPlayer, 1);
    selected.push(prop);
    if (playersWithProps.size >= params.maxPlayersWithProps) break;
  }

  for (const prop of ranked) {
    const normalizedPlayer = normalizeRadarPlayerName(prop.player);
    if (!normalizedPlayer || !playersWithProps.has(normalizedPlayer)) continue;
    const currentCount = propsPerPlayer.get(normalizedPlayer) || 0;
    if (currentCount >= params.maxPropsPerPlayer) continue;
    if (selected.includes(prop)) continue;
    propsPerPlayer.set(normalizedPlayer, currentCount + 1);
    selected.push(prop);
  }

  return selected;
}

async function buildSupplementalFeaturedPlayerRows(params: {
  eventId: string;
  curated: any | null;
  lineups: PlayerRadarLineupSnapshot | null;
  existingRows: any[];
}): Promise<any[]> {
  const curated = params.curated;
  if (!curated?.league || !curated?.starts_at || !params.lineups) return [];

  const league = String(curated.league || '').toLowerCase();
  const startsAt = String(curated.starts_at || '');
  const homeShort = String(curated.home_short || '').trim();
  const awayShort = String(curated.away_short || '').trim();
  if (!league || !startsAt || !homeShort || !awayShort) return [];

  const existingKeys = new Set(
    params.existingRows
      .map((row) => buildExistingFeaturedPropKey(row))
      .filter((key): key is string => Boolean(key)),
  );

  const eventPiff = getPiffPropsForGame(
    homeShort,
    awayShort,
    loadPiffPropsForDate(getEtDateKey(startsAt) || undefined),
    league,
  );
  const supplementalRows: any[] = [];

  for (const side of ['away', 'home'] as const) {
    const lineupPlayers = getLineupPlayerNames(params.lineups, side);
    if (lineupPlayers.length === 0) continue;
    const projectedMinutes = getLineupProjectedMinutesMap(params.lineups, side);

    const lineupPlayerSet = new Set(lineupPlayers.map((name) => normalizeRadarPlayerName(name)));
    const teamShort = side === 'home' ? homeShort : awayShort;
    const opponentShort = side === 'home' ? awayShort : homeShort;
    const teamName = side === 'home' ? String(curated.home_team || '').trim() : String(curated.away_team || '').trim();
    const opponentName = side === 'home' ? String(curated.away_team || '').trim() : String(curated.home_team || '').trim();
    const teamPiff = eventPiff.filter((leg: any) => String(leg.team || '').toUpperCase() === teamShort.toUpperCase());

    if (league === 'mlb') {
      const candidates = (await fetchMlbPropCandidates({
        teamShort,
        teamName,
        opponentShort,
        startsAt,
      }).catch(() => [] as any[]))
        .filter((candidate: any) => lineupPlayerSet.has(normalizeRadarPlayerName(candidate.player)));

      const fallbackProps = selectFeaturedFallbackPropsByPlayer({
        lineupPlayers,
        projectedMinutes,
        fallbackProps: buildMlbFallbackProps(candidates),
        maxPlayersWithProps: Math.min(5, lineupPlayers.length),
        maxPropsPerPlayer: 2,
      });
      for (const prop of fallbackProps) {
        const key = buildExistingFeaturedPropKey({ player_name: prop.player, forecast_payload: prop });
        if (!key || existingKeys.has(key)) continue;
        existingKeys.add(key);
        supplementalRows.push(buildSupplementalFeaturedRow({
          eventId: params.eventId,
          league,
          teamId: teamShort,
          teamSide: side,
          playerRole: null,
          prop,
        }));
      }
      continue;
    }

    const candidates = (await fetchTeamPropMarketCandidates({
      league,
      teamShort,
      opponentShort,
      teamName,
      opponentName,
      homeTeam: String(curated.home_team || '').trim(),
      awayTeam: String(curated.away_team || '').trim(),
      startsAt,
    }).catch(() => [] as any[]))
      .filter((candidate: any) => lineupPlayerSet.has(normalizeRadarPlayerName(candidate.player)));

    const fallbackProps = selectFeaturedFallbackPropsByPlayer({
      lineupPlayers,
      projectedMinutes,
      fallbackProps: buildSourceBackedFallbackProps({
        league,
        teamName,
        candidates,
        teamProps: teamPiff,
        isHome: side === 'home',
        limit: Math.max(10, lineupPlayers.length * 3),
      }),
      maxPlayersWithProps: Math.min(5, lineupPlayers.length),
      maxPropsPerPlayer: 2,
    });

    for (const prop of fallbackProps) {
      const key = buildExistingFeaturedPropKey({ player_name: prop.player, forecast_payload: prop });
      if (!key || existingKeys.has(key)) continue;
      existingKeys.add(key);
      supplementalRows.push(buildSupplementalFeaturedRow({
        eventId: params.eventId,
        league,
        teamId: teamShort,
        teamSide: side,
        playerRole: null,
        prop,
      }));
    }
  }

  return supplementalRows;
}

async function filterVisiblePlayerPropRows(rows: any[], eventId: string, logLabel: string): Promise<any[]> {
  if (rows.length === 0) return rows;

  const league = rows[0].league;
  const outPlayers = await getOutPlayerNames(league);
  let visibleRows = rows;

  if (outPlayers.size > 0) {
    const before = visibleRows.length;
    visibleRows = visibleRows.filter((row: any) => !outPlayers.has(row.player_name?.toLowerCase()));
    if (visibleRows.length < before) {
      console.log(`[player-props] Filtered ${before - visibleRows.length} OUT players for ${eventId}${logLabel}`);
    }
  }

  visibleRows = await filterSiblingSuppressedRows(visibleRows, eventId);
  visibleRows = visibleRows.filter((row: any) => hasPublicPlayerPropPricing(row.forecast_payload));
  visibleRows = visibleRows.filter((row: any) => passesPlayerPropConfidenceGate(row));

  if (PROP_DEDUP_ENABLED()) {
    visibleRows = deduplicateProps(visibleRows);
  }

  return visibleRows;
}

async function resolveVisiblePlayerPropRows(
  eventId: string,
  team: 'home' | 'away' | null,
  logLabel = '',
): Promise<{ rows: any[]; rawRowCount: number }> {
  const candidateRows = await queryServeablePlayerPropRows(eventId, team);
  const activeRows = candidateRows.filter((row: any) => String(row.status || 'ACTIVE').toUpperCase() === 'ACTIVE');
  const staleRows = candidateRows.filter((row: any) => String(row.status || 'ACTIVE').toUpperCase() === 'STALE');

  const visibleActiveRows = await filterVisiblePlayerPropRows(activeRows, eventId, logLabel);
  if (visibleActiveRows.length > 0) {
    return { rows: visibleActiveRows, rawRowCount: activeRows.length };
  }

  const visibleStaleRows = await filterVisiblePlayerPropRows(staleRows, eventId, `${logLabel} (stale fallback)`);
  if (visibleStaleRows.length > 0) {
    return { rows: visibleStaleRows, rawRowCount: staleRows.length };
  }

  return {
    rows: [],
    rawRowCount: activeRows.length > 0 ? activeRows.length : staleRows.length,
  };
}

function hasPersistablePlayerPropPricing(odds: any): boolean {
  return odds !== null && odds !== undefined && odds !== '' && Number.isFinite(Number(odds));
}

async function queryTodayTopPropsRows(filters: TopBoardFilters): Promise<any[]> {
  const params: any[] = [];
  const leagueClause = buildTopBoardLeagueClause('fp.league', filters.leagues, params);
  const dateClause = buildTopBoardDateClause('fp.date_et', filters.maxLookaheadDays, params);

  const { rows } = await pool.query(
    `
      SELECT
        fp.id,
        fp.event_id,
        fp.league,
        fp.player_name,
        fp.team_id,
        fp.team_side,
        fp.confidence_score,
        fp.forecast_payload,
        fp.date_et,
        fp.generated_at,
        e.starts_at,
        e.home_team,
        e.away_team,
        rp.odds_snapshot
      FROM rm_forecast_precomputed fp
      LEFT JOIN rm_events e ON e.event_id = fp.event_id
      LEFT JOIN LATERAL (
        SELECT odds_snapshot
        FROM rm_forecast_picks
        WHERE forecast_asset_id = fp.id
        ORDER BY timestamp_locked DESC NULLS LAST, id DESC
        LIMIT 1
      ) rp ON true
      WHERE fp.forecast_type = 'PLAYER_PROP'
        AND fp.status = 'ACTIVE'
        ${dateClause}
        AND (e.starts_at IS NULL OR e.starts_at > NOW())
        ${leagueClause}
      ORDER BY fp.generated_at DESC NULLS LAST, fp.id DESC
    `,
    params,
  );

  return filterTopBoardRowsToWindow(rows, filters, { dateKeyField: 'date_et' });
}

async function queryTodayTopGameRows(filters: TopBoardFilters): Promise<any[]> {
  const params: any[] = [];
  const leagueClause = buildTopBoardLeagueClause('e.league', filters.leagues, params);
  const dateClause = buildTopBoardDateClause(
    `DATE(e.starts_at AT TIME ZONE 'America/New_York')`,
    filters.maxLookaheadDays,
    params,
  );

  const { rows } = await pool.query(
    `
      SELECT
        e.event_id,
        e.league,
        e.starts_at,
        e.home_team,
        e.away_team,
        e.home_short,
        e.away_short,
        e.source,
        e.spread,
        COALESCE(fc_exact.public_confidence, fc_fallback.public_confidence) AS fc_confidence,
        COALESCE(fc_exact.forecast_data, fc_fallback.forecast_data) AS fc_data
      FROM rm_events e
      ${TOP_PICKS_FORECAST_LATERAL}
      WHERE 1=1
        ${dateClause}
        AND e.starts_at > NOW()
        ${TOP_PICKS_ODDS_FILTER}
        ${leagueClause}
      ORDER BY e.starts_at ASC
    `,
    params,
  );

  return filterTopBoardRowsToWindow(rows, filters).filter((row: any) => {
    const fit = assessPublicGameFit({
      league: row.league,
      forecastData: row.fc_data,
      homeTeam: row.home_team,
      awayTeam: row.away_team,
    });
    return fit.eligible;
  });
}

function inferTopGameEdge(row: any): number | null {
  const fcData = row.fc_data || {};
  let edge: number | null = fcData.spread_edge ?? null;
  if (edge == null) {
    const projMargin = fcData.projected_margin;
    const marketHomeSpread = row.spread?.home?.line ?? (typeof row.spread?.home === 'string' ? parseFloat(row.spread.home) : null);
    if (projMargin != null && marketHomeSpread != null) {
      edge = Math.round(Math.abs(marketHomeSpread + projMargin) * 10) / 10;
    } else if (row.fc_confidence != null && Number.isFinite(Number(row.fc_confidence))) {
      edge = Math.round((Number(row.fc_confidence) - 0.5) * 120) / 10;
    }
  }
  return edge != null && Number.isFinite(Number(edge)) ? Number(edge) : null;
}

function buildTodayTopPicksBody(propRows: any[], gameRows: any[], scopeLabel: string, limit: number): any {
  const propCards = buildTopPropsOfDay(
    propRows.map((row: any) => {
      const payload = row.forecast_payload || {};
      const team = row.team_id || null;
      const opponent = row.team_side === 'home'
        ? (row.away_team || null)
        : row.team_side === 'away'
          ? (row.home_team || null)
          : null;
      return {
        assetId: row.id,
        eventId: row.event_id,
        league: row.league || null,
        startsAt: row.starts_at || null,
        playerName: row.player_name || null,
        team,
        opponent,
        teamSide: row.team_side || null,
        prop: payload.prop || null,
        statType: payload.stat_type || null,
        normalizedStatType: payload.normalized_stat_type || payload.stat_type || null,
        marketLine: payload.market_line_value ?? payload.line ?? null,
        odds: payload.odds ?? row.odds_snapshot ?? null,
        projectedProbability: payload.prob ?? null,
        projectedOutcome: payload.projected_stat_value ?? null,
        recommendation: payload.recommendation || null,
        playerRole: payload.player_role || null,
        confidenceFactor: row.confidence_score ?? null,
        marketSource: payload.market_source || null,
        marketCompletenessStatus: payload.market_completeness_status || null,
        sourceBacked: payload.source_backed ?? true,
        marketQualityScore: payload.market_quality_score ?? null,
        modelContext: payload.model_context ?? null,
        closingLineValue: payload.closing_line_value ?? null,
      };
    }),
    { limit: Math.max(limit * 2, 16), maxPropsPerPlayer: 2, maxPerPropTypePerPlayer: 1 },
  );

  const gameCards = gameRows
    .map((row: any): PublicTopGameCard | null => {
      if (!hasNamedMarketSource(row.source)) return null;
      const rawConfidence = row.fc_confidence == null ? null : Number(row.fc_confidence);
      if (rawConfidence == null || !Number.isFinite(rawConfidence)) return null;
      const fcData = row.fc_data || {};
      const sourceMeta = getPublicMarketSourceMeta(row.source);
      return {
        eventId: row.event_id,
        league: row.league || null,
        startsAt: row.starts_at || null,
        homeTeam: row.home_team || '',
        awayTeam: row.away_team || '',
        homeShort: row.home_short || null,
        awayShort: row.away_short || null,
        forecastSide: fcData.forecast_side || null,
        confidencePct: Math.round(rawConfidence * 100),
        edge: inferTopGameEdge(row),
        valueRating: Number(fcData.value_rating || 5),
        hasSharpSignal: SHARP_UNLOCK_ENABLED(),
        hasSteamSignal: STEAM_UNLOCK_ENABLED(),
        verificationLabel: sourceMeta.label,
        verificationType: sourceMeta.type,
      };
    })
    .filter((card): card is PublicTopGameCard => card !== null);

  const entries = buildTopPickEntries(propCards, gameCards, limit);
  const propTypes = Array.from(new Set(propCards.map((card) => card.propType))).sort();
  const teams = Array.from(new Set([
    ...propCards.map((card) => card.team).filter(Boolean),
    ...gameCards.flatMap((card: any) => [card.homeShort, card.awayShort].filter(Boolean)),
  ])).sort();
  const players = Array.from(new Set(propCards.map((card) => card.playerName))).sort();

  return {
    contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
    league: scopeLabel,
    generatedAt: new Date().toISOString(),
    count: entries.length,
    entries,
    filters: {
      propTypes,
      teams,
      players,
      kinds: ['game', 'prop'],
    },
    emptyMessage: 'No pregame picks available in this window',
  };
}

function buildTodayTopPropsBody(rows: any[], scopeLabel: string, limit: number): any {
  const cards = buildTopPropsOfDay(
    rows.map((row: any) => {
      const payload = row.forecast_payload || {};
      const team = row.team_id || null;
      const opponent = row.team_side === 'home'
        ? (row.away_team || null)
        : row.team_side === 'away'
          ? (row.home_team || null)
          : null;
      return {
        assetId: row.id,
        eventId: row.event_id,
        league: row.league || null,
        startsAt: row.starts_at || null,
        playerName: row.player_name || null,
        team,
        opponent,
        teamSide: row.team_side || null,
        prop: payload.prop || null,
        statType: payload.stat_type || null,
        normalizedStatType: payload.normalized_stat_type || payload.stat_type || null,
        marketLine: payload.market_line_value ?? payload.line ?? null,
        odds: payload.odds ?? row.odds_snapshot ?? null,
        projectedProbability: payload.prob ?? null,
        projectedOutcome: payload.projected_stat_value ?? null,
        recommendation: payload.recommendation || null,
        playerRole: payload.player_role || null,
        confidenceFactor: row.confidence_score ?? null,
        marketSource: payload.market_source || null,
        marketCompletenessStatus: payload.market_completeness_status || null,
        sourceBacked: payload.source_backed ?? true,
        marketQualityScore: payload.market_quality_score ?? null,
        modelContext: payload.model_context ?? null,
        closingLineValue: payload.closing_line_value ?? null,
      };
    }),
    { limit, maxPropsPerPlayer: 2, maxPerPropTypePerPlayer: 1 },
  );

  const propTypes = Array.from(new Set(cards.map((card) => card.propType))).sort();
  const teams = Array.from(new Set(cards.map((card) => card.team).filter(Boolean))).sort();
  const players = Array.from(new Set(cards.map((card) => card.playerName))).sort();

  return {
    contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
    league: scopeLabel,
    generatedAt: new Date().toISOString(),
    count: cards.length,
    cards,
    filters: {
      propTypes,
      teams,
      players,
    },
    emptyMessage: 'No player prop edges available in this window',
  };
}

async function getTodayTopProps(filters: TopBoardFilters, limit: number): Promise<any> {
  const cacheKey = getTopBoardScopeKey(filters, limit);
  const now = Date.now();

  const rows = await queryTodayTopPropsRows(filters);
  const latestGeneratedAt = rows.reduce((latest: string, row: any) => {
    const next = row.generated_at ? new Date(row.generated_at).toISOString() : '';
    return next > latest ? next : latest;
  }, '');
  const fingerprint = `${rows.length}|${latestGeneratedAt}`;

  const cached = TOP_PROPS_CACHE.get(cacheKey);
  if (cached && cached.expiresAt > now && cached.fingerprint === fingerprint) {
    return cached.body;
  }

  const body = buildTodayTopPropsBody(rows, getTopBoardScopeLabel(filters), limit);
  TOP_PROPS_CACHE.set(cacheKey, {
    fingerprint,
    expiresAt: now + TOP_PROPS_CACHE_TTL_MS,
    body,
  });
  return body;
}

async function getTodayTopPicks(filters: TopBoardFilters, limit: number): Promise<any> {
  const cacheKey = getTopBoardScopeKey(filters, limit);
  const now = Date.now();

  const [propRows, gameRows] = await Promise.all([
    queryTodayTopPropsRows(filters),
    queryTodayTopGameRows(filters),
  ]);

  const latestPropGeneratedAt = propRows.reduce((latest: string, row: any) => {
    const next = row.generated_at ? new Date(row.generated_at).toISOString() : '';
    return next > latest ? next : latest;
  }, '');
  const latestGameStart = gameRows.reduce((latest: string, row: any) => {
    const next = row.starts_at ? new Date(row.starts_at).toISOString() : '';
    return next > latest ? next : latest;
  }, '');
  const fingerprint = `${propRows.length}|${gameRows.length}|${latestPropGeneratedAt}|${latestGameStart}`;

  const cached = TOP_PICKS_CACHE.get(cacheKey);
  if (cached && cached.expiresAt > now && cached.fingerprint === fingerprint) {
    return cached.body;
  }

  const body = buildTodayTopPicksBody(propRows, gameRows, getTopBoardScopeLabel(filters), limit);
  TOP_PICKS_CACHE.set(cacheKey, {
    fingerprint,
    expiresAt: now + TOP_PICKS_CACHE_TTL_MS,
    body,
  });
  return body;
}

/**
 * RULE: RMF always includes current market pricing from data feeds.
 * Priority: rm_events (curated) > SGO API (live feed) > rm_forecast_cache (stored)
 */

/** Build opening lines from curated event or cached forecast */
function buildOpeningLines(curated: any, cached: any): { moneyline: any; spread: any; total: any } | null {
  const src = curated || cached;
  if (!src) return null;
  const sanitized = sanitizeGameOddsForLeague(src.league, {
    moneyline: sanitizeMoneylinePair(src.opening_moneyline || null),
    spread: src.opening_spread || { home: null, away: null },
    total: src.opening_total || { over: null, under: null },
  });
  const ml = sanitized.moneyline;
  const sp = src.opening_spread == null ? src.opening_spread : sanitized.spread;
  const tl = src.opening_total == null ? src.opening_total : sanitized.total;
  const hasMoneyline = ml.home != null || ml.away != null;
  if (!hasMoneyline && !sp && !tl) return null;
  return { moneyline: ml, spread: sp, total: tl };
}

/** Extract model projected lines from forecast data */
function buildModelLines(forecastData: any): { moneyline: any; spread: any; total: any } | null {
  const normalizedForecast = reconcilePublicForecastProjection(forecastData);
  const proj = normalizedForecast?.projected_lines;
  const projectedMargin = normalizedForecast?.projected_margin;
  const projectedTotal = normalizedForecast?.projected_total_points;
  const numericMargin = projectedMargin != null && Number.isFinite(Number(projectedMargin))
    ? Number(projectedMargin)
    : null;
  const numericTotal = projectedTotal != null && Number.isFinite(Number(projectedTotal))
    ? Number(projectedTotal)
    : null;

  const spread = proj?.spread || (
    numericMargin != null
      ? {
          home: Math.round(-numericMargin * 10) / 10,
          away: Math.round(numericMargin * 10) / 10,
        }
      : null
  );
  const total = proj?.total ?? numericTotal ?? null;
  const moneyline = proj?.moneyline || null;

  if (!moneyline && !spread && total == null) return null;

  return {
    moneyline,
    spread,
    total,
  };
}

function normalizeLegacyTeamPropEntry(prop: any, league: string | null | undefined): any {
  if (!prop || typeof prop !== 'object') return prop;
  const mlbPropContext = serializeMlbPropContext(prop.model_context, league, true);
  return {
    ...prop,
    modelContext: camelizeObjectKeys(prop.model_context),
    ...(((league || '').toLowerCase() === 'mlb') ? { mlbPropContext } : {}),
  };
}

function normalizeLegacyTeamPropsResponse(propsResult: any, league: string | null | undefined): any {
  if (!propsResult || typeof propsResult !== 'object') return propsResult;

  const legacyProps = Array.isArray(propsResult.props)
    ? propsResult.props
    : Array.isArray(propsResult.recommendations)
      ? propsResult.recommendations
      : propsResult.props;

  const normalizeArray = (value: any) => (
    Array.isArray(value)
      ? value.map((prop) => normalizeLegacyTeamPropEntry(prop, league))
      : value
  );

  return {
    ...propsResult,
    props: normalizeArray(legacyProps),
    playerProps: normalizeArray(propsResult.playerProps),
  };
}

function pushDigilanderSummarySupportPlayer(
  players: Set<string>,
  player: any,
  teamTag?: any,
): void {
  const normalizedPlayer = String(player || '').trim().toLowerCase();
  if (!normalizedPlayer) return;
  const normalizedTeam = String(teamTag || '').trim().toLowerCase();
  players.add(normalizedTeam ? `${normalizedPlayer}|${normalizedTeam}` : normalizedPlayer);
}

function countDigilanderLegacySummarySupportItems(params: {
  propsResult: any;
  league: string | null | undefined;
  teamTag?: string | null;
  players: Set<string>;
}): number {
  const normalized = normalizeLegacyTeamPropsResponse(params.propsResult, params.league);
  const items = Array.isArray(normalized?.playerProps)
    ? normalized.playerProps
    : Array.isArray(normalized?.props)
      ? normalized.props
      : [];
  let marketPropsCount = 0;

  for (const item of items) {
    if (!shouldRenderTeamPropBundleEntry(item)) continue;
    marketPropsCount += 1;
    pushDigilanderSummarySupportPlayer(
      params.players,
      item?.player,
      item?.team
        || item?.team_id
        || item?.teamId
        || item?.team_side
        || item?.teamSide
        || params.teamTag,
    );
  }

  return marketPropsCount;
}

function countDigilanderPrecomputedSummarySupportItems(params: {
  rows: any[];
  players: Set<string>;
}): number {
  let marketPropsCount = 0;

  for (const row of params.rows) {
    const payload = row?.forecast_payload || {};
    const propsArray = Array.isArray(payload.props) ? payload.props : [];
    let countedRow = false;

    for (const item of propsArray) {
      if (!shouldRenderTeamPropBundleEntry(item)) continue;
      countedRow = true;
      marketPropsCount += 1;
      pushDigilanderSummarySupportPlayer(
        params.players,
        item?.player || row?.player_name,
        item?.team_side || item?.teamSide || row?.team_side,
      );
    }

    if (!countedRow && payload?.player && shouldRenderTeamPropBundleEntry(payload)) {
      marketPropsCount += 1;
      pushDigilanderSummarySupportPlayer(
        params.players,
        payload.player || row?.player_name,
        payload?.team_side || payload?.teamSide || row?.team_side,
      );
    }
  }

  return marketPropsCount;
}

function buildDigilanderSummarySupportTeamVariants(teamShort: string, teamName: string): string[] {
  const variants = new Set<string>();
  if (teamShort) variants.add(teamShort.toUpperCase());
  if (teamName) variants.add(teamName.toUpperCase());
  return [...variants];
}

function normalizeDigilanderSourceSummaryPlayer(row: any): string {
  const externalId = String(row?.playerExternalId || '')
    .replace(/_\d+_[A-Z]+$/, '')
    .replace(/_/g, ' ')
    .trim();
  const marketName = String(row?.marketName || '')
    .replace(/\s+[^\s]+\s+Over\/Under$/i, '')
    .replace(/\s+Over\/Under$/i, '')
    .trim();
  return String(externalId || marketName).trim().toLowerCase();
}

function normalizeDigilanderSourceSummarySide(row: any): 'over' | 'under' | null {
  const sideRaw = String(row?.raw?.side || row?.market || '').trim().toLowerCase();
  if (
    sideRaw === 'over'
    || sideRaw.startsWith('over')
    || sideRaw.includes('_over')
    || sideRaw === 'o'
  ) {
    return 'over';
  }
  if (
    sideRaw === 'under'
    || sideRaw.startsWith('under')
    || sideRaw.includes('_under')
    || sideRaw === 'u'
  ) {
    return 'under';
  }
  return null;
}

function summarizeDigilanderSourceSummaryRows(rows: any[]): {
  playerCount: number;
  marketPropsCount: number;
} {
  const players = new Set<string>();
  const marketKeys = new Set<string>();

  for (const row of rows) {
    const player = normalizeDigilanderSourceSummaryPlayer(row);
    if (!player) continue;
    players.add(player);

    const side = normalizeDigilanderSourceSummarySide(row);
    if (!side) continue;

    const lineValue = Number(row?.lineValue);
    if (!Number.isFinite(lineValue)) continue;

    const propType = String(row?.propType || '').trim().toLowerCase();
    marketKeys.add(`${player}|${propType}|${Math.round(lineValue * 1000) / 1000}|${side}`);
  }

  return {
    playerCount: players.size,
    marketPropsCount: marketKeys.size,
  };
}

async function loadDigilanderNonMlbTeamPropSummarySupport(eventId: string, league: string): Promise<{
  playerCount: number;
  marketPropsCount: number;
  source: 'cached_team_props' | 'precomputed_team_props' | 'none';
}> {
  const players = new Set<string>();
  let marketPropsCount = 0;

  const { rows: cacheRows } = await pool.query(
    'SELECT team, props_data FROM rm_team_props_cache WHERE event_id = $1',
    [eventId],
  ).catch(() => ({ rows: [] as any[] }));

  for (const row of cacheRows) {
    if (!row?.props_data || hasPoisonedLegacyTeamProps(row.props_data)) continue;
    marketPropsCount += countDigilanderLegacySummarySupportItems({
      propsResult: row.props_data,
      league,
      teamTag: row.team,
      players,
    });
  }
  if (marketPropsCount > 0) {
    return {
      playerCount: players.size,
      marketPropsCount,
      source: 'cached_team_props',
    };
  }

  const { rows: precomputedRows } = await pool.query(
    `SELECT team_side, player_name, forecast_payload
     FROM rm_forecast_precomputed
     WHERE event_id = $1
       AND forecast_type = 'TEAM_PROPS'
       AND status = 'ACTIVE'
     ORDER BY generated_at DESC`,
    [eventId],
  ).catch(() => ({ rows: [] as any[] }));

  const validPrecomputedRows = precomputedRows.filter((row: any) => !hasPoisonedLegacyTeamProps(row?.forecast_payload));
  marketPropsCount += countDigilanderPrecomputedSummarySupportItems({
    rows: validPrecomputedRows,
    players,
  });

  return {
    playerCount: players.size,
    marketPropsCount,
    source: marketPropsCount > 0 ? 'precomputed_team_props' : 'none',
  };
}

async function loadDigilanderSourceTeamPropSummarySupport(params: {
  league: string;
  startsAt: string;
  homeShort: string;
  awayShort: string;
  homeTeam: string;
  awayTeam: string;
}): Promise<{
  playerCount: number;
  marketPropsCount: number;
}> {
  const supportedQueryValues = getSupportedPlayerPropQueryValues(params.league);
  if (supportedQueryValues.length === 0) {
    return {
      playerCount: 0,
      marketPropsCount: 0,
    };
  }

  const homeVariants = buildDigilanderSummarySupportTeamVariants(params.homeShort, params.homeTeam);
  const awayVariants = buildDigilanderSummarySupportTeamVariants(params.awayShort, params.awayTeam);
  if (homeVariants.length === 0 || awayVariants.length === 0) {
    return {
      playerCount: 0,
      marketPropsCount: 0,
    };
  }

  const { rows } = await pool.query(
    `SELECT
       "playerExternalId",
       "marketName",
       "propType",
       "lineValue",
       market,
       raw
     FROM "PlayerPropLine"
     WHERE LOWER(league) = LOWER($1)
       AND (
         COALESCE("marketScope", '') IN ('full_game', 'game')
         OR LOWER(COALESCE(raw->>'period', raw #>> '{sportsgameodds,periodID}', '')) = 'game'
         OR LOWER(COALESCE(market, '')) LIKE 'game_%'
       )
       AND ("gameStart")::timestamptz BETWEEN ($2::timestamptz - INTERVAL '4 hours') AND ($2::timestamptz + INTERVAL '4 hours')
       AND (
         (
           UPPER(COALESCE("homeTeam", '')) = ANY($3::text[])
           AND UPPER(COALESCE("awayTeam", '')) = ANY($4::text[])
         )
         OR (
           UPPER(COALESCE("homeTeam", '')) = ANY($4::text[])
           AND UPPER(COALESCE("awayTeam", '')) = ANY($3::text[])
         )
       )
       AND "oddsAmerican" IS NOT NULL
       AND "lineValue" IS NOT NULL
       AND LOWER(COALESCE("propType", '')) = ANY($5::text[])`,
    [params.league, params.startsAt, homeVariants, awayVariants, supportedQueryValues],
  ).catch(() => ({ rows: [] as any[] }));
  return summarizeDigilanderSourceSummaryRows(Array.isArray(rows) ? rows : []);
}

function normalizeLegacyTeamPropConfidence(prob: any, confidence: any): number | null {
  const raw = confidence ?? prob;
  if (raw == null) return null;
  const numeric = Number(raw);
  if (!Number.isFinite(numeric)) return null;
  if (numeric > 1) return Math.max(0, Math.min(1, numeric / 100));
  return Math.max(0, Math.min(1, numeric));
}

function normalizeLegacyTeamPropItem(prop: any, league: string | null | undefined): any {
  if (!prop || typeof prop !== 'object') return prop;

  const rawModelContext = prop.model_context || prop.modelContext || null;
  const modelContext = camelizeObjectKeys(rawModelContext);
  return {
    ...prop,
    prop: prop.prop
      ? sanitizePublicTextBlock(String(prop.prop), { totalBrandMentions: 0, warnings: [] }, 'legacy-team-prop.prop')
      : null,
    reasoning: prop.reasoning
      ? sanitizePublicTextBlock(String(prop.reasoning), { totalBrandMentions: 0, warnings: [] }, 'legacy-team-prop.reasoning')
      : null,
    confidence: normalizeLegacyTeamPropConfidence(prop.prob, prop.confidence),
    line: prop.line ?? prop.market_line_value ?? null,
    projectedOutcome: prop.projectedOutcome ?? prop.projected_stat_value ?? null,
    modelContext,
    ...(((league || '').toLowerCase() === 'mlb')
      ? { mlbPropContext: serializeMlbPropContext(rawModelContext, league, true) }
      : {}),
  };
}

async function buildMlbBreakdownMarketProps(params: {
  curated: any;
  side: 'home' | 'away';
}): Promise<any[]> {
  const homeShort = String(params.curated?.home_short || '').trim().toUpperCase();
  const awayShort = String(params.curated?.away_short || '').trim().toUpperCase();
  const startsAtRaw = params.curated?.starts_at;
  const startsAt = startsAtRaw instanceof Date
    ? startsAtRaw.toISOString()
    : startsAtRaw
      ? new Date(startsAtRaw).toISOString()
      : '';
  if (!homeShort || !awayShort || !startsAt) return [];

  const teamShort = params.side === 'home' ? homeShort : awayShort;
  const opponentShort = params.side === 'home' ? awayShort : homeShort;
  const teamName = params.side === 'home'
    ? String(params.curated?.home_team || teamShort)
    : String(params.curated?.away_team || teamShort);

  const localCandidates = await fetchMlbPropCandidates({
    teamShort,
    teamName,
    opponentShort,
    startsAt,
  }).catch(() => []);

  const directCandidates = localCandidates.length === 0
    ? await fetchDirectMlbPropCandidates({
        teamShort,
        teamName,
        opponentShort,
        startsAt,
      }).catch(() => [])
    : [];

  const candidates = localCandidates.length > 0 ? localCandidates : directCandidates;
  return candidates
    .map((candidate) => ({
      player: resolveCanonicalName(candidate.player, 'mlb') || candidate.player || null,
      prop: candidate.prop
        ? sanitizePublicTextBlock(String(candidate.prop), { totalBrandMentions: 0, warnings: [] }, 'mlb-market-prop.prop')
        : null,
      marketType: candidate.normalizedStatType || candidate.statType || null,
      line: candidate.marketLineValue ?? null,
      overOdds: candidate.overOdds ?? null,
      underOdds: candidate.underOdds ?? null,
      availableSides: Array.isArray(candidate.availableSides) ? candidate.availableSides : [],
      completenessStatus: candidate.completenessStatus || null,
      source: candidate.source || null,
      books: countUniqueMarketBooks(candidate.sourceMap),
      playerRole: candidate.playerRole || null,
    }))
    .filter((item) => item.player && item.prop && item.line != null && (item.overOdds != null || item.underOdds != null))
    .sort((a, b) => {
      const sideDelta = (b.availableSides?.length || 0) - (a.availableSides?.length || 0);
      if (sideDelta !== 0) return sideDelta;
      const bookDelta = (b.books || 0) - (a.books || 0);
      if (bookDelta !== 0) return bookDelta;
      const pitcherDelta = Number(b.playerRole === 'pitcher') - Number(a.playerRole === 'pitcher');
      if (pitcherDelta !== 0) return pitcherDelta;
      return String(a.player).localeCompare(String(b.player));
    })
    .slice(0, 12);
}

function normalizeMarketBoardLabel(value: any): string {
  return String(value || '')
    .toLowerCase()
    .replace(/\b(over|under)\b/g, ' ')
    .replace(/[+-]?\d+(\.\d+)?/g, ' ')
    .replace(/[^a-z0-9]+/g, ' ')
    .trim();
}

function countUniqueMarketBooks(sourceMap: Array<{ source?: string | null; sportsbookId?: string | null }> | null | undefined): number {
  const uniqueBooks = new Set(
    (sourceMap || [])
      .map((entry) => String(entry?.sportsbookId || entry?.source || '').trim().toLowerCase())
      .filter(Boolean),
  );
  return uniqueBooks.size;
}

function normalizeMarketBoardPlayer(value: any): string {
  return String(value || '')
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/[^a-z0-9 ]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function normalizeMarketBoardLine(value: any): string {
  const numeric = Number(value);
  return Number.isFinite(numeric) ? String(Math.round(numeric * 1000) / 1000) : '';
}

function buildMarketBoardKey(params: {
  teamSide?: string | null;
  player?: string | null;
  label?: string | null;
  line?: number | null;
}): string {
  const teamSide = String(params.teamSide || '').trim().toLowerCase();
  const player = normalizeMarketBoardPlayer(params.player || '');
  const label = normalizeMarketBoardLabel(params.label);
  const line = normalizeMarketBoardLine(params.line);
  if (!player || !label || !line) return '';
  return `${teamSide}|${player}|${label}|${line}`;
}

function filterSupplementalMarketProps<T extends {
  teamSide?: string | null;
  player?: string | null;
  prop?: string | null;
  line?: number | null;
}>(marketProps: T[], existingProps: Array<Record<string, any>> = []): T[] {
  if (!Array.isArray(marketProps) || marketProps.length === 0) return [];
  if (!Array.isArray(existingProps) || existingProps.length === 0) return marketProps;

  const existingKeys = new Set(
    existingProps
      .map((prop) => buildMarketBoardKey({
        teamSide: prop.teamSide ?? prop.team_side ?? null,
        player: prop.player ?? prop.playerName ?? null,
        label: prop.propType ?? prop.statType ?? prop.prop ?? null,
        line: prop.marketLine ?? prop.marketLineValue ?? prop.market_line_value ?? prop.line ?? null,
      }))
      .filter(Boolean),
  );

  if (existingKeys.size === 0) return marketProps;
  return marketProps.filter((prop) => !existingKeys.has(buildMarketBoardKey(prop)));
}

async function maybeBuildMlbBreakdownMarketProps(params: {
  league: string;
  curated: any;
  side: 'home' | 'away';
  existingProps?: Array<Record<string, any>>;
}): Promise<any[]> {
  if (params.league !== 'mlb') return [];
  const marketProps = await buildMlbBreakdownMarketProps({
    curated: params.curated,
    side: params.side,
  }).catch(() => []);
  return filterSupplementalMarketProps(marketProps, params.existingProps);
}

async function buildSourceBackedPlayerPropMarketBoard(params: {
  curated: any | null;
  team: 'home' | 'away' | null;
  existingProps?: Array<Record<string, any>>;
}): Promise<any[]> {
  if (!params.curated) return [];
  const league = String(params.curated?.league || '').trim().toLowerCase();

  const sides: Array<'home' | 'away'> = params.team ? [params.team] : ['away', 'home'];
  const teamEntries = await Promise.all(sides.map(async (side) => {
    const teamShort = side === 'home'
      ? String(params.curated?.home_short || params.curated?.home_team || '').trim().toUpperCase()
      : String(params.curated?.away_short || params.curated?.away_team || '').trim().toUpperCase();
    const opponentShort = side === 'home'
      ? String(params.curated?.away_short || params.curated?.away_team || '').trim().toUpperCase()
      : String(params.curated?.home_short || params.curated?.home_team || '').trim().toUpperCase();
    const teamName = side === 'home'
      ? String(params.curated?.home_team || teamShort)
      : String(params.curated?.away_team || teamShort);
    const opponentName = side === 'home'
      ? String(params.curated?.away_team || opponentShort)
      : String(params.curated?.home_team || opponentShort);
    const startsAtRaw = params.curated?.starts_at;
    const startsAt = startsAtRaw instanceof Date
      ? startsAtRaw.toISOString()
      : startsAtRaw
        ? new Date(startsAtRaw).toISOString()
        : '';
    if (!teamShort || !opponentShort || !startsAt) return [];

    const marketProps = league === 'mlb'
      ? await buildMlbBreakdownMarketProps({
          curated: params.curated,
          side,
        }).catch(() => [])
      : await fetchTeamPropMarketCandidates({
          league,
          teamShort,
          opponentShort,
          teamName,
          opponentName,
          homeTeam: String(params.curated?.home_team || '').trim(),
          awayTeam: String(params.curated?.away_team || '').trim(),
          startsAt,
        }).then((candidates) => candidates
          .map((candidate) => ({
            player: resolveCanonicalName(candidate.player, league) || candidate.player || null,
            prop: sanitizePublicTextBlock(
              String(
                candidate.propLabel
                || getPlayerPropLabelForLeague(league, candidate.normalizedStatType || candidate.statType)
                || candidate.statType
                || '',
              ),
              { totalBrandMentions: 0, warnings: [] },
              'source-market-prop.prop',
            ) || null,
            marketType: candidate.normalizedStatType || candidate.statType || null,
            line: candidate.marketLineValue ?? null,
            overOdds: candidate.overOdds ?? null,
            underOdds: candidate.underOdds ?? null,
            availableSides: Array.isArray(candidate.availableSides) ? candidate.availableSides : [],
            completenessStatus: candidate.completenessStatus || null,
            source: candidate.source || null,
            books: countUniqueMarketBooks(candidate.sourceMap),
            playerRole: null,
          }))
          .filter((item) => item.player && item.prop && item.line != null && (item.overOdds != null || item.underOdds != null))
          .sort((a, b) => {
            const sideDelta = (b.availableSides?.length || 0) - (a.availableSides?.length || 0);
            if (sideDelta !== 0) return sideDelta;
            const bookDelta = (b.books || 0) - (a.books || 0);
            if (bookDelta !== 0) return bookDelta;
            return String(a.player).localeCompare(String(b.player));
          })
          .slice(0, 12))
        .catch(() => []);
    return marketProps.map((prop) => ({
      ...prop,
      team: teamShort || null,
      teamSide: side,
    }));
  }));

  return filterSupplementalMarketProps(teamEntries.flat(), params.existingProps);
}

function isPoisonedLegacyTeamPropEntry(prop: any): boolean {
  if (!prop || typeof prop !== 'object') return false;

  const modelContext = prop.model_context || prop.modelContext || null;
  const projectionBasis = String(
    modelContext?.projection_basis
    ?? modelContext?.projectionBasis
    ?? '',
  )
    .trim()
    .toLowerCase();
  if (projectionBasis === 'source_market_fallback') {
    return true;
  }

  const reasoning = String(prop.reasoning || '').toLowerCase();
  return reasoning.includes('fallback is anchored to an exact source-backed market');
}

function hasPoisonedLegacyTeamProps(propsResult: any): boolean {
  if (!propsResult || typeof propsResult !== 'object') return false;

  const items = [
    ...(Array.isArray(propsResult.props) ? propsResult.props : []),
    ...(Array.isArray(propsResult.playerProps) ? propsResult.playerProps : []),
    ...(Array.isArray(propsResult.recommendations) ? propsResult.recommendations : []),
  ];
  return items.some((prop) => isPoisonedLegacyTeamPropEntry(prop));
}

function countLegacyTeamProps(propsResult: any): number {
  if (!propsResult || typeof propsResult !== 'object') return 0;
  const propsCount = Array.isArray(propsResult.props)
    ? propsResult.props.length
    : Array.isArray(propsResult.recommendations)
      ? propsResult.recommendations.length
      : 0;
  const playerPropsCount = Array.isArray(propsResult.playerProps) ? propsResult.playerProps.length : 0;
  return propsCount + playerPropsCount;
}

function readLegacyTeamPropsSuppressedReason(propsResult: any): string | null {
  if (!propsResult || typeof propsResult !== 'object') return null;
  return propsResult?.metadata?.suppressed_reason || propsResult?.metadata?.mlb_suppressed_reason || null;
}

function shouldRegenerateLegacyTeamProps(params: {
  league: string | null | undefined;
  propsResult: any;
  latestPrecomputedStatus: string | null;
}): boolean {
  const normalizedPropsResult = normalizeLegacyTeamPropsResponse(params.propsResult, params.league);
  if (!normalizedPropsResult || typeof normalizedPropsResult !== 'object') return true;
  if (hasPoisonedLegacyTeamProps(normalizedPropsResult)) return true;

  if (countLegacyTeamProps(normalizedPropsResult) > 0) return false;
  if (readLegacyTeamPropsSuppressedReason(normalizedPropsResult)) return false;

  const latestStatus = String(params.latestPrecomputedStatus || '').toUpperCase();
  const isMlb = String(params.league || '').toLowerCase() === 'mlb';
  const publishableCandidateCount = Number(normalizedPropsResult?.metadata?.mlb_publishable_candidate_count || 0);

  if (isMlb && publishableCandidateCount > 0) return true;
  return latestStatus !== 'ACTIVE';
}

async function loadOrRegenerateLegacyTeamPropsForSide(params: {
  eventId: string;
  curated: any;
  team: 'home' | 'away';
}): Promise<any> {
  const teamName = params.team === 'home' ? params.curated.home_team : params.curated.away_team;
  const teamShort = params.team === 'home' ? params.curated.home_short : params.curated.away_short;
  const opponentName = params.team === 'home' ? params.curated.away_team : params.curated.home_team;
  const opponentShort = params.team === 'home' ? params.curated.away_short : params.curated.home_short;

  const { rows: cachedProps } = await pool.query(
    'SELECT * FROM rm_team_props_cache WHERE event_id = $1 AND team = $2',
    [params.eventId, params.team],
  ).catch(() => ({ rows: [] as any[] }));
  const { rows: precomputedProps } = await pool.query(
    `SELECT status, generated_at
     FROM rm_forecast_precomputed
     WHERE event_id = $1
       AND forecast_type = 'TEAM_PROPS'
       AND team_side = $2
     ORDER BY generated_at DESC
     LIMIT 1`,
    [params.eventId, params.team],
  ).catch(() => ({ rows: [] as any[] }));

  const latestPrecomputedStatus = precomputedProps[0]?.status || null;
  const cachedPropsPayload = cachedProps[0]?.props_data || null;
  const shouldRegenerateCachedProps = shouldRegenerateLegacyTeamProps({
    league: params.curated.league,
    propsResult: cachedPropsPayload,
    latestPrecomputedStatus,
  });

  let propsResult = cachedPropsPayload
    ? normalizeLegacyTeamPropsResponse(cachedPropsPayload, params.curated.league)
    : null;

  if (!propsResult || shouldRegenerateCachedProps) {
    propsResult = normalizeLegacyTeamPropsResponse(await generateTeamProps({
      teamName,
      teamShort: teamShort || '',
      opponentName,
      opponentShort: opponentShort || '',
      league: params.curated.league || '',
      isHome: params.team === 'home',
      startsAt: params.curated.starts_at,
      moneyline: params.curated.moneyline || { home: null, away: null },
      spread: params.curated.spread || { home: null, away: null },
      total: params.curated.total || { over: null, under: null },
    }), params.curated.league);

    await pool.query(
      `INSERT INTO rm_team_props_cache (event_id, team, team_name, team_short, league, props_data)
       VALUES ($1, $2, $3, $4, $5, $6)
       ON CONFLICT (event_id, team) DO UPDATE SET props_data = EXCLUDED.props_data`,
      [params.eventId, params.team, teamName, teamShort, params.curated.league, JSON.stringify(propsResult)],
    ).catch(() => ({ rows: [] as any[] }));

    if (countLegacyTeamProps(propsResult) > 0) {
      await upsertGeneratedTeamPropsAssets({
        eventId: params.eventId,
        league: params.curated.league || '',
        teamName,
        teamShort,
        teamSide: params.team,
        startsAt: params.curated.starts_at,
        propsResult,
      });
    }
  }

  return propsResult;
}

async function backfillMissingMlbPlayerProps(params: {
  eventId: string;
  team?: 'home' | 'away' | null;
}): Promise<{ wroteAssets: boolean; curated: any | null }> {
  const curated = await getCuratedEvent(params.eventId);
  if (!curated || String(curated.league || '').toLowerCase() !== 'mlb') {
    return { wroteAssets: false, curated };
  }

  const startsAt = curated.starts_at ? new Date(curated.starts_at) : null;
  if (!startsAt || Number.isNaN(startsAt.getTime()) || startsAt.getTime() <= Date.now()) {
    return { wroteAssets: false, curated };
  }

  const requestedTeams: Array<'home' | 'away'> = params.team ? [params.team] : ['home', 'away'];
  let wroteAssets = false;

  for (const team of requestedTeams) {
    const propsResult = await loadOrRegenerateLegacyTeamPropsForSide({
      eventId: params.eventId,
      curated,
      team,
    });

    if (countLegacyTeamProps(propsResult) === 0) continue;
    wroteAssets = true;
  }

  return { wroteAssets, curated };
}

async function refreshStaleMlbPlayerPropsOnRead(params: {
  eventId: string;
  team?: 'home' | 'away' | null;
  rows: any[];
}): Promise<{ wroteAssets: boolean; curated: any | null }> {
  if (params.rows.length === 0) {
    return { wroteAssets: false, curated: null };
  }

  const league = String(params.rows[0]?.league || '').toLowerCase();
  if (league !== 'mlb') {
    return { wroteAssets: false, curated: null };
  }

  const servedOnlyStaleRows = params.rows.every((row: any) => String(row.status || '').toUpperCase() === 'STALE');
  if (!servedOnlyStaleRows) {
    return { wroteAssets: false, curated: null };
  }

  return backfillMissingMlbPlayerProps({ eventId: params.eventId, team: params.team });
}

async function buildLegacyTeamPropsUnlockResponse(params: {
  userId: string;
  eventId: string;
  team: 'home' | 'away';
  league: string | null | undefined;
  propsResult: any;
}): Promise<any> {
  const normalizedPropsResult = normalizeLegacyTeamPropsResponse(params.propsResult, params.league);
  const balance = await getPickBalance(params.userId);
  const normalizedLegacyProps = Array.isArray(normalizedPropsResult?.props)
    ? normalizedPropsResult.props.map((prop: any) => normalizeLegacyTeamPropItem(prop, params.league))
    : [];

  let playerProps: any[] = [];
  try {
    const { rows: ppRows } = await pool.query(
      `SELECT player_name, forecast_payload, confidence_score
       FROM rm_forecast_precomputed
       WHERE event_id = $1 AND forecast_type = 'PLAYER_PROP' AND team_side = $2 AND status = 'ACTIVE'
       ORDER BY confidence_score DESC NULLS LAST`,
      [params.eventId, params.team]
    );
    playerProps = ppRows.map((r: any) => ({
      player: r.player_name,
      prop: r.forecast_payload?.prop
        ? sanitizePublicTextBlock(String(r.forecast_payload.prop), { totalBrandMentions: 0, warnings: [] }, `dual-read-player-prop:${r.id}.prop`)
        : null,
      recommendation: r.forecast_payload?.recommendation || null,
      reasoning: r.forecast_payload?.reasoning || null,
      edge: r.forecast_payload?.edge || null,
      prob: r.forecast_payload?.prob || null,
      ...buildPlayerPropTaxonomy(r.forecast_payload),
      confidence: r.confidence_score,
      modelContext: camelizeObjectKeys(r.forecast_payload?.model_context),
      ...(((params.league || '').toLowerCase() === 'mlb')
        ? { mlbPropContext: serializeMlbPropContext(r.forecast_payload?.model_context, params.league, true) }
        : {}),
    }));
  } catch {
    // Non-fatal — precomputed rows may not exist yet
  }

  if (typeof normalizedPropsResult === 'object' && normalizedPropsResult !== null) {
    return {
      ...normalizedPropsResult,
      props: normalizedLegacyProps,
      suppressed_reason:
        normalizedPropsResult?.metadata?.suppressed_reason
        || normalizedPropsResult?.metadata?.mlb_suppressed_reason
        || null,
      playerProps: playerProps.length > 0 ? playerProps : normalizedLegacyProps,
      forecastBalance: computeTotalBalance(balance),
      forecast_balance: computeTotalBalance(balance),
    };
  }

  return {
    data: normalizedPropsResult,
    playerProps,
    forecastBalance: computeTotalBalance(balance),
    forecast_balance: computeTotalBalance(balance),
  };
}

/** Lookup curated event from rm_events table */
async function getCuratedEvent(eventId: string): Promise<any | null> {
  const { rows } = await pool.query(
    'SELECT * FROM rm_events WHERE event_id = $1',
    [eventId]
  ).catch(() => ({ rows: [] }));
  return rows[0] || null;
}

async function getGameInjuriesForEvent(curated: any | null): Promise<any[]> {
  if (!curated?.league || !curated?.home_team || !curated?.away_team) return [];

  const league = String(curated.league).toLowerCase();
  const homeTeam = String(curated.home_team || '');
  const awayTeam = String(curated.away_team || '');
  const homeShort = String(curated.home_short || '');
  const awayShort = String(curated.away_short || '');

  const injuryStatuses = ['out', 'ir', 'suspension', 'doubtful', 'questionable', 'day-to-day', 'gtd', 'dtd'];

  const [injuries, scInjuries, activePlayers] = await Promise.all([
    pool.query(
      `SELECT "playerName", team, position, status, "injuryType", description, source, "reportedAt"
       FROM "PlayerInjury"
       WHERE LOWER(league) = $1
         AND (team ILIKE $2 OR team ILIKE $3 OR team ILIKE $4 OR team ILIKE $5)
         AND LOWER(status) = ANY($6::text[])
       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 || '', injuryStatuses],
    ).catch(() => ({ rows: [] as any[] })),
    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 LOWER(league) = $1
         AND (team ILIKE $2 OR team ILIKE $3 OR team ILIKE $4 OR team ILIKE $5)
         AND LOWER(status) = ANY($6::text[])
         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 || '', injuryStatuses],
    ).catch(() => ({ rows: [] as any[] })),
    pool.query(
      `SELECT "playerName", team
       FROM "PlayerInjury"
       WHERE LOWER(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: [] as any[] })),
  ]);

  const activeSet = new Set(
    activePlayers.rows.map((row: any) => `${String(row.playerName || '').toLowerCase()}-${String(row.team || '').toLowerCase()}`),
  );

  const merged = [];
  const seen = new Set<string>();
  for (const row of [...scInjuries.rows, ...injuries.rows]) {
    const key = `${String(row.playerName || '').toLowerCase()}-${String(row.team || '').toLowerCase()}`;
    if (seen.has(key) || activeSet.has(key)) continue;
    seen.add(key);
    merged.push({
      playerName: row.playerName || null,
      team: row.team || null,
      position: row.position || null,
      status: row.status || null,
      injuryType: row.injuryType || null,
      description: row.description || null,
      source: row.source || null,
      reportedAt: row.reportedAt || null,
    });
  }

  return merged;
}

/** Try SGO API for live odds */
async function fetchSgoEvent(eventId: string, league: string): Promise<any | null> {
  try {
    if (!league || !LEAGUE_MAP[league]) return null;
    const events = await fetchEvents(league);
    return events.find((e) => e.eventID === eventId) || null;
  } catch {
    return null;
  }
}

async function resolveCachedForecastForRequest(params: {
  eventId: string;
  curated: any | null;
  sgoEvent: any | null;
}): Promise<any | null> {
  let cached = await getCachedForecast(params.eventId);
  if (cached) return cached;

  const homeTeam = params.curated?.home_team || params.sgoEvent?.teams?.home?.names?.long || null;
  const awayTeam = params.curated?.away_team || params.sgoEvent?.teams?.away?.names?.long || null;
  const startsAt = params.curated?.starts_at || params.sgoEvent?.status?.startsAt || null;

  if (!homeTeam || !awayTeam || !startsAt) return null;
  return getCachedForecastByTeams(homeTeam, awayTeam, startsAt);
}

/** Helper: compute total balance from PickBalance */
function computeTotalBalance(balance: {
  single_picks: number;
  signup_bonus_forecasts?: number;
  survey_bonus_forecasts?: number;
  daily_pass_picks: number;
  daily_pass_valid: boolean;
  daily_free_forecasts: number;
}): number {
  return (balance.daily_free_forecasts || 0)
    + (balance.daily_pass_valid ? balance.daily_pass_picks : 0)
    + (balance.signup_bonus_forecasts || 0)
    + (balance.survey_bonus_forecasts || 0)
    + balance.single_picks;
}

async function filterSiblingSuppressedRows(rows: any[], eventId: string): Promise<any[]> {
  if (!PI_SIBLING_SUPPRESSION_ENABLED() || rows.length === 0) return rows;
  try {
    const { rows: suppressedRows } = await pool.query(
      `SELECT conflict_payload
       FROM pi_prop_eligibility
       WHERE game_id = $1
         AND publish_allowed = false
         AND suppress_reason = 'SUPPRESSED_SIBLING'`,
      [eventId]
    ).catch(() => ({ rows: [] }));

    if (suppressedRows.length === 0) return rows;
    const suppressedIds = new Set(
      suppressedRows
        .map((row: any) => {
          try {
            const payload = typeof row.conflict_payload === 'string'
              ? JSON.parse(row.conflict_payload)
              : row.conflict_payload;
            return payload?.suppressedAssetId;
          } catch {
            return null;
          }
        })
        .filter(Boolean)
    );
    if (suppressedIds.size === 0) return rows;
    return rows.filter((row: any) => !suppressedIds.has(row.id));
  } catch {
  return rows.filter((row: any) => passesPlayerPropConfidenceGate(row));
}
}

/** Get insight availability and user's unlocked insights for an event */
async function getInsightMeta(eventId: string, userId: string | null, forecastData: any, league?: string): Promise<{
  insightAvailability: { steam: boolean; sharp: boolean; dvp: boolean; hcw: boolean };
  unlockedInsights: string[];
}> {
  let dvpAvailable = false;
  if (DVP_UNLOCK_ENABLED() && league) {
    const cacheKey = league.toLowerCase();
    const cached = DVP_AVAILABILITY_CACHE.get(cacheKey);
    if (cached && cached.expiresAt > Date.now()) {
      dvpAvailable = cached.available;
    } else {
      dvpAvailable = await hasDvpData(league);
      DVP_AVAILABILITY_CACHE.set(cacheKey, {
        available: dvpAvailable,
        expiresAt: Date.now() + DVP_AVAILABILITY_CACHE_TTL_MS,
      });
    }
  }
  const hcwAvailable = HCW_UNLOCK_ENABLED() && !!league && hasHcwData(league);

  const insightAvailability = {
    // Unlockability is feature-gated here. Event-card signal badges use stricter
    // heuristics separately, but the forecast modal should expose any insight that
    // the backend can actually generate on demand.
    steam: STEAM_UNLOCK_ENABLED(),
    sharp: SHARP_UNLOCK_ENABLED(),
    dvp: dvpAvailable,
    hcw: hcwAvailable,
  };

  let unlockedInsights: string[] = [];
  if (userId) {
    const { rows } = await pool.query(
      'SELECT insight_type FROM rm_insight_unlocks WHERE user_id = $1 AND event_id = $2',
      [userId, eventId]
    ).catch(() => ({ rows: [] }));
    unlockedInsights = rows.map((r: any) => r.insight_type);
  }

  return { insightAvailability, unlockedInsights };
}

function computeBenchmarkEdge(
  forecastData: any,
  odds: any,
): number | null {
  const explicitSpreadEdge =
    typeof forecastData?.spread_edge === 'number'
      ? forecastData.spread_edge
      : typeof forecastData?.spreadEdge === 'number'
        ? forecastData.spreadEdge
        : null;
  if (explicitSpreadEdge != null) return explicitSpreadEdge;

  const projectedMargin = Number(forecastData?.projected_margin ?? forecastData?.projectedMargin);
  const marketHomeSpread = Number(
    odds?.spread?.home?.line
      ?? (typeof odds?.spread?.home === 'number' ? odds.spread.home : null)
      ?? null,
  );
  if (Number.isFinite(projectedMargin) && Number.isFinite(marketHomeSpread)) {
    return Math.round(Math.abs(projectedMargin + marketHomeSpread) * 10) / 10;
  }

  return null;
}

function buildSafePreviewSummary(params: {
  homeTeam: string;
  awayTeam: string;
  league?: string | null;
}): string {
  const leagueLabel = String(params.league || '').trim().toUpperCase();
  const matchup = params.homeTeam && params.awayTeam
    ? `${params.awayTeam} at ${params.homeTeam}`
    : 'This matchup';

  return leagueLabel
    ? `${matchup} has a live ${leagueLabel} forecast with market context, matchup notes, and paid signal modules behind the unlock.`
    : `${matchup} has a live forecast with market context, matchup notes, and paid signal modules behind the unlock.`;
}

function createRouteError(status: number, message: string): Error & { status: number } {
  const error = new Error(message) as Error & { status: number };
  error.status = status;
  return error;
}

function leagueToRainwireSport(league: string | null | undefined): string {
  const key = String(league || '').trim().toLowerCase();
  return ['epl', 'la_liga', 'bundesliga', 'serie_a', 'ligue_1', 'champions_league', 'mls'].includes(key)
    ? 'soccer'
    : key;
}

function buildNewsSearchTerms(teamName: string, teamShort: string, league?: string | null): string[] {
  const terms = new Set<string>();
  addTeamSearchVariant(terms, teamName);
  addTeamSearchVariant(terms, teamShort);
  if (isSoccerLeague(league)) {
    addSoccerNewsAliasVariants(terms, teamName, teamShort);
  }
  return [...terms].filter(Boolean);
}

function countNewsTeamMentions(text: string, teamName: string, teamShort: string, league?: string | null): number {
  const normalized = ` ${String(text || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ')} `;
  return buildNewsSearchTerms(teamName, teamShort, league).reduce((score, term) => (
    normalized.includes(` ${term} `) ? score + 1 : score
  ), 0);
}

function isStructuredBlogEventMatch(item: any, curated: any): boolean {
  if (String(item?.type || '').toLowerCase() !== 'blog') return false;

  const itemHomeTeam = String(item?.home_team || '').trim();
  const itemAwayTeam = String(item?.away_team || '').trim();
  const curatedHomeTeam = String(curated?.home_team || '').trim();
  const curatedAwayTeam = String(curated?.away_team || '').trim();

  if (!itemHomeTeam || !itemAwayTeam || !curatedHomeTeam || !curatedAwayTeam) return false;

  const teamScore = scoreTeamNamedRow(
    { home_team: itemHomeTeam, away_team: itemAwayTeam },
    curatedHomeTeam,
    curatedAwayTeam,
  );
  if (teamScore < 6) return false;

  const blogDateKey = item?.game_date ? getEtDateKey(item.game_date) : null;
  const eventDateKey = curated?.starts_at ? getEtDateKey(curated.starts_at) : null;
  if (blogDateKey && eventDateKey && blogDateKey !== eventDateKey) return false;

  return true;
}

function getEventNewsWindowBounds(curated: any): { lowerBoundMs: number; upperBoundMs: number } | null {
  const eventStartsAt = curated?.starts_at ? new Date(curated.starts_at) : null;
  if (!eventStartsAt || Number.isNaN(eventStartsAt.getTime())) return null;

  const lookbackHours = isSoccerLeague(curated?.league) ? 14 * 24 : 72;
  return {
    lowerBoundMs: eventStartsAt.getTime() - (lookbackHours * 60 * 60 * 1000),
    upperBoundMs: eventStartsAt.getTime() + (12 * 60 * 60 * 1000),
  };
}

function isEventNewsItemWithinWindow(item: any, curated: any): boolean {
  const eventStartsAt = curated?.starts_at ? new Date(curated.starts_at) : null;
  if (!eventStartsAt || Number.isNaN(eventStartsAt.getTime())) return true;

  const eventDateKey = getEtDateKey(eventStartsAt.toISOString());
  const gameDateKey = item?.game_date ? getEtDateKey(item.game_date) : null;
  if (gameDateKey && eventDateKey) {
    return gameDateKey === eventDateKey;
  }

  const publishedAt = item?.published_at ? new Date(item.published_at) : null;
  if (!publishedAt || Number.isNaN(publishedAt.getTime())) return true;

  const windowBounds = getEventNewsWindowBounds(curated);
  if (!windowBounds) return true;

  const publishedAtMs = publishedAt.getTime();
  return publishedAtMs >= windowBounds.lowerBoundMs && publishedAtMs <= windowBounds.upperBoundMs;
}

function buildEventNewsScore(item: any, curated: any): number {
  const awayTeam = String(curated?.away_team || '');
  const homeTeam = String(curated?.home_team || '');
  const awayShort = String(curated?.away_short || '');
  const homeShort = String(curated?.home_short || '');
  const league = String(curated?.league || '');
  const haystack = [
    item?.custom_headline,
    item?.custom_summary,
    item?.title,
    item?.description,
  ]
    .filter(Boolean)
    .join(' ')
    .replace(/\s+/g, ' ');

  const awayScore = countNewsTeamMentions(haystack, awayTeam, awayShort, league);
  const homeScore = countNewsTeamMentions(haystack, homeTeam, homeShort, league);
  const exactStructuredMatch = isStructuredBlogEventMatch(item, curated);
  const inWindow = isEventNewsItemWithinWindow(item, curated);
  const bothTeamsMentioned = awayScore > 0 && homeScore > 0;
  const publishedDateKey = item?.published_at ? getEtDateKey(item.published_at) : null;
  const eventDateKey = curated?.starts_at ? getEtDateKey(curated.starts_at) : null;
  const sameEtDate = Boolean(publishedDateKey && eventDateKey && publishedDateKey === eventDateKey);

  if (!exactStructuredMatch && !inWindow) return 0;
  if (!exactStructuredMatch && awayScore <= 0 && homeScore <= 0) return 0;

  let score = 0;
  if (exactStructuredMatch) score += 100;
  if (bothTeamsMentioned) score += 20;
  else if (awayScore > 0 || homeScore > 0) score += 5;
  if (sameEtDate) score += 3;
  score += awayScore + homeScore;

  return score;
}

function isTimelyEventNews(item: any, curated: any): boolean {
  const publishedAt = item?.published_at ? new Date(item.published_at).getTime() : NaN;
  const windowBounds = getEventNewsWindowBounds(curated);

  if (!Number.isFinite(publishedAt) || !windowBounds) return true;
  return publishedAt >= windowBounds.lowerBoundMs && publishedAt <= windowBounds.upperBoundMs;
}

function isOlderSoccerEventNews(item: any, curated: any): boolean {
  if (!isSoccerLeague(curated?.league)) return false;
  const publishedAt = item?.published_at ? new Date(item.published_at).getTime() : NaN;
  const startsAt = curated?.starts_at ? new Date(curated.starts_at).getTime() : NaN;
  if (!Number.isFinite(publishedAt) || !Number.isFinite(startsAt)) return false;
  return (startsAt - publishedAt) > (4 * 24 * 60 * 60 * 1000);
}

function filterRainwireNewsForCuratedEvent(items: any[], curated: any): any[] {
  return items
    .map((item) => {
      const score = buildEventNewsScore(item, curated);
      const exactStructuredMatch = isStructuredBlogEventMatch(item, curated);
      const league = String(curated?.league || '');
      const haystack = [
        item?.custom_headline,
        item?.custom_summary,
        item?.title,
        item?.description,
      ]
        .filter(Boolean)
        .join(' ')
        .replace(/\s+/g, ' ');
      const headlineHaystack = [
        item?.custom_headline,
        item?.title,
      ]
        .filter(Boolean)
        .join(' ')
        .replace(/\s+/g, ' ');
      const awayScore = countNewsTeamMentions(
        haystack,
        String(curated?.away_team || ''),
        String(curated?.away_short || ''),
        league,
      );
      const homeScore = countNewsTeamMentions(
        haystack,
        String(curated?.home_team || ''),
        String(curated?.home_short || ''),
        league,
      );
      const headlineAwayScore = countNewsTeamMentions(
        headlineHaystack,
        String(curated?.away_team || ''),
        String(curated?.away_short || ''),
        league,
      );
      const headlineHomeScore = countNewsTeamMentions(
        headlineHaystack,
        String(curated?.home_team || ''),
        String(curated?.home_short || ''),
        league,
      );
      return {
        item,
        score,
        exactStructuredMatch,
        timely: exactStructuredMatch || isTimelyEventNews(item, curated),
        bothTeamsMentioned: awayScore > 0 && homeScore > 0,
        headlineTeamMentioned: headlineAwayScore > 0 || headlineHomeScore > 0,
        isOlderSoccer: isOlderSoccerEventNews(item, curated),
      };
    })
    .filter((entry) => {
      if (!entry.timely || entry.score <= 0) return false;
      if (entry.exactStructuredMatch || !entry.isOlderSoccer) return true;
      return entry.bothTeamsMentioned || (entry.headlineTeamMentioned && entry.score >= 7);
    })
    .sort((left, right) => {
      if (left.exactStructuredMatch !== right.exactStructuredMatch) {
        return left.exactStructuredMatch ? -1 : 1;
      }
      return right.score - left.score;
    })
    .slice(0, 4)
    .map((entry) => entry.item);
}

function buildEventNewsQueryTerms(curated: any): string[] {
  const terms = new Set<string>();
  for (const value of [
    ...buildNewsSearchTerms(String(curated?.home_team || ''), String(curated?.home_short || ''), String(curated?.league || '')),
    ...buildNewsSearchTerms(String(curated?.away_team || ''), String(curated?.away_short || ''), String(curated?.league || '')),
  ]) {
    const normalized = normalizeSearchText(value);
    if (!normalized || normalized.length < 3) continue;
    terms.add(normalized);
  }
  return [...terms];
}

async function fetchRainwireNewsItemsForCuratedEvent(curated: any, limit = 24): Promise<any[]> {
  const sport = leagueToRainwireSport(curated?.league);
  const startsAt = curated?.starts_at || null;
  const windowBounds = getEventNewsWindowBounds(curated);
  const publishedAfter = windowBounds ? new Date(windowBounds.lowerBoundMs).toISOString() : null;
  const publishedBefore = windowBounds ? new Date(windowBounds.upperBoundMs).toISOString() : null;
  const likePatterns = buildEventNewsQueryTerms(curated).map((term) => `%${term}%`);
  const normalizedHomeKey = normalizeTeamNameKey(curated?.home_team);
  const normalizedAwayKey = normalizeTeamNameKey(curated?.away_team);
  const queryLimit = isSoccerLeague(curated?.league)
    ? Math.max(limit * 4, 96)
    : limit;

  if (!sport || likePatterns.length === 0) return [];

  const newsQuery = `
    SELECT * FROM (
      SELECT
        id::text, title, url, source, source_display, sport, description,
        is_featured, is_breaking, trending_score, clicks AS engagement,
        published_at, 'external' AS type, NULL AS slug,
        image_url, custom_headline, custom_summary,
        engagement_score, celebrity_names, source_type, is_curated,
        NULL::text AS home_team, NULL::text AS away_team, NULL::timestamptz AS game_date
      FROM rm_news_links
      WHERE expires_at > NOW()
        AND sport = $1
        AND (
          $3::timestamptz IS NULL
          OR $4::timestamptz IS NULL
          OR published_at BETWEEN $3::timestamptz AND $4::timestamptz
        )
        AND EXISTS (
          SELECT 1
          FROM unnest($2::text[]) AS pattern
          WHERE LOWER(
            CONCAT_WS(' ',
              COALESCE(title, ''),
              COALESCE(description, ''),
              COALESCE(custom_headline, ''),
              COALESCE(custom_summary, '')
            )
          ) LIKE pattern
        )

      UNION ALL

      SELECT
        id::text, title,
        '/rain-wire/' || sport || '/' || slug AS url,
        'rainmaker' AS source, 'Rainmaker' AS source_display, sport,
        excerpt AS description,
        COALESCE(is_featured, FALSE) AS is_featured, FALSE AS is_breaking,
        CASE WHEN COALESCE(published_at, created_at) > NOW() - INTERVAL '24 hours' THEN 1500 ELSE 500 END AS trending_score, views AS engagement,
        COALESCE(published_at, created_at) AS published_at,
        'blog' AS type, slug, NULL AS image_url,
        NULL AS custom_headline, NULL AS custom_summary,
        0 AS engagement_score, NULL AS celebrity_names,
        'blog' AS source_type, FALSE AS is_curated,
        home_team, away_team, game_date
      FROM rm_blog_posts
      WHERE status = 'published'
        AND sport = $1
        AND (
          (
            game_date IS NOT NULL
            AND DATE(game_date) = DATE($5::timestamptz)
            AND (
              (${teamNameKeySql('home_team')} = $6 AND ${teamNameKeySql('away_team')} = $7)
              OR (${teamNameKeySql('home_team')} = $7 AND ${teamNameKeySql('away_team')} = $6)
            )
          )
          OR EXISTS (
            SELECT 1
            FROM unnest($2::text[]) AS pattern
            WHERE LOWER(
              CONCAT_WS(' ',
                COALESCE(title, ''),
                COALESCE(excerpt, ''),
                COALESCE(home_team, ''),
                COALESCE(away_team, '')
              )
            ) LIKE pattern
          )
        )
    ) AS combined
    ORDER BY
      CASE WHEN type = 'blog' AND game_date IS NOT NULL AND DATE(game_date) = DATE($5::timestamptz) THEN 0 ELSE 1 END ASC,
      is_breaking DESC,
      is_curated DESC,
      engagement_score DESC NULLS LAST,
      trending_score DESC NULLS LAST,
      published_at DESC
    LIMIT $8
  `;

  const { rows } = await pool.query(newsQuery, [
    sport,
    likePatterns,
    publishedAfter,
    publishedBefore,
    startsAt,
    normalizedHomeKey,
    normalizedAwayKey,
    queryLimit,
  ]);
  return rows;
}

async function buildDigilanderNewsModuleWithDiagnostics(curated: any): Promise<{
  module: DigilanderModuleResponse<any>;
  fallbackUsage: {
    source: string;
    fallbackUsed: boolean;
    itemCount: number;
  };
}> {
  const cacheKey = getDigilanderNewsCacheKey(curated);
  const cachedItems = readDigilanderNewsCache(cacheKey);
  if (cachedItems) {
    return {
      module: cachedItems.length > 0
        ? digilanderModuleOk({ items: cachedItems }, cachedItems.length)
        : digilanderModuleEmpty('No matchup-specific news hits'),
      fallbackUsage: buildDigilanderNewsFallbackUsage('fresh_cache', cachedItems.length),
    };
  }

  type DigilanderNewsFetchResult =
    | { status: 'ok'; items: any[] }
    | { status: 'error'; error: unknown }
    | { status: 'timeout' };

  const guardedResult = await withTimeout<DigilanderNewsFetchResult>(
    fetchRainwireNewsItemsForCuratedEvent(curated, 32)
      .then((items) => ({ status: 'ok' as const, items }))
      .catch((error) => ({ status: 'error' as const, error })),
    DIGILANDER_NEWS_TIMEOUT_MS,
    { status: 'timeout' as const },
  );

  if (guardedResult.status === 'ok') {
    const filteredItems = filterRainwireNewsForCuratedEvent(guardedResult.items, curated);
    writeDigilanderNewsCache(cacheKey, filteredItems);
    return {
      module: filteredItems.length > 0
        ? digilanderModuleOk({ items: filteredItems }, filteredItems.length)
        : digilanderModuleEmpty('No matchup-specific news hits'),
      fallbackUsage: buildDigilanderNewsFallbackUsage('live', filteredItems.length),
    };
  }

  const staleItems = readDigilanderNewsCache(cacheKey, { allowStale: true });
  if (staleItems) {
    return {
      module: staleItems.length > 0
        ? digilanderModuleOk({ items: staleItems }, staleItems.length)
        : digilanderModuleEmpty('No matchup-specific news hits'),
      fallbackUsage: buildDigilanderNewsFallbackUsage('stale_cache', staleItems.length),
    };
  }

  if (guardedResult.status === 'timeout') {
    return {
      module: digilanderModuleFailed('News timed out'),
      fallbackUsage: buildDigilanderNewsFallbackUsage('timeout'),
    };
  }
  return {
    module: digilanderModuleFailed('News failed'),
    fallbackUsage: buildDigilanderNewsFallbackUsage('error'),
  };
}

async function buildDigilanderNewsModule(curated: any): Promise<DigilanderModuleResponse<any>> {
  return (await buildDigilanderNewsModuleWithDiagnostics(curated)).module;
}

async function buildForecastPreviewBody(eventId: string): Promise<any> {
  let cached = await getCachedForecast(eventId);
  const curated = await getCuratedEvent(eventId);
  if (!cached && curated) {
    cached = await getCachedForecastByTeams(curated.home_team, curated.away_team, curated.starts_at);
  }
  if (!cached) {
    throw createRouteError(404, 'No forecast available');
  }
  const previewLeague = curated?.league || cached.league || '';
  const previewHomeTeam = curated?.home_team || cached.home_team || '';
  const previewAwayTeam = curated?.away_team || cached.away_team || '';
  const previewStartsAt = curated?.starts_at || cached.starts_at || null;
  const { homeShort, awayShort } = await resolveEventShortNames({
    eventId,
    homeShort: curated?.home_short || null,
    awayShort: curated?.away_short || null,
    homeTeam: previewHomeTeam,
    awayTeam: previewAwayTeam,
    startsAt: previewStartsAt,
  });
  const curatedOdds = curated ? {
    moneyline: curated.moneyline || { home: null, away: null },
    spread: curated.spread || { home: null, away: null },
    total: curated.total || { over: null, under: null },
  } : null;
  const odds = sanitizeOddsBundle(mergePreviewOdds(curatedOdds, cached.odds_data || null), cached.league || curated?.league || null);
  const insightMeta = await getInsightMeta(eventId, null, cached.forecast_data, previewLeague || undefined);
  const summaryPreview = buildSafePreviewSummary({
    homeTeam: previewHomeTeam,
    awayTeam: previewAwayTeam,
    league: previewLeague,
  });

  return {
    winnerPick: null,
    forecastSide: null,
    projectedWinner: null,
    confidence: null,
    summaryPreview,
    odds,
    modelEdge: null,
    homeTeam: previewHomeTeam,
    awayTeam: previewAwayTeam,
    homeShort: homeShort || '',
    awayShort: awayShort || '',
    league: previewLeague,
    valueRating: null,
    insightAvailability: insightMeta.insightAvailability,
    basemonSummary: null,
  };
}

async function buildForecastGameDataBody(eventId: string): Promise<any> {
  const curated = await getCuratedEvent(eventId);
  if (!curated) {
    throw createRouteError(404, 'Event not found');
  }
  const leagueKey = String(curated.league || '').toLowerCase();

  const injuries = await getGameInjuriesForEvent(curated);
  const eventPiff = getPiffPropsForGame(
    curated.home_short || '',
    curated.away_short || '',
    loadPiffPropsForDate(getEtDateKey(curated.starts_at) || undefined),
    leagueKey,
  );
  const digimonPicks = leagueKey === 'nba'
    ? getDigimonForGame(curated.home_short || '', curated.away_short || '')
    : [];
  let featuredPlayers: Array<{
    player: string;
    team: string | null;
    teamSide: 'home' | 'away' | null;
    playerRole: string | null;
    availability: 'confirmed' | 'projected' | 'active' | 'unknown';
    projectedMinutes: number | null;
    signalStrength: 'COIN_FLIP' | 'FAIR' | 'GOOD' | 'STRONG' | null;
    maxVarianceEdgePct: number | null;
    propCount: number;
  }> = [];
  let lineProps: any[] = [];

  const { rows } = await resolveVisiblePlayerPropRows(eventId, null).catch(() => ({ rows: [] as any[], rawRowCount: 0 }));
  const lineupSnapshot = leagueKey === 'mlb' && rows.length === 0
    ? null
    : await withTimeout(fetchEventLineupSnapshot(curated), 2500, null as PlayerRadarLineupSnapshot | null);
  const liveFallbackRows = leagueKey === 'mlb' && rows.length < MLB_FALLBACK_ROW_THRESHOLD
    ? await withTimeout(fetchMlbLiveFallbackRows({ eventId, curated, lineupSnapshot }), 9000, [] as SyntheticPlayerPropRow[])
    : [];
  const mergedRows = mergePlayerPropRowsWithFallback(rows, liveFallbackRows);
  if (leagueKey === 'mlb' && liveFallbackRows.length > 0) {
    logMlbFallbackUsage({
      eventId,
      mode: 'game_data',
      localRows: rows.length,
      fallbackRows: liveFallbackRows.length,
      mergedRows: mergedRows.length,
    }).catch(() => {});
  }
  const historicalSamples = mergedRows.length > 0
    ? await withTimeout(fetchHistoricalDigiviewSamples({
        league: leagueKey,
        curated,
        rows: mergedRows,
      }), 2500, new Map<string, { last5Samples: HistoricalGameSample[]; last10Samples: HistoricalGameSample[]; h2hSamples: HistoricalGameSample[] }>())
    : new Map<string, { last5Samples: HistoricalGameSample[]; last10Samples: HistoricalGameSample[]; h2hSamples: HistoricalGameSample[] }>();
  if (mergedRows.length > 0) {
    const featuredRows = mergedRows.map((row: any) => {
      const descriptor = describeForecastAsset('PLAYER_PROP', row.forecast_payload);
      return {
        ...row,
        locked: false,
        playerRole: descriptor.playerRole,
      };
    });
    const supplementalFeaturedRows = lineupSnapshot
      ? await withTimeout(buildSupplementalFeaturedPlayerRows({
          eventId,
          curated,
          lineups: lineupSnapshot,
          existingRows: featuredRows,
        }), 2500, [] as any[])
      : [];
    featuredPlayers = buildFeaturedPlayersFromRows(
      [...featuredRows, ...supplementalFeaturedRows],
      {
        lineups: lineupSnapshot,
        limit: 10,
        perTeamTarget: 5,
        homeTeamId: curated?.home_short || curated?.home_team || null,
        awayTeamId: curated?.away_short || curated?.away_team || null,
      },
    ).map((player) => ({
      player: player.player,
      team: player.team || null,
      teamSide: player.teamSide || null,
      playerRole: player.playerRole || null,
      availability: player.availability,
      projectedMinutes: player.projectedMinutes,
      signalStrength: player.signalStrength || null,
      maxVarianceEdgePct: player.maxVarianceEdgePct ?? null,
      propCount: Array.isArray(player.props) ? player.props.length : 0,
    }));

    lineProps = mergedRows
      .map((row: any) => {
        const taxonomy = buildPlayerPropTaxonomy(row.forecast_payload);
        const publicProp = serializePublicPlayerProp({
          assetId: row.id,
          player: row.player_name,
          team: row.team_id,
          teamSide: row.team_side,
          league: row.league,
          prop: row.forecast_payload?.prop || null,
          locked: false,
          confidence: row.confidence_score,
          payload: row.forecast_payload,
          marketType: taxonomy.marketType,
          marketFamily: taxonomy.marketFamily,
          marketOrigin: taxonomy.marketOrigin,
          sourceBacked: taxonomy.sourceBacked,
          playerRole: taxonomy.playerRole,
          includeMlbPropContext: MLB_PROP_CONTEXT_V2(),
        });
        if (!publicProp) return null;
        const digimonPick = findMatchingDigimonPick({
          picks: digimonPicks,
          player: row.player_name,
          team: row.team_id,
          statHint: row.forecast_payload?.stat_type || row.forecast_payload?.normalized_stat_type || row.forecast_payload?.prop,
          line: publicProp.marketLine ?? publicProp.marketLineValue ?? null,
        });
        const piffLeg = findMatchingPiffLeg({
          legs: eventPiff,
          player: row.player_name,
          team: row.team_id,
          statHint: row.forecast_payload?.stat_type || row.forecast_payload?.normalized_stat_type || row.forecast_payload?.prop,
          line: publicProp.marketLine ?? publicProp.marketLineValue ?? null,
        });
        return {
          ...publicProp,
          digiview: buildPublicDigiviewEvidence({
            payload: row.forecast_payload,
            commentary: publicProp.reasoning || null,
            piffLeg,
            history: historicalSamples.get(row.id) || null,
            digimonPick,
          }),
        };
      })
      .filter((row): row is NonNullable<typeof row> => Boolean(row));
  }

  const marketProps = await buildSourceBackedPlayerPropMarketBoard({
    curated,
    team: null,
    existingProps: lineProps,
  }).catch(() => []);

  return {
    eventId,
    league: curated.league || null,
    homeTeam: curated.home_team || null,
    awayTeam: curated.away_team || null,
    homeShort: curated.home_short || null,
    awayShort: curated.away_short || null,
    injuries,
    players: featuredPlayers,
    lineProps,
    marketProps,
  };
}

async function buildForecastInjuriesBody(eventId: string): Promise<any[]> {
  const curated = await getCuratedEvent(eventId);
  if (!curated) {
    throw createRouteError(404, 'Event not found');
  }

  return getGameInjuriesForEvent(curated);
}

async function getForecastMarketHistoryPayload(eventId: string): Promise<{
  body: any;
  fallbackUsage: {
    lineMovementSource: string;
    bookSnapshotSource: string;
    fallbackUsed: boolean;
    recovered: boolean;
  };
}> {
  const curated = await getCuratedEvent(eventId);
  if (!curated) {
    throw createRouteError(404, 'Event not found');
  }

  const league = String(curated.league || '').toLowerCase();
  const emptySummary = buildGameMarketSummary({
    startsAt: curated.starts_at || null,
    lineMovement: [],
    bookSnapshots: [],
  });
  const marketHistory = await withTimeout(fetchGameMarketHistory({
    league,
    startsAt: curated.starts_at || null,
    homeTeam: curated.home_team || null,
    awayTeam: curated.away_team || null,
    homeShort: curated.home_short || null,
    awayShort: curated.away_short || null,
  }), 3000, {
    lineMovement: [] as GameMarketHistoryRow[],
    bookSnapshots: [] as GameBookSnapshotRow[],
    summary: emptySummary,
    diagnostics: {
      fallbackUsage: buildDigilanderMarketFallbackUsage(),
    },
  });

  return {
    body: {
      eventId,
      league: curated.league || null,
      homeTeam: curated.home_team || null,
      awayTeam: curated.away_team || null,
      lineMovement: marketHistory.lineMovement,
      bookSnapshots: marketHistory.bookSnapshots,
      summary: marketHistory.summary,
    },
    fallbackUsage: marketHistory.diagnostics?.fallbackUsage || buildDigilanderMarketFallbackUsage(),
  };
}

async function buildForecastMarketHistoryBody(eventId: string): Promise<any> {
  return (await getForecastMarketHistoryPayload(eventId)).body;
}

async function buildDigilanderTopPicksBody(eventId: string, league: string): Promise<any> {
  const body = await getTodayTopPicks(normalizeTopBoardFilters(league || null, null), 24);
  const entries = Array.isArray(body?.entries) ? body.entries.filter((entry: any) => {
    if (entry?.kind === 'game' && entry?.game) return entry.game.eventId === eventId;
    if (entry?.kind === 'prop' && entry?.prop) return entry.prop.eventId === eventId;
    return false;
  }) : [];
  return { ...body, entries, count: entries.length };
}

async function buildForecastTeamPropsBreakdownBody(eventId: string): Promise<any> {
  const curated = await getCuratedEvent(eventId);
  if (!curated) {
    return { home: null, away: null, available: false };
  }

  const league = String(curated.league || '').toLowerCase();
  const { rows: precomputedRows } = await pool.query(
    `SELECT team_side, player_name, forecast_payload, confidence_score
     FROM rm_forecast_precomputed
     WHERE event_id = $1 AND forecast_type = 'TEAM_PROPS' AND status = 'ACTIVE'
     ORDER BY confidence_score DESC NULLS LAST`,
    [eventId],
  ).catch(() => ({ rows: [] as any[] }));
  const { rows: cacheRows } = await pool.query(
    'SELECT team, props_data FROM rm_team_props_cache WHERE event_id = $1',
    [eventId],
  ).catch(() => ({ rows: [] as any[] }));

  const buildTeamData = async (side: 'home' | 'away') => {
    const teamName = side === 'home' ? curated.home_team : curated.away_team;
    const teamShort = side === 'home' ? curated.home_short : curated.away_short;

    const precomputed = precomputedRows.filter((row: any) => row.team_side === side);
    const hasPoisonedPrecomputed = precomputed.some((row: any) => hasPoisonedLegacyTeamProps(row.forecast_payload));
    if (precomputed.length > 0 && !hasPoisonedPrecomputed) {
      const allProps: any[] = [];
      let suppressedReason: string | null = null;
      for (const row of precomputed) {
        const payload = row.forecast_payload || {};
        const propsArray = Array.isArray(payload.props)
          ? payload.props.filter((prop: any) => shouldRenderTeamPropBundleEntry(prop))
          : [];
        suppressedReason = suppressedReason
          || payload?.suppressed_reason
          || payload?.metadata?.suppressed_reason
          || payload?.metadata?.mlb_suppressed_reason
          || null;
        for (const prop of propsArray) {
          allProps.push({
            player: prop.player || row.player_name || null,
            prop: prop.prop ? sanitizePublicTextBlock(String(prop.prop), { totalBrandMentions: 0, warnings: [] }, `breakdown:${prop.player}`) : null,
            recommendation: prop.recommendation || null,
            reasoning: prop.reasoning ? sanitizePublicTextBlock(String(prop.reasoning), { totalBrandMentions: 0, warnings: [] }, `breakdown-reason:${prop.player}`) : null,
            edge: prop.edge ?? prop.edge_pct ?? null,
            prob: prop.prob ?? prop.projected_probability ?? null,
            line: prop.market_line_value ?? prop.line ?? null,
            odds: prop.odds ?? null,
            projectedOutcome: prop.projected_stat_value ?? prop.projectedOutcome ?? null,
            confidence: prop.prob ?? row.confidence_score ?? null,
            modelContext: camelizeObjectKeys(prop.model_context),
            ...(league === 'mlb' ? { mlbPropContext: serializeMlbPropContext(prop.model_context, league, true) } : {}),
          });
        }
        if (propsArray.length === 0 && payload.player && shouldRenderTeamPropBundleEntry(payload)) {
          allProps.push({
            player: payload.player || row.player_name || null,
            prop: payload.prop ? sanitizePublicTextBlock(String(payload.prop), { totalBrandMentions: 0, warnings: [] }, `breakdown:${payload.player}`) : null,
            recommendation: payload.recommendation || null,
            reasoning: payload.reasoning ? sanitizePublicTextBlock(String(payload.reasoning), { totalBrandMentions: 0, warnings: [] }, `breakdown-reason:${payload.player}`) : null,
            edge: payload.edge ?? payload.edge_pct ?? null,
            prob: payload.prob ?? payload.projected_probability ?? null,
            line: payload.market_line_value ?? payload.line ?? null,
            odds: payload.odds ?? null,
            projectedOutcome: payload.projected_stat_value ?? payload.projectedOutcome ?? null,
            confidence: payload.prob ?? row.confidence_score ?? null,
            modelContext: camelizeObjectKeys(payload.model_context),
            ...(league === 'mlb' ? { mlbPropContext: serializeMlbPropContext(payload.model_context, league, true) } : {}),
          });
        }
      }
      const marketProps = await maybeBuildMlbBreakdownMarketProps({
        league,
        curated,
        side,
        existingProps: allProps,
      });
      return {
        team: teamName,
        short: teamShort,
        side,
        props: allProps,
        marketProps,
        suppressed_reason: suppressedReason,
      };
    }

    const cached = cacheRows.find((row: any) => row.team === side);
    if (cached?.props_data && !hasPoisonedLegacyTeamProps(cached.props_data)) {
      const normalized = normalizeLegacyTeamPropsResponse(cached.props_data, league);
      const items = Array.isArray(normalized?.playerProps)
        ? normalized.playerProps
        : Array.isArray(normalized?.props)
          ? normalized.props
          : [];
      const filteredItems = items
        .filter((prop: any) => shouldRenderTeamPropBundleEntry(prop))
        .map((prop: any) => normalizeLegacyTeamPropItem(prop, league));
      const marketProps = await maybeBuildMlbBreakdownMarketProps({
        league,
        curated,
        side,
        existingProps: filteredItems,
      });
      return {
        team: teamName,
        short: teamShort,
        side,
        props: filteredItems,
        marketProps,
        suppressed_reason: normalized?.metadata?.suppressed_reason || normalized?.metadata?.mlb_suppressed_reason || null,
      };
    }

    // Do not regenerate MLB team props on a public Digilander read.
    // Source-backed market rows are sufficient fallback context here and avoid blocking on LLM generation.
    if (league === 'mlb') {
      const marketProps = await maybeBuildMlbBreakdownMarketProps({
        league,
        curated,
        side,
        existingProps: [],
      });
      if (marketProps.length > 0) {
        return {
          team: teamName,
          short: teamShort,
          side,
          props: [],
          marketProps,
          suppressed_reason: null,
        };
      }
      return null;
    }

    const regenerated = await loadOrRegenerateLegacyTeamPropsForSide({
      eventId,
      curated,
      team: side,
    });
    if (countLegacyTeamProps(regenerated) > 0) {
      const items = Array.isArray(regenerated?.playerProps)
        ? regenerated.playerProps
        : Array.isArray(regenerated?.props)
          ? regenerated.props
          : [];
      const filteredItems = items
        .filter((prop: any) => shouldRenderTeamPropBundleEntry(prop))
        .map((prop: any) => normalizeLegacyTeamPropItem(prop, league));
      const marketProps = await maybeBuildMlbBreakdownMarketProps({
        league,
        curated,
        side,
        existingProps: filteredItems,
      });
      return {
        team: teamName,
        short: teamShort,
        side,
        props: filteredItems,
        marketProps,
        suppressed_reason: regenerated?.metadata?.suppressed_reason || regenerated?.metadata?.mlb_suppressed_reason || null,
      };
    }

    const marketProps = await maybeBuildMlbBreakdownMarketProps({
      league,
      curated,
      side,
      existingProps: [],
    });
    if (marketProps.length > 0) {
      return {
        team: teamName,
        short: teamShort,
        side,
        props: [],
        marketProps,
        suppressed_reason: null,
      };
    }

    return null;
  };

  const [home, away] = await Promise.all([
    buildTeamData('home'),
    buildTeamData('away'),
  ]);

  return {
    available: Boolean(home || away),
    home,
    away,
  };
}

function roundDigilanderPropNumber(value: number): number {
  return Math.round(value * 10) / 10;
}

function normalizeDigilanderPropPercent(value: number | null | undefined): number | null {
  if (value == null || !Number.isFinite(Number(value))) return null;
  const numeric = Number(value);
  return roundDigilanderPropNumber(numeric <= 1 ? numeric * 100 : numeric);
}

function buildDigilanderPlayerPropIdentityKey(params: {
  league: string;
  player: string | null | undefined;
  statType: string | null | undefined;
  line: number | null | undefined;
  recommendation?: string | null | undefined;
}): string {
  const player = normalizeDigiviewText(resolveCanonicalName(params.player || '', params.league) || params.player || '');
  const statIdentity = resolvePlayerPropStatIdentity({
    league: params.league,
    statType: params.statType || null,
    normalizedStatType: params.statType || null,
    prop: params.statType || null,
  });
  const statType = String(statIdentity.normalizedStatType || statIdentity.statType || '').trim().toLowerCase();
  const line = Number(params.line);
  const recommendation = String(params.recommendation || '').trim().toLowerCase();
  if (!player || !statType || !Number.isFinite(line)) return '';
  return `${player}|${statType}|${roundDigilanderPropNumber(line).toFixed(1)}|${recommendation}`;
}

function buildPiffFallbackPlayerProps(params: {
  eventId: string;
  league: string;
  curated: any;
  team: 'home' | 'away' | null;
  existingRows: any[];
}): any[] {
  if (params.league !== 'mlb' || !params.curated) return [];

  const homeShort = String(params.curated?.home_short || '').trim().toUpperCase();
  const awayShort = String(params.curated?.away_short || '').trim().toUpperCase();
  if (!homeShort || !awayShort) return [];

  const existingKeys = new Set(
    params.existingRows
      .map((row: any) => buildDigilanderPlayerPropIdentityKey({
        league: params.league,
        player: row.player_name,
        statType: row.forecast_payload?.normalized_stat_type || row.forecast_payload?.stat_type || null,
        line: row.forecast_payload?.market_line_value ?? row.forecast_payload?.line ?? null,
        recommendation: row.forecast_payload?.forecast_direction || row.forecast_payload?.recommendation || null,
      }))
      .filter(Boolean),
  );

  const eventPiff = getPiffPropsForGame(
    homeShort,
    awayShort,
    loadPiffPropsForDate(getEtDateKey(params.curated?.starts_at) || undefined),
    params.league,
  );

  return eventPiff
    .filter((leg) => {
      const teamShort = String(leg.team || '').trim().toUpperCase();
      if (!teamShort || (teamShort !== homeShort && teamShort !== awayShort)) return false;
      const side = teamShort === homeShort ? 'home' : 'away';
      if (params.team && side !== params.team) return false;
      return true;
    })
    .map((leg) => {
      const teamShort = String(leg.team || '').trim().toUpperCase();
      const side = teamShort === homeShort ? 'home' : 'away';
      const recommendation = String(leg.direction || '').trim().toLowerCase();
      const marketLineValue = Number(leg.line);
      if (!leg.name || !teamShort || !Number.isFinite(marketLineValue) || (recommendation !== 'over' && recommendation !== 'under')) {
        return null;
      }

      const canonicalPlayer = resolveCanonicalName(String(leg.name || '').trim(), params.league) || String(leg.name || '').trim();
      if (!canonicalPlayer || !isPlayerOnTeam(canonicalPlayer, teamShort, params.league)) return null;

      const statIdentity = resolvePlayerPropStatIdentity({
        league: params.league,
        statType: leg.stat || null,
        normalizedStatType: leg.stat || null,
        prop: leg.stat || null,
      });
      const normalizedStatType = String(statIdentity.normalizedStatType || statIdentity.statType || leg.stat || '').trim();
      if (!normalizedStatType) return null;

      const identityKey = buildDigilanderPlayerPropIdentityKey({
        league: params.league,
        player: canonicalPlayer,
        statType: normalizedStatType,
        line: marketLineValue,
        recommendation,
      });
      if (!identityKey || existingKeys.has(identityKey)) return null;
      existingKeys.add(identityKey);

      const playerRole = getMlbPlayerRole(normalizedStatType);
      const statLabel = getPlayerPropLabelForLeague(params.league, normalizedStatType)
        || String(leg.stat || 'Prop')
          .replace(/[_+]+/g, ' ')
          .trim()
          .split(/\s+/)
          .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
          .join(' ');
      const edgePct = normalizeDigilanderPropPercent(Number(leg.edge));
      const projectedProbability = normalizeDigilanderPropPercent(Number(leg.prob));
      const gap = Number(leg.gap);
      const projectedStatValue = Number.isFinite(gap)
        ? roundDigilanderPropNumber(recommendation === 'under'
            ? Math.max(0, marketLineValue - Math.abs(gap))
            : marketLineValue + Math.abs(gap))
        : null;
      const reasoning = sanitizePublicTextBlock(
        [
          `Rain Man prop intel leans ${recommendation} ${marketLineValue} ${statLabel.toLowerCase()} for ${canonicalPlayer}.`,
          leg.tier_label ? `${String(leg.tier_label).trim()} support.` : null,
          edgePct != null ? `Modeled edge ${edgePct.toFixed(1)}%.` : null,
          projectedProbability != null ? `Projected hit rate ${projectedProbability.toFixed(1)}%.` : null,
        ].filter(Boolean).join(' '),
        { totalBrandMentions: 0, warnings: [] },
        `digilander-piff:${params.eventId}:${canonicalPlayer}:${normalizedStatType}.reasoning`,
      );
      const payload = {
        prop: sanitizePublicTextBlock(
          `${statLabel} ${recommendation === 'over' ? 'Over' : 'Under'} ${marketLineValue}`,
          { totalBrandMentions: 0, warnings: [] },
          `digilander-piff:${params.eventId}:${canonicalPlayer}:${normalizedStatType}.prop`,
        ),
        recommendation,
        reasoning,
        edge: edgePct,
        edge_pct: edgePct,
        prob: projectedProbability,
        projected_probability: projectedProbability,
        odds: Number.isFinite(Number(leg.odds)) ? Number(leg.odds) : null,
        projected_stat_value: projectedStatValue,
        stat_type: normalizedStatType,
        normalized_stat_type: normalizedStatType,
        market_line_value: marketLineValue,
        forecast_direction: recommendation.toUpperCase(),
        player_role: playerRole,
        source_backed: false,
        model_context: {
          projection_basis: 'piff_signal',
          tier_label: leg.tier_label || null,
          edge_source: 'piff',
          lineup_status: leg.lineup_status || null,
          lineup_certainty: leg.lineup_certainty ?? null,
          available_books: leg.available_books ?? null,
          context_summary: typeof leg.decision_context === 'string' ? leg.decision_context : reasoning,
        },
      };

      return serializePublicPlayerProp({
        assetId: `piff:${params.eventId}:${teamShort}:${canonicalPlayer.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}:${normalizedStatType}:${marketLineValue}:${recommendation}`,
        player: canonicalPlayer,
        team: teamShort,
        teamSide: side,
        league: params.league,
        prop: payload.prop,
        locked: false,
        confidence: projectedProbability != null ? projectedProbability / 100 : null,
        payload,
        marketType: normalizedStatType,
        marketFamily: playerRole === 'pitcher' ? 'pitcher_props' : playerRole === 'batter' ? 'batter_props' : 'player_props',
        marketOrigin: 'modeled',
        sourceBacked: false,
        playerRole,
        includeMlbPropContext: MLB_PROP_CONTEXT_V2(),
      });
    })
    .filter((prop): prop is NonNullable<typeof prop> => Boolean(prop));
}

async function buildForecastPlayerPropsBody(params: {
  eventId: string;
  team?: 'home' | 'away' | null;
}): Promise<any> {
  const team = params.team || null;
  const curatedEvent = await getCuratedEvent(params.eventId);
  const leagueKey = String(curatedEvent?.league || '').toLowerCase();
  let { rows, rawRowCount: preFilterRowCount } = await resolveVisiblePlayerPropRows(params.eventId, team);
  let fallbackCurated: any | null = null;

  const getGameStarted = async () => {
    const { rows: eventRows } = await pool.query(
      'SELECT starts_at FROM rm_events WHERE event_id = $1 LIMIT 1',
      [params.eventId],
    ).catch(() => ({ rows: [] as any[] }));
    const startsAt = eventRows[0]?.starts_at ? new Date(eventRows[0].starts_at) : null;
    return Boolean(startsAt && !Number.isNaN(startsAt.getTime()) && startsAt.getTime() <= Date.now());
  };

  if (preFilterRowCount === 0 && leagueKey !== 'mlb') {
    const backfill = await backfillMissingMlbPlayerProps({ eventId: params.eventId, team });
    fallbackCurated = backfill.curated;
    if (backfill.wroteAssets) {
      ({ rows, rawRowCount: preFilterRowCount } = await resolveVisiblePlayerPropRows(params.eventId, team));
    }
  } else if (preFilterRowCount > 0 && leagueKey !== 'mlb') {
    const staleRefresh = await refreshStaleMlbPlayerPropsOnRead({ eventId: params.eventId, team, rows });
    fallbackCurated = staleRefresh.curated;
    if (staleRefresh.wroteAssets) {
      ({ rows, rawRowCount: preFilterRowCount } = await resolveVisiblePlayerPropRows(params.eventId, team));
    }
  }

  const playerProps = rows
    .map((row: any) => {
      const publicProp = row.forecast_payload?.prop
        ? sanitizePublicTextBlock(String(row.forecast_payload.prop), { totalBrandMentions: 0, warnings: [] }, `digilander-player-prop:${row.id}.prop`)
        : null;
      const taxonomy = buildPlayerPropTaxonomy(row.forecast_payload);

      return serializePublicPlayerProp({
        assetId: row.id,
        player: row.player_name,
        team: row.team_id,
        teamSide: row.team_side,
        league: row.league,
        prop: publicProp,
        locked: false,
        confidence: row.confidence_score,
        payload: row.forecast_payload,
        marketType: taxonomy.marketType,
        marketFamily: taxonomy.marketFamily,
        marketOrigin: taxonomy.marketOrigin,
        sourceBacked: taxonomy.sourceBacked,
        playerRole: taxonomy.playerRole,
        includeMlbPropContext: MLB_PROP_CONTEXT_V2(),
      });
    })
    .filter((prop): prop is NonNullable<typeof prop> => Boolean(prop));
  const piffFallbackPlayerProps = buildPiffFallbackPlayerProps({
    eventId: params.eventId,
    league: leagueKey,
    curated: curatedEvent,
    team,
    existingRows: rows,
  });
  const surfacedPlayerProps = [...playerProps, ...piffFallbackPlayerProps];

  const lineupSnapshot = leagueKey === 'mlb' && rows.length === 0
    ? null
    : await fetchEventLineupSnapshot(curatedEvent);
  const marketProps = await buildSourceBackedPlayerPropMarketBoard({
    curated: curatedEvent,
    team,
    existingProps: surfacedPlayerProps,
  });
  const featuredRows = rows.map((row: any) => {
    const descriptor = describeForecastAsset('PLAYER_PROP', row.forecast_payload);
    return {
      ...row,
      locked: false,
      playerRole: descriptor.playerRole,
    };
  });
  const supplementalFeaturedRows = lineupSnapshot
    ? await buildSupplementalFeaturedPlayerRows({
        eventId: params.eventId,
        curated: curatedEvent,
        lineups: lineupSnapshot,
        existingRows: featuredRows,
      })
    : [];
  const featuredPlayers = buildFeaturedPlayersFromRows(
    [...featuredRows, ...supplementalFeaturedRows],
    {
      lineups: lineupSnapshot,
      limit: 10,
      perTeamTarget: 5,
      homeTeamId: curatedEvent?.home_short || curatedEvent?.home_team || null,
      awayTeamId: curatedEvent?.away_short || curatedEvent?.away_team || null,
    },
  ).map((player) => ({
    ...player,
    analysis: player.analysis
      ? sanitizePublicTextBlock(player.analysis, { totalBrandMentions: 0, warnings: [] }, `digilander-player-radar:${params.eventId}:${player.player}.analysis`)
      : null,
    props: player.props.map((prop) => ({
      ...prop,
      analysis: prop.analysis
        ? sanitizePublicTextBlock(prop.analysis, { totalBrandMentions: 0, warnings: [] }, `digilander-player-radar:${params.eventId}:${player.player}:${prop.assetId}.analysis`)
        : null,
    })),
  }));

  const fallbackStartsAt = fallbackCurated?.starts_at ? new Date(fallbackCurated.starts_at) : null;
  const gameStarted = preFilterRowCount === 0
    ? (fallbackStartsAt && !Number.isNaN(fallbackStartsAt.getTime())
        ? fallbackStartsAt.getTime() <= Date.now()
        : await getGameStarted())
    : false;

  return {
    contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
    playerProps: surfacedPlayerProps,
    marketProps,
    players: buildPublicGroupedPlayers(surfacedPlayerProps),
    featuredPlayers,
    count: surfacedPlayerProps.length,
    mode: surfacedPlayerProps.length > 0 ? 'per_player' : 'team',
    fallbackReason: preFilterRowCount === 0
      ? gameStarted
        ? 'game_started'
        : 'no_precomputed_assets'
      : surfacedPlayerProps.length === 0
        ? 'all_assets_filtered_out'
        : null,
  };
}

function mergePreviewOdds(primary: any, fallback: any): any {
  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 }),
  };
}

/** Get cached insight data for already-unlocked insights */
async function getUnlockedInsightData(eventId: string, unlockedTypes: string[]): Promise<Record<string, any>> {
  const result: Record<string, any> = {};
  if (unlockedTypes.length === 0) return result;
  const { rows } = await pool.query(
    'SELECT insight_type, insight_data FROM rm_insight_cache WHERE event_id = $1 AND insight_type = ANY($2)',
    [eventId, unlockedTypes]
  ).catch(() => ({ rows: [] }));
  for (const row of rows) {
    result[row.insight_type] = row.insight_data;
  }
  return result;
}

function refreshCachedOddsAsync(eventId: string, odds: any): void {
  Promise.resolve(updateCachedOdds(eventId, odds)).catch((err) => {
    console.error(`Forecast odds refresh error for ${eventId}:`, err);
  });
}

async function loadInsightBundle(
  eventId: string,
  userId: string | null,
  forecastData: any,
  league?: string,
): Promise<{
  insightMeta: {
    insightAvailability: { steam: boolean; sharp: boolean; dvp: boolean; hcw: boolean };
    unlockedInsights: string[];
  };
  unlockedInsightData: Record<string, any>;
}> {
  const insightMeta = await getInsightMeta(eventId, userId, forecastData, league);
  const unlockedInsightData = await getUnlockedInsightData(eventId, insightMeta.unlockedInsights);
  return { insightMeta, unlockedInsightData };
}

async function sendForecastNotReadyResponse(params: {
  res: Response;
  trace: ForecastOpenTrace;
  eventId: string;
  resolvedEventId: string;
  userId: string;
  league: string;
  homeTeam: string;
  awayTeam: string;
  homeShort: string;
  awayShort: string;
  odds: any;
  responsePath: string;
}): Promise<void> {
  const insightBundleNotReady = await traceForecastOpenStep(params.trace, 'loadInsightBundle', () => loadInsightBundle(
    params.resolvedEventId,
    params.userId,
    null,
    params.league,
  ));
  params.res.status(202).json({
    contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
    status: 'not_ready',
    message: 'Forecast is being generated. Check back shortly.',
    eventId: params.eventId,
    homeTeam: params.homeTeam,
    awayTeam: params.awayTeam,
    homeShort: params.homeShort,
    awayShort: params.awayShort,
    league: params.league,
    odds: params.odds,
    insightAvailability: insightBundleNotReady.insightMeta.insightAvailability,
    unlockedInsights: insightBundleNotReady.insightMeta.unlockedInsights,
    unlockedInsightData: insightBundleNotReady.unlockedInsightData,
  });
  setForecastOpenTraceNote(params.trace, 'responsePath', params.responsePath);
  reportForecastOpen({
    eventId: params.eventId,
    league: params.league,
    userId: params.userId,
    cacheStatus: 'not_ready',
    trace: params.trace,
  });
}

// GET /api/forecast/top-props — ranked daily player props, public and additive
router.get('/top-props', async (req: Request, res: Response) => {
  try {
    const rawLeague = req.query.league ? String(req.query.league).trim().toLowerCase() : null;
    const rawSport = req.query.sport ? String(req.query.sport).trim().toLowerCase() : null;
    const filters = normalizeTopBoardFilters(rawLeague || null, rawSport || null);
    const limit = Math.min(Math.max(Number(req.query.limit) || 12, 1), 12);
    const body = await getTodayTopProps(filters, limit);
    res.json(body);
  } catch (err) {
    console.error('Top props error:', err);
    res.status(500).json({ error: 'Failed to fetch top props' });
  }
});

// GET /api/forecast/top-picks — ranked daily board mixing props and team forecasts
router.get('/top-picks', async (req: Request, res: Response) => {
  try {
    const rawLeague = req.query.league ? String(req.query.league).trim().toLowerCase() : null;
    const rawSport = req.query.sport ? String(req.query.sport).trim().toLowerCase() : null;
    const filters = normalizeTopBoardFilters(rawLeague || null, rawSport || null);
    const limit = Math.min(Math.max(Number(req.query.limit) || 12, 1), 12);
    const body = await getTodayTopPicks(filters, limit);
    res.json(body);
  } catch (err) {
    console.error('Top picks error:', err);
    res.status(500).json({ error: 'Failed to fetch top picks' });
  }
});

// GET /api/forecast/:eventId/preview — no auth, limited data
router.get('/:eventId/preview', async (req: Request, res: Response) => {
  try {
    const eventId = String(req.params.eventId);
    res.json(await buildForecastPreviewBody(eventId));
  } catch (err) {
    if ((err as any)?.status) {
      res.status((err as any).status).json({ error: (err as Error).message });
      return;
    }
    console.error('Preview error:', err);
    res.status(500).json({ error: 'Failed to fetch preview' });
  }
});

/** Log forecast open event for cache performance tracking */
async function logForecastOpen(eventId: string, league: string, userId: string, cacheStatus: 'hit' | 'not_ready' | 'miss', startMs: number): Promise<void> {
  try {
    const responseTimeMs = Date.now() - startMs;
    await pool.query(
      `INSERT INTO rm_forecast_open_log (event_id, league, user_id, cache_status, served_from_cache, response_time_ms)
       VALUES ($1, $2, $3, $4, $5, $6)`,
      [eventId, league || null, userId, cacheStatus, cacheStatus === 'hit', responseTimeMs]
    );
  } catch (err) {
    // Non-fatal — analytics logging should never block the response
    console.error('Forecast open log error:', err);
  }
}

async function logMlbFallbackUsage(params: {
  eventId: string;
  mode: 'game_data' | 'player_digiview';
  localRows: number;
  fallbackRows: number;
  mergedRows: number;
}): Promise<void> {
  try {
    await pool.query(
      `INSERT INTO rm_api_usage (category, subcategory, event_id, league, provider, model, success, metadata)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)`,
      [
        'mlb_fallback',
        params.mode,
        params.eventId,
        'mlb',
        'internal',
        'live-prop-fallback',
        true,
        JSON.stringify({
          localRows: params.localRows,
          fallbackRows: params.fallbackRows,
          mergedRows: params.mergedRows,
        }),
      ],
    );
  } catch (err) {
    console.error('MLB fallback usage log error:', err);
  }
}

type ForecastOpenTrace = {
  startedAt: number;
  steps: Record<string, number>;
  notes: Record<string, any>;
};

const FORECAST_OPEN_TRACE_THRESHOLD_MS = Math.max(0, Number(process.env.FORECAST_OPEN_TRACE_THRESHOLD_MS || 250));

function createForecastOpenTrace(eventId: string, league: string, userId: string): ForecastOpenTrace {
  return {
    startedAt: Date.now(),
    steps: {},
    notes: {
      eventId,
      requestedLeague: league || null,
      userId,
    },
  };
}

async function traceForecastOpenStep<T>(
  trace: ForecastOpenTrace,
  label: string,
  action: () => Promise<T>,
): Promise<T> {
  const startedAt = Date.now();
  try {
    return await action();
  } finally {
    trace.steps[label] = (trace.steps[label] || 0) + (Date.now() - startedAt);
  }
}

function setForecastOpenTraceNote(trace: ForecastOpenTrace, key: string, value: any): void {
  trace.notes[key] = value;
}

async function logForecastOpenTrace(params: {
  eventId: string;
  league: string;
  cacheStatus: 'hit' | 'not_ready' | 'miss' | 'error';
  success: boolean;
  trace: ForecastOpenTrace;
  errorMessage?: string;
}): Promise<void> {
  const responseTimeMs = Date.now() - params.trace.startedAt;
  if (params.success && responseTimeMs < FORECAST_OPEN_TRACE_THRESHOLD_MS) {
    return;
  }

  try {
    await pool.query(
      `INSERT INTO rm_api_usage (category, subcategory, event_id, league, provider, model, response_time_ms, success, error_message, metadata)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
      [
        'forecast_open',
        params.cacheStatus,
        params.eventId,
        params.league || null,
        'internal',
        'forecast-route',
        responseTimeMs,
        params.success,
        params.errorMessage || null,
        JSON.stringify({
          cacheStatus: params.cacheStatus,
          thresholdMs: FORECAST_OPEN_TRACE_THRESHOLD_MS,
          steps: params.trace.steps,
          notes: params.trace.notes,
        }),
      ],
    );
  } catch (err) {
    console.error('Forecast open trace error:', err);
  }
}

function reportForecastOpen(params: {
  eventId: string;
  league: string;
  userId: string;
  cacheStatus: 'hit' | 'not_ready' | 'miss';
  trace: ForecastOpenTrace;
}): void {
  logForecastOpen(params.eventId, params.league, params.userId, params.cacheStatus, params.trace.startedAt).catch(() => {});
  logForecastOpenTrace({
    eventId: params.eventId,
    league: params.league,
    cacheStatus: params.cacheStatus,
    success: true,
    trace: params.trace,
  }).catch(() => {});
}

// GET /api/forecast/:eventId/game-data — no auth, game-level essentials
router.get('/:eventId/game-data', async (req: Request, res: Response) => {
  try {
    const eventId = String(req.params.eventId);
    res.json(await buildForecastGameDataBody(eventId));
  } catch (err) {
    if ((err as any)?.status) {
      res.status((err as any).status).json({ error: (err as Error).message });
      return;
    }
    console.error('Game data error:', err);
    res.status(500).json({ error: 'Failed to fetch game data' });
  }
});

// GET /api/forecast/:eventId/player-digiview — no auth, player-level evidence
router.get('/:eventId/player-digiview', async (req: Request, res: Response) => {
  try {
    const eventId = String(req.params.eventId || '');
    const assetId = String(req.query.assetId || '').trim();
    const playerQuery = String(req.query.player || '').trim();
    const teamQuery = String(req.query.team || '').trim().toUpperCase();

    const curated = await getCuratedEvent(eventId);
    if (!curated) {
      res.status(404).json({ error: 'Event not found' });
      return;
    }

    const visible = await resolveVisiblePlayerPropRows(eventId, null).catch(() => ({ rows: [] as any[], rawRowCount: 0 }));
    const visibleRows = visible.rows || [];
    const lineupSnapshot = await withTimeout(fetchEventLineupSnapshot(curated), 2500, null as PlayerRadarLineupSnapshot | null);
    const liveFallbackRows = String(curated.league || '').toLowerCase() === 'mlb' && visibleRows.length < MLB_FALLBACK_ROW_THRESHOLD
      ? await withTimeout(fetchMlbLiveFallbackRows({ eventId, curated, lineupSnapshot }), 9000, [] as SyntheticPlayerPropRow[])
      : [];
    const rows = mergePlayerPropRowsWithFallback(visibleRows, liveFallbackRows);
    if (String(curated.league || '').toLowerCase() === 'mlb' && liveFallbackRows.length > 0) {
      logMlbFallbackUsage({
        eventId,
        mode: 'player_digiview',
        localRows: visibleRows.length,
        fallbackRows: liveFallbackRows.length,
        mergedRows: rows.length,
      }).catch(() => {});
    }
    if (rows.length === 0) {
      res.status(404).json({ error: 'No player props available for this event' });
      return;
    }

    const seedRow = rows.find((row: any) => assetId && row.id === assetId)
      || rows.find((row: any) => (
        playerQuery
        && normalizeDigiviewText(row.player_name) === normalizeDigiviewText(playerQuery)
        && (!teamQuery || String(row.team_id || '').trim().toUpperCase() === teamQuery)
      ))
      || null;

    if (!seedRow?.player_name) {
      res.status(404).json({ error: 'Player not found for this event' });
      return;
    }

    const league = String(curated.league || '').toLowerCase();
    const targetPlayerKey = normalizeDigiviewText(seedRow.player_name);
    const targetTeamKey = String(seedRow.team_id || '').trim().toUpperCase();
    const playerRows = rows.filter((row: any) =>
      normalizeDigiviewText(row.player_name) === targetPlayerKey
      && String(row.team_id || '').trim().toUpperCase() === targetTeamKey,
    );

    const injuries = await getGameInjuriesForEvent(curated);
    const playerInjury = injuries.find((injury: any) =>
      normalizeDigiviewText(injury.playerName) === targetPlayerKey
      && String(injury.team || '').trim().toUpperCase() === targetTeamKey,
    ) || injuries.find((injury: any) => normalizeDigiviewText(injury.playerName) === targetPlayerKey) || null;

    const eventPiff = getPiffPropsForGame(
      curated.home_short || '',
      curated.away_short || '',
      loadPiffPropsForDate(getEtDateKey(curated.starts_at) || undefined),
      league,
    );
    const digimonPicks = league === 'nba'
      ? getDigimonForGame(curated.home_short || '', curated.away_short || '')
      : [];

    const historicalSamples = await withTimeout(fetchHistoricalDigiviewSamples({
      league,
      curated,
      rows: playerRows,
    }), 2500, new Map<string, { last5Samples: HistoricalGameSample[]; last10Samples: HistoricalGameSample[]; h2hSamples: HistoricalGameSample[] }>());

    const props = playerRows
      .map((row: any) => {
        const taxonomy = buildPlayerPropTaxonomy(row.forecast_payload);
        const publicProp = serializePublicPlayerProp({
          assetId: row.id,
          player: row.player_name,
          team: row.team_id,
          teamSide: row.team_side,
          league: row.league,
          prop: row.forecast_payload?.prop || null,
          locked: false,
          confidence: row.confidence_score,
          payload: row.forecast_payload,
          marketType: taxonomy.marketType,
          marketFamily: taxonomy.marketFamily,
          marketOrigin: taxonomy.marketOrigin,
          sourceBacked: taxonomy.sourceBacked,
          playerRole: taxonomy.playerRole,
          includeMlbPropContext: MLB_PROP_CONTEXT_V2(),
        });
        if (!publicProp) return null;
        const statHint = row.forecast_payload?.normalized_stat_type || row.forecast_payload?.stat_type || row.forecast_payload?.prop || null;
        const digimonPick = findMatchingDigimonPick({
          picks: digimonPicks,
          player: row.player_name,
          team: row.team_id,
          statHint,
          line: publicProp.marketLine ?? publicProp.marketLineValue ?? null,
        });
        const piffLeg = findMatchingPiffLeg({
          legs: eventPiff,
          player: row.player_name,
          team: row.team_id,
          statHint,
          line: publicProp.marketLine ?? publicProp.marketLineValue ?? null,
        });
        return {
          ...publicProp,
          digiview: buildPublicDigiviewEvidence({
            payload: row.forecast_payload,
            commentary: publicProp.reasoning || null,
            piffLeg,
            history: historicalSamples.get(row.id) || null,
            digimonPick,
          }),
        };
      })
      .filter((row): row is NonNullable<typeof row> => Boolean(row))
      .sort((a, b) => Number(b.edgePct ?? b.edge ?? -Infinity) - Number(a.edgePct ?? a.edge ?? -Infinity));

    const supplementalMarketProps = (await buildSourceBackedPlayerPropMarketBoard({
      curated,
      team: seedRow.team_side === 'home' || seedRow.team_side === 'away' ? seedRow.team_side : null,
      existingProps: props,
    }))
      .filter((marketProp) =>
        normalizeDigiviewText(marketProp.player) === targetPlayerKey
        && String(marketProp.team || '').trim().toUpperCase() === targetTeamKey,
      );

    const snapshotMarketTypes = Array.from(new Set(
      playerRows.flatMap((row: any) => buildPlayerDigiviewSnapshotMarketKeys(
        league,
        row.forecast_payload?.normalized_stat_type || row.forecast_payload?.stat_type || row.forecast_payload?.prop || null,
      )),
    ));
    const lineSnapshots = await withTimeout(fetchPlayerDigiviewLineSnapshots({
      league,
      startsAt: curated.starts_at || null,
      homeTeam: curated.home_team || null,
      awayTeam: curated.away_team || null,
      playerName: String(seedRow.player_name || '').trim(),
      marketTypes: snapshotMarketTypes,
    }), 3000, [] as PlayerDigiviewLineSnapshot[]);

    const descriptor = describeForecastAsset('PLAYER_PROP', seedRow.forecast_payload);
    res.json({
      eventId,
      league: curated.league || null,
      homeTeam: curated.home_team || null,
      awayTeam: curated.away_team || null,
      selectedAssetId: props.find((prop) => prop.assetId === assetId)?.assetId || props[0]?.assetId || null,
      player: {
        name: seedRow.player_name || null,
        team: seedRow.team_id || null,
        teamSide: seedRow.team_side || null,
        playerRole: descriptor.playerRole || null,
        injury: playerInjury,
      },
      props,
      marketProps: supplementalMarketProps,
      lineSnapshots,
    });
  } catch (err) {
    console.error('Player digiview error:', err);
    res.status(500).json({ error: 'Failed to fetch player digiview' });
  }
});

// GET /api/forecast/:eventId/market-history — no auth, game-level market evidence
router.get('/:eventId/market-history', async (req: Request, res: Response) => {
  try {
    const eventId = String(req.params.eventId || '');
    res.json(await buildForecastMarketHistoryBody(eventId));
  } catch (err) {
    if ((err as any)?.status) {
      res.status((err as any).status).json({ error: (err as Error).message });
      return;
    }
    console.error('Market history error:', err);
    res.status(500).json({ error: 'Failed to fetch market history' });
  }
});

function countMlbCandidateRows(candidates: MlbPropCandidate[]): number {
  return candidates.reduce((sum, candidate) => {
    const sides = Array.isArray(candidate.availableSides) ? candidate.availableSides : [];
    for (const side of sides) {
      if (side === 'over' && candidate.overOdds != null) sum += 1;
      if (side === 'under' && candidate.underOdds != null) sum += 1;
    }
    return sum;
  }, 0);
}

function countTeamMarketCandidateRows(candidates: TeamPropMarketCandidate[]): number {
  return candidates.reduce((sum, candidate) => {
    const sides = Array.isArray(candidate.availableSides) ? candidate.availableSides : [];
    for (const side of sides) {
      if (side === 'over' && candidate.overOdds != null) sum += 1;
      if (side === 'under' && candidate.underOdds != null) sum += 1;
    }
    return sum;
  }, 0);
}

function getCuratedEventStartsAtIso(curated: any): string {
  const startsAtRaw = curated?.starts_at;
  if (startsAtRaw instanceof Date) return startsAtRaw.toISOString();
  if (!startsAtRaw) return '';
  const parsed = new Date(startsAtRaw);
  return Number.isNaN(parsed.getTime()) ? '' : parsed.toISOString();
}

async function primeMlbDigilanderPropCandidates(curated: any): Promise<void> {
  if (String(curated?.league || '').toLowerCase() !== 'mlb') return;

  const startsAt = getCuratedEventStartsAtIso(curated);
  const homeShort = String(curated?.home_short || '').trim().toUpperCase();
  const awayShort = String(curated?.away_short || '').trim().toUpperCase();
  if (!startsAt || !homeShort || !awayShort) return;

  await Promise.all([
    fetchMlbPropCandidates({
      teamShort: homeShort,
      teamName: String(curated?.home_team || homeShort),
      opponentShort: awayShort,
      startsAt,
    }).catch(() => [] as MlbPropCandidate[]),
    fetchMlbPropCandidates({
      teamShort: awayShort,
      teamName: String(curated?.away_team || awayShort),
      opponentShort: homeShort,
      startsAt,
    }).catch(() => [] as MlbPropCandidate[]),
  ]);
}

async function buildDigilanderSummarySupport(eventId: string, curated: any): Promise<{
  injuriesCount: number;
  playerCount: number;
  linePropsCount: number;
  marketPropsCount: number;
  fallbackUsage: {
    source: string;
    fallbackUsed: boolean;
    playerCount: number;
    linePropsCount: number;
    marketPropsCount: number;
  };
}> {
  const injuriesPromise = getGameInjuriesForEvent(curated)
    .then((items) => items.length)
    .catch(() => 0);
  const visiblePromise = resolveVisiblePlayerPropRows(eventId, null)
    .catch(() => ({ rows: [] as any[], rawRowCount: 0 }));

  const [injuriesCount, visible] = await Promise.all([injuriesPromise, visiblePromise]);
  const visibleRows = Array.isArray(visible.rows) ? visible.rows : [];
  if (visibleRows.length > 0) {
    const uniquePlayers = new Set(
      visibleRows
        .map((row: any) => {
          const player = String(row.player_name || '').trim().toLowerCase();
          const team = String(row.team_id || row.team_side || '').trim().toLowerCase();
          return player ? `${player}|${team}` : '';
        })
        .filter(Boolean),
    );
    return {
      injuriesCount,
      playerCount: uniquePlayers.size,
      linePropsCount: visibleRows.length,
      marketPropsCount: 0,
      fallbackUsage: buildDigilanderSummarySupportFallbackUsage({
        source: 'visible_rows',
        playerCount: uniquePlayers.size,
        linePropsCount: visibleRows.length,
      }),
    };
  }

  const league = String(curated?.league || '').toLowerCase();
  const startsAt = curated?.starts_at instanceof Date
    ? curated.starts_at.toISOString()
    : curated?.starts_at
      ? new Date(curated.starts_at).toISOString()
      : '';
  const homeShort = String(curated?.home_short || '').trim().toUpperCase();
  const awayShort = String(curated?.away_short || '').trim().toUpperCase();
  const homeTeam = String(curated?.home_team || '').trim();
  const awayTeam = String(curated?.away_team || '').trim();

  if (league === 'mlb') {
    if (!startsAt || !homeShort || !awayShort) {
      return {
        injuriesCount,
        playerCount: 0,
        linePropsCount: 0,
        marketPropsCount: 0,
        fallbackUsage: buildDigilanderSummarySupportFallbackUsage(),
      };
    }
    const normalizedSummary = await pool.query(
      `SELECT
         COUNT(*)::int AS market_count,
         COUNT(DISTINCT LOWER(COALESCE(player_name, player_id, '')))::int AS player_count
       FROM rm_mlb_normalized_player_prop_markets
       WHERE event_id = $1`,
      [eventId],
    ).catch(() => ({ rows: [] as any[] }));
    const normalizedMarketPropsCount = Number(normalizedSummary.rows[0]?.market_count || 0);
    const normalizedPlayerCount = Number(normalizedSummary.rows[0]?.player_count || 0);
    if (normalizedMarketPropsCount > 0) {
      return {
        injuriesCount,
        playerCount: normalizedPlayerCount,
        linePropsCount: 0,
        marketPropsCount: normalizedMarketPropsCount,
        fallbackUsage: buildDigilanderSummarySupportFallbackUsage({
          source: 'mlb_normalized_markets',
          playerCount: normalizedPlayerCount,
          marketPropsCount: normalizedMarketPropsCount,
        }),
      };
    }

    const feedSummary = await getMlbPropFeedSummary({
      teamShort: homeShort,
      teamName: homeTeam || homeShort,
      opponentShort: awayShort,
      startsAt,
    }).catch(() => ({ playerCount: 0, marketPropsCount: 0 }));
    if (feedSummary.marketPropsCount > 0) {
      return {
        injuriesCount,
        playerCount: feedSummary.playerCount,
        linePropsCount: 0,
        marketPropsCount: feedSummary.marketPropsCount,
        fallbackUsage: buildDigilanderSummarySupportFallbackUsage({
          source: 'mlb_feed_summary',
          playerCount: feedSummary.playerCount,
          marketPropsCount: feedSummary.marketPropsCount,
        }),
      };
    }

    const [homeLocal, awayLocal] = await Promise.all([
      fetchMlbPropCandidates({
        teamShort: homeShort,
        teamName: homeTeam || homeShort,
        opponentShort: awayShort,
        startsAt,
      }).catch(() => [] as MlbPropCandidate[]),
      fetchMlbPropCandidates({
        teamShort: awayShort,
        teamName: awayTeam || awayShort,
        opponentShort: homeShort,
        startsAt,
      }).catch(() => [] as MlbPropCandidate[]),
    ]);
    const localCandidates = [...homeLocal, ...awayLocal];
    const candidates = localCandidates.length > 0
      ? localCandidates
      : await Promise.all([
          fetchDirectMlbPropCandidates({
            teamShort: homeShort,
            teamName: homeTeam || homeShort,
            opponentShort: awayShort,
            startsAt,
          }).catch(() => [] as MlbPropCandidate[]),
          fetchDirectMlbPropCandidates({
            teamShort: awayShort,
            teamName: awayTeam || awayShort,
            opponentShort: homeShort,
            startsAt,
          }).catch(() => [] as MlbPropCandidate[]),
        ]).then((groups) => groups.flat());

    const uniquePlayers = new Set(
      candidates
        .map((candidate) => {
          const player = String(candidate.player || '').trim().toLowerCase();
          const team = String(candidate.teamShort || '').trim().toLowerCase();
          return player ? `${player}|${team}` : '';
        })
        .filter(Boolean),
    );
    const marketPropsCount = countMlbCandidateRows(candidates);
    return {
      injuriesCount,
      playerCount: uniquePlayers.size,
      linePropsCount: 0,
      marketPropsCount,
      fallbackUsage: buildDigilanderSummarySupportFallbackUsage({
        source: marketPropsCount > 0 ? 'mlb_candidate_markets' : 'empty',
        playerCount: uniquePlayers.size,
        marketPropsCount,
      }),
    };
  }

  const cachedTeamPropsSupport = await loadDigilanderNonMlbTeamPropSummarySupport(eventId, league);
  if (cachedTeamPropsSupport.marketPropsCount > 0) {
    return {
      injuriesCount,
      playerCount: cachedTeamPropsSupport.playerCount,
      linePropsCount: 0,
      marketPropsCount: cachedTeamPropsSupport.marketPropsCount,
      fallbackUsage: buildDigilanderSummarySupportFallbackUsage({
        source: cachedTeamPropsSupport.source,
        playerCount: cachedTeamPropsSupport.playerCount,
        marketPropsCount: cachedTeamPropsSupport.marketPropsCount,
      }),
    };
  }

  if (!startsAt || !homeShort || !awayShort) {
    return {
      injuriesCount,
      playerCount: 0,
      linePropsCount: 0,
      marketPropsCount: 0,
      fallbackUsage: buildDigilanderSummarySupportFallbackUsage(),
    };
  }

  const sourceTeamPropsSupport = await loadDigilanderSourceTeamPropSummarySupport({
    league,
    startsAt,
    homeShort,
    awayShort,
    homeTeam,
    awayTeam,
  });
  if (sourceTeamPropsSupport.marketPropsCount > 0) {
    return {
      injuriesCount,
      playerCount: sourceTeamPropsSupport.playerCount,
      linePropsCount: 0,
      marketPropsCount: sourceTeamPropsSupport.marketPropsCount,
      fallbackUsage: buildDigilanderSummarySupportFallbackUsage({
        source: 'source_team_props',
        playerCount: sourceTeamPropsSupport.playerCount,
        marketPropsCount: sourceTeamPropsSupport.marketPropsCount,
      }),
    };
  }

  const [homeCandidates, awayCandidates] = await Promise.all([
    fetchTeamPropMarketCandidates({
      league,
      teamShort: homeShort,
      opponentShort: awayShort,
      teamName: homeTeam || homeShort,
      opponentName: awayTeam || awayShort,
      homeTeam: homeTeam || homeShort,
      awayTeam: awayTeam || awayShort,
      startsAt,
      skipTheOddsVerification: true,
    }).catch(() => [] as TeamPropMarketCandidate[]),
    fetchTeamPropMarketCandidates({
      league,
      teamShort: awayShort,
      opponentShort: homeShort,
      teamName: awayTeam || awayShort,
      opponentName: homeTeam || homeShort,
      homeTeam: homeTeam || homeShort,
      awayTeam: awayTeam || awayShort,
      startsAt,
      skipTheOddsVerification: true,
    }).catch(() => [] as TeamPropMarketCandidate[]),
  ]);
  const candidates = [
    ...homeCandidates.map((candidate) => ({ ...candidate, _team: homeShort })),
    ...awayCandidates.map((candidate) => ({ ...candidate, _team: awayShort })),
  ];
  const uniquePlayers = new Set(
    candidates
      .map((candidate) => {
        const player = String(candidate.player || '').trim().toLowerCase();
        const team = String((candidate as any)._team || '').trim().toLowerCase();
        return player ? `${player}|${team}` : '';
      })
      .filter(Boolean),
  );

  const marketPropsCount = countTeamMarketCandidateRows(candidates);
  return {
    injuriesCount,
    playerCount: uniquePlayers.size,
    linePropsCount: 0,
    marketPropsCount,
    fallbackUsage: buildDigilanderSummarySupportFallbackUsage({
      source: marketPropsCount > 0 ? 'team_prop_candidates' : 'empty',
      playerCount: uniquePlayers.size,
      marketPropsCount,
    }),
  };
}

function getFirstDigilanderModuleReason(...modules: Array<{ status: string; reason: string | null }>): string | null {
  for (const module of modules) {
    if (module.status !== 'ready' && module.reason) {
      return module.reason;
    }
  }
  return null;
}

type DigilanderCoverageReadiness = 'full' | 'partial' | 'empty' | 'locked' | 'failed';
type DigilanderCoverageConfidence = 'high' | 'medium' | 'low';

function getDigilanderCoverageReadiness(
  status: string,
  readyReadiness: Extract<DigilanderCoverageReadiness, 'full' | 'partial'>,
): DigilanderCoverageReadiness {
  if (status === 'ready') return readyReadiness;
  if (status === 'locked') return 'locked';
  if (status === 'failed') return 'failed';
  return 'empty';
}

function getDigilanderCoverageConfidence(readiness: DigilanderCoverageReadiness): DigilanderCoverageConfidence {
  if (readiness === 'full') return 'high';
  if (readiness === 'partial') return 'medium';
  return 'low';
}

function buildDigilanderModuleReadinessReport(params: {
  status: string;
  available: boolean;
  count: number;
  reason: string | null;
  readyReadiness: Extract<DigilanderCoverageReadiness, 'full' | 'partial'>;
  signals?: Record<string, any>;
}) {
  const readiness = getDigilanderCoverageReadiness(params.status, params.readyReadiness);
  return {
    status: params.status,
    available: params.available,
    count: params.count,
    reason: params.reason,
    readiness,
    coverageConfidence: getDigilanderCoverageConfidence(readiness),
    signals: params.signals || {},
  };
}

async function buildDigilanderBundleBodyBase(params: {
  eventId: string;
  signedIn?: boolean;
  summaryOnly?: boolean;
}): Promise<any> {
  const eventId = String(params.eventId || '');
  const summaryOnly = Boolean(params.summaryOnly);
  const cacheKey = getDigilanderBundleCacheKey(params);
  const timingsMs: DigilanderTimingMap = {};
  const buildStartedAt = Date.now();

  const curated = await getCuratedEvent(eventId);
  if (!curated) {
    throw createRouteError(404, 'Event not found');
  }

  const league = String(curated.league || '').toLowerCase();
  const sport = leagueToRainwireSport(league);
  const fallbackUsage = {
    news: buildDigilanderNewsFallbackUsage('error'),
    market: buildDigilanderMarketFallbackUsage(),
    summarySupport: buildDigilanderSummarySupportFallbackUsage(),
  };
  const mlbPrewarmPromise = league === 'mlb'
    ? primeMlbDigilanderPropCandidates(curated).catch(() => {})
    : null;

  const [previewResult, topPicksResult, newsResult, marketResult] = await Promise.allSettled([
    runTimedDigilanderStep(timingsMs, 'preview', () => buildForecastPreviewBody(eventId)),
    runTimedDigilanderStep(timingsMs, 'topPicks', () => buildDigilanderTopPicksBody(eventId, league)),
    runTimedDigilanderStep(timingsMs, 'news', async () => {
      const result = await buildDigilanderNewsModuleWithDiagnostics(curated);
      fallbackUsage.news = result.fallbackUsage;
      return result.module;
    }),
    runTimedDigilanderStep(timingsMs, 'market', async () => {
      const result = await getForecastMarketHistoryPayload(eventId);
      fallbackUsage.market = result.fallbackUsage;
      return result.body;
    }),
  ]);

  const previewModule =
    previewResult.status === 'fulfilled'
      ? digilanderModuleOk(previewResult.value)
      : digilanderModuleFromError(previewResult.reason, 'Preview failed');

  const topPicksModule =
    topPicksResult.status === 'fulfilled'
      ? (Array.isArray(topPicksResult.value?.entries) && topPicksResult.value.entries.length > 0
          ? digilanderModuleOk(topPicksResult.value, topPicksResult.value.entries.length)
          : digilanderModuleEmpty('No top picks for this game'))
      : digilanderModuleFromError(topPicksResult.reason, 'Top picks failed');

  const newsModule =
    newsResult.status === 'fulfilled'
      ? newsResult.value
      : digilanderModuleFromError(newsResult.reason, 'News failed');

  let gameDataModule: ReturnType<typeof digilanderModuleOk<any>> | ReturnType<typeof digilanderModuleEmpty> | ReturnType<typeof digilanderModuleFailed> | ReturnType<typeof digilanderModuleLocked>;
  let injuriesModule: ReturnType<typeof digilanderModuleOk<any>> | ReturnType<typeof digilanderModuleEmpty> | ReturnType<typeof digilanderModuleFailed> | ReturnType<typeof digilanderModuleLocked>;
  let playerPropsModule: ReturnType<typeof digilanderModuleOk<any>> | ReturnType<typeof digilanderModuleEmpty> | ReturnType<typeof digilanderModuleFailed> | ReturnType<typeof digilanderModuleLocked>;
  let teamBreakdownModule: ReturnType<typeof digilanderModuleOk<any>> | ReturnType<typeof digilanderModuleEmpty> | ReturnType<typeof digilanderModuleFailed> | ReturnType<typeof digilanderModuleLocked>;
  let teamBreakdownCount = 0;
  let summarySupport: Awaited<ReturnType<typeof buildDigilanderSummarySupport>> | null = null;

  if (summaryOnly) {
    summarySupport = await runTimedDigilanderStep(
      timingsMs,
      'summarySupport',
      () => buildDigilanderSummarySupport(eventId, curated),
    );
    fallbackUsage.summarySupport = summarySupport.fallbackUsage;
    const summaryPlayerPropsCount = summarySupport.linePropsCount + summarySupport.marketPropsCount;
    gameDataModule = summarySupport.playerCount > 0 || summarySupport.injuriesCount > 0 || summaryPlayerPropsCount > 0
      ? digilanderModuleOk(null, summarySupport.playerCount + summarySupport.injuriesCount + summaryPlayerPropsCount)
      : digilanderModuleEmpty('No game data for this event');
    injuriesModule = summarySupport.injuriesCount > 0
      ? digilanderModuleOk(null, summarySupport.injuriesCount)
      : digilanderModuleEmpty('No injuries for this game');
    playerPropsModule = summaryPlayerPropsCount > 0
      ? digilanderModuleOk(null, summaryPlayerPropsCount)
      : digilanderModuleEmpty('No player props for this game');
    teamBreakdownCount = summarySupport.marketPropsCount;
    teamBreakdownModule = summarySupport.marketPropsCount > 0
      ? digilanderModuleOk(null, summarySupport.marketPropsCount)
      : digilanderModuleEmpty('No team prop board for this game');
  } else {
    await mlbPrewarmPromise;
    const [gameDataResult, injuriesResult, playerPropsResult, teamBreakdownResult] = await Promise.allSettled([
      runTimedDigilanderStep(timingsMs, 'gameData', () => buildForecastGameDataBody(eventId)),
      runTimedDigilanderStep(timingsMs, 'injuries', () => buildForecastInjuriesBody(eventId)),
      runTimedDigilanderStep(timingsMs, 'playerProps', () => buildForecastPlayerPropsBody({ eventId })),
      runTimedDigilanderStep(timingsMs, 'teamBreakdown', () => buildForecastTeamPropsBreakdownBody(eventId)),
    ]);

    gameDataModule =
      gameDataResult.status === 'fulfilled'
        ? digilanderModuleOk(
            gameDataResult.value,
            (gameDataResult.value?.players?.length || 0)
            + (gameDataResult.value?.lineProps?.length || 0)
            + (gameDataResult.value?.marketProps?.length || 0)
            + (gameDataResult.value?.injuries?.length || 0),
          )
        : digilanderModuleFromError(gameDataResult.reason, 'Game data failed');
    const injuriesCount =
      injuriesResult.status === 'fulfilled'
        ? digilanderSafeArrayLength(injuriesResult.value)
        : 0;
    injuriesModule =
      injuriesResult.status === 'fulfilled'
        ? (injuriesCount > 0
            ? digilanderModuleOk(injuriesResult.value, injuriesCount)
            : digilanderModuleEmpty('No injuries for this game'))
        : digilanderModuleFromError(injuriesResult.reason, 'Injuries failed');

    const playerPropsCount =
      playerPropsResult.status === 'fulfilled'
        ? (playerPropsResult.value?.playerProps?.length || 0) + (playerPropsResult.value?.marketProps?.length || 0)
        : 0;
    playerPropsModule =
      playerPropsResult.status === 'fulfilled'
        ? (playerPropsCount > 0
            ? digilanderModuleOk(playerPropsResult.value, playerPropsCount)
            : digilanderModuleEmpty(playerPropsResult.value?.fallbackReason === 'game_started'
                ? 'Game already started'
                : 'No player props for this game'))
        : digilanderModuleFromError(playerPropsResult.reason, 'Player props failed');

    teamBreakdownCount =
      teamBreakdownResult.status === 'fulfilled'
        ? (
            (teamBreakdownResult.value?.home?.props?.length || 0)
            + (teamBreakdownResult.value?.home?.marketProps?.length || 0)
            + (teamBreakdownResult.value?.away?.props?.length || 0)
            + (teamBreakdownResult.value?.away?.marketProps?.length || 0)
          )
        : 0;
    teamBreakdownModule =
      teamBreakdownResult.status === 'fulfilled'
        ? (teamBreakdownCount > 0 || teamBreakdownResult.value?.available
            ? digilanderModuleOk(teamBreakdownResult.value, teamBreakdownCount)
            : digilanderModuleEmpty('No team prop board for this game'))
        : digilanderModuleFromError(teamBreakdownResult.reason, 'Team breakdown failed');
  }

  const marketCount =
    marketResult.status === 'fulfilled'
      ? (marketResult.value?.lineMovement?.length || 0) + (marketResult.value?.bookSnapshots?.length || 0)
      : 0;
  const marketModule =
    marketResult.status === 'fulfilled'
      ? (marketCount > 0 ? digilanderModuleOk(marketResult.value, marketCount) : digilanderModuleEmpty('No market history for this game'))
      : digilanderModuleFromError(marketResult.reason, 'Market history failed');

  const rawGamePlayerCount = summaryOnly
    ? (summarySupport?.playerCount || 0)
    : digilanderSafeArrayLength(gameDataModule.data?.players);
  const rawGameLinePropsCount = summaryOnly
    ? (summarySupport?.linePropsCount || 0)
    : digilanderSafeArrayLength(gameDataModule.data?.lineProps);
  const rawGameMarketPropsCount = summaryOnly
    ? (summarySupport?.marketPropsCount || 0)
    : digilanderSafeArrayLength(gameDataModule.data?.marketProps);
  const derivedPlayerPropsAvailable =
    digilanderModuleAvailable(playerPropsModule)
    || rawGameLinePropsCount > 0
    || rawGameMarketPropsCount > 0;
  const derivedPlayerPropsStatus =
    derivedPlayerPropsAvailable
      ? 'ready'
      : playerPropsModule.status === 'locked'
        ? 'locked'
        : playerPropsModule.status === 'failed'
          ? 'failed'
          : 'empty';
  const derivedPlayerPropsReason = derivedPlayerPropsStatus === 'ready'
    ? null
    : playerPropsModule.reason;

  const bestPlaysStatus =
    digilanderModuleAvailable(topPicksModule) || previewModule.status === 'ready' || derivedPlayerPropsAvailable
      ? 'ready'
      : previewModule.status === 'locked' || topPicksModule.status === 'locked' || playerPropsModule.status === 'locked'
        ? 'locked'
        : previewModule.status === 'failed' || topPicksModule.status === 'failed' || playerPropsModule.status === 'failed'
          ? 'failed'
          : 'empty';
  const bestPlaysReason = bestPlaysStatus === 'ready'
    ? null
    : getFirstDigilanderModuleReason(topPicksModule, playerPropsModule, previewModule);
  const playerDataHasSignals = summaryOnly
    ? (rawGamePlayerCount > 0 || rawGameLinePropsCount > 0 || rawGameMarketPropsCount > 0)
    : rawGamePlayerCount > 0 || rawGameLinePropsCount > 0 || rawGameMarketPropsCount > 0 || teamBreakdownCount > 0;
  const playerDataStatus =
    gameDataModule.status === 'locked' || teamBreakdownModule.status === 'locked'
      ? 'locked'
      : gameDataModule.status === 'failed' || teamBreakdownModule.status === 'failed'
        ? 'failed'
        : playerDataHasSignals
          ? 'ready'
          : 'empty';
  const playerDataAvailable = playerDataStatus === 'ready';
  const playerDataReason = playerDataStatus === 'ready'
    ? null
    : getFirstDigilanderModuleReason(gameDataModule, teamBreakdownModule) || 'No player data for this game';
  const injuriesHasSignals = summaryOnly
    ? (summarySupport?.injuriesCount || 0) > 0
    : digilanderSafeArrayLength(injuriesModule.data) > 0;
  const injuriesStatus =
    injuriesModule.status === 'locked'
      ? 'locked'
      : injuriesModule.status === 'failed'
        ? 'failed'
        : injuriesHasSignals
          ? 'ready'
          : 'empty';
  const injuriesAvailable = injuriesStatus === 'ready';
  const injuriesReason = injuriesStatus === 'ready'
    ? null
    : getFirstDigilanderModuleReason(injuriesModule) || 'No injuries for this game';
  const previewAvailable = previewModule.status === 'ready';
  const previewHasSummaryPreview = Boolean(previewModule.status === 'ready' && previewModule.data?.summaryPreview);
  const bestPlaysAvailable =
    digilanderModuleAvailable(topPicksModule)
    || previewAvailable
    || derivedPlayerPropsAvailable;
  const playerPropsPublishedSignalCount = summaryOnly
    ? rawGameLinePropsCount
    : Math.max(digilanderSafeArrayLength(playerPropsModule.data?.playerProps), rawGameLinePropsCount);
  const playerPropsMarketSignalCount = summaryOnly
    ? rawGameMarketPropsCount
    : Math.max(digilanderSafeArrayLength(playerPropsModule.data?.marketProps), rawGameMarketPropsCount);
  const playerPropsSourceSignalCount = playerPropsMarketSignalCount;
  const playerPropsNormalizedSignalCount = summaryOnly && summarySupport?.fallbackUsage?.source === 'mlb_normalized_markets'
    ? playerPropsMarketSignalCount
    : 0;
  const playerPropsFallbackSignalCount = summaryOnly
    ? (summarySupport?.fallbackUsage?.fallbackUsed ? playerPropsMarketSignalCount : 0)
    : (playerPropsModule.data?.fallbackReason === 'no_precomputed_assets' ? playerPropsMarketSignalCount : 0);
  const playerPropsExposedSignalCount = playerPropsPublishedSignalCount + playerPropsMarketSignalCount;
  const bestPlaysCount = topPicksModule.count + playerPropsExposedSignalCount + (previewAvailable && topPicksModule.count + playerPropsExposedSignalCount === 0 ? 1 : 0);
  const playerPropsFeaturedSignalCount = summaryOnly
    ? 0
    : digilanderSafeArrayLength(playerPropsModule.data?.featuredPlayers);
  const playerDataCount = summaryOnly
    ? rawGamePlayerCount + rawGameLinePropsCount + rawGameMarketPropsCount
    : rawGamePlayerCount + rawGameLinePropsCount + rawGameMarketPropsCount + teamBreakdownCount;
  const playerDataPlayerCount = rawGamePlayerCount;
  const playerDataLineBackedCount = summaryOnly
    ? rawGameLinePropsCount + rawGameMarketPropsCount
    : rawGameLinePropsCount + rawGameMarketPropsCount + teamBreakdownCount;
  const injuriesCount = summaryOnly
    ? (summarySupport?.injuriesCount || 0)
    : digilanderSafeArrayLength(injuriesModule.data);
  const marketLineMovementCount = digilanderSafeArrayLength(marketModule.data?.lineMovement);
  const marketBookSnapshotCount = digilanderSafeArrayLength(marketModule.data?.bookSnapshots);
  const marketBookmakerCount = Number(marketModule.data?.summary?.bookmakerCount || 0);
  const marketLastSnapshotAt = marketModule.data?.summary?.lastSnapshotAt || null;
  const marketFreshness = marketModule.data?.summary?.freshness || 'none';
  const teamBreakdownHomeCount = summaryOnly
    ? 0
    : (
        digilanderSafeArrayLength(teamBreakdownModule.data?.home?.props)
        + digilanderSafeArrayLength(teamBreakdownModule.data?.home?.marketProps)
      );
  const teamBreakdownAwayCount = summaryOnly
    ? 0
    : (
        digilanderSafeArrayLength(teamBreakdownModule.data?.away?.props)
        + digilanderSafeArrayLength(teamBreakdownModule.data?.away?.marketProps)
      );
  const moduleReadiness = {
    preview: buildDigilanderModuleReadinessReport({
      status: previewModule.status,
      available: previewAvailable,
      count: previewModule.count,
      reason: previewModule.reason,
      readyReadiness: 'full',
      signals: {
        hasSummaryPreview: previewHasSummaryPreview,
      },
    }),
    bestPlays: buildDigilanderModuleReadinessReport({
      status: bestPlaysStatus,
      available: bestPlaysAvailable,
      count: bestPlaysCount,
      reason: bestPlaysReason,
      readyReadiness: topPicksModule.count > 0 ? 'full' : 'partial',
      signals: {
        topPickCount: topPicksModule.count,
        previewSummaryAvailable: previewHasSummaryPreview,
        playerPropsCount: playerPropsModule.count,
      },
    }),
    playerProps: buildDigilanderModuleReadinessReport({
      status: derivedPlayerPropsStatus,
      available: derivedPlayerPropsAvailable,
      count: playerPropsExposedSignalCount,
      reason: derivedPlayerPropsReason,
      readyReadiness: playerPropsPublishedSignalCount > 0 && playerPropsMarketSignalCount > 0 ? 'full' : 'partial',
      signals: {
        publishedCount: playerPropsPublishedSignalCount,
        sourceCount: playerPropsSourceSignalCount,
        normalizedCount: playerPropsNormalizedSignalCount,
        fallbackCount: playerPropsFallbackSignalCount,
        exposedCount: playerPropsExposedSignalCount,
        marketCount: playerPropsMarketSignalCount,
        featuredPlayerCount: playerPropsFeaturedSignalCount,
      },
    }),
    playerData: buildDigilanderModuleReadinessReport({
      status: playerDataStatus,
      available: playerDataAvailable,
      count: playerDataCount,
      reason: playerDataReason,
      readyReadiness: playerDataPlayerCount > 0 && playerDataLineBackedCount > 0 ? 'full' : 'partial',
      signals: {
        playerCount: playerDataPlayerCount,
        lineBackedCount: playerDataLineBackedCount,
        teamBreakdownCount,
      },
    }),
    injuries: buildDigilanderModuleReadinessReport({
      status: injuriesStatus,
      available: injuriesAvailable,
      count: injuriesCount,
      reason: injuriesReason,
      readyReadiness: 'full',
      signals: {
        injuryCount: injuriesCount,
      },
    }),
    news: buildDigilanderModuleReadinessReport({
      status: newsModule.status,
      available: digilanderModuleAvailable(newsModule),
      count: newsModule.count,
      reason: newsModule.reason,
      readyReadiness: newsModule.count > 1 ? 'full' : 'partial',
      signals: {
        directHitCount: newsModule.count,
      },
    }),
    market: buildDigilanderModuleReadinessReport({
      status: marketModule.status,
      available: digilanderModuleAvailable(marketModule),
      count: marketModule.count,
      reason: marketModule.reason,
      readyReadiness: marketLineMovementCount > 0 && marketBookSnapshotCount > 0 && marketFreshness === 'fresh' ? 'full' : 'partial',
      signals: {
        lineMovementCount: marketLineMovementCount,
        bookSnapshotCount: marketBookSnapshotCount,
        bookmakerCount: marketBookmakerCount,
        freshness: marketFreshness,
        lastSnapshotAt: marketLastSnapshotAt,
      },
    }),
    teamBreakdown: buildDigilanderModuleReadinessReport({
      status: teamBreakdownModule.status,
      available: digilanderModuleAvailable(teamBreakdownModule),
      count: teamBreakdownModule.count,
      reason: teamBreakdownModule.reason,
      readyReadiness: !summaryOnly && teamBreakdownHomeCount > 0 && teamBreakdownAwayCount > 0 ? 'full' : 'partial',
      signals: {
        homeCount: teamBreakdownHomeCount,
        awayCount: teamBreakdownAwayCount,
        summaryOnly,
      },
    }),
  };
  const totalTimingMs = Date.now() - buildStartedAt;

  const coverage = {
      preview: {
        available: previewAvailable,
        status: previewModule.status,
        count: previewModule.count,
        reason: previewModule.reason,
        readiness: moduleReadiness.preview.readiness,
        coverageConfidence: moduleReadiness.preview.coverageConfidence,
      },
      bestPlays: {
        available: bestPlaysAvailable,
        status: bestPlaysStatus,
        count: bestPlaysCount,
        reason: bestPlaysReason,
        readiness: moduleReadiness.bestPlays.readiness,
        coverageConfidence: moduleReadiness.bestPlays.coverageConfidence,
      },
      playerProps: {
        available: derivedPlayerPropsAvailable,
        status: derivedPlayerPropsStatus,
        count: playerPropsExposedSignalCount,
        reason: derivedPlayerPropsReason,
        readiness: moduleReadiness.playerProps.readiness,
        coverageConfidence: moduleReadiness.playerProps.coverageConfidence,
        publishedCount: playerPropsPublishedSignalCount,
        sourceCount: playerPropsSourceSignalCount,
        normalizedCount: playerPropsNormalizedSignalCount,
        fallbackCount: playerPropsFallbackSignalCount,
        exposedCount: playerPropsExposedSignalCount,
        marketCount: playerPropsMarketSignalCount,
        featuredPlayerCount: summaryOnly ? 0 : digilanderSafeArrayLength(playerPropsModule.data?.featuredPlayers),
      },
      playerData: {
        available: playerDataAvailable,
        status: playerDataStatus,
        count: playerDataCount,
        reason: playerDataReason,
        readiness: moduleReadiness.playerData.readiness,
        coverageConfidence: moduleReadiness.playerData.coverageConfidence,
        playerCount: playerDataPlayerCount,
        lineBackedCount: playerDataLineBackedCount,
      },
      injuries: {
        available: injuriesAvailable,
        status: injuriesStatus,
        count: injuriesCount,
        reason: injuriesReason,
        readiness: moduleReadiness.injuries.readiness,
        coverageConfidence: moduleReadiness.injuries.coverageConfidence,
      },
      news: {
        available: digilanderModuleAvailable(newsModule),
        status: newsModule.status,
        count: newsModule.count,
        reason: newsModule.reason,
        readiness: moduleReadiness.news.readiness,
        coverageConfidence: moduleReadiness.news.coverageConfidence,
      },
      newsCoverageReason: newsModule.status === 'ready' ? null : newsModule.reason,
      market: {
        available: digilanderModuleAvailable(marketModule),
        status: marketModule.status,
        count: marketModule.count,
        reason: marketModule.reason,
        readiness: moduleReadiness.market.readiness,
        coverageConfidence: moduleReadiness.market.coverageConfidence,
        lineMovementCount: marketLineMovementCount,
        bookSnapshotCount: marketBookSnapshotCount,
        bookmakerCount: marketBookmakerCount,
        lastSnapshotAt: marketLastSnapshotAt,
        freshness: marketFreshness,
      },
      teamBreakdown: {
        available: digilanderModuleAvailable(teamBreakdownModule),
        status: teamBreakdownModule.status,
        count: teamBreakdownModule.count,
        reason: teamBreakdownModule.reason,
        readiness: moduleReadiness.teamBreakdown.readiness,
        coverageConfidence: moduleReadiness.teamBreakdown.coverageConfidence,
      },
  };

  const body = {
    contractVersion: 'digilander-v1',
    eventId,
    signedIn: Boolean(params.signedIn),
    event: {
      league: curated.league || null,
      sport,
      homeTeam: curated.home_team || null,
      awayTeam: curated.away_team || null,
      homeShort: curated.home_short || null,
      awayShort: curated.away_short || null,
      startsAt: curated.starts_at || null,
    },
    coverage,
    preview: digilanderSummarizeModule(previewModule, summaryOnly),
    topPicks: digilanderSummarizeModule(topPicksModule, summaryOnly),
    news: digilanderSummarizeModule(newsModule, summaryOnly),
    gameData: digilanderSummarizeModule(gameDataModule, summaryOnly),
    injuries: digilanderSummarizeModule(injuriesModule, summaryOnly),
    market: digilanderSummarizeModule(marketModule, summaryOnly),
    playerProps: digilanderSummarizeModule(playerPropsModule, summaryOnly),
    teamBreakdown: digilanderSummarizeModule(teamBreakdownModule, summaryOnly),
    diagnostics: {
      scope: params.signedIn ? 'signed-in' : 'public',
      summaryOnly,
      cacheable: Boolean(cacheKey),
      fallbackUsage,
      moduleReadiness,
      timeBudgetsMs: DIGILANDER_MODULE_TIMEOUTS_MS,
      timingsMs: {
        ...timingsMs,
        total: totalTimingMs,
      },
    },
  };

  writeDigilanderBundleCache(cacheKey, summaryOnly, body);
  return body;
}

export async function buildDigilanderBundleBody(params: {
  eventId: string;
  signedIn?: boolean;
  summaryOnly?: boolean;
}): Promise<any> {
  const cacheKey = getDigilanderBundleCacheKey(params);
  const cachedBody = readDigilanderBundleCache(cacheKey);
  if (cachedBody) {
    return attachDigilanderRequestDiagnostics(cachedBody, { cacheStatus: 'hit', coalesced: false });
  }

  const requestKey = getDigilanderBundleRequestKey(params);
  const inFlight = DIGILANDER_BUNDLE_IN_FLIGHT.get(requestKey);
  if (inFlight) {
    const body = await inFlight;
    return attachDigilanderRequestDiagnostics(body, { cacheStatus: 'miss', coalesced: true });
  }

  const pendingBuild = buildDigilanderBundleBodyBase(params)
    .finally(() => {
      DIGILANDER_BUNDLE_IN_FLIGHT.delete(requestKey);
    });
  DIGILANDER_BUNDLE_IN_FLIGHT.set(requestKey, pendingBuild);

  const body = await pendingBuild;
  return attachDigilanderRequestDiagnostics(body, { cacheStatus: 'miss', coalesced: false });
}

// GET /api/forecast/:eventId/digilander — bundled game-view contract for Digilander
router.get('/:eventId/digilander', optionalAuth, async (req: Request, res: Response) => {
  try {
    const eventId = String(req.params.eventId || '');
    const summaryOnly = ['1', 'true', 'yes'].includes(String(req.query.summary || '').trim().toLowerCase());
    res.json(await buildDigilanderBundleBody({
      eventId,
      signedIn: Boolean(req.user?.userId),
      summaryOnly,
    }));
  } catch (err) {
    if ((err as any)?.status) {
      res.status((err as any).status).json({ error: (err as Error).message });
      return;
    }
    console.error('Digilander bundle error:', err);
    res.status(500).json({ error: 'Failed to build Digilander view' });
  }
});

// GET /api/forecast/:eventId?league=nba
router.get('/:eventId', authMiddleware, async (req: Request, res: Response) => {
  const eventId = String(req.params.eventId);
  const league = String(req.query.league || '').toLowerCase();
  const userId = req.user!.userId;
  const trace = createForecastOpenTrace(eventId, league, userId);

  try {
    // 1. Look up cheap local sources first. Do not block cached opens on SGO.
    const curated = await traceForecastOpenStep(trace, 'getCuratedEvent', () => getCuratedEvent(eventId));
    let cached = await traceForecastOpenStep(trace, 'getCachedForecastExact', () => getCachedForecast(eventId));
    const sgoEvent = !curated && !cached
      ? await traceForecastOpenStep(trace, 'fetchSgoEvent', () => fetchSgoEvent(eventId, league))
      : null;
    setForecastOpenTraceNote(trace, 'hasCuratedEvent', !!curated);
    setForecastOpenTraceNote(trace, 'exactCacheHit', !!cached);
    setForecastOpenTraceNote(trace, 'usedSgoLookup', !!sgoEvent);

    // Build odds from best available source
    let eventOdds: { moneyline: any; spread: any; total: any } | null = null;
    let homeTeam = '';
    let awayTeam = '';
    let homeShort = '';
    let awayShort = '';

    if (curated) {
      eventOdds = {
        moneyline: curated.moneyline || { home: null, away: null },
        spread: curated.spread || { home: null, away: null },
        total: curated.total || { over: null, under: null },
      };
      homeTeam = curated.home_team;
      awayTeam = curated.away_team;
      homeShort = curated.home_short;
      awayShort = curated.away_short;
    } else if (sgoEvent) {
      const parsed = parseOdds(sgoEvent);
      eventOdds = { moneyline: parsed.moneyline, spread: parsed.spread, total: parsed.total };
      homeTeam = sgoEvent.teams?.home?.names?.long || '';
      awayTeam = sgoEvent.teams?.away?.names?.long || '';
      homeShort = sgoEvent.teams?.home?.names?.short || '';
      awayShort = sgoEvent.teams?.away?.names?.short || '';
    }

    // 2. Resolve the cache row early so ownership survives alias event IDs.
    if (!cached) {
      cached = await traceForecastOpenStep(trace, 'resolveCachedForecast', () => resolveCachedForecastForRequest({ eventId, curated, sgoEvent }));
    }
    const resolvedEventId = cached?.event_id || curated?.event_id || eventId;
    setForecastOpenTraceNote(trace, 'resolvedEventId', resolvedEventId);
    setForecastOpenTraceNote(trace, 'resolvedCacheHit', !!cached);

    if ((!homeShort || !awayShort) && cached) {
      const resolvedShorts = await traceForecastOpenStep(trace, 'resolveEventShortNames', () => resolveEventShortNames({
        eventId: resolvedEventId,
        homeShort: homeShort || null,
        awayShort: awayShort || null,
        homeTeam: homeTeam || cached.home_team,
        awayTeam: awayTeam || cached.away_team,
        startsAt: cached.starts_at || curated?.starts_at || sgoEvent?.status?.startsAt || null,
      }));
      homeShort = homeShort || resolvedShorts.homeShort || '';
      awayShort = awayShort || resolvedShorts.awayShort || '';
      setForecastOpenTraceNote(trace, 'usedShortNameFallback', true);
    }

    const user = await traceForecastOpenStep(trace, 'findUserById', () => findUserById(userId));
    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }
    const isRainMan = user.is_weatherman;

    // 3. Check if already purchased
    const alreadyPurchased = await traceForecastOpenStep(trace, 'hasUserPurchasedPick', () => hasUserPurchasedPick(userId, resolvedEventId, cached?.id || null));
    setForecastOpenTraceNote(trace, 'alreadyPurchased', alreadyPurchased);
    if (alreadyPurchased) {
      if (cached) {
        if (eventOdds) {
          refreshCachedOddsAsync(cached.event_id || eventId, eventOdds);
        }
        const [ownedBal, insightBundle, basemonSummary] = await Promise.all([
          traceForecastOpenStep(trace, 'getPickBalance', () => getPickBalance(userId)),
          traceForecastOpenStep(trace, 'loadInsightBundle', () => loadInsightBundle(resolvedEventId, userId, cached.forecast_data, cached.league || league)),
          traceForecastOpenStep(trace, 'fetchBasemonSummary', () => fetchBasemonSummary({
            eventId: resolvedEventId,
            league: cached.league || league,
            startsAt: cached.starts_at || curated?.starts_at || null,
            homeShort: homeShort || null,
            awayShort: awayShort || null,
            homeTeam: homeTeam || cached.home_team,
            awayTeam: awayTeam || cached.away_team,
          })),
        ]);
        const filteredForecast = await traceForecastOpenStep(trace, 'buildForecastPayload', async () => buildPublicForecastContent(
          await filterPropsByIntegrity(
            await hydrateForecastPropHighlightsForResponse(cached, resolvedEventId, basemonSummary),
            resolvedEventId,
          ),
          `forecast:${eventId}:owned`,
          {
            homeTeam: homeTeam || cached.home_team,
            awayTeam: awayTeam || cached.away_team,
            homeShort,
            awayShort,
            league: cached.league || league,
            odds: eventOdds || cached.odds_data || null,
          },
        ));
        setForecastOpenTraceNote(trace, 'responsePath', 'already_purchased_cached');
        void trackTikTokForecastView({
          req,
          user: { id: user.id, email: user.email },
          eventId: resolvedEventId,
          league: cached.league || league,
          homeTeam: homeTeam || cached.home_team,
          awayTeam: awayTeam || cached.away_team,
        }).catch((err) => {
          console.error('[forecast] TikTok ViewContent tracking failed (non-blocking):', err);
        });
        res.json({
          forecast: filteredForecast,
          confidence: cached.composite_confidence ?? cached.confidence_score,
          homeTeam: homeTeam || cached.home_team,
          awayTeam: awayTeam || cached.away_team,
          homeShort,
          awayShort,
          league: cached.league,
          alreadyOwned: true,
          odds: sanitizeOddsBundle(eventOdds || cached.odds_data || null, cached.league || league || null),
          openingLines: buildOpeningLines(curated, cached),
          modelLines: buildModelLines(cached.forecast_data),
          ...buildForecastBalanceFields(computeTotalBalance(ownedBal)),
          insightAvailability: insightBundle.insightMeta.insightAvailability,
          unlockedInsights: insightBundle.insightMeta.unlockedInsights,
          unlockedInsightData: insightBundle.unlockedInsightData,
          basemonSummary,
          ...buildForecastTransparencyFields(cached, cached.league),
        });
        reportForecastOpen({
          eventId,
          league: cached.league || league,
          userId,
          cacheStatus: 'hit',
          trace,
        });
        return;
      }
    }

    // 4. Check user + email verification
    // Gate: require email verification for non-weathermen
    if (!isRainMan && !alreadyPurchased && !user.email_verified && !isInGracePeriod(user)) {
      res.status(403).json({
        error: 'Email verification required',
        needsVerification: true,
        message: 'Verify your email to unlock forecasts.',
      });
      return;
    }

    // 5. Ensure daily grant + check balance (skip for weathermen)
    if (!isRainMan && !alreadyPurchased) {
      await traceForecastOpenStep(trace, 'ensureDailyGrant', () => ensureDailyGrant(userId));
      const balance = await traceForecastOpenStep(trace, 'getPickBalance', () => getPickBalance(userId));
      const total = computeTotalBalance(balance);
      if (total <= 0) {
        // Check if user has ever completed a survey — if not, prompt survey first
        const { rows: surveyCheck } = await pool.query(
          `SELECT 1 FROM rm_survey_responses WHERE user_id = $1 LIMIT 1`,
          [userId]
        );
        const needsSurvey = surveyCheck.length === 0;

        res.status(402).json({
          error: 'No forecasts remaining',
          message: needsSurvey
            ? 'Complete a quick survey to unlock your free forecasts!'
            : 'Get more forecasts to unlock this analysis',
          needsSurvey,
          options: [
            { type: 'single_pick', label: 'Single Forecast', price: '$1', picks: 1 },
            { type: 'daily_pass', label: 'Day Pass (999 Forecasts)', price: '$4.99', picks: 999 },
          ],
        });
        return;
      }
    }

    // 6. If we have a curated event, generate forecast directly
    if (curated) {
      if (!cached) {
        // No inline generation — workers are the only write path
        await sendForecastNotReadyResponse({
          res,
          trace,
          eventId,
          resolvedEventId,
          userId,
          league: curated.league,
          homeTeam: curated.home_team,
          awayTeam: curated.away_team,
          homeShort: curated.home_short,
          awayShort: curated.away_short,
          odds: eventOdds,
          responsePath: 'curated_not_ready',
        });
        return;
      } else {
        if (eventOdds) {
          refreshCachedOddsAsync(cached.event_id || eventId, eventOdds);
        }
      }

      const claimedPick = !alreadyPurchased
        ? await traceForecastOpenStep(trace, 'recordPick', () => recordPick({ userId, forecastId: cached.id, eventId: resolvedEventId, wasRainMan: isRainMan }))
        : null;

      if (!alreadyPurchased && claimedPick?.inserted && !isRainMan) {
        const { success, source } = await traceForecastOpenStep(trace, 'deductPick', () => deductPick(userId));
        if (!success) {
          await deletePick(userId, resolvedEventId);
          res.status(402).json({ error: 'Failed to deduct forecast' });
          return;
        }
        const newBalance = await traceForecastOpenStep(trace, 'getPickBalanceAfterDeduct', () => getPickBalance(userId));
        const totalAfter = computeTotalBalance(newBalance);
        await recordLedgerEntry(userId, -1, 'FORECAST_UNLOCK', totalAfter, { eventId: resolvedEventId, source });
      }

      const [bal, insightBundle2, basemonSummary] = await Promise.all([
        traceForecastOpenStep(trace, 'getPickBalance', () => getPickBalance(userId)),
        traceForecastOpenStep(trace, 'loadInsightBundle', () => loadInsightBundle(resolvedEventId, userId, cached.forecast_data, curated.league)),
        traceForecastOpenStep(trace, 'fetchBasemonSummary', () => fetchBasemonSummary({
          eventId: resolvedEventId,
          league: curated.league,
          startsAt: curated.starts_at || cached.starts_at || null,
          homeShort: curated.home_short || null,
          awayShort: curated.away_short || null,
          homeTeam: curated.home_team,
          awayTeam: curated.away_team,
        })),
      ]);

      // Capture CLV picks (non-blocking)
      captureGameClv(
        eventId,
        curated.league,
        cached.forecast_data,
        eventOdds,
        curated.home_team,
        curated.away_team,
        cached.composite_confidence
      ).catch(() => {});

      const filteredForecast2 = await traceForecastOpenStep(trace, 'buildForecastPayload', async () => buildPublicForecastContent(
        await filterPropsByIntegrity(
          await hydrateForecastPropHighlightsForResponse(cached, resolvedEventId, basemonSummary),
          resolvedEventId,
        ),
        `forecast:${eventId}:curated`,
        {
          homeTeam: curated.home_team,
          awayTeam: curated.away_team,
          homeShort: curated.home_short,
          awayShort: curated.away_short,
          league: curated.league,
          odds: eventOdds,
        },
      ));
      setForecastOpenTraceNote(trace, 'responsePath', 'curated_cached_hit');
      void trackTikTokForecastView({
        req,
        user: { id: user.id, email: user.email },
        eventId: resolvedEventId,
        league: curated.league,
        homeTeam: curated.home_team,
        awayTeam: curated.away_team,
      }).catch((err) => {
        console.error('[forecast] TikTok ViewContent tracking failed (non-blocking):', err);
      });
      res.json({
        forecast: filteredForecast2,
        confidence: cached.composite_confidence ?? cached.confidence_score,
        homeTeam: curated.home_team,
        awayTeam: curated.away_team,
        homeShort: curated.home_short,
        awayShort: curated.away_short,
        league: curated.league,
        alreadyOwned: true,
        odds: sanitizeOddsBundle(eventOdds, curated.league || league || null),
        openingLines: buildOpeningLines(curated, cached),
        modelLines: buildModelLines(cached.forecast_data),
        ...buildForecastBalanceFields(computeTotalBalance(bal)),
        insightAvailability: insightBundle2.insightMeta.insightAvailability,
        unlockedInsights: insightBundle2.insightMeta.unlockedInsights,
        unlockedInsightData: insightBundle2.unlockedInsightData,
        basemonSummary,
        ...buildForecastTransparencyFields(cached, curated.league),
      });
      reportForecastOpen({
        eventId,
        league: curated.league,
        userId,
        cacheStatus: 'hit',
        trace,
      });
      return;
    }

    // 7. SGO path — need league for lookup
    if (!league || !LEAGUE_MAP[league]) {
      res.status(400).json({ error: 'League parameter required' });
      return;
    }

    if (cached) {
      if (eventOdds) {
        refreshCachedOddsAsync(cached.event_id || eventId, eventOdds);
      }
      const claimedPick = !alreadyPurchased
        ? await traceForecastOpenStep(trace, 'recordPick', () => recordPick({ userId, forecastId: cached.id, eventId: resolvedEventId, wasRainMan: isRainMan }))
        : null;
      if (!alreadyPurchased && claimedPick?.inserted && !isRainMan) {
        const { success, source } = await traceForecastOpenStep(trace, 'deductPick', () => deductPick(userId));
        if (!success) {
          await deletePick(userId, resolvedEventId);
          res.status(402).json({ error: 'Failed to deduct forecast' });
          return;
        }
        const newBalance = await traceForecastOpenStep(trace, 'getPickBalanceAfterDeduct', () => getPickBalance(userId));
        await recordLedgerEntry(userId, -1, 'FORECAST_UNLOCK', computeTotalBalance(newBalance), { eventId: resolvedEventId, source });
      }
      const [bal2, insightBundle3, basemonSummary] = await Promise.all([
        traceForecastOpenStep(trace, 'getPickBalance', () => getPickBalance(userId)),
        traceForecastOpenStep(trace, 'loadInsightBundle', () => loadInsightBundle(resolvedEventId, userId, cached.forecast_data, cached.league || league)),
        traceForecastOpenStep(trace, 'fetchBasemonSummary', () => fetchBasemonSummary({
          eventId: resolvedEventId,
          league: cached.league || league,
          startsAt: cached.starts_at || sgoEvent?.status?.startsAt || null,
          homeShort: homeShort || null,
          awayShort: awayShort || null,
          homeTeam: homeTeam || cached.home_team,
          awayTeam: awayTeam || cached.away_team,
        })),
      ]);
      const filteredForecast3 = await traceForecastOpenStep(trace, 'buildForecastPayload', async () => buildPublicForecastContent(
        await filterPropsByIntegrity(
          await hydrateForecastPropHighlightsForResponse(cached, resolvedEventId, basemonSummary),
          resolvedEventId,
        ),
        `forecast:${eventId}:fallback`,
        {
          homeTeam: homeTeam || cached.home_team,
          awayTeam: awayTeam || cached.away_team,
          homeShort: homeShort || null,
          awayShort: awayShort || null,
          league: cached.league || league,
          odds: eventOdds || cached.odds_data || null,
        },
      ));
      setForecastOpenTraceNote(trace, 'responsePath', 'sgo_cached_hit');
      void trackTikTokForecastView({
        req,
        user: { id: user.id, email: user.email },
        eventId: resolvedEventId,
        league: cached.league || league,
        homeTeam: homeTeam || cached.home_team,
        awayTeam: awayTeam || cached.away_team,
      }).catch((err) => {
        console.error('[forecast] TikTok ViewContent tracking failed (non-blocking):', err);
      });
      res.json({
        forecast: filteredForecast3,
        confidence: cached.composite_confidence ?? cached.confidence_score,
        homeTeam: homeTeam || cached.home_team,
        awayTeam: awayTeam || cached.away_team,
        homeShort: homeShort || '',
        awayShort: awayShort || '',
        league: cached.league,
        alreadyOwned: true,
        odds: sanitizeOddsBundle(eventOdds || cached.odds_data || null, cached.league || league || null),
        openingLines: buildOpeningLines(null, cached),
        modelLines: buildModelLines(cached.forecast_data),
        ...buildForecastBalanceFields(computeTotalBalance(bal2)),
        insightAvailability: insightBundle3.insightMeta.insightAvailability,
        unlockedInsights: insightBundle3.insightMeta.unlockedInsights,
        unlockedInsightData: insightBundle3.unlockedInsightData,
        basemonSummary,
        ...buildForecastTransparencyFields(cached, cached.league),
      });
      reportForecastOpen({
        eventId,
        league: cached.league || league,
        userId,
        cacheStatus: 'hit',
        trace,
      });
      return;
    }

    if (!sgoEvent) {
      res.status(404).json({ error: 'Event not found' });
      setForecastOpenTraceNote(trace, 'responsePath', 'event_not_found');
      reportForecastOpen({
        eventId,
        league,
        userId,
        cacheStatus: 'miss',
        trace,
      });
      return;
    }

    // 8. No inline generation from SGO — return 202 "not ready"
    await sendForecastNotReadyResponse({
      res,
      trace,
      eventId,
      resolvedEventId,
      userId,
      league,
      homeTeam: sgoEvent.teams?.home?.names?.long || '',
      awayTeam: sgoEvent.teams?.away?.names?.long || '',
      homeShort: sgoEvent.teams?.home?.names?.short || '',
      awayShort: sgoEvent.teams?.away?.names?.short || '',
      odds: eventOdds,
      responsePath: 'sgo_not_ready',
    });
  } catch (err) {
    console.error('Forecast error:', err);
    setForecastOpenTraceNote(trace, 'responsePath', 'error');
    logForecastOpenTrace({
      eventId,
      league,
      cacheStatus: 'error',
      success: false,
      trace,
      errorMessage: err instanceof Error ? err.message : 'Forecast route failed',
    }).catch(() => {});
    res.status(500).json({ error: 'Failed to generate forecast' });
  }
});

// GET /api/forecast/:eventId/team-props-breakdown — returns both teams' props for the Game Props Breakdown section
// Available to all logged-in users (no credit deduction). Reads from precomputed + cache.
router.get('/:eventId/team-props-breakdown', authMiddleware, async (req: Request, res: Response) => {
  try {
    const eventId = String(req.params.eventId);

    const curated = await getCuratedEvent(eventId);
    if (!curated) {
      res.json({ home: null, away: null, available: false });
      return;
    }

    const league = (curated.league || '').toLowerCase();

    // Pull precomputed TEAM_PROPS for both sides
    const { rows: precomputedRows } = await pool.query(
      `SELECT team_side, player_name, forecast_payload, confidence_score
       FROM rm_forecast_precomputed
       WHERE event_id = $1 AND forecast_type = 'TEAM_PROPS' AND status = 'ACTIVE'
       ORDER BY confidence_score DESC NULLS LAST`,
      [eventId]
    ).catch(() => ({ rows: [] as any[] }));

    // Also pull from rm_team_props_cache as fallback
    const { rows: cacheRows } = await pool.query(
      'SELECT team, props_data FROM rm_team_props_cache WHERE event_id = $1',
      [eventId]
    ).catch(() => ({ rows: [] as any[] }));

    const buildTeamData = async (side: 'home' | 'away') => {
      const teamName = side === 'home' ? curated.home_team : curated.away_team;
      const teamShort = side === 'home' ? curated.home_short : curated.away_short;

      // Precomputed TEAM_PROPS rows contain forecast_payload.props[] as an array of player props
      const precomputed = precomputedRows.filter((r: any) => r.team_side === side);
      const hasPoisonedPrecomputed = precomputed.some((row: any) => hasPoisonedLegacyTeamProps(row.forecast_payload));
      if (precomputed.length > 0 && !hasPoisonedPrecomputed) {
        const allProps: any[] = [];
        let suppressedReason: string | null = null;
        for (const r of precomputed) {
          const payload = r.forecast_payload || {};
          const propsArray = Array.isArray(payload.props)
            ? payload.props.filter((prop: any) => shouldRenderTeamPropBundleEntry(prop))
            : [];
          suppressedReason = suppressedReason
            || payload?.suppressed_reason
            || payload?.metadata?.suppressed_reason
            || payload?.metadata?.mlb_suppressed_reason
            || null;
          for (const p of propsArray) {
            allProps.push({
              player: p.player || r.player_name || null,
              prop: p.prop ? sanitizePublicTextBlock(String(p.prop), { totalBrandMentions: 0, warnings: [] }, `breakdown:${p.player}`) : null,
              recommendation: p.recommendation || null,
              reasoning: p.reasoning ? sanitizePublicTextBlock(String(p.reasoning), { totalBrandMentions: 0, warnings: [] }, `breakdown-reason:${p.player}`) : null,
              edge: p.edge ?? p.edge_pct ?? null,
              prob: p.prob ?? p.projected_probability ?? null,
              line: p.market_line_value ?? p.line ?? null,
              odds: p.odds ?? null,
              projectedOutcome: p.projected_stat_value ?? p.projectedOutcome ?? null,
              confidence: p.prob ?? r.confidence_score ?? null,
              modelContext: camelizeObjectKeys(p.model_context),
              ...(league === 'mlb' ? { mlbPropContext: serializeMlbPropContext(p.model_context, league, true) } : {}),
            });
          }
          // If no props array, treat as single prop entry (older format)
          if (propsArray.length === 0 && payload.player && shouldRenderTeamPropBundleEntry(payload)) {
            allProps.push({
              player: payload.player || r.player_name || null,
              prop: payload.prop ? sanitizePublicTextBlock(String(payload.prop), { totalBrandMentions: 0, warnings: [] }, `breakdown:${payload.player}`) : null,
              recommendation: payload.recommendation || null,
              reasoning: payload.reasoning ? sanitizePublicTextBlock(String(payload.reasoning), { totalBrandMentions: 0, warnings: [] }, `breakdown-reason:${payload.player}`) : null,
              edge: payload.edge ?? payload.edge_pct ?? null,
              prob: payload.prob ?? payload.projected_probability ?? null,
              line: payload.market_line_value ?? payload.line ?? null,
              odds: payload.odds ?? null,
              projectedOutcome: payload.projected_stat_value ?? payload.projectedOutcome ?? null,
              confidence: payload.prob ?? r.confidence_score ?? null,
              modelContext: camelizeObjectKeys(payload.model_context),
              ...(league === 'mlb' ? { mlbPropContext: serializeMlbPropContext(payload.model_context, league, true) } : {}),
            });
          }
        }
        const marketProps = await maybeBuildMlbBreakdownMarketProps({
          league,
          curated,
          side,
          existingProps: allProps,
        });
        return {
          team: teamName,
          short: teamShort,
          side,
          props: allProps,
          marketProps,
          suppressed_reason: suppressedReason,
        };
      }

      // Fallback: cached team props
      const cached = cacheRows.find((r: any) => r.team === side);
      if (cached?.props_data && !hasPoisonedLegacyTeamProps(cached.props_data)) {
        const normalized = normalizeLegacyTeamPropsResponse(cached.props_data, league);
        const items = Array.isArray(normalized?.playerProps)
          ? normalized.playerProps
          : Array.isArray(normalized?.props)
            ? normalized.props
            : [];
        const filteredItems = items
          .filter((p: any) => shouldRenderTeamPropBundleEntry(p))
          .map((p: any) => normalizeLegacyTeamPropItem(p, league));
        const marketProps = await maybeBuildMlbBreakdownMarketProps({
          league,
          curated,
          side,
          existingProps: filteredItems,
        });
        return {
          team: teamName,
          short: teamShort,
          side,
          props: filteredItems,
          marketProps,
          suppressed_reason: normalized?.metadata?.suppressed_reason || normalized?.metadata?.mlb_suppressed_reason || null,
        };
      }

      const regenerated = await loadOrRegenerateLegacyTeamPropsForSide({
        eventId,
        curated,
        team: side,
      });
      if (countLegacyTeamProps(regenerated) > 0) {
        const items = Array.isArray(regenerated?.playerProps)
          ? regenerated.playerProps
          : Array.isArray(regenerated?.props)
            ? regenerated.props
            : [];
        const filteredItems = items
          .filter((p: any) => shouldRenderTeamPropBundleEntry(p))
          .map((p: any) => normalizeLegacyTeamPropItem(p, league));
        const marketProps = await maybeBuildMlbBreakdownMarketProps({
          league,
          curated,
          side,
          existingProps: filteredItems,
        });
        return {
          team: teamName,
          short: teamShort,
          side,
          props: filteredItems,
          marketProps,
          suppressed_reason: regenerated?.metadata?.suppressed_reason || regenerated?.metadata?.mlb_suppressed_reason || null,
        };
      }

      const marketProps = await maybeBuildMlbBreakdownMarketProps({
        league,
        curated,
        side,
        existingProps: [],
      });
      if (marketProps.length > 0) {
        return {
          team: teamName,
          short: teamShort,
          side,
          props: [],
          marketProps,
          suppressed_reason: null,
        };
      }

      return null;
    };

    const home = await buildTeamData('home');
    const away = await buildTeamData('away');

    res.json({
      available: !!(home || away),
      home,
      away,
    });
  } catch (err) {
    console.error('Team props breakdown error:', err);
    res.json({ home: null, away: null, available: false });
  }
});

// GET /api/forecast/:eventId/player-props — precomputed individual player props
router.get('/:eventId/player-props', authMiddleware, async (req: Request, res: Response) => {
  try {
    const eventId = String(req.params.eventId);
    const teamQuery = req.query.team ? String(req.query.team) : null;
    const team: 'home' | 'away' | null = teamQuery === 'home' || teamQuery === 'away' ? teamQuery : null;
    const userId = req.user!.userId;

    const user = await findUserById(userId);
    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    const isRainMan = user.is_weatherman;
    const getGameStarted = async () => {
      const { rows: eventRows } = await pool.query(
        'SELECT starts_at FROM rm_events WHERE event_id = $1 LIMIT 1',
        [eventId],
      );
      const startsAt = eventRows[0]?.starts_at ? new Date(eventRows[0].starts_at) : null;
      return Boolean(startsAt && !Number.isNaN(startsAt.getTime()) && startsAt.getTime() <= Date.now());
    };

    // ── Per-player unlock mode ──
    if (PER_PLAYER_UNLOCK_ENABLED()) {
      let { rows, rawRowCount: preFilterRowCount } = await resolveVisiblePlayerPropRows(eventId, team);
      let fallbackCurated: any | null = null;

      if (preFilterRowCount === 0) {
        const backfill = await backfillMissingMlbPlayerProps({ eventId, team });
        fallbackCurated = backfill.curated;
        if (backfill.wroteAssets) {
          ({ rows, rawRowCount: preFilterRowCount } = await resolveVisiblePlayerPropRows(eventId, team));
        }
      } else {
        const staleRefresh = await refreshStaleMlbPlayerPropsOnRead({ eventId, team, rows });
        fallbackCurated = staleRefresh.curated;
        if (staleRefresh.wroteAssets) {
          ({ rows, rawRowCount: preFilterRowCount } = await resolveVisiblePlayerPropRows(eventId, team));
        }
      }

      // Determine which assets are unlocked for this user
      let unlockedAssetIds = new Set<string>();

      if (isRainMan) {
        // Weathermen see everything unlocked
        rows.forEach((r: any) => unlockedAssetIds.add(r.id));
      } else {
        // Check per-player unlocks
        const { rows: perPlayerUnlocks } = await pool.query(
          'SELECT forecast_asset_id FROM rm_player_prop_unlocks WHERE user_id = $1',
          [userId]
        );
        perPlayerUnlocks.forEach((u: any) => unlockedAssetIds.add(u.forecast_asset_id));

        // Dual-read: also check legacy team unlocks
        if (DUAL_READ_ENABLED()) {
          const { rows: teamUnlocks } = await pool.query(
            'SELECT team FROM rm_team_props_unlocks WHERE user_id = $1 AND event_id = $2',
            [userId, eventId]
          );
          const unlockedTeams = new Set(teamUnlocks.map((u: any) => u.team));
          rows.forEach((r: any) => {
            if (unlockedTeams.has(r.team_side)) {
              unlockedAssetIds.add(r.id);
            }
          });
        }
      }

      const playerProps = rows
        .map((r: any) => {
          const isUnlocked = unlockedAssetIds.has(r.id);
          const publicProp = r.forecast_payload?.prop
            ? sanitizePublicTextBlock(String(r.forecast_payload.prop), { totalBrandMentions: 0, warnings: [] }, `player-prop:${r.id}.prop`)
            : null;
          const taxonomy = buildPlayerPropTaxonomy(r.forecast_payload);

          return serializePublicPlayerProp({
            assetId: r.id,
            player: r.player_name,
            team: r.team_id,
            teamSide: r.team_side,
            league: r.league,
            prop: publicProp,
            locked: !isUnlocked,
            confidence: isUnlocked ? r.confidence_score : null,
            payload: r.forecast_payload,
            marketType: taxonomy.marketType,
            marketFamily: taxonomy.marketFamily,
            marketOrigin: taxonomy.marketOrigin,
            sourceBacked: taxonomy.sourceBacked,
            playerRole: taxonomy.playerRole,
            includeMlbPropContext: MLB_PROP_CONTEXT_V2(),
          });
        })
        .filter((prop): prop is NonNullable<typeof prop> => Boolean(prop));
      const curatedEvent = await getCuratedEvent(eventId);
      const lineupSnapshot = await fetchEventLineupSnapshot(curatedEvent);
      const marketProps = await buildSourceBackedPlayerPropMarketBoard({
        curated: curatedEvent,
        team,
        existingProps: playerProps,
      });
      const featuredRows = rows.map((row: any) => {
          const descriptor = describeForecastAsset('PLAYER_PROP', row.forecast_payload);
          return {
            ...row,
            locked: !unlockedAssetIds.has(row.id),
            playerRole: descriptor.playerRole,
          };
        });
      const supplementalFeaturedRows = await buildSupplementalFeaturedPlayerRows({
        eventId,
        curated: curatedEvent,
        lineups: lineupSnapshot,
        existingRows: featuredRows,
      });
      const featuredPlayers = buildFeaturedPlayersFromRows(
        [...featuredRows, ...supplementalFeaturedRows],
        {
          lineups: lineupSnapshot,
          limit: 10,
          perTeamTarget: 5,
          homeTeamId: curatedEvent?.home_short || curatedEvent?.home_team || null,
          awayTeamId: curatedEvent?.away_short || curatedEvent?.away_team || null,
        },
      ).map((player) => ({
        ...player,
        analysis: player.analysis
          ? sanitizePublicTextBlock(player.analysis, { totalBrandMentions: 0, warnings: [] }, `player-radar:${eventId}:${player.player}.analysis`)
          : null,
        props: player.props.map((prop) => ({
          ...prop,
          analysis: prop.analysis
            ? sanitizePublicTextBlock(prop.analysis, { totalBrandMentions: 0, warnings: [] }, `player-radar:${eventId}:${player.player}:${prop.assetId}.analysis`)
            : null,
        })),
      }));

      const fallbackStartsAt = fallbackCurated?.starts_at ? new Date(fallbackCurated.starts_at) : null;
      const gameStarted = preFilterRowCount === 0
        ? (fallbackStartsAt && !Number.isNaN(fallbackStartsAt.getTime())
            ? fallbackStartsAt.getTime() <= Date.now()
            : await getGameStarted())
        : false;
      const fallbackReason = preFilterRowCount === 0
        ? gameStarted
          ? 'game_started'
          : 'no_precomputed_assets'
        : playerProps.length === 0
          ? 'all_assets_filtered_out'
          : null;
      const bal = await getPickBalance(userId);
      const hasPrecomputedPlayerProps = playerProps.length > 0;
      res.json({
        contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
        playerProps,
        marketProps,
        players: buildPublicGroupedPlayers(playerProps.filter((prop) => !prop.locked)),
        featuredPlayers,
        count: playerProps.length,
        mode: hasPrecomputedPlayerProps ? 'per_player' : 'team',
        fallbackReason,
        ...buildForecastBalanceFields(computeTotalBalance(bal)),
      });
      return;
    }

    // ── Legacy team-level gate (flag off) ──
    if (!isRainMan) {
      const { rows: unlocks } = await pool.query(
        'SELECT team FROM rm_team_props_unlocks WHERE user_id = $1 AND event_id = $2',
        [userId, eventId]
      );
      if (unlocks.length === 0) {
        res.status(403).json({ error: 'Unlock team props first to view player props' });
        return;
      }
      if (team && !unlocks.some((u: any) => u.team === team)) {
        res.status(403).json({ error: `Team props for ${team} not unlocked` });
        return;
      }
    }

    let { rows, rawRowCount: preFilterRowCount } = await resolveVisiblePlayerPropRows(eventId, team, ' (legacy)');
    let fallbackCurated: any | null = null;

    if (preFilterRowCount === 0) {
      const backfill = await backfillMissingMlbPlayerProps({ eventId, team });
      fallbackCurated = backfill.curated;
      if (backfill.wroteAssets) {
        ({ rows, rawRowCount: preFilterRowCount } = await resolveVisiblePlayerPropRows(eventId, team, ' (legacy)'));
      }
    } else {
      const staleRefresh = await refreshStaleMlbPlayerPropsOnRead({ eventId, team, rows });
      fallbackCurated = staleRefresh.curated;
      if (staleRefresh.wroteAssets) {
        ({ rows, rawRowCount: preFilterRowCount } = await resolveVisiblePlayerPropRows(eventId, team, ' (legacy)'));
      }
    }

    const playerProps = rows
      .map((r: any) => {
        const publicProp = r.forecast_payload?.prop
          ? sanitizePublicTextBlock(String(r.forecast_payload.prop), { totalBrandMentions: 0, warnings: [] }, `legacy-player-prop:${r.id}.prop`)
          : null;
        const taxonomy = buildPlayerPropTaxonomy(r.forecast_payload);

        return serializePublicPlayerProp({
          assetId: r.id,
          player: r.player_name,
          team: r.team_id,
          teamSide: r.team_side,
          league: r.league,
          prop: publicProp,
          locked: false,
          confidence: r.confidence_score,
          payload: r.forecast_payload,
          marketType: taxonomy.marketType,
          marketFamily: taxonomy.marketFamily,
          marketOrigin: taxonomy.marketOrigin,
          sourceBacked: taxonomy.sourceBacked,
          playerRole: taxonomy.playerRole,
          includeMlbPropContext: MLB_PROP_CONTEXT_V2(),
        });
      })
      .filter((prop): prop is NonNullable<typeof prop> => Boolean(prop));
    const curatedEvent = await getCuratedEvent(eventId);
    const lineupSnapshot = await fetchEventLineupSnapshot(curatedEvent);
    const marketProps = await buildSourceBackedPlayerPropMarketBoard({
      curated: curatedEvent,
      team,
      existingProps: playerProps,
    });
    const featuredRows = rows.map((row: any) => {
        const descriptor = describeForecastAsset('PLAYER_PROP', row.forecast_payload);
        return {
          ...row,
          locked: false,
          playerRole: descriptor.playerRole,
        };
      });
    const supplementalFeaturedRows = await buildSupplementalFeaturedPlayerRows({
      eventId,
      curated: curatedEvent,
      lineups: lineupSnapshot,
      existingRows: featuredRows,
    });
    const featuredPlayers = buildFeaturedPlayersFromRows(
      [...featuredRows, ...supplementalFeaturedRows],
      {
        lineups: lineupSnapshot,
        limit: 10,
        perTeamTarget: 5,
        homeTeamId: curatedEvent?.home_short || curatedEvent?.home_team || null,
        awayTeamId: curatedEvent?.away_short || curatedEvent?.away_team || null,
      },
    ).map((player) => ({
      ...player,
      analysis: player.analysis
        ? sanitizePublicTextBlock(player.analysis, { totalBrandMentions: 0, warnings: [] }, `legacy-player-radar:${eventId}:${player.player}.analysis`)
        : null,
      props: player.props.map((prop) => ({
        ...prop,
        analysis: prop.analysis
          ? sanitizePublicTextBlock(prop.analysis, { totalBrandMentions: 0, warnings: [] }, `legacy-player-radar:${eventId}:${player.player}:${prop.assetId}.analysis`)
          : null,
      })),
    }));
    const bal = await getPickBalance(userId);
    const fallbackStartsAt = fallbackCurated?.starts_at ? new Date(fallbackCurated.starts_at) : null;
    const gameStarted = preFilterRowCount === 0
      ? (fallbackStartsAt && !Number.isNaN(fallbackStartsAt.getTime())
          ? fallbackStartsAt.getTime() <= Date.now()
          : await getGameStarted())
      : false;
    res.json({
      contractVersion: FORECAST_PUBLIC_CONTRACT_VERSION,
      playerProps,
      marketProps,
      players: buildPublicGroupedPlayers(playerProps),
      featuredPlayers,
      count: playerProps.length,
      mode: 'team',
      fallbackReason: preFilterRowCount === 0
        ? gameStarted
          ? 'game_started'
          : 'no_precomputed_assets'
        : playerProps.length === 0
          ? 'all_assets_filtered_out'
          : null,
      ...buildForecastBalanceFields(computeTotalBalance(bal)),
    });
  } catch (err) {
    console.error('Player props error:', err);
    res.status(500).json({ error: 'Failed to fetch player props' });
  }
});

// POST /api/forecast/unlock — Token-gated unlock for TEAM_PROPS
router.post('/unlock', authMiddleware, async (req: Request, res: Response) => {
  try {
    const { eventId, type, team, league } = req.body;
    const userId = req.user!.userId;

    if (!eventId || !type) {
      res.status(400).json({ error: 'eventId and type are required' });
      return;
    }

    if (!VALID_UNLOCK_TYPES.includes(type)) {
      res.status(400).json({ error: `Invalid type. Use one of: ${VALID_UNLOCK_TYPES.join(', ')}` });
      return;
    }

    // Route insight types to dedicated handler
    if (type === 'STEAM_INSIGHT' || type === 'SHARP_INSIGHT' || type === 'DVP_INSIGHT' || type === 'HCW_INSIGHT') {
      return handleInsightUnlock(req, res, type, eventId, userId);
    }

    if (!team || (team !== 'home' && team !== 'away')) {
      res.status(400).json({ error: 'team must be "home" or "away"' });
      return;
    }

    // 1. Check if already unlocked (idempotent)
    const { rows: existingUnlocks } = await pool.query(
      'SELECT id FROM rm_team_props_unlocks WHERE user_id = $1 AND event_id = $2 AND team = $3',
      [userId, eventId, team]
    );

    // 2. Check user permissions
    const user = await findUserById(userId);
    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    const isRainMan = user.is_weatherman;

    // Gate: require email verification for non-weathermen
    if (!isRainMan && existingUnlocks.length === 0 && !user.email_verified && !isInGracePeriod(user)) {
      res.status(403).json({
        error: 'Email verification required',
        needsVerification: true,
        message: 'Verify your email to unlock forecasts.',
      });
      return;
    }

    // 3. Check balance (skip for weathermen and already-unlocked)
    if (!isRainMan && existingUnlocks.length === 0) {
      await ensureDailyGrant(userId);
      const balance = await getPickBalance(userId);
      const total = computeTotalBalance(balance);
      if (total <= 0) {
        res.status(402).json({
          error: 'No forecasts remaining',
          message: 'Get more forecasts to unlock player props',
          options: [
            { type: 'single_pick', label: 'Single Forecast', price: '$1', picks: 1 },
            { type: 'daily_pass', label: 'Day Pass (999 Forecasts)', price: '$4.99', picks: 999 },
          ],
        });
        return;
      }
    }

    // 4. Get event info
    const curated = await getCuratedEvent(eventId);
    if (!curated) {
      res.status(404).json({ error: 'Event not found' });
      return;
    }

    const teamName = team === 'home' ? curated.home_team : curated.away_team;
    const teamShort = team === 'home' ? curated.home_short : curated.away_short;
    const opponentName = team === 'home' ? curated.away_team : curated.home_team;
    const opponentShort = team === 'home' ? curated.away_short : curated.home_short;
    const isMlb = (curated.league || league || '').toLowerCase() === 'mlb';

    // 5. Check cache first
    const { rows: cachedProps } = await pool.query(
      'SELECT * FROM rm_team_props_cache WHERE event_id = $1 AND team = $2',
      [eventId, team]
    );
    const { rows: precomputedProps } = await pool.query(
      `SELECT status, generated_at
       FROM rm_forecast_precomputed
       WHERE event_id = $1
         AND forecast_type = 'TEAM_PROPS'
         AND team_side = $2
       ORDER BY generated_at DESC
       LIMIT 1`,
      [eventId, team]
    ).catch(() => ({ rows: [] as any[] }));

    const latestPrecomputedStatus = precomputedProps[0]?.status || null;
    const cachedPropsPayload = cachedProps[0]?.props_data || null;
    const shouldRegenerateCachedProps = shouldRegenerateLegacyTeamProps({
      league: curated.league || league || null,
      propsResult: cachedPropsPayload,
      latestPrecomputedStatus,
    });

    let propsResult: any;
    if (cachedPropsPayload && !shouldRegenerateCachedProps) {
      propsResult = normalizeLegacyTeamPropsResponse(cachedPropsPayload, curated.league || league || null);
    } else {
      propsResult = normalizeLegacyTeamPropsResponse(await generateTeamProps({
        teamName,
        teamShort: teamShort || '',
        opponentName,
        opponentShort: opponentShort || '',
        league: curated.league || league || '',
        isHome: team === 'home',
        startsAt: curated.starts_at,
        moneyline: curated.moneyline || { home: null, away: null },
        spread: curated.spread || { home: null, away: null },
        total: curated.total || { over: null, under: null },
      }), curated.league || league || null);

      await pool.query(
        `INSERT INTO rm_team_props_cache (event_id, team, team_name, team_short, league, props_data)
         VALUES ($1, $2, $3, $4, $5, $6)
         ON CONFLICT (event_id, team) DO UPDATE SET props_data = EXCLUDED.props_data`,
        [eventId, team, teamName, teamShort, curated.league, JSON.stringify(propsResult)]
      );

      await upsertGeneratedTeamPropsAssets({
        eventId,
        league: curated.league || league || '',
        teamName,
        teamShort,
        teamSide: team,
        startsAt: curated.starts_at,
        propsResult,
      });
    }

    // 6. Deduct and record unlock (skip for weathermen and already-unlocked)
    if (!isRainMan && existingUnlocks.length === 0) {
      const { success, source } = await deductPick(userId);
      if (!success) {
        res.status(402).json({ error: 'Failed to deduct forecast' });
        return;
      }
      const newBalance = await getPickBalance(userId);
      await recordLedgerEntry(userId, -1, 'TEAM_PROPS_UNLOCK', computeTotalBalance(newBalance), { eventId, team, source });
    }

    // 7. Record unlock (idempotent via UNIQUE constraint)
    if (existingUnlocks.length === 0) {
      await pool.query(
        `INSERT INTO rm_team_props_unlocks (user_id, event_id, team, was_weatherman)
         VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`,
        [userId, eventId, team, isRainMan]
      );
    }

    res.json(await buildLegacyTeamPropsUnlockResponse({
      userId,
      eventId,
      team,
      league: curated.league || league || null,
      propsResult,
    }));
  } catch (err) {
    console.error('Team props unlock error:', err);
    res.status(500).json({ error: 'Failed to unlock team props' });
  }
});

// POST /api/forecast/unlock handler for STEAM_INSIGHT, SHARP_INSIGHT, and DVP_INSIGHT
async function handleInsightUnlock(req: Request, res: Response, type: string, eventId: string, userId: string) {
  try {
    const insightType = type === 'STEAM_INSIGHT' ? 'STEAM' : type === 'DVP_INSIGHT' ? 'DVP' : type === 'HCW_INSIGHT' ? 'HCW' : 'SHARP';
    const ledgerReason: LedgerReason = type === 'STEAM_INSIGHT' ? 'STEAM_INSIGHT_UNLOCK' : type === 'DVP_INSIGHT' ? 'DVP_INSIGHT_UNLOCK' : type === 'HCW_INSIGHT' ? 'HCW_INSIGHT_UNLOCK' : 'SHARP_INSIGHT_UNLOCK';

    // Feature flag gate
    if (insightType === 'STEAM' && !STEAM_UNLOCK_ENABLED()) {
      res.status(400).json({ error: 'Steam insight unlocks are currently disabled' });
      return;
    }
    if (insightType === 'SHARP' && !SHARP_UNLOCK_ENABLED()) {
      res.status(400).json({ error: 'Sharp insight unlocks are currently disabled' });
      return;
    }
    if (insightType === 'DVP' && !DVP_UNLOCK_ENABLED()) {
      res.status(400).json({ error: 'DVP insight unlocks are currently disabled' });
      return;
    }
    if (insightType === 'HCW' && !HCW_UNLOCK_ENABLED()) {
      res.status(400).json({ error: 'Home Crowd + Weather insight unlocks are currently disabled' });
      return;
    }

    // 1. Check if already unlocked (idempotent)
    const { rows: existingUnlocks } = await pool.query(
      'SELECT id FROM rm_insight_unlocks WHERE user_id = $1 AND event_id = $2 AND insight_type = $3',
      [userId, eventId, insightType]
    );

    if (existingUnlocks.length > 0) {
      const { rows: cachedInsight } = await pool.query(
        'SELECT insight_data FROM rm_insight_cache WHERE event_id = $1 AND insight_type = $2',
        [eventId, insightType]
      );
      if (cachedInsight[0]) {
        const bal = await getPickBalance(userId);
        res.json({
          insightType,
          insightData: cachedInsight[0].insight_data,
          ...buildForecastBalanceFields(computeTotalBalance(bal)),
        });
        return;
      }
    }

    // 2. Check user permissions
    const user = await findUserById(userId);
    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }
    const isRainMan = user.is_weatherman;

    // Gate: require email verification
    if (!isRainMan && existingUnlocks.length === 0 && !user.email_verified && !isInGracePeriod(user)) {
      res.status(403).json({
        error: 'Email verification required',
        needsVerification: true,
        message: 'Verify your email to unlock insights.',
      });
      return;
    }

    // 3. Check balance (skip for weathermen and already-unlocked)
    if (!isRainMan && existingUnlocks.length === 0) {
      await ensureDailyGrant(userId);
      const balance = await getPickBalance(userId);
      const total = computeTotalBalance(balance);
      if (total <= 0) {
        res.status(402).json({
          error: 'No forecasts remaining',
          message: 'Get more forecasts to unlock this insight',
          options: [
            { type: 'single_pick', label: 'Single Forecast', price: '$1', picks: 1 },
            { type: 'daily_pass', label: 'Day Pass (999 Forecasts)', price: '$4.99', picks: 999 },
          ],
        });
        return;
      }
    }

    // 4. Get event info
    const curated = await getCuratedEvent(eventId);
    let homeTeam = '', awayTeam = '', eventLeague = '';
    let homeShort = '', awayShort = '';
    if (curated) {
      homeTeam = curated.home_team;
      awayTeam = curated.away_team;
      homeShort = curated.home_short || '';
      awayShort = curated.away_short || '';
      eventLeague = curated.league;
    } else {
      // Fallback to forecast cache
      const cached = await getCachedForecast(eventId);
      if (cached) {
        homeTeam = cached.home_team;
        awayTeam = cached.away_team;
        eventLeague = cached.league;
      } else {
        res.status(404).json({ error: 'Event not found' });
        return;
      }
    }

    // 5. Check cache first
    const { rows: cachedInsight } = await pool.query(
      'SELECT insight_data FROM rm_insight_cache WHERE event_id = $1 AND insight_type = $2',
      [eventId, insightType]
    );

    let insightData: any;
    if (cachedInsight[0]) {
      insightData = cachedInsight[0].insight_data;
    } else {
      // Fetch source data from SharpMove + LineMovement
      const sourceData = await fetchInsightSourceData({
        homeTeam,
        awayTeam,
        league: eventLeague,
      });

      // Get forecast summary for context
      const fcCached = await getCachedForecast(eventId);
      const forecastData = fcCached?.forecast_data || {};

      if (insightType === 'HCW') {
        insightData = await generateHcwInsight({
          homeTeam, awayTeam, homeShort, awayShort,
          league: eventLeague, startsAt: curated?.starts_at || '',
          forecastSummary: forecastData.summary || '',
          eventId,
        });
      } else if (insightType === 'DVP') {
        insightData = await generateDvpInsight(homeTeam, awayTeam, eventLeague, homeShort, awayShort);
      } else if (insightType === 'STEAM') {
        insightData = await generateSteamInsight({
          homeTeam,
          awayTeam,
          league: eventLeague,
          forecastSummary: forecastData.summary || '',
          sharpMoves: sourceData.sharpMoves,
          lineMovements: sourceData.lineMovements,
        });
      } else {
        insightData = await generateSharpInsight({
          homeTeam,
          awayTeam,
          league: eventLeague,
          forecastSummary: forecastData.summary || '',
          sharpMoneyIndicator: forecastData.sharp_money_indicator || '',
          sharpMoves: sourceData.sharpMoves,
          lineMovements: sourceData.lineMovements,
        });
      }

      // Cache the insight
      await pool.query(
        `INSERT INTO rm_insight_cache (event_id, insight_type, league, home_team, away_team, insight_data, source_data)
         VALUES ($1, $2, $3, $4, $5, $6, $7)
         ON CONFLICT (event_id, insight_type) DO UPDATE SET insight_data = EXCLUDED.insight_data, source_data = EXCLUDED.source_data`,
        [eventId, insightType, eventLeague, homeTeam, awayTeam, JSON.stringify(insightData), JSON.stringify(sourceData)]
      );
    }

    // 6. Deduct and record unlock
    if (!isRainMan && existingUnlocks.length === 0) {
      const { success, source } = await deductPick(userId);
      if (!success) {
        res.status(402).json({ error: 'Failed to deduct forecast' });
        return;
      }
      const newBalance = await getPickBalance(userId);
      await recordLedgerEntry(userId, -1, ledgerReason, computeTotalBalance(newBalance), { eventId, insightType, source });
    }

    // 7. Record unlock (idempotent via UNIQUE constraint)
    if (existingUnlocks.length === 0) {
      await pool.query(
        `INSERT INTO rm_insight_unlocks (user_id, event_id, insight_type, was_weatherman)
         VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`,
        [userId, eventId, insightType, isRainMan]
      );
    }

    const finalBal = await getPickBalance(userId);
    res.json({
      insightType,
      insightData,
      ...buildForecastBalanceFields(computeTotalBalance(finalBal)),
    });
  } catch (err) {
    console.error('Insight unlock error:', err);
    res.status(500).json({ error: 'Failed to unlock insight' });
  }
}

// POST /api/forecast/batch-generate — admin: fill uncached forecasts for today
router.post('/batch-generate', authMiddleware, requireAdminAccess, auditAdminAccess('forecast-admin'), async (_req: Request, res: Response) => {
  try {
    // Find today's events that have no cached forecast
    const { rows: uncached } = await pool.query(
      `SELECT e.event_id, e.home_team, e.away_team, e.league, e.starts_at,
              e.moneyline, e.spread, e.total, e.home_short, e.away_short
       FROM rm_events e
       LEFT JOIN rm_forecast_cache c ON c.event_id = e.event_id
       WHERE e.starts_at::date = CURRENT_DATE
         AND c.id IS NULL`
    );

    if (uncached.length === 0) {
      res.json({ status: 'ok', message: 'All events already cached', count: 0 });
      return;
    }

    // Return immediately with count, generate async
    res.json({ status: 'ok', message: `Generating ${uncached.length} forecasts in background`, count: uncached.length });

    // Fire-and-forget background generation
    (async () => {
      for (const evt of uncached) {
        try {
          await buildForecastFromCuratedEvent(evt);
          console.log(`[batch-generate] Cached forecast for ${evt.home_team} vs ${evt.away_team}`);
          // 3s rate limit between Grok calls
          await new Promise(resolve => setTimeout(resolve, 3000));
        } catch (err) {
          console.error(`[batch-generate] Failed for ${evt.event_id}:`, err);
        }
      }
      console.log(`[batch-generate] Completed ${uncached.length} events`);
    })();
  } catch (err) {
    console.error('Batch generate error:', err);
    res.status(500).json({ error: 'Failed to start batch generation' });
  }
});

// POST /api/forecast/:eventId/regenerate — admin: regenerate a single forecast
router.post('/:eventId/regenerate', authMiddleware, requireAdminAccess, auditAdminAccess('forecast-admin'), async (req: Request, res: Response) => {
  try {
    const eventId = String(req.params.eventId);
    const curated = await getCuratedEvent(eventId);
    if (!curated) {
      res.status(404).json({ error: 'Event not found in rm_events' });
      return;
    }

    const result = await buildForecastFromCuratedEvent(curated);
    const saved = await getCachedForecast(eventId);
    if (!saved) {
      throw new Error(`Forecast cache missing after regenerate for ${eventId}`);
    }

    res.json({
      status: 'ok',
      eventId,
      homeTeam: curated.home_team,
      awayTeam: curated.away_team,
      league: curated.league,
      forecastId: saved.id,
      confidenceScore: result.confidenceScore,
      generatedAt: saved.created_at,
      lastRefreshAt: saved.last_refresh_at || null,
      lastRefreshType: saved.last_refresh_type || null,
      materialChange: saved.material_change || null,
    });
  } catch (err) {
    console.error('Regenerate error:', err);
    res.status(500).json({ error: 'Failed to regenerate forecast' });
  }
});

export default router;
