import pool from '../db';
import { getTeamName, getTeamRoster, resolveCanonicalName } from './canonical-names';
import {
  getPlayerPropLabelForLeague,
  getPlayerPropSortWeightForLeague,
  getSupportedPlayerPropQueryValues,
  getSupportedPlayerPropTypes,
  normalizePlayerPropMarketStat,
} from './player-prop-market-registry';
import {
  fetchCurrentEventOdds,
  fetchCurrentOdds,
  getTheOddsPlayerPropMarketKey,
  getTheOddsSportKey,
  hasTheOddsApiConfigured,
  matchCurrentEvent,
  selectBestPlayerPropOutcome,
  type TheOddsBookmaker,
  type TheOddsBookmakerMarket,
  type TheOddsCurrentEvent,
  type TheOddsEventOddsSnapshot,
} from './the-odds';
import { fetchMlbPropCandidates } from './mlb-prop-markets';

type MarketSide = 'over' | 'under';
type MarketCompletenessStatus = 'source_complete' | 'multi_source_complete' | 'incomplete';
type MarketCompletionMethod = 'none' | 'source' | 'multi_source';

type RawTeamPropMarketRow = {
  playerExternalId?: string | null;
  propType?: string | null;
  lineValue?: number | null;
  oddsAmerican?: number | null;
  vendor?: string | null;
  market?: string | null;
  marketName?: string | null;
  marketScope?: string | null;
  updatedAt?: string | null;
  oddId?: string | null;
  raw?: Record<string, any> | null;
};

export interface TeamPropMarketSourceMapEntry {
  side: MarketSide;
  source: string;
  odds: number | null;
  timestamp: string | null;
  rawMarketId: string | null;
}

export interface TeamPropMarketCandidate {
  player: string;
  statType: string;
  normalizedStatType: string;
  marketLineValue: number;
  overOdds: number | null;
  underOdds: number | null;
  availableSides: MarketSide[];
  source: string | null;
  marketName: string;
  propLabel: string;
  completenessStatus?: MarketCompletenessStatus;
  completionMethod?: MarketCompletionMethod;
  isGapFilled?: boolean;
  sourceMap?: TeamPropMarketSourceMapEntry[];
}

export interface TeamPropMarketCandidateParams {
  league: string;
  teamShort: string;
  opponentShort: string;
  startsAt: string;
  teamName?: string;
  opponentName?: string;
  homeTeam?: string;
  awayTeam?: string;
  skipTheOddsVerification?: boolean;
  throwOnSourceQueryError?: boolean;
}

export interface GeneratedTeamPropCandidateLike {
  player: string;
  prop: string;
  recommendation: string;
  reasoning: string;
  edge?: number;
  prob?: number;
  projected_stat_value?: number;
  stat_type?: string;
  market_line_value?: number;
  model_context?: Record<string, any>;
}

const SOURCE_PRIORITY = ['fanduel', 'draftkings', 'williamhill_us', 'bet365', 'betmgm', 'caesars', 'espnbet', 'betrivers', 'fanatics', 'consensus', 'sportsgameodds', 'unknown'] as const;

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

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

function normalizeLine(value: any): number | null {
  const parsed = Number(value);
  if (!Number.isFinite(parsed)) return null;
  return Math.round(parsed * 1000) / 1000;
}

function parsePlayerNameFromExternalId(playerExternalId: string | null | undefined): string {
  const raw = String(playerExternalId || '').trim();
  if (!raw) return '';
  return raw
    .replace(/_\d+_[A-Z]+$/, '')
    .replace(/_/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
    .toLowerCase()
    .replace(/\b\w/g, (char) => char.toUpperCase());
}

function parsePlayerNameFromMarketName(
  marketName: string | null | undefined,
  propLabel: string,
): string {
  const escapedLabel = propLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  return String(marketName || '')
    .replace(new RegExp(`\\s+${escapedLabel}\\s+Over\\/Under$`, 'i'), '')
    .replace(/\s+Over\/Under$/i, '')
    .trim();
}

function normalizeDirection(value: string | null | undefined): MarketSide | null {
  const normalized = String(value || '').trim().toLowerCase();
  if (!normalized) return null;
  if (normalized === 'over' || normalized.startsWith('over') || normalized.endsWith('over') || normalized.includes('_over')) return 'over';
  if (normalized === 'under' || normalized.startsWith('under') || normalized.endsWith('under') || normalized.includes('_under')) return 'under';
  if (normalized.startsWith('o')) return 'over';
  if (normalized.startsWith('u')) return 'under';
  return null;
}

function getVendorPriority(value: string | null | undefined): number {
  switch (normalizeBookmaker(value)) {
    case 'fanduel':
      return 1;
    case 'draftkings':
      return 2;
    case 'williamhill_us':
      return 3;
    case 'bet365':
      return 4;
    case 'betmgm':
      return 5;
    case 'caesars':
      return 6;
    case 'espnbet':
      return 7;
    case 'betrivers':
      return 8;
    case 'fanatics':
      return 9;
    case 'consensus':
      return 10;
    case 'sportsgameodds':
      return 11;
    default:
      return 12;
  }
}

function normalizeBookmaker(value: string | null | undefined): string {
  const normalized = String(value || '')
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9_]+/g, '');

  switch (normalized) {
    case '':
      return 'unknown';
    case 'draftking':
    case 'draftkings':
      return 'draftkings';
    case 'fanduel':
      return 'fanduel';
    case 'caesars':
    case 'williamhillus':
    case 'williamhill_us':
      return 'williamhill_us';
    case 'bet365':
      return 'bet365';
    case 'betmgm':
      return 'betmgm';
    case 'espnbet':
      return 'espnbet';
    case 'betrivers':
      return 'betrivers';
    case 'fanatics':
      return 'fanatics';
    case 'consensus':
      return 'consensus';
    case 'sportsgameodds':
      return 'sportsgameodds';
    default:
      return normalized || 'unknown';
  }
}

