import { Router, Request, Response } from 'express';
import { optionalAuth } from '../middleware/auth';
import pool from '../db';
import { teamNameKeySql } from '../lib/team-abbreviations';
import { deduplicateProps } from '../services/prop-dedup';
import { sanitizeGameOddsForLeague, sanitizeMoneylinePair } from '../services/sgo';
import { EU_LEAGUE_KEYS, isEuLeague } from '../lib/scheduler-config';
import { EU_EVENT_LOOKAHEAD_DAYS } from '../lib/league-windows';
import { getPublicMarketSourceMeta } from '../lib/market-source';
import { assessPublicGameFit } from '../services/top-game-profile';
import { fetchDirectMlbPropCandidates, fetchMlbPropCandidates, type MlbPropCandidate } from '../services/mlb-prop-markets';
import { fetchTeamPropMarketCandidates, type TeamPropMarketCandidate } from '../services/team-prop-market-candidates';

const router = Router();
const EVENTS_MARKET_BREAKDOWN = process.env.EVENTS_MARKET_BREAKDOWN === 'true';
const PROP_DEDUP_ENABLED = () => process.env.PROP_DEDUP_ENABLED === 'true';
const PI_SIBLING_SUPPRESSION_ENABLED = () => process.env.PI_SIBLING_SUPPRESSION_ENABLED === 'true';
const STEAM_UNLOCK_ENABLED = () => process.env.FEATURE_STEAM_UNLOCK === 'true';
const SHARP_UNLOCK_ENABLED = () => process.env.FEATURE_SHARP_UNLOCK === 'true';
const EVENTS_ROUTE_CACHE_TTL_MS = 60 * 1000;
const EU_LEAGUES_SQL = EU_LEAGUE_KEYS.map((league) => `'${league}'`).join(', ');

type EventsRouteResponse = {
  leagues: string[];
  events: Record<string, any[]>;
  totalEvents: number;
};

type EventsRouteCacheEntry = {
  payload: EventsRouteResponse;
  expiresAt: number;
};

const EVENTS_ROUTE_CACHE = new Map<string, EventsRouteCacheEntry>();

const 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 FORECAST_CACHE_SELECT = `fc.event_id AS matched_event_id,
                              COALESCE(NULLIF(fc.composite_confidence, 'NaN'::float8), fc.confidence_score) AS public_confidence,
                              fc.confidence_score,
                              fc.composite_confidence,
                              fc.forecast_data,
                              fc.odds_data AS fc_odds_data,
                              fc.model_signals,
                              fc.input_quality`;

const FORECAST_EXACT_LATERAL = `LEFT JOIN LATERAL (
                            SELECT
                              ${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`;

const FORECAST_FALLBACK_LATERAL = `LEFT JOIN LATERAL (
                            SELECT
                              ${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`;

const FORECAST_LATERAL = `${FORECAST_EXACT_LATERAL}
               ${FORECAST_FALLBACK_LATERAL}`;

const FORECAST_PUBLIC_SELECT = `COALESCE(fc_exact.public_confidence, fc_fallback.public_confidence) AS fc_confidence,
               COALESCE(fc_exact.forecast_data, fc_fallback.forecast_data) AS fc_data,
               COALESCE(fc_exact.model_signals, fc_fallback.model_signals) AS fc_model_signals,
               COALESCE(fc_exact.input_quality, fc_fallback.input_quality) AS fc_input_quality,
               COALESCE(fc_exact.fc_odds_data, fc_fallback.fc_odds_data) AS fc_odds_data`;

