import { getMlbPlayerRole } from './forecast-asset-taxonomy';

export const SUPPORTED_MLB_PROP_TYPES: Record<string, string> = {
  pitching_strikeouts: 'Strikeouts',
  pitching_earnedRuns: 'Earned Runs',
  pitching_outs: 'Outs Recorded',
  pitching_hits: 'Hits Allowed',
  pitching_basesOnBalls: 'Walks Allowed',
  batting_singles: 'Singles',
  batting_doubles: 'Doubles',
  batting_triples: 'Triples',
  batting_basesOnBalls: 'Walks',
  batting_hits: 'Hits',
  batting_strikeouts: 'Strikeouts',
  'batting_hits+runs+rbi': 'Hits + Runs + RBIs',
  batting_totalBases: 'Total Bases',
  batting_runs: 'Runs',
  batting_RBI: 'RBIs',
  batting_homeRuns: 'Home Runs',
  batting_stolenBases: 'Stolen Bases',
  fantasyScore: 'Fantasy Score',
  points: 'Fantasy Points',
};

const NORMALIZED_PROP_TYPES: Record<string, string> = {
  points: 'fantasyScore',
};

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

export interface RawMlbMarketSource {
  eventId: string;
  startsAt: string | null;
  homeTeam: string | null;
  awayTeam: string | null;
  playerId: string;
  playerName: string;
  statType: string;
  normalizedStatType: string;
  line: number;
  side: MarketSide;
  odds: number | null;
  impliedProb: number | null;
  provider: string | null;
  source: string;
  sportsbookName: string | null;
  sportsbookId: string | null;
  timestamp: string | null;
  rawMarketId: string | null;
  marketName: string | null;
  teamShort: string | null;
}

export interface GroupedMlbMarket {
  eventId: string;
  startsAt: string | null;
  homeTeam: string | null;
  awayTeam: string | null;
  playerId: string;
  playerName: string;
  statType: string;
  normalizedStatType: string;
  line: number;
  marketName: string | null;
  teamShort: string | null;
  over: RawMlbMarketSource[];
  under: RawMlbMarketSource[];
}

export interface NormalizedMarketSide {
  odds: number | null;
  impliedProb: number | null;
  source: string;
  provider: string | null;
  sportsbookName: string | null;
  sportsbookId: string | null;
  timestamp: string | null;
  rawMarketId: string | null;
}

export interface MarketSourceMapEntry {
  side: MarketSide;
  source: string;
  odds: number | null;
  impliedProb: number | null;
  provider: string | null;
  sportsbookName: string | null;
  sportsbookId: string | null;
  timestamp: string | null;
  rawMarketId: string | null;
}

export interface NormalizedPlayerPropMarket {
  eventId: string;
  startsAt: string | null;
  homeTeam: string | null;
  awayTeam: string | null;
  playerId: string;
  playerName: string;
  teamShort: string | null;
  statType: string;
  normalizedStatType: string;
  line: number;
  marketName: string | null;
  over: NormalizedMarketSide | null;
  under: NormalizedMarketSide | null;
  primarySource: string | null;
  completionMethod: MarketCompletionMethod;
  completenessStatus: MarketCompletenessStatus;
  sourceMap: MarketSourceMapEntry[];
  isGapFilled: boolean;
  availableSides: MarketSide[];
  rawMarketCount: number;
  updatedAt: string | null;
}

export function normalizeMlbPropType(propType: string): string {
  return NORMALIZED_PROP_TYPES[propType] || propType;
}

export function getMlbPropLabel(propType: string): string | null {
  return SUPPORTED_MLB_PROP_TYPES[propType] || null;
}

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

export function titleCase(input: string): string {
  return input
    .split(' ')
    .filter(Boolean)
    .map((word) => word[0].toUpperCase() + word.slice(1))
    .join(' ');
}