function getRowBookmaker(row: Pick<RawTeamPropMarketRow, 'vendor' | 'raw'>): string | null {
  const rawBookmaker = normalizeBookmaker(String(row.raw?.bookmaker || row.raw?.sportsgameodds?.bookmaker || ''));
  if (rawBookmaker !== 'unknown') return rawBookmaker;
  return normalizeBookmaker(String(row.vendor || ''));
}

function isFullGameMarket(row: Pick<RawTeamPropMarketRow, 'market' | 'raw'> & { marketScope?: string | null }): boolean {
  const scope = String(row.marketScope || '').trim().toLowerCase();
  if (scope === 'full_game' || scope === 'game') {
    return true;
  }
  if (scope) {
    return false;
  }

  const period = String(row.raw?.period || row.raw?.sportsgameodds?.periodID || '').trim().toLowerCase();
  if (period === 'game' || period === 'full_game') {
    return true;
  }
  if (period) {
    return false;
  }

  const market = String(row.market || '').trim().toLowerCase();
  if (!market) {
    return true;
  }
  if (market.startsWith('game_')) {
    return true;
  }
  if (/^(1q|2q|3q|4q|q1|q2|q3|q4|1h|2h|h1|h2|ot|first_half|second_half|first_quarter|second_quarter|third_quarter|fourth_quarter)_/.test(market)) {
    return false;
  }
  return true;
}

function getSupportedPropTypes(league: string): string[] {
  return getSupportedPlayerPropTypes(league);
}

function normalizeSupportedStatType(league: string, value: string | null | undefined): string | null {
  const statType = normalizePlayerPropMarketStat(league, value);
  return statType && getSupportedPropTypes(league).includes(statType) ? statType : null;
}

function getPropLabel(league: string, statType: string): string {
  return getPlayerPropLabelForLeague(league, statType) || statType;
}

function buildCandidateKey(player: string, statType: string, marketLineValue: number): string {
  return `${normalizeName(player)}|${normalizeToken(statType)}|${marketLineValue}`;
}

function extractLineFromPropLabel(value: string | null | undefined): number | null {
  const match = String(value || '').match(/(-?\d+(?:\.\d+)?)/);
  return match ? normalizeLine(match[1]) : null;
}

function getCandidateSortWeight(league: string, statType: string): number {
  return getPlayerPropSortWeightForLeague(league, statType);
}

function buildPlayerStatKey(player: string, statType: string): string {
  return `${normalizeName(player)}|${normalizeToken(statType)}`;
}

function buildTeamIdentityVariants(league: string, teamShort: string | null | undefined, teamName?: string | null): string[] {
  const variants = new Set<string>();
  const push = (value: string | null | undefined) => {
    const cleaned = String(value || '').trim();
    if (!cleaned) return;
    variants.add(cleaned.toUpperCase());
  };

  push(teamShort);
  push(teamName);

  if (teamShort) {
    push(getTeamName(teamShort, league));
  }

  return [...variants];
}