const PLAYER_PROPS_LATERAL = `LEFT JOIN LATERAL (
                               SELECT
                                 COUNT(*) FILTER (
                                   WHERE fp.forecast_type = 'PLAYER_PROP'
                                     AND (
                                       fp.status = 'ACTIVE'
                                       OR (fp.status = 'STALE' AND fp.expires_at > NOW())
                                     )
                                 )::int AS ready_count,
                                 COUNT(*) FILTER (WHERE fp.forecast_type = 'TEAM_PROPS' AND fp.status = 'ACTIVE')::int AS team_props_count,
                                 COUNT(*) FILTER (
                                   WHERE fp.forecast_type = 'TEAM_PROPS'
                                     AND fp.status = 'ACTIVE'
                                     AND COALESCE(fp.forecast_payload->'metadata'->>'suppressed_reason', '') = 'no_source_market_candidates'
                                 )::int AS no_source_market_count,
                                 COUNT(*) FILTER (
                                   WHERE fp.forecast_type = 'TEAM_PROPS'
                                     AND fp.status = 'ACTIVE'
                                     AND COALESCE(fp.forecast_payload->'metadata'->>'suppressed_reason', '') = 'no_valid_source_market_matches'
                                 )::int AS no_valid_market_match_count
                               FROM rm_forecast_precomputed fp
                               WHERE fp.event_id = e.event_id
                             ) ppc ON true`;

type PublicMoneylinePair = {
  home: number | null;
  away: number | null;
  draw?: number | null;
};

type PublicTotalPair = {
  over: { line: number | null; odds: number | null } | null;
  under: { line: number | null; odds: number | null } | null;
};

function hasAnyMoneylineValue(pair: PublicMoneylinePair): boolean {
  return pair.home != null || pair.away != null;
}

function hasCompleteMoneylinePair(pair: PublicMoneylinePair): boolean {
  return pair.home != null && pair.away != null;
}

function hasAnyTotalValue(total: PublicTotalPair): boolean {
  return Boolean(
    total.over?.line != null
    || total.over?.odds != null
    || total.under?.line != null
    || total.under?.odds != null,
  );
}

function hasCompleteTotalPair(total: PublicTotalPair): boolean {
  return total.over?.line != null && total.under?.line != null;
}

function normalizePublicMoneylinePair(
  pair: { home?: number | null; away?: number | null; draw?: number | null } | null | undefined,
  options?: { allowPositivePositive?: boolean },
): PublicMoneylinePair {
  const home = pair?.home == null ? null : Number(pair.home);
  const away = pair?.away == null ? null : Number(pair.away);
  const rawDraw = pair?.draw == null ? null : Number(pair.draw);
  const draw = Number.isFinite(rawDraw) ? rawDraw : null;

  if (
    options?.allowPositivePositive
    && home != null
    && away != null
    && Number.isFinite(home)
    && Number.isFinite(away)
    && home > 0
    && away > 0
  ) {
    return draw != null ? { home, away, draw } : { home, away };
  }

  const sanitized = sanitizeMoneylinePair({ home, away });
  const normalized: PublicMoneylinePair = {
    home: sanitized.home === 0 ? null : sanitized.home,
    away: sanitized.away === 0 ? null : sanitized.away,
  };
  if (draw != null) normalized.draw = draw;
  return normalized;
}

function inferProjectedFavorite(fcData: any): 'home' | 'away' | null {
  const projectedSpread = fcData?.projected_lines?.spread;
  const homeSpread = Number(projectedSpread?.home);
  const awaySpread = Number(projectedSpread?.away);
  if (Number.isFinite(homeSpread) && Number.isFinite(awaySpread)) {
    if (homeSpread < awaySpread) return 'home';
    if (awaySpread < homeSpread) return 'away';
  }

  const projectedMargin = Number(fcData?.projected_margin);
  if (Number.isFinite(projectedMargin)) {
    if (projectedMargin > 0) return 'home';
    if (projectedMargin < 0) return 'away';
  }

  return null;
}

function normalizeProjectedMoneylinePair(fcData: any): PublicMoneylinePair {
  const projected = normalizePublicMoneylinePair(fcData?.projected_lines?.moneyline || null);
  if (hasAnyMoneylineValue(projected)) return projected;

  const rawHome = Number(fcData?.projected_lines?.moneyline?.home);
  const rawAway = Number(fcData?.projected_lines?.moneyline?.away);
  const favorite = inferProjectedFavorite(fcData);
  if (!favorite || !Number.isFinite(rawHome) || !Number.isFinite(rawAway)) {
    return projected;
  }

  if (rawHome > 0 && rawAway > 0) {
    return favorite === 'home'
      ? { home: -Math.abs(rawHome), away: Math.abs(rawAway) }
      : { home: Math.abs(rawHome), away: -Math.abs(rawAway) };
  }

  return projected;
}