export function parseMlbPlayerName(raw: any): string | null {
  const marketName = raw?.sportsgameodds?.marketName || raw?.marketName || '';
  const nameFromMarket = typeof marketName === 'string'
    ? marketName.replace(/\s+(Strikeouts|Earned Runs|Outs Recorded|Hits Allowed|Walks Allowed|Walks|Hits \+ Runs \+ RBIs|Hits|Total Bases|Runs|RBIs|Runs Batted In|Home Runs|Stolen Bases|Doubles|Triples|Singles|Fantasy Score|Fantasy Points)\s+Over\/Under$/i, '').trim()
    : '';
  if (nameFromMarket) return nameFromMarket;

  const playerId = raw?.sportsgameodds?.playerID || raw?.sportsgameodds?.statEntityID || raw?.playerExternalId || '';
  if (typeof playerId === 'string' && playerId.endsWith('_MLB')) {
    const stem = playerId.replace(/_\d+_MLB$/i, '').replace(/_/g, ' ').trim();
    if (stem) return titleCase(stem.toLowerCase());
  }

  return null;
}

export function parseMarketSide(raw: any): MarketSide | null {
  const side = String(raw?.side || raw?.sportsgameodds?.sideID || '').trim().toLowerCase();
  if (side === 'over' || side === 'under') return side;
  return null;
}

export function toNumber(value: unknown): number | null {
  if (value == null || value === '') return null;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : null;
}

export function impliedProbabilityFromAmerican(odds: number | null): number | null {
  if (odds == null || odds === 0) return null;
  if (odds > 0) return 100 / (odds + 100);
  return Math.abs(odds) / (Math.abs(odds) + 100);
}

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

  switch (normalized) {
    case '':
      return 'unknown';
    case 'draftkings':
    case 'draftking':
      return 'draftkings';
    case 'fanduel':
      return 'fanduel';
    case 'hardrock':
    case 'hardrocksportsbook':
    case 'hardrockbet':
      return 'hardrockbet';
    case 'betmgm':
      return 'betmgm';
    case 'espnbet':
      return 'espnbet';
    default:
      return normalized;
  }
}

function resolveSourceLine(row: any, source: string): number | null {
  const bookLine = toNumber(
    row?.raw?.sportsgameodds?.bookOverUnder
    ?? row?.raw?.sportsgameodds?.openBookOverUnder
    ?? row?.raw?.sportsgameodds?.fairOverUnder
  );
  if (bookLine != null) return bookLine;
  if (source === 'fanduel') return toNumber(row.fdLine) ?? toNumber(row.lineValue) ?? toNumber(row.dkLine);
  if (source === 'draftkings') return toNumber(row.dkLine) ?? toNumber(row.lineValue) ?? toNumber(row.fdLine);
  return toNumber(row.lineValue) ?? toNumber(row.fdLine) ?? toNumber(row.dkLine);
}

function resolveSourceOdds(row: any, source: string): number | null {
  const rawOdds = toNumber(row?.raw?.sportsgameodds?.bookOdds ?? row?.raw?.sportsgameodds?.openBookOdds);
  if (rawOdds != null) return rawOdds;
  if (source === 'fanduel') return toNumber(row.fdOverOdds) ?? toNumber(row.oddsAmerican);
  if (source === 'draftkings') return toNumber(row.dkOverOdds) ?? toNumber(row.oddsAmerican);
  return toNumber(row.oddsAmerican);
}

function chooseTimestamp(row: any): string | null {
  const candidates = [row.snapshotAt, row.updatedAt, row.createdAt, row.gameStart];
  for (const candidate of candidates) {
    if (!candidate) continue;
    const date = new Date(candidate);
    if (!Number.isNaN(date.getTime())) return date.toISOString();
  }
  return null;
}

function compareRawSources(a: RawMlbMarketSource, b: RawMlbMarketSource): number {
  const aHasOdds = a.odds != null ? 1 : 0;
  const bHasOdds = b.odds != null ? 1 : 0;
  if (aHasOdds !== bHasOdds) return bHasOdds - aHasOdds;

  const aTime = a.timestamp ? Date.parse(a.timestamp) : 0;
  const bTime = b.timestamp ? Date.parse(b.timestamp) : 0;
  if (aTime !== bTime) return bTime - aTime;

  return a.source.localeCompare(b.source);
}

function shouldReplaceExistingSource(next: RawMlbMarketSource, current: RawMlbMarketSource): boolean {
  return compareRawSources(next, current) < 0;
}

export function buildFallbackMlbEventKey(params: {
  startsAt?: string | null;
  homeTeam?: string | null;
  awayTeam?: string | null;
}): string {
  const startsAt = params.startsAt || 'unknown-start';
  const homeTeam = (params.homeTeam || 'unknown-home').toUpperCase();
  const awayTeam = (params.awayTeam || 'unknown-away').toUpperCase();
  return `mlb:${homeTeam}:${awayTeam}:${startsAt}`;
}