function getLineBandScore(league: string, statType: string, line: number): number {
  const lg = String(league || '').toLowerCase();
  switch (lg) {
    case 'nba':
    case 'ncaab':
      switch (statType) {
        case 'points':
          if (line < 6 || line > 38) return -6;
          if (line >= 10 && line <= 32) return 8;
          return 3;
        case 'rebounds':
          if (line < 2.5 || line > 15.5) return -4;
          if (line >= 4.5 && line <= 11.5) return 6;
          return 2;
        case 'assists':
          if (line < 1.5 || line > 12.5) return -4;
          if (line >= 3.5 && line <= 9.5) return 6;
          return 2;
        case 'threes':
          if (line < 0.5 || line > 5.5) return -3;
          if (line >= 1.5 && line <= 3.5) return 5;
          return 1;
        case 'blocks':
        case 'steals':
          if (line < 0.5 || line > 3.5) return -3;
          if (line >= 0.5 && line <= 1.5) return 4;
          return 1;
        case 'turnovers':
          if (line < 0.5 || line > 5.5) return -3;
          if (line >= 1.5 && line <= 3.5) return 4;
          return 1;
        case 'pointsreboundsassists':
          if (line < 12.5 || line > 52.5) return -4;
          if (line >= 20.5 && line <= 42.5) return 6;
          return 2;
        case 'pointsrebounds':
        case 'pointsassists':
        case 'reboundsassists':
          if (line < 8.5 || line > 42.5) return -4;
          if (line >= 14.5 && line <= 30.5) return 5;
          return 2;
        default:
          return 0;
      }
    case 'nhl':
      switch (statType) {
        case 'points':
          if (line === 0.5) return 6;
          if (line === 1.5) return -4;
          return -2;
        case 'assists':
          if (line === 0.5) return 5;
          if (line === 1.5) return -3;
          return -1;
        case 'shots_onGoal':
          if (line >= 1.5 && line <= 4.5) return 6;
          return line < 1.5 || line > 5.5 ? -3 : 1;
        case 'goalie_saves':
          if (line >= 22.5 && line <= 34.5) return 5;
          return 0;
        default:
          return 0;
      }
    default:
      return 0;
  }
}

function isExtremeSingleSidedPrice(odds: number | null): boolean {
  if (odds == null || !Number.isFinite(odds)) return false;
  return odds >= 1000 || odds <= -450;
}

function hasTwoWayMarket(candidate: TeamPropMarketCandidate): boolean {
  return candidate.availableSides.includes('over') && candidate.availableSides.includes('under');
}

function shouldSuppressCandidate(candidate: TeamPropMarketCandidate, league: string): boolean {
  const sides = new Set(candidate.availableSides);
  const overOnly = sides.size === 1 && sides.has('over');
  const underOnly = sides.size === 1 && sides.has('under');
  const lg = String(league || '').toLowerCase();
  const twoWay = hasTwoWayMarket(candidate);

  if ((overOnly && isExtremeSingleSidedPrice(candidate.overOdds)) || (underOnly && isExtremeSingleSidedPrice(candidate.underOdds))) {
    return true;
  }

  if (lg === 'nba' || lg === 'ncaab' || lg === 'wnba') {
    if (!twoWay && ['blocks', 'steals', 'turnovers'].includes(candidate.statType)) {
      return true;
    }

    if (underOnly) {
      if (candidate.statType === 'points' && candidate.marketLineValue <= 9.5) return true;
      if (candidate.statType === 'rebounds' && candidate.marketLineValue <= 4.5) return true;
      if (candidate.statType === 'assists' && candidate.marketLineValue <= 2.5) return true;
    }
  }

  if (lg === 'nhl') {
    if (candidate.statType === 'points') {
      if (underOnly && candidate.marketLineValue <= 0.5) return true;
      if (overOnly && candidate.marketLineValue >= 1.5) return true;
    }
    if (candidate.statType === 'assists') {
      if (underOnly && candidate.marketLineValue <= 0.5) return true;
      if (overOnly && candidate.marketLineValue >= 1.5) return true;
    }
  }

  return false;
}

type CandidateSourceEntry = {
  side: MarketSide;
  source: string;
  odds: number;
  timestamp: string | null;
  rawMarketId: string | null;
};

type CandidateGroup = {
  player: string;
  statType: string;
  marketLineValue: number;
  marketName: string;
  propLabel: string;
  over: CandidateSourceEntry[];
  under: CandidateSourceEntry[];
};

function sourceRank(source: string): number {
  const rank = SOURCE_PRIORITY.indexOf(source as (typeof SOURCE_PRIORITY)[number]);
  return rank >= 0 ? rank : SOURCE_PRIORITY.length;
}

function timestampValue(value: string | null | undefined): number {
  if (!value) return 0;
  const parsed = Date.parse(value);
  return Number.isFinite(parsed) ? parsed : 0;
}

function compareSourceEntries(a: CandidateSourceEntry, b: CandidateSourceEntry): number {
  const sourceDelta = sourceRank(a.source) - sourceRank(b.source);
  if (sourceDelta !== 0) return sourceDelta;
  return timestampValue(b.timestamp) - timestampValue(a.timestamp);
}

function selectPreferredEntry(entries: CandidateSourceEntry[]): CandidateSourceEntry | null {
  return [...entries].sort(compareSourceEntries)[0] || null;
}