function normalizeNullableNumber(value: unknown): number | null {
  if (value == null) return null;
  const numeric = Number(value);
  return Number.isFinite(numeric) ? numeric : null;
}

function sanitizePublicGameMarket(league: string, market: {
  moneyline?: { home?: number | null; away?: number | null; draw?: number | null } | null;
  spread?: {
    home?: { line?: number | null; odds?: number | null } | null;
    away?: { line?: number | null; odds?: number | null } | null;
  } | null;
  total?: {
    over?: { line?: number | null; odds?: number | null } | null;
    under?: { line?: number | null; odds?: number | null } | null;
  } | null;
  } | null | undefined) {
  const normalized = {
    moneyline: {
      home: normalizeNullableNumber(market?.moneyline?.home),
      away: normalizeNullableNumber(market?.moneyline?.away),
      draw: normalizeNullableNumber(market?.moneyline?.draw),
    },
    spread: {
      home: market?.spread?.home
        ? {
            line: normalizeNullableNumber(market.spread.home.line),
            odds: normalizeNullableNumber(market.spread.home.odds),
          }
        : null,
      away: market?.spread?.away
        ? {
            line: normalizeNullableNumber(market.spread.away.line),
            odds: normalizeNullableNumber(market.spread.away.odds),
          }
        : null,
    },
    total: {
      over: market?.total?.over
        ? {
            line: normalizeNullableNumber(market.total.over.line),
            odds: normalizeNullableNumber(market.total.over.odds),
          }
        : null,
      under: market?.total?.under
        ? {
            line: normalizeNullableNumber(market.total.under.line),
            odds: normalizeNullableNumber(market.total.under.odds),
          }
        : null,
    },
  };

  if (league !== 'mlb') {
    return normalized;
  }

  const sanitized = sanitizeGameOddsForLeague('mlb', {
    moneyline: {
      home: normalized.moneyline.home,
      away: normalized.moneyline.away,
    },
    spread: normalized.spread,
    total: normalized.total,
  });

  return {
    moneyline: {
      ...sanitized.moneyline,
      draw: normalized.moneyline.draw,
    },
    spread: sanitized.spread,
    total: sanitized.total,
  };
}

function resolvePublicMoneyline(
  current: PublicMoneylinePair,
  cached: PublicMoneylinePair,
  opening: PublicMoneylinePair,
  projected: PublicMoneylinePair,
): PublicMoneylinePair {
  if (hasCompleteMoneylinePair(current)) return current;
  if (hasCompleteMoneylinePair(cached)) return cached;
  if (hasCompleteMoneylinePair(opening)) return opening;
  if (hasCompleteMoneylinePair(projected)) return projected;
  if (hasAnyMoneylineValue(current)) return current;
  if (hasAnyMoneylineValue(cached)) return cached;
  if (hasAnyMoneylineValue(opening)) return opening;
  return projected;
}

function resolvePublicTotal(
  current: PublicTotalPair,
  cached: PublicTotalPair,
  opening: PublicTotalPair,
): PublicTotalPair {
  if (hasCompleteTotalPair(current)) return current;
  if (hasCompleteTotalPair(cached)) return cached;
  if (hasCompleteTotalPair(opening)) return opening;
  if (hasAnyTotalValue(current)) return current;
  if (hasAnyTotalValue(cached)) return cached;
  return opening;
}

function derivePlayerPropsStatus(
  readyCount: number,
  teamPropsCount: number,
  rawPlayerPropsCount = readyCount,
  noSourceMarketCount = 0,
  noValidMarketMatchCount = 0,
): 'ready' | 'empty_after_run' | 'not_generated' | 'no_source_market_candidates' | 'no_valid_source_market_matches' {
  if (readyCount > 0) return 'ready';
  if (teamPropsCount > 0) {
    if (noSourceMarketCount > 0 && noSourceMarketCount === teamPropsCount) return 'no_source_market_candidates';
    if (noValidMarketMatchCount > 0 && noValidMarketMatchCount === teamPropsCount) return 'no_valid_source_market_matches';
    return 'empty_after_run';
  }
  if (rawPlayerPropsCount > 0) return 'empty_after_run';
  return 'not_generated';
}

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);
}