export function mapPlayerPropLineRowToRawMarketSource(
  row: any,
  context: {
    eventId?: string | null;
    startsAt?: string | null;
    homeTeam?: string | null;
    awayTeam?: string | null;
  },
): RawMlbMarketSource | null {
  const propType = String(row.propType || '').trim();
  if (!propType || !SUPPORTED_MLB_PROP_TYPES[propType]) return null;

  const playerName = parseMlbPlayerName({
    ...row.raw,
    playerExternalId: row.playerExternalId,
    marketName: row.marketName,
  });
  if (!playerName) return null;

  const side = parseMarketSide(row.raw);
  if (!side) return null;

  const source = normalizeSportsbookName(
    row?.raw?.bookmaker
    || row?.raw?.sportsgameodds?.bookmaker
    || row?.vendor
    || 'unknown',
  );
  const line = resolveSourceLine(row, source);
  if (line == null) return null;

  const playerId = String(
    row.playerExternalId
    || row?.raw?.sportsgameodds?.playerID
    || row?.raw?.sportsgameodds?.statEntityID
    || normalizeText(playerName).replace(/\s+/g, '_')
  ).trim();
  if (!playerId) return null;

  const odds = resolveSourceOdds(row, source);
  const eventId = String(
    context.eventId
    || row.eventId
    || buildFallbackMlbEventKey({
      startsAt: context.startsAt || row.gameStart,
      homeTeam: context.homeTeam || row.homeTeam,
      awayTeam: context.awayTeam || row.awayTeam,
    }),
  );

  return {
    eventId,
    startsAt: context.startsAt || row.gameStart || null,
    homeTeam: (context.homeTeam || row.homeTeam || null),
    awayTeam: (context.awayTeam || row.awayTeam || null),
    playerId,
    playerName,
    statType: propType,
    normalizedStatType: normalizeMlbPropType(propType),
    line,
    side,
    odds,
    impliedProb: impliedProbabilityFromAmerican(odds),
    provider: row.vendor || null,
    source,
    sportsbookName: source,
    sportsbookId: source,
    timestamp: chooseTimestamp(row),
    rawMarketId: String(row.oddId || row?.raw?.sportsgameodds?.oddID || '').trim() || null,
    marketName: row.marketName || row?.raw?.sportsgameodds?.marketName || null,
    teamShort: null,
  };
}

export function groupRawMlbMarketSources(rows: RawMlbMarketSource[]): GroupedMlbMarket[] {
  const grouped = new Map<string, GroupedMlbMarket>();

  for (const row of rows) {
    const key = [
      row.eventId,
      row.playerId,
      row.statType,
      row.line.toFixed(3),
    ].join('|');

    if (!grouped.has(key)) {
      grouped.set(key, {
        eventId: row.eventId,
        startsAt: row.startsAt,
        homeTeam: row.homeTeam,
        awayTeam: row.awayTeam,
        playerId: row.playerId,
        playerName: row.playerName,
        statType: row.statType,
        normalizedStatType: row.normalizedStatType,
        line: row.line,
        marketName: row.marketName,
        teamShort: row.teamShort,
        over: [],
        under: [],
      });
    }

    const group = grouped.get(key)!;
    if (!group.marketName && row.marketName) group.marketName = row.marketName;

    const bucket = row.side === 'over' ? group.over : group.under;
    const existingIndex = bucket.findIndex((entry) => entry.source === row.source);
    if (existingIndex === -1) {
      bucket.push(row);
      continue;
    }

    const existing = bucket[existingIndex];
    if (!shouldReplaceExistingSource(row, existing)) continue;
    bucket[existingIndex] = row;
  }

  return Array.from(grouped.values())
    .map((group) => ({
      ...group,
      over: [...group.over].sort(compareRawSources),
      under: [...group.under].sort(compareRawSources),
    }))
    .sort((a, b) => a.playerName.localeCompare(b.playerName) || a.statType.localeCompare(b.statType) || a.line - b.line);
}

export function getMlbPropRole(statType: string): 'batter' | 'pitcher' | 'unknown' | null {
  return getMlbPlayerRole(statType);
}