function selectBestSameSourcePair(group: CandidateGroup): { source: string; over: CandidateSourceEntry; under: CandidateSourceEntry } | null {
  const bySource = new Map<string, { over?: CandidateSourceEntry; under?: CandidateSourceEntry }>();

  for (const over of group.over) {
    const existing = bySource.get(over.source) || {};
    if (!existing.over || compareSourceEntries(over, existing.over) < 0) {
      bySource.set(over.source, { ...existing, over });
    }
  }
  for (const under of group.under) {
    const existing = bySource.get(under.source) || {};
    if (!existing.under || compareSourceEntries(under, existing.under) < 0) {
      bySource.set(under.source, { ...existing, under });
    }
  }

  return Array.from(bySource.entries())
    .filter(([, pair]) => pair.over && pair.under)
    .map(([source, pair]) => ({ source, over: pair.over!, under: pair.under! }))
    .sort((a, b) => {
      const sourceDelta = sourceRank(a.source) - sourceRank(b.source);
      if (sourceDelta !== 0) return sourceDelta;
      return Math.max(timestampValue(b.over.timestamp), timestampValue(b.under.timestamp))
        - Math.max(timestampValue(a.over.timestamp), timestampValue(a.under.timestamp));
    })[0] || null;
}

function toSourceMap(group: CandidateGroup): TeamPropMarketSourceMapEntry[] {
  return [...group.over, ...group.under]
    .map((entry) => ({
      side: entry.side,
      source: entry.source,
      odds: entry.odds,
      timestamp: entry.timestamp,
      rawMarketId: entry.rawMarketId,
    }))
    .sort((a, b) => {
      const sideDelta = a.side.localeCompare(b.side);
      if (sideDelta !== 0) return sideDelta;
      const sourceDelta = sourceRank(a.source) - sourceRank(b.source);
      if (sourceDelta !== 0) return sourceDelta;
      return timestampValue(b.timestamp) - timestampValue(a.timestamp);
    });
}

function buildCandidateFromGroup(group: CandidateGroup): TeamPropMarketCandidate {
  const sameSourcePair = selectBestSameSourcePair(group);
  let overEntry: CandidateSourceEntry | null = null;
  let underEntry: CandidateSourceEntry | null = null;
  let completionMethod: MarketCompletionMethod = 'none';
  let completenessStatus: MarketCompletenessStatus = 'incomplete';
  let isGapFilled = false;
  let primarySource: string | null = null;

  if (sameSourcePair) {
    overEntry = sameSourcePair.over;
    underEntry = sameSourcePair.under;
    completionMethod = 'source';
    completenessStatus = 'source_complete';
    primarySource = sameSourcePair.source;
  } else {
    overEntry = selectPreferredEntry(group.over);
    underEntry = selectPreferredEntry(group.under);

    if (overEntry && underEntry) {
      completionMethod = 'multi_source';
      completenessStatus = 'multi_source_complete';
      isGapFilled = overEntry.source !== underEntry.source;
      primarySource = [...[overEntry.source, underEntry.source]].sort((a, b) => sourceRank(a) - sourceRank(b))[0] || null;
    } else {
      primarySource = overEntry?.source || underEntry?.source || null;
    }
  }

  const availableSides: MarketSide[] = [];
  if (group.over.length > 0) availableSides.push('over');
  if (group.under.length > 0) availableSides.push('under');

  return {
    player: group.player,
    statType: group.statType,
    normalizedStatType: group.statType,
    marketLineValue: group.marketLineValue,
    overOdds: overEntry?.odds ?? null,
    underOdds: underEntry?.odds ?? null,
    availableSides,
    source: primarySource,
    marketName: group.marketName,
    propLabel: group.propLabel,
    completenessStatus,
    completionMethod,
    isGapFilled,
    sourceMap: toSourceMap(group),
  };
}

function getCandidateQualityScore(candidate: TeamPropMarketCandidate, league: string): number {
  let score = 0;
  const hasOver = candidate.overOdds != null;
  const hasUnder = candidate.underOdds != null;
  const vendorScore = 10 - getVendorPriority(candidate.source);

  score += vendorScore;
  score += getLineBandScore(league, candidate.statType, candidate.marketLineValue);
  if (candidate.completenessStatus === 'source_complete') score += 8;
  else if (candidate.completenessStatus === 'multi_source_complete') score += 5;
  else score -= 2;

  if (hasOver && hasUnder) {
    score += 30;
    const midpoint = (Math.abs(candidate.overOdds || 0) + Math.abs(candidate.underOdds || 0)) / 2;
    score += Math.max(0, 10 - Math.abs(midpoint - 110) / 25);
  } else {
    score -= 6;
    const loneOdds = hasOver ? candidate.overOdds : candidate.underOdds;
    if (loneOdds != null) {
      score += Math.max(-10, 8 - Math.abs(Math.abs(loneOdds) - 120) / 35);
    }
  }

  return score;
}