async function buildVisiblePlayerPropCounts(events: Array<{ event_id: string; league: string }>): Promise<Map<string, number>> {
  const eventIds = [...new Set(events.map((event) => event.event_id).filter(Boolean))];
  if (eventIds.length === 0) return new Map();

  const { rows: propRows } = await pool.query(
    `SELECT id, event_id, player_name, forecast_payload, forecast_type, league, status
     FROM rm_forecast_precomputed
     WHERE forecast_type = 'PLAYER_PROP'
       AND (
         status = 'ACTIVE'
         OR (status = 'STALE' AND expires_at > NOW())
       )
       AND event_id = ANY($1::text[])`,
    [eventIds]
  );

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

  const leagues = [...new Set(events.map((event) => String(event.league || '').toLowerCase()).filter(Boolean))];
  const outPlayersByLeague = new Map<string, Set<string>>();

  if (leagues.length > 0) {
    const { rows: outRows } = await pool.query(
      `SELECT LOWER("playerName") AS name, LOWER(league) AS league
       FROM "PlayerInjury"
       WHERE LOWER(league) = ANY($1::text[])
         AND status IN ('Out', 'IR', 'Suspension')
       GROUP BY LOWER("playerName"), LOWER(league)
       HAVING COUNT(DISTINCT source) >= 2`,
      [leagues]
    );

    for (const row of outRows) {
      const league = String(row.league || '').toLowerCase();
      if (!outPlayersByLeague.has(league)) outPlayersByLeague.set(league, new Set());
      outPlayersByLeague.get(league)!.add(String(row.name || '').toLowerCase());
    }
  }

  const suppressedIdsByEvent = new Map<string, Set<string>>();
  if (PI_SIBLING_SUPPRESSION_ENABLED()) {
    const { rows: suppressedRows } = await pool.query(
      `SELECT game_id, conflict_payload
       FROM pi_prop_eligibility
       WHERE game_id = ANY($1::text[])
         AND publish_allowed = false
         AND suppress_reason = 'SUPPRESSED_SIBLING'`,
      [eventIds]
    ).catch(() => ({ rows: [] as any[] }));

    for (const row of suppressedRows) {
      try {
        const payload = typeof row.conflict_payload === 'string'
          ? JSON.parse(row.conflict_payload)
          : row.conflict_payload;
        const suppressedAssetId = payload?.suppressedAssetId;
        if (!suppressedAssetId) continue;
        const eventId = String(row.game_id || '');
        if (!suppressedIdsByEvent.has(eventId)) suppressedIdsByEvent.set(eventId, new Set());
        suppressedIdsByEvent.get(eventId)!.add(String(suppressedAssetId));
      } catch {
        continue;
      }
    }
  }

  const visibleCounts = new Map<string, number>();
  const propsByEvent = new Map<string, any[]>();
  for (const row of propRows) {
    const eventId = String(row.event_id);
    if (!propsByEvent.has(eventId)) propsByEvent.set(eventId, []);
    propsByEvent.get(eventId)!.push(row);
  }

  for (const event of events) {
    const eventId = String(event.event_id);
    const league = String(event.league || '').toLowerCase();
    const outPlayers = outPlayersByLeague.get(league) ?? new Set<string>();
    const suppressedIds = suppressedIdsByEvent.get(eventId) ?? new Set<string>();
    const eventRows = propsByEvent.get(eventId) ?? [];
    const filterVisibleRows = (rows: any[]) => {
      let visibleRows = rows.filter((row: any) => {
        const playerName = String(row.player_name || '').toLowerCase();
        return !outPlayers.has(playerName)
          && !suppressedIds.has(String(row.id))
          && hasPublicPlayerPropPricing(row.forecast_payload);
      });

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

      return visibleRows;
    };

    const activeRows = eventRows.filter((row: any) => String(row.status || 'ACTIVE').toUpperCase() === 'ACTIVE');
    const staleRows = eventRows.filter((row: any) => String(row.status || 'ACTIVE').toUpperCase() === 'STALE');
    const visibleActiveRows = filterVisibleRows(activeRows);
    const visibleRows = visibleActiveRows.length > 0
      ? visibleActiveRows
      : filterVisibleRows(staleRows);

    visibleCounts.set(eventId, visibleRows.length);
  }

  return visibleCounts;
}