function buildCurrentOddsSnapshot(event: TheOddsCurrentEvent): TheOddsEventOddsSnapshot {
  const snapshotTimestamp = event.bookmakers.find((bookmaker) => bookmaker?.last_update)?.last_update
    || event.commence_time;
  return {
    timestamp: snapshotTimestamp,
    eventId: event.id,
    sportKey: event.sport_key,
    commenceTime: event.commence_time,
    homeTeam: event.home_team,
    awayTeam: event.away_team,
    bookmakers: event.bookmakers || [],
  };
}

function mergeBookmakerMarkets(
  snapshots: TheOddsEventOddsSnapshot[],
  fallbackEvent: TheOddsCurrentEvent,
): TheOddsEventOddsSnapshot {
  const bookmakerMap = new Map<string, TheOddsBookmaker>();

  for (const snapshot of snapshots) {
    for (const bookmaker of snapshot.bookmakers || []) {
      const key = `${bookmaker.key}|${bookmaker.title}`;
      const existing = bookmakerMap.get(key);
      if (!existing) {
        bookmakerMap.set(key, {
          ...bookmaker,
          markets: [...(bookmaker.markets || [])],
        });
        continue;
      }

      const marketMap = new Map<string, TheOddsBookmakerMarket>();
      for (const market of existing.markets || []) {
        marketMap.set(market.key, market);
      }
      for (const market of bookmaker.markets || []) {
        marketMap.set(market.key, market);
      }
      existing.markets = [...marketMap.values()];
      existing.last_update = existing.last_update || bookmaker.last_update || null;
      bookmakerMap.set(key, existing);
    }
  }

  return {
    timestamp: snapshots.find((snapshot) => snapshot.timestamp)?.timestamp || fallbackEvent.commence_time,
    eventId: fallbackEvent.id,
    sportKey: fallbackEvent.sport_key,
    commenceTime: fallbackEvent.commence_time,
    homeTeam: fallbackEvent.home_team,
    awayTeam: fallbackEvent.away_team,
    bookmakers: [...bookmakerMap.values()],
  };
}

async function fetchVerifiedPlayerPropSnapshot(params: {
  sportKey: string;
  event: TheOddsCurrentEvent;
  marketKeys: string[];
}): Promise<TheOddsEventOddsSnapshot | null> {
  if (params.marketKeys.length === 0) return null;

  try {
    return await fetchCurrentEventOdds({
      sportKey: params.sportKey,
      eventId: params.event.id,
      markets: params.marketKeys,
    });
  } catch (err: any) {
    const message = String(err?.message || '');
    if (!message.includes('INVALID_MARKET') || params.marketKeys.length === 1) {
      throw err;
    }
  }

  const snapshots: TheOddsEventOddsSnapshot[] = [];
  for (const marketKey of params.marketKeys) {
    try {
      const snapshot = await fetchCurrentEventOdds({
        sportKey: params.sportKey,
        eventId: params.event.id,
        markets: [marketKey],
      });
      snapshots.push(snapshot);
    } catch (err: any) {
      const message = String(err?.message || '');
      if (message.includes('INVALID_MARKET')) {
        continue;
      }
      throw err;
    }
  }

  return snapshots.length > 0 ? mergeBookmakerMarkets(snapshots, params.event) : null;
}

function toRoundedAmericanOdds(value: number | null | undefined): number | null {
  if (value == null || !Number.isFinite(value)) return null;
  return Math.round(value);
}

function sameMarketLine(left: number | null | undefined, right: number | null | undefined): boolean {
  if (left == null || right == null) return false;
  return Math.abs(Number(left) - Number(right)) <= 0.001;
}

function buildCandidateGroupFromCandidate(candidate: TeamPropMarketCandidate): CandidateGroup {
  const group: CandidateGroup = {
    player: candidate.player,
    statType: candidate.statType,
    marketLineValue: candidate.marketLineValue,
    marketName: candidate.marketName,
    propLabel: candidate.propLabel,
    over: [],
    under: [],
  };

  for (const entry of candidate.sourceMap || []) {
    if (entry.odds == null || !Number.isFinite(entry.odds)) continue;
    const sourceEntry: CandidateSourceEntry = {
      side: entry.side,
      source: normalizeBookmaker(entry.source),
      odds: entry.odds,
      timestamp: entry.timestamp,
      rawMarketId: entry.rawMarketId,
    };
    if (entry.side === 'over') group.over.push(sourceEntry);
    if (entry.side === 'under') group.under.push(sourceEntry);
  }

  if (group.over.length === 0 && candidate.overOdds != null) {
    group.over.push({
      side: 'over',
      source: normalizeBookmaker(candidate.source),
      odds: candidate.overOdds,
      timestamp: null,
      rawMarketId: null,
    });
  }
  if (group.under.length === 0 && candidate.underOdds != null) {
    group.under.push({
      side: 'under',
      source: normalizeBookmaker(candidate.source),
      odds: candidate.underOdds,
      timestamp: null,
      rawMarketId: null,
    });
  }

  return group;
}

function buildTheOddsSourceEntry(
  side: MarketSide,
  outcome: {
    bookmakerKey?: string | null;
    bookmakerTitle?: string | null;
    odds?: number | null;
    marketUpdatedAt?: string | null;
    bookmakerUpdatedAt?: string | null;
  } | null | undefined,
): CandidateSourceEntry | null {
  const odds = toRoundedAmericanOdds(outcome?.odds);
  if (odds == null) return null;

  return {
    side,
    source: normalizeBookmaker(outcome?.bookmakerKey || outcome?.bookmakerTitle || 'unknown'),
    odds,
    timestamp: outcome?.marketUpdatedAt || outcome?.bookmakerUpdatedAt || null,
    rawMarketId: null,
  };
}

function overlayCandidateWithVerifiedOdds(
  candidate: TeamPropMarketCandidate,
  outcomes: {
    over?: {
      bookmakerKey?: string | null;
      bookmakerTitle?: string | null;
      odds?: number | null;
      marketUpdatedAt?: string | null;
      bookmakerUpdatedAt?: string | null;
    } | null;
    under?: {
      bookmakerKey?: string | null;
      bookmakerTitle?: string | null;
      odds?: number | null;
      marketUpdatedAt?: string | null;
      bookmakerUpdatedAt?: string | null;
    } | null;
  },
): TeamPropMarketCandidate {
  const group = buildCandidateGroupFromCandidate(candidate);
  let appended = false;

  if (candidate.overOdds == null) {
    const overEntry = buildTheOddsSourceEntry('over', outcomes.over);
    if (overEntry) {
      group.over.push(overEntry);
      appended = true;
    }
  }

  if (candidate.underOdds == null) {
    const underEntry = buildTheOddsSourceEntry('under', outcomes.under);
    if (underEntry) {
      group.under.push(underEntry);
      appended = true;
    }
  }

  return appended ? buildCandidateFromGroup(group) : candidate;
}

async function verifyCandidatesWithTheOdds(
  candidates: TeamPropMarketCandidate[],
  params: TeamPropMarketCandidateParams,
): Promise<TeamPropMarketCandidate[]> {
  if (candidates.length === 0 || !hasTheOddsApiConfigured()) {
    return candidates;
  }

  const sportKey = getTheOddsSportKey(params.league);
  if (!sportKey || !params.homeTeam || !params.awayTeam) {
    return candidates;
  }

  const marketKeys = Array.from(new Set(
    candidates
      .map((candidate) => getTheOddsPlayerPropMarketKey(params.league, candidate.statType))
      .filter((value): value is string => Boolean(value)),
  ));
  if (marketKeys.length === 0) {
    return candidates;
  }

  try {
    const events = await fetchCurrentOdds({
      sportKey,
      markets: ['h2h'],
    });
    const matchedEvent = matchCurrentEvent(events, {
      homeTeam: params.homeTeam,
      awayTeam: params.awayTeam,
      startsAt: params.startsAt,
    });
    if (!matchedEvent) {
      return candidates;
    }

    const snapshot = await fetchVerifiedPlayerPropSnapshot({
      sportKey,
      event: matchedEvent,
      marketKeys,
    });
    if (!snapshot) {
      return candidates;
    }
    return candidates.map((candidate) => {
      const marketKey = getTheOddsPlayerPropMarketKey(params.league, candidate.statType);
      if (!marketKey) return candidate;

      const overOutcome = selectBestPlayerPropOutcome(snapshot, {
        marketKey,
        playerName: candidate.player,
        direction: 'over',
        targetLine: candidate.marketLineValue,
        preferNearestLine: true,
      });
      const underOutcome = selectBestPlayerPropOutcome(snapshot, {
        marketKey,
        playerName: candidate.player,
        direction: 'under',
        targetLine: candidate.marketLineValue,
        preferNearestLine: true,
      });

      const verifiedLines = [overOutcome?.line, underOutcome?.line]
        .filter((value): value is number => value != null);
      if (verifiedLines.length > 0 && !verifiedLines.some((line) => sameMarketLine(line, candidate.marketLineValue))) {
        return candidate;
      }

      return overlayCandidateWithVerifiedOdds(candidate, {
        over: overOutcome,
        under: underOutcome,
      });
    });
  } catch (err: any) {
    console.warn(`[team-props] The Odds verification failed: ${err.message}`);
    return candidates;
  }
}