function countMlbCandidateSides(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 countSourceCandidateSides(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);
}

async function buildSourceBackedPlayerPropCounts(events: Array<{
  event_id: string;
  league: string;
  home_short?: string | null;
  away_short?: string | null;
  home_team?: string | null;
  away_team?: string | null;
  starts_at?: string | Date | null;
}>, baseCounts: Map<string, number>): Promise<Map<string, number>> {
  const unresolvedEvents = events.filter((event) => (baseCounts.get(String(event.event_id)) ?? 0) === 0);
  if (unresolvedEvents.length === 0) return new Map();

  const entries = await Promise.all(unresolvedEvents.map(async (event) => {
    const eventId = String(event.event_id || '');
    const league = String(event.league || '').toLowerCase();
    const startsAt = event.starts_at instanceof Date
      ? event.starts_at.toISOString()
      : event.starts_at
        ? new Date(event.starts_at).toISOString()
        : '';
    const homeShort = String(event.home_short || '').trim().toUpperCase();
    const awayShort = String(event.away_short || '').trim().toUpperCase();
    if (!eventId || !startsAt || !homeShort || !awayShort) return [eventId, 0] as const;

    if (league !== 'mlb') {
      const [homeCandidates, awayCandidates] = await Promise.all([
        fetchTeamPropMarketCandidates({
          league,
          teamShort: homeShort,
          opponentShort: awayShort,
          teamName: String(event.home_team || homeShort),
          opponentName: String(event.away_team || awayShort),
          homeTeam: String(event.home_team || homeShort),
          awayTeam: String(event.away_team || awayShort),
          startsAt,
          skipTheOddsVerification: true,
        }).catch(() => [] as TeamPropMarketCandidate[]),
        fetchTeamPropMarketCandidates({
          league,
          teamShort: awayShort,
          opponentShort: homeShort,
          teamName: String(event.away_team || awayShort),
          opponentName: String(event.home_team || homeShort),
          homeTeam: String(event.home_team || homeShort),
          awayTeam: String(event.away_team || awayShort),
          startsAt,
          skipTheOddsVerification: true,
        }).catch(() => [] as TeamPropMarketCandidate[]),
      ]);

      return [eventId, countSourceCandidateSides(homeCandidates) + countSourceCandidateSides(awayCandidates)] as const;
    }

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

    const localCount = countMlbCandidateSides(homeLocal) + countMlbCandidateSides(awayLocal);
    if (localCount > 0) return [eventId, localCount] as const;

    const [homeDirect, awayDirect] = await Promise.all([
      fetchDirectMlbPropCandidates({
        teamShort: homeShort,
        teamName: String(event.home_team || homeShort),
        opponentShort: awayShort,
        startsAt,
      }).catch(() => [] as MlbPropCandidate[]),
      fetchDirectMlbPropCandidates({
        teamShort: awayShort,
        teamName: String(event.away_team || awayShort),
        opponentShort: homeShort,
        startsAt,
      }).catch(() => [] as MlbPropCandidate[]),
    ]);

    return [eventId, countMlbCandidateSides(homeDirect) + countMlbCandidateSides(awayDirect)] as const;
  }));

  return new Map(entries.filter((entry) => entry[0] && entry[1] > 0));
}

function buildMarketBreakdown(fcData: any): Record<string, number> | null {
  if (!fcData || typeof fcData !== 'object') return null;
  const breakdown: Record<string, number> = {};
  if (fcData.spread_edge != null) breakdown.spread = Number(fcData.spread_edge);
  if (fcData.total_edge != null) breakdown.total = Number(fcData.total_edge);
  if (fcData.moneyline_edge != null) breakdown.moneyline = Number(fcData.moneyline_edge);
  return Object.keys(breakdown).length > 0 ? breakdown : null;
}