export function buildTeamPropMarketCandidatesFromRows(
  rows: RawTeamPropMarketRow[],
  params: Pick<TeamPropMarketCandidateParams, 'league' | 'teamShort'>,
): TeamPropMarketCandidate[] {
  const supportedTypes = new Set(getSupportedPropTypes(params.league));
  if (supportedTypes.size === 0) return [];

  const roster = getTeamRoster(params.teamShort, params.league);
  const rosterSet = new Set(roster.map((player) => normalizeName(player)));
  const grouped = new Map<string, CandidateGroup>();

  const sortedRows = [...rows].sort((a, b) => {
    const vendorDelta = getVendorPriority(getRowBookmaker(a)) - getVendorPriority(getRowBookmaker(b));
    if (vendorDelta !== 0) return vendorDelta;
    return timestampValue(b.updatedAt) - timestampValue(a.updatedAt);
  });

  for (const row of sortedRows) {
    if (!isFullGameMarket(row)) {
      continue;
    }

    const statType = normalizeSupportedStatType(params.league, row.propType);
    const marketLineValue = normalizeLine(row.lineValue);
    const odds = Number(row.oddsAmerican);
    if (!statType || !supportedTypes.has(statType) || marketLineValue == null || !Number.isFinite(odds)) {
      continue;
    }

    const propLabel = getPropLabel(params.league, statType);
    const rawPlayer = parsePlayerNameFromExternalId(row.playerExternalId)
      || parsePlayerNameFromMarketName(row.marketName, propLabel);
    const canonicalPlayer = resolveCanonicalName(rawPlayer, params.league);
    if (!canonicalPlayer) continue;
    if (rosterSet.size > 0 && !rosterSet.has(normalizeName(canonicalPlayer))) continue;

    const direction = normalizeDirection(row.raw?.side || row.market);
    if (!direction) continue;

    const key = buildCandidateKey(canonicalPlayer, statType, marketLineValue);
    const existing = grouped.get(key) || {
      player: canonicalPlayer,
      statType,
      marketLineValue,
      marketName: String(row.marketName || `${canonicalPlayer} ${propLabel} Over/Under`).trim(),
      propLabel: `${propLabel} ${marketLineValue}`,
      over: [],
      under: [],
    };

    const entry: CandidateSourceEntry = {
      side: direction,
      source: getRowBookmaker(row) || 'unknown',
      odds,
      timestamp: row.updatedAt ? new Date(row.updatedAt).toISOString() : null,
      rawMarketId: row.oddId || null,
    };

    if (direction === 'over') existing.over.push(entry);
    else existing.under.push(entry);

    grouped.set(key, existing);
  }

  const filtered = [...grouped.values()]
    .map((group) => buildCandidateFromGroup(group))
    .filter((candidate) => !shouldSuppressCandidate(candidate, params.league));
  const dedupedByPlayerStat = new Map<string, TeamPropMarketCandidate>();

  for (const candidate of filtered) {
    const key = buildPlayerStatKey(candidate.player, candidate.statType);
    const existing = dedupedByPlayerStat.get(key);
    if (!existing) {
      dedupedByPlayerStat.set(key, candidate);
      continue;
    }

    const existingScore = getCandidateQualityScore(existing, params.league);
    const candidateScore = getCandidateQualityScore(candidate, params.league);
    if (candidateScore > existingScore) {
      dedupedByPlayerStat.set(key, candidate);
    }
  }

  return [...dedupedByPlayerStat.values()].sort((a, b) => {
    const qualityDelta = getCandidateQualityScore(b, params.league) - getCandidateQualityScore(a, params.league);
    if (qualityDelta !== 0) return qualityDelta;
    const weightDelta = getCandidateSortWeight(params.league, a.statType) - getCandidateSortWeight(params.league, b.statType);
    if (weightDelta !== 0) return weightDelta;
    const playerDelta = a.player.localeCompare(b.player);
    if (playerDelta !== 0) return playerDelta;
    return a.marketLineValue - b.marketLineValue;
  });
}

export async function fetchTeamPropMarketCandidates(
  params: TeamPropMarketCandidateParams,
): Promise<TeamPropMarketCandidate[]> {
  if (String(params.league || '').toLowerCase() === 'mlb') {
    const mlbCandidates = await fetchMlbPropCandidates({
      teamShort: params.teamShort,
      teamName: params.teamName || params.teamShort,
      opponentShort: params.opponentShort,
      startsAt: params.startsAt,
    }).catch(() => []);

    return mlbCandidates.map((candidate) => ({
      player: candidate.player,
      statType: candidate.statType,
      normalizedStatType: candidate.normalizedStatType,
      marketLineValue: candidate.marketLineValue,
      overOdds: candidate.overOdds,
      underOdds: candidate.underOdds,
      availableSides: candidate.availableSides,
      source: candidate.source,
      marketName: candidate.marketName,
      propLabel: candidate.prop,
      completenessStatus: candidate.completenessStatus,
      completionMethod: candidate.completionMethod,
      isGapFilled: candidate.isGapFilled,
      sourceMap: candidate.sourceMap,
    }));
  }

  const supportedTypes = getSupportedPropTypes(params.league);
  const supportedQueryValues = getSupportedPlayerPropQueryValues(params.league);
  if (supportedTypes.length === 0 || supportedQueryValues.length === 0) return [];

  const teamVariants = buildTeamIdentityVariants(params.league, params.teamShort, params.teamName);
  const opponentVariants = buildTeamIdentityVariants(params.league, params.opponentShort, params.opponentName);
  if (teamVariants.length === 0 || opponentVariants.length === 0) return [];

  const { rows } = await pool.query(
    `SELECT "playerExternalId", "propType", "lineValue", "oddsAmerican", vendor, market, "marketName", raw, "marketScope", "updatedAt", "oddId"
     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[])
         OR UPPER(COALESCE("awayTeam", '')) = ANY($3::text[])
       )
       AND (
         UPPER(COALESCE("homeTeam", '')) = ANY($4::text[])
         OR UPPER(COALESCE("awayTeam", '')) = ANY($4::text[])
       )
       AND "oddsAmerican" IS NOT NULL
       AND "lineValue" IS NOT NULL
       AND LOWER(COALESCE("propType", '')) = ANY($5::text[])
     ORDER BY
       CASE LOWER(COALESCE(raw->>'bookmaker', raw #>> '{sportsgameodds,bookmaker}', vendor, ''))
         WHEN 'fanduel' THEN 1
         WHEN 'draftkings' THEN 2
         WHEN 'williamhill_us' THEN 3
         WHEN 'bet365' THEN 4
         WHEN 'betmgm' THEN 5
         WHEN 'caesars' THEN 6
         WHEN 'espnbet' THEN 7
         WHEN 'betrivers' THEN 8
         WHEN 'fanatics' THEN 9
         WHEN 'consensus' THEN 10
         ELSE 11
       END,
       "updatedAt" DESC`,
    [params.league, params.startsAt, teamVariants, opponentVariants, supportedQueryValues],
  ).catch((err) => {
    if (params.throwOnSourceQueryError) {
      throw err;
    }
    return { rows: [] as RawTeamPropMarketRow[] };
  });

  const candidates = buildTeamPropMarketCandidatesFromRows(rows as RawTeamPropMarketRow[], params);
  if (params.skipTheOddsVerification) {
    return candidates;
  }
  return verifyCandidatesWithTheOdds(candidates, params);
}

export function formatTeamPropMarketCandidatesForPrompt(
  candidates: TeamPropMarketCandidate[],
  teamName: string,
): string {
  if (candidates.length === 0) return '';

  const lines = candidates.slice(0, 24).map((candidate) => {
    const over = candidate.overOdds != null ? `over ${candidate.overOdds}` : 'over N/A';
    const under = candidate.underOdds != null ? `under ${candidate.underOdds}` : 'under N/A';
    return `- ${candidate.player} — ${candidate.propLabel} (${over} / ${under})`;
  });

  return `\nSOURCE-BACKED PLAYER PROP MARKETS FOR ${teamName.toUpperCase()}:\n${lines.join('\n')}\nUse ONLY these player/stat/line combinations when this section is present.`;
}

export function validateTeamPropsAgainstMarketCandidates(
  props: GeneratedTeamPropCandidateLike[],
  candidates: TeamPropMarketCandidate[],
  league: string,
): GeneratedTeamPropCandidateLike[] {
  if (candidates.length === 0) return props;

  const candidateMap = new Map<string, TeamPropMarketCandidate>();
  for (const candidate of candidates) {
    candidateMap.set(
      buildCandidateKey(candidate.player, candidate.normalizedStatType, candidate.marketLineValue),
      candidate,
    );
  }

  const validated: GeneratedTeamPropCandidateLike[] = [];
  for (const prop of props) {
    const canonicalPlayer = resolveCanonicalName(prop.player, league);
    const statType = normalizeSupportedStatType(league, prop.stat_type || prop.prop);
    const marketLineValue = normalizeLine(prop.market_line_value ?? extractLineFromPropLabel(prop.prop));
    if (!canonicalPlayer || !statType || marketLineValue == null) continue;

    const candidate = candidateMap.get(buildCandidateKey(canonicalPlayer, statType, marketLineValue));
    if (!candidate) continue;

    const recommendation = normalizeDirection(prop.recommendation || prop.prop);
    if (recommendation && !candidate.availableSides.includes(recommendation)) {
      continue;
    }

    validated.push({
      ...prop,
      player: canonicalPlayer,
      recommendation: recommendation ?? prop.recommendation,
      stat_type: candidate.normalizedStatType,
      market_line_value: candidate.marketLineValue,
      prop: prop.prop || candidate.propLabel,
    });
  }

  return validated;
}