function extractDigimonLockCount(fcData: any, modelSignals: any, inputQuality: any): number {
  const candidates = [
    fcData?.input_quality?.digimon?.lockCount,
    inputQuality?.digimon?.lockCount,
    modelSignals?.modelSignals?.digimon?.lockCount,
    modelSignals?.digimon?.lockCount,
  ];

  for (const candidate of candidates) {
    if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate >= 0) {
      return candidate;
    }
  }

  return 0;
}

function getEtCacheDayKey(now = new Date()): string {
  return new Intl.DateTimeFormat('en-CA', {
    timeZone: 'America/New_York',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(now);
}

function getEventsRouteCacheKey(league: string): string {
  return `${getEtCacheDayKey()}:${league || 'all'}`;
}

function getCachedEventsRoutePayload(key: string, now = Date.now()): EventsRouteResponse | null {
  const cached = EVENTS_ROUTE_CACHE.get(key);
  if (!cached) return null;
  if (cached.expiresAt <= now) {
    EVENTS_ROUTE_CACHE.delete(key);
    return null;
  }
  return cached.payload;
}

function writeEventsRouteCache(key: string, payload: EventsRouteResponse, now = Date.now()): void {
  EVENTS_ROUTE_CACHE.set(key, {
    payload,
    expiresAt: now + EVENTS_ROUTE_CACHE_TTL_MS,
  });
}

/**
 * RULE:
 * - US sports stay current-day only.
 * - EU soccer leagues get a forward window so the product does not disappear between match days.
 * Events still come exclusively from rm_events (curated/seeded games).
 */

// GET /api/events?league=nba
router.get('/', optionalAuth, async (req: Request, res: Response) => {
  try {
    const league = (req.query.league as string || '').toLowerCase();
    const cacheKey = getEventsRouteCacheKey(league);
    const cachedPayload = getCachedEventsRoutePayload(cacheKey);
    if (cachedPayload) {
      res.setHeader('Cache-Control', 'public, max-age=30');
      res.setHeader('X-Rainmaker-Cache', 'HIT');
      return res.json(cachedPayload);
    }

    // US leagues: today's curated events only.
    // EU soccer leagues: show the next configured ET-day window.
    let query: string;
    const params: any[] = [];

    if (league) {
      const isEu = isEuLeague(league);
      query = `SELECT e.*, ${FORECAST_PUBLIC_SELECT}, COALESCE(ppc.ready_count, 0) AS player_props_ready_count, COALESCE(ppc.team_props_count, 0) AS team_props_count, COALESCE(ppc.no_source_market_count, 0) AS no_source_market_count, COALESCE(ppc.no_valid_market_match_count, 0) AS no_valid_market_match_count
               FROM rm_events e
               ${FORECAST_LATERAL}
               ${PLAYER_PROPS_LATERAL}
               WHERE e.league = $1
                 AND DATE(e.starts_at AT TIME ZONE 'America/New_York') ${isEu ? 'BETWEEN (NOW() AT TIME ZONE \'America/New_York\')::date AND ((NOW() AT TIME ZONE \'America/New_York\')::date + $2::int)' : '= (NOW() AT TIME ZONE \'America/New_York\')::date'}
                 ${ODDS_FILTER}
               ORDER BY e.starts_at ASC`;
      params.push(league);
      if (isEu) params.push(EU_EVENT_LOOKAHEAD_DAYS);
    } else {
      query = `SELECT e.*, ${FORECAST_PUBLIC_SELECT}, COALESCE(ppc.ready_count, 0) AS player_props_ready_count, COALESCE(ppc.team_props_count, 0) AS team_props_count, COALESCE(ppc.no_source_market_count, 0) AS no_source_market_count, COALESCE(ppc.no_valid_market_match_count, 0) AS no_valid_market_match_count
               FROM rm_events e
               ${FORECAST_LATERAL}
               ${PLAYER_PROPS_LATERAL}
               WHERE (
                 (e.league IN (${EU_LEAGUES_SQL}) AND DATE(e.starts_at AT TIME ZONE 'America/New_York') BETWEEN (NOW() AT TIME ZONE 'America/New_York')::date AND ((NOW() AT TIME ZONE 'America/New_York')::date + $1::int))
                 OR
                 (e.league NOT IN (${EU_LEAGUES_SQL}) AND DATE(e.starts_at AT TIME ZONE 'America/New_York') = (NOW() AT TIME ZONE 'America/New_York')::date)
               )
                 ${ODDS_FILTER}
               ORDER BY e.starts_at ASC`;
      params.push(EU_EVENT_LOOKAHEAD_DAYS);
    }

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

    const visiblePlayerPropCounts = await buildVisiblePlayerPropCounts(rows);
    const sourceBackedPlayerPropCounts = await buildSourceBackedPlayerPropCounts(rows, visiblePlayerPropCounts);
    const grouped: Record<string, any[]> = {};
    for (const row of rows) {
      const lg = row.league;
      if (!grouped[lg]) grouped[lg] = [];
      const fcData = row.fc_data || {};
      const visiblePlayerPropsCount = visiblePlayerPropCounts.get(row.event_id) ?? 0;
      const resolvedPlayerPropsCount = visiblePlayerPropsCount > 0
        ? visiblePlayerPropsCount
        : (sourceBackedPlayerPropCounts.get(String(row.event_id)) ?? 0);
      const rawPlayerPropsCount = row.player_props_ready_count || 0;
      const digimonLockCount = lg === 'nba'
        ? extractDigimonLockCount(fcData, row.fc_model_signals, row.fc_input_quality)
        : 0;
      const parsedConfidence = row.fc_confidence === null || row.fc_confidence === undefined
        ? null
        : Number(row.fc_confidence);
      const hasFiniteConfidence = Number.isFinite(parsedConfidence);
      const publicFit = hasFiniteConfidence
        ? assessPublicGameFit({
          league: lg,
          forecastData: fcData,
          homeTeam: row.home_team,
          awayTeam: row.away_team,
        })
        : null;
      const hasEligibleForecast = hasFiniteConfidence && (publicFit?.eligible ?? false);
      const forecastStatus: 'ready' | 'generating' =
        hasEligibleForecast
          ? 'ready'
          : 'generating';
      // Build forecastMeta from LEFT JOIN data
      let forecastMeta: any = null;
      if (hasEligibleForecast) {
        const conf = Number(parsedConfidence);
        const valueRating = fcData.value_rating || 5;
        // BENCHMARK RULE: Use server-computed spread_edge from forecast_data
        // The spread_edge is computed from model margin vs market spread, NOT from moneyline probability
        let edge: number | null = fcData.spread_edge ?? null;
        if (edge == null) {
          // Fallback for legacy forecasts without spread_edge: compute from projected_margin vs market spread
          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 (conf > 0) {
            edge = Math.round((conf - 0.5) * 120) / 10;
          }
        }
        forecastMeta = {
          confidence: conf,
          edge,
          valueRating,
          forecastSide: fcData.forecast_side || null,
          // Match the forecast route: these badges indicate unlockable insight types,
          // not whether the cached preview text already contains a prequalified signal.
          hasSharpSignal: SHARP_UNLOCK_ENABLED(),
          hasSteamSignal: STEAM_UNLOCK_ENABLED(),
          hasForecast: true,
          ...(EVENTS_MARKET_BREAKDOWN ? { marketBreakdown: buildMarketBreakdown(fcData) } : {}),
        };
      }

      const allowPositivePositive = isEuLeague(lg);
      const currentMarket = sanitizePublicGameMarket(lg, row);
      const cachedMarket = sanitizePublicGameMarket(lg, row.fc_odds_data || null);
      const openingMarket = sanitizePublicGameMarket(lg, {
        moneyline: row.opening_moneyline || null,
        spread: row.opening_spread || null,
        total: row.opening_total || null,
      });
      const projectedMoneyline = normalizeProjectedMoneylinePair(row.fc_data);
      const currentMoneyline = normalizePublicMoneylinePair(currentMarket.moneyline, { allowPositivePositive });
      const cachedMoneyline = normalizePublicMoneylinePair(cachedMarket.moneyline, { allowPositivePositive });
      const openingMoneyline = normalizePublicMoneylinePair(openingMarket.moneyline, { allowPositivePositive });
      const lineSourceMeta = getPublicMarketSourceMeta(row.source);

      grouped[lg].push({
        id: row.event_id,
        league: lg,
        homeTeam: row.home_team,
        homeShort: row.home_short,
        awayTeam: row.away_team,
        awayShort: row.away_short,
        startsAt: row.starts_at,
        oddsUpdatedAt: row.odds_updated_at || null,
        verificationLabel: lineSourceMeta.label,
        verificationType: lineSourceMeta.type,
        status: row.status || 'scheduled',
        moneyline: resolvePublicMoneyline(currentMoneyline, cachedMoneyline, openingMoneyline, projectedMoneyline),
        spread: currentMarket.spread,
        total: resolvePublicTotal(currentMarket.total, cachedMarket.total, openingMarket.total),
        openingSpread: openingMarket.spread,
        openingMoneyline: row.opening_moneyline ? openingMoneyline : null,
        openingTotal: openingMarket.total,
        forecastStatus,
        playerPropsCount: resolvedPlayerPropsCount,
        playerPropsAvailable: resolvedPlayerPropsCount > 0,
        playerPropsStatus: derivePlayerPropsStatus(
          resolvedPlayerPropsCount,
          row.team_props_count || 0,
          rawPlayerPropsCount,
          row.no_source_market_count || 0,
          row.no_valid_market_match_count || 0,
        ),
        digimonLockCount,
        forecastMeta,
      });
    }

    const availableLeagues = Object.keys(grouped).sort();

    const payload = {
      leagues: availableLeagues,
      events: grouped,
      totalEvents: Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0),
    };

    writeEventsRouteCache(cacheKey, payload);
    res.setHeader('Cache-Control', 'public, max-age=30');
    res.setHeader('X-Rainmaker-Cache', 'MISS');
    res.json(payload);
  } catch (err) {
    console.error('Events fetch error:', err);
    res.status(500).json({ error: 'Failed to fetch events' });
  }
});

// GET /api/events/sports — supported sports with today's counts (no auth needed)
router.get('/sports', async (_req: Request, res: Response) => {
  try {
    const { rows } = await pool.query(`
      SELECT e.league, COUNT(*) as count
      FROM rm_events e
      ${FORECAST_LATERAL}
      WHERE (
        (e.league IN (${EU_LEAGUES_SQL}) AND DATE(e.starts_at AT TIME ZONE 'America/New_York') BETWEEN (NOW() AT TIME ZONE 'America/New_York')::date AND ((NOW() AT TIME ZONE 'America/New_York')::date + $1::int))
        OR
        (e.league NOT IN (${EU_LEAGUES_SQL}) AND DATE(e.starts_at AT TIME ZONE 'America/New_York') = (NOW() AT TIME ZONE 'America/New_York')::date)
      )
        ${ODDS_FILTER}
      GROUP BY e.league
      ORDER BY e.league
    `, [EU_EVENT_LOOKAHEAD_DAYS]);

    const SPORT_LABELS: Record<string, string> = {
      nba: 'NBA',
      nfl: 'NFL',
      mlb: 'MLB',
      nhl: 'NHL',
      ncaab: "NCAA Men's Basketball",
      ncaaf: 'NCAA Football',
      ncaaw: "NCAA Women's Basketball",
      wncaab: "NCAA Women's Basketball",
      wnba: 'WNBA',
      mma: 'MMA / UFC',
      epl: 'English Premier League',
      la_liga: 'La Liga',
      bundesliga: 'Bundesliga',
      serie_a: 'Serie A',
      ligue_1: 'Ligue 1',
      champions_league: 'UEFA Champions League',
    };

    const sports = rows.map(r => ({
      key: r.league,
      label: SPORT_LABELS[r.league] || r.league.toUpperCase(),
      todayCount: parseInt(r.count, 10),
    }));

    res.json({ sports, total: sports.reduce((sum: number, s: any) => sum + s.todayCount, 0) });
  } catch (err) {
    console.error('Sports endpoint error:', err);
    res.status(500).json({ error: 'Failed to fetch sports' });
  }
});

export default router;
