import { getEtDateKey } from '../lib/league-windows';
import { TheOddsBookmaker, teamMatchScore } from './the-odds';

export interface RainmakerMoneyline {
  home: number | null;
  away: number | null;
}

export interface RainmakerSpreadSide {
  line: number | null;
  odds: number | null;
}

export interface RainmakerTotalSide {
  line: number | null;
  odds: number | null;
}

export interface RainmakerPublicEvent {
  id: string;
  league: string;
  homeTeam: string;
  awayTeam: string;
  startsAt: string;
  moneyline: RainmakerMoneyline;
  spread: {
    home: RainmakerSpreadSide | null;
    away: RainmakerSpreadSide | null;
  };
  total: {
    over: RainmakerTotalSide | null;
    under: RainmakerTotalSide | null;
  };
  forecastStatus: string;
  forecastMeta?: {
    confidence?: number | null;
    forecastSide?: string | null;
  } | null;
}

export interface RainmakerPublicEventsFeed {
  totalEvents: number;
  leagues: string[];
  events: Record<string, RainmakerPublicEvent[]>;
}

export interface TheOddsCurrentEvent {
  id: string;
  sport_key: string;
  sport_title: string;
  commence_time: string;
  home_team: string;
  away_team: string;
  bookmakers: TheOddsBookmaker[];
}

export type ForecastAuditIssueCode =
  | 'event_unmatched'
  | 'start_time_mismatch'
  | 'ready_without_confidence'
  | 'moneyline_out_of_range'
  | 'spread_out_of_range'
  | 'total_out_of_range';

export interface ForecastAuditNumberComparison {
  available: boolean;
  exact: boolean;
  within: boolean;
  deltaFromMedian: number | null;
  range: { min: number; max: number } | null;
  median: number | null;
  sampleSize: number;
}

export interface ForecastAuditEventReport {
  league: string;
  eventId: string;
  matchup: string;
  forecastStatus: string;
  forecastSide: string | null;
  confidence: number | null;
  thirdPartyEventId: string | null;
  thirdPartyBooks: number;
  startDeltaMin: number | null;
  issues: ForecastAuditIssueCode[];
  moneyline: {
    home: ForecastAuditNumberComparison;
    away: ForecastAuditNumberComparison;
  };
  spread: {
    homeLine: ForecastAuditNumberComparison;
    awayLine: ForecastAuditNumberComparison;
    homeOdds: ForecastAuditNumberComparison;
    awayOdds: ForecastAuditNumberComparison;
  };
  total: {
    overLine: ForecastAuditNumberComparison;
    underLine: ForecastAuditNumberComparison;
    overOdds: ForecastAuditNumberComparison;
    underOdds: ForecastAuditNumberComparison;
  };
}

export interface ForecastAuditSummary {
  rainmakerEvents: number;
  matchedEvents: number;
  unmatchedEvents: number;
  readyForecasts: number;
  generatingForecasts: number;
  readyWithoutConfidence: number;
  exactStartTimes: number;
  startTimeMismatches: number;
  moneylineSidesOutsideRange: number;
  spreadSidesOutsideRange: number;
  totalSidesOutsideRange: number;
}

export interface ForecastAuditExactRollups {
  exactMoneylineSides: number;
  exactSpreadLineSides: number;
  exactSpreadOddsSides: number;
  exactTotalLineSides: number;
  exactTotalOddsSides: number;
}

export interface ForecastAuditIssueCounts {
  event_unmatched: number;
  start_time_mismatch: number;
  ready_without_confidence: number;
  moneyline_out_of_range: number;
  spread_out_of_range: number;
  total_out_of_range: number;
}

export interface ForecastFeedAuditReport {
  auditedAt: string;
  summary: ForecastAuditSummary;
  exactRollups: ForecastAuditExactRollups;
  issueCounts: ForecastAuditIssueCounts;
  startDeltaDistribution: Array<{ delta: number; count: number }>;
  issueEventCount: number;
  passed: boolean;
  events: ForecastAuditEventReport[];
}

interface NumericSummary {
  values: number[];
  count: number;
  median: number | null;
  range: { min: number; max: number } | null;
}

interface TheOddsMarketOutcome {
  name: string;
  price: number | null;
  point: number | null;
}

const AMERICAN_ODDS_TOLERANCE = 5;
const MIN_RANGE_SAMPLE_SIZE = 3;
const THIRD_PARTY_HORIZON_BUFFER_MS = 12 * 60 * 60 * 1000;

const EMPTY_ISSUE_COUNTS: ForecastAuditIssueCounts = {
  event_unmatched: 0,
  start_time_mismatch: 0,
  ready_without_confidence: 0,
  moneyline_out_of_range: 0,
  spread_out_of_range: 0,
  total_out_of_range: 0,
};

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

function median(values: number[]): number | null {
  if (values.length === 0) return null;
  const sorted = [...values].sort((a, b) => a - b);
  const middle = Math.floor(sorted.length / 2);
  return sorted.length % 2 === 1
    ? sorted[middle]
    : (sorted[middle - 1] + sorted[middle]) / 2;
}

function summarizeNumbers(values: Array<number | null | undefined>): NumericSummary {
  const filtered = values.filter((value): value is number => Number.isFinite(value));
  if (filtered.length === 0) {
    return {
      values: [],
      count: 0,
      median: null,
      range: null,
    };
  }

  return {
    values: filtered,
    count: filtered.length,
    median: median(filtered),
    range: {
      min: Math.min(...filtered),
      max: Math.max(...filtered),
    },
  };
}

function compareNumber(
  rainmakerValue: number | null | undefined,
  summary: NumericSummary,
  tolerance = 0,
): ForecastAuditNumberComparison {
  if (!Number.isFinite(rainmakerValue) || summary.count === 0 || !summary.range) {
    return {
      available: false,
      exact: false,
      within: false,
      deltaFromMedian: null,
      range: null,
      median: null,
      sampleSize: summary.count,
    };
  }

  const safeRainmakerValue = Number(rainmakerValue);

  return {
    available: true,
    exact: summary.values.some((value) => value === safeRainmakerValue),
    within: safeRainmakerValue >= (summary.range.min - tolerance) && safeRainmakerValue <= (summary.range.max + tolerance),
    deltaFromMedian: summary.median == null ? null : Number((safeRainmakerValue - summary.median).toFixed(3)),
    range: summary.range,
    median: summary.median,
    sampleSize: summary.count,
  };
}

function extractMarketOutcomes(bookmakers: TheOddsBookmaker[], marketKey: string): TheOddsMarketOutcome[] {
  const outcomes: TheOddsMarketOutcome[] = [];

  for (const bookmaker of bookmakers) {
    for (const market of bookmaker.markets || []) {
      if (market.key !== marketKey) continue;
      for (const outcome of market.outcomes || []) {
        outcomes.push({
          name: String(outcome.name || ''),
          price: Number.isFinite(outcome.price) ? Number(outcome.price) : null,
          point: Number.isFinite(outcome.point) ? Number(outcome.point) : null,
        });
      }
    }
  }

  return outcomes;
}

function filterTeamOutcomes(
  outcomes: TheOddsMarketOutcome[],
  teamName: string,
): TheOddsMarketOutcome[] {
  return outcomes.filter((outcome) => teamMatchScore(outcome.name, teamName) >= 2);
}

function filterDirectionOutcomes(
  outcomes: TheOddsMarketOutcome[],
  direction: 'over' | 'under',
): TheOddsMarketOutcome[] {
  return outcomes.filter((outcome) => normalizeToken(outcome.name) === direction);
}

function filterOutcomesByPoint(
  outcomes: TheOddsMarketOutcome[],
  targetPoint: number | null | undefined,
): TheOddsMarketOutcome[] {
  if (!Number.isFinite(targetPoint)) return [];
  return outcomes.filter((outcome) => Number.isFinite(outcome.point) && Number(outcome.point) === Number(targetPoint));
}

export function flattenRainmakerEventsFeed(feed: RainmakerPublicEventsFeed): RainmakerPublicEvent[] {
  return Object.entries(feed.events || {}).flatMap(([league, events]) => (
    (events || []).map((event) => ({
      ...event,
      league: event.league || league,
    }))
  ));
}

export function filterRainmakerFeedByEtDate(
  feed: RainmakerPublicEventsFeed,
  dateEt: string,
): RainmakerPublicEventsFeed {
  const scopedEvents = Object.entries(feed.events || {}).reduce<Record<string, RainmakerPublicEvent[]>>((acc, [league, events]) => {
    const filtered = (events || []).filter((event) => getEtDateKey(event.startsAt) === dateEt);
    if (filtered.length > 0) acc[league] = filtered;
    return acc;
  }, {});

  return {
    totalEvents: Object.values(scopedEvents).reduce((sum, events) => sum + events.length, 0),
    leagues: Object.keys(scopedEvents),
    events: scopedEvents,
  };
}

export function matchRainmakerEvent(
  event: RainmakerPublicEvent,
  candidates: TheOddsCurrentEvent[],
): { event: TheOddsCurrentEvent; score: number; deltaMs: number } | null {
  const targetStart = new Date(event.startsAt).getTime();
  let best: { event: TheOddsCurrentEvent; score: number; deltaMs: number } | null = null;

  for (const candidate of candidates) {
    const homeScore = teamMatchScore(candidate.home_team, event.homeTeam);
    const awayScore = teamMatchScore(candidate.away_team, event.awayTeam);
    if (homeScore <= 0 || awayScore <= 0) continue;

    const candidateStart = new Date(candidate.commence_time).getTime();
    const deltaMs = Number.isFinite(targetStart) && Number.isFinite(candidateStart)
      ? Math.abs(candidateStart - targetStart)
      : Number.MAX_SAFE_INTEGER;
    const score = homeScore + awayScore;

    if (!best || score > best.score || (score === best.score && deltaMs < best.deltaMs)) {
      best = { event: candidate, score, deltaMs };
    }
  }

  return best;
}

function buildEmptyComparison(): ForecastAuditNumberComparison {
  return {
    available: false,
    exact: false,
    within: false,
    deltaFromMedian: null,
    range: null,
    median: null,
    sampleSize: 0,
  };
}

function countExact(values: ForecastAuditNumberComparison[]): number {
  return values.filter((value) => value.exact).length;
}

function countOutside(values: ForecastAuditNumberComparison[]): number {
  return values.filter((value) => value.available && value.sampleSize >= MIN_RANGE_SAMPLE_SIZE && !value.within).length;
}

function isBeyondThirdPartyHorizon(
  event: RainmakerPublicEvent,
  candidates: TheOddsCurrentEvent[],
): boolean {
  const targetStart = new Date(event.startsAt).getTime();
  if (!Number.isFinite(targetStart)) return false;

  const candidateStarts = candidates
    .map((candidate) => new Date(candidate.commence_time).getTime())
    .filter((value) => Number.isFinite(value));
  if (candidateStarts.length === 0) return false;

  const maxCandidateStart = Math.max(...candidateStarts);
  return targetStart > maxCandidateStart + THIRD_PARTY_HORIZON_BUFFER_MS;
}

function buildStartDeltaDistribution(events: ForecastAuditEventReport[]): Array<{ delta: number; count: number }> {
  const counts = new Map<number, number>();
  for (const event of events) {
    if (event.startDeltaMin == null) continue;
    counts.set(event.startDeltaMin, (counts.get(event.startDeltaMin) || 0) + 1);
  }
  return [...counts.entries()]
    .sort((a, b) => a[0] - b[0])
    .map(([delta, count]) => ({ delta, count }));
}

export function buildForecastFeedAuditReport(params: {
  feed: RainmakerPublicEventsFeed;
  thirdPartyByLeague: Record<string, TheOddsCurrentEvent[]>;
  auditedAt?: string;
}): ForecastFeedAuditReport {
  const events = flattenRainmakerEventsFeed(params.feed);
  const issueCounts: ForecastAuditIssueCounts = { ...EMPTY_ISSUE_COUNTS };
  const reportEvents: ForecastAuditEventReport[] = [];
  const exactRollups: ForecastAuditExactRollups = {
    exactMoneylineSides: 0,
    exactSpreadLineSides: 0,
    exactSpreadOddsSides: 0,
    exactTotalLineSides: 0,
    exactTotalOddsSides: 0,
  };

  const summary: ForecastAuditSummary = {
    rainmakerEvents: events.length,
    matchedEvents: 0,
    unmatchedEvents: 0,
    readyForecasts: 0,
    generatingForecasts: 0,
    readyWithoutConfidence: 0,
    exactStartTimes: 0,
    startTimeMismatches: 0,
    moneylineSidesOutsideRange: 0,
    spreadSidesOutsideRange: 0,
    totalSidesOutsideRange: 0,
  };

  for (const event of events) {
    if (event.forecastStatus === 'ready') summary.readyForecasts += 1;
    else summary.generatingForecasts += 1;

    const eventReport: ForecastAuditEventReport = {
      league: event.league,
      eventId: event.id,
      matchup: `${event.awayTeam} @ ${event.homeTeam}`,
      forecastStatus: event.forecastStatus,
      forecastSide: event.forecastMeta?.forecastSide ?? null,
      confidence: event.forecastMeta?.confidence ?? null,
      thirdPartyEventId: null,
      thirdPartyBooks: 0,
      startDeltaMin: null,
      issues: [],
      moneyline: {
        home: buildEmptyComparison(),
        away: buildEmptyComparison(),
      },
      spread: {
        homeLine: buildEmptyComparison(),
        awayLine: buildEmptyComparison(),
        homeOdds: buildEmptyComparison(),
        awayOdds: buildEmptyComparison(),
      },
      total: {
        overLine: buildEmptyComparison(),
        underLine: buildEmptyComparison(),
        overOdds: buildEmptyComparison(),
        underOdds: buildEmptyComparison(),
      },
    };

    if (event.forecastStatus === 'ready' && !Number.isFinite(event.forecastMeta?.confidence)) {
      eventReport.issues.push('ready_without_confidence');
      issueCounts.ready_without_confidence += 1;
      summary.readyWithoutConfidence += 1;
    }

    const leagueCandidates = params.thirdPartyByLeague[event.league] || [];
    const matched = matchRainmakerEvent(event, leagueCandidates);
    if (!matched) {
      if (!isBeyondThirdPartyHorizon(event, leagueCandidates)) {
        eventReport.issues.push('event_unmatched');
        issueCounts.event_unmatched += 1;
        summary.unmatchedEvents += 1;
      }
      reportEvents.push(eventReport);
      continue;
    }

    summary.matchedEvents += 1;
    eventReport.thirdPartyEventId = matched.event.id;
    eventReport.thirdPartyBooks = matched.event.bookmakers.length;

    const startDeltaMin = Math.round(
      (new Date(matched.event.commence_time).getTime() - new Date(event.startsAt).getTime()) / 60000,
    );
    eventReport.startDeltaMin = startDeltaMin;

    if (startDeltaMin === 0) {
      summary.exactStartTimes += 1;
    } else {
      eventReport.issues.push('start_time_mismatch');
      issueCounts.start_time_mismatch += 1;
      summary.startTimeMismatches += 1;
    }

    const moneylineOutcomes = extractMarketOutcomes(matched.event.bookmakers, 'h2h');
    const spreadOutcomes = extractMarketOutcomes(matched.event.bookmakers, 'spreads');
    const totalOutcomes = extractMarketOutcomes(matched.event.bookmakers, 'totals');

    eventReport.moneyline.home = compareNumber(
      event.moneyline?.home,
      summarizeNumbers(filterTeamOutcomes(moneylineOutcomes, event.homeTeam).map((outcome) => outcome.price)),
      AMERICAN_ODDS_TOLERANCE,
    );
    eventReport.moneyline.away = compareNumber(
      event.moneyline?.away,
      summarizeNumbers(filterTeamOutcomes(moneylineOutcomes, event.awayTeam).map((outcome) => outcome.price)),
      AMERICAN_ODDS_TOLERANCE,
    );

    eventReport.spread.homeLine = compareNumber(
      event.spread?.home?.line,
      summarizeNumbers(filterTeamOutcomes(spreadOutcomes, event.homeTeam).map((outcome) => outcome.point)),
    );
    eventReport.spread.awayLine = compareNumber(
      event.spread?.away?.line,
      summarizeNumbers(filterTeamOutcomes(spreadOutcomes, event.awayTeam).map((outcome) => outcome.point)),
    );
    eventReport.spread.homeOdds = compareNumber(
      event.spread?.home?.odds,
      summarizeNumbers(
        filterOutcomesByPoint(
          filterTeamOutcomes(spreadOutcomes, event.homeTeam),
          event.spread?.home?.line,
        ).map((outcome) => outcome.price),
      ),
      AMERICAN_ODDS_TOLERANCE,
    );
    eventReport.spread.awayOdds = compareNumber(
      event.spread?.away?.odds,
      summarizeNumbers(
        filterOutcomesByPoint(
          filterTeamOutcomes(spreadOutcomes, event.awayTeam),
          event.spread?.away?.line,
        ).map((outcome) => outcome.price),
      ),
      AMERICAN_ODDS_TOLERANCE,
    );

    eventReport.total.overLine = compareNumber(
      event.total?.over?.line,
      summarizeNumbers(filterDirectionOutcomes(totalOutcomes, 'over').map((outcome) => outcome.point)),
    );
    eventReport.total.underLine = compareNumber(
      event.total?.under?.line,
      summarizeNumbers(filterDirectionOutcomes(totalOutcomes, 'under').map((outcome) => outcome.point)),
    );
    eventReport.total.overOdds = compareNumber(
      event.total?.over?.odds,
      summarizeNumbers(
        filterOutcomesByPoint(
          filterDirectionOutcomes(totalOutcomes, 'over'),
          event.total?.over?.line,
        ).map((outcome) => outcome.price),
      ),
      AMERICAN_ODDS_TOLERANCE,
    );
    eventReport.total.underOdds = compareNumber(
      event.total?.under?.odds,
      summarizeNumbers(
        filterOutcomesByPoint(
          filterDirectionOutcomes(totalOutcomes, 'under'),
          event.total?.under?.line,
        ).map((outcome) => outcome.price),
      ),
      AMERICAN_ODDS_TOLERANCE,
    );

    exactRollups.exactMoneylineSides += countExact([
      eventReport.moneyline.home,
      eventReport.moneyline.away,
    ]);
    exactRollups.exactSpreadLineSides += countExact([
      eventReport.spread.homeLine,
      eventReport.spread.awayLine,
    ]);
    exactRollups.exactSpreadOddsSides += countExact([
      eventReport.spread.homeOdds,
      eventReport.spread.awayOdds,
    ]);
    exactRollups.exactTotalLineSides += countExact([
      eventReport.total.overLine,
      eventReport.total.underLine,
    ]);
    exactRollups.exactTotalOddsSides += countExact([
      eventReport.total.overOdds,
      eventReport.total.underOdds,
    ]);

    const moneylineOutside = countOutside([
      eventReport.moneyline.home,
      eventReport.moneyline.away,
    ]);
    const spreadOutside = countOutside([
      eventReport.spread.homeLine,
      eventReport.spread.awayLine,
      eventReport.spread.homeOdds,
      eventReport.spread.awayOdds,
    ]);
    const totalOutside = countOutside([
      eventReport.total.overLine,
      eventReport.total.underLine,
      eventReport.total.overOdds,
      eventReport.total.underOdds,
    ]);

    summary.moneylineSidesOutsideRange += moneylineOutside;
    summary.spreadSidesOutsideRange += spreadOutside;
    summary.totalSidesOutsideRange += totalOutside;

    if (moneylineOutside > 0) {
      eventReport.issues.push('moneyline_out_of_range');
      issueCounts.moneyline_out_of_range += 1;
    }
    if (spreadOutside > 0) {
      eventReport.issues.push('spread_out_of_range');
      issueCounts.spread_out_of_range += 1;
    }
    if (totalOutside > 0) {
      eventReport.issues.push('total_out_of_range');
      issueCounts.total_out_of_range += 1;
    }

    reportEvents.push(eventReport);
  }

  const startDeltaDistribution = buildStartDeltaDistribution(reportEvents);
  const issueEventCount = reportEvents.filter((event) => event.issues.length > 0).length;

  return {
    auditedAt: params.auditedAt || new Date().toISOString(),
    summary,
    exactRollups,
    issueCounts,
    startDeltaDistribution,
    issueEventCount,
    passed: issueEventCount === 0,
    events: reportEvents,
  };
}

export function renderForecastFeedAuditMarkdown(report: ForecastFeedAuditReport): string {
  const lines: string[] = [];
  const issueSamples = report.events.filter((event) => event.issues.length > 0).slice(0, 10);

  lines.push('# Forecast Feed Audit');
  lines.push('');
  lines.push(`- Audited at: ${report.auditedAt}`);
  lines.push(`- Passed: ${report.passed ? 'yes' : 'no'}`);
  lines.push(`- Rainmaker events: ${report.summary.rainmakerEvents}`);
  lines.push(`- Matched events: ${report.summary.matchedEvents}`);
  lines.push(`- Issue events: ${report.issueEventCount}`);
  lines.push('');
  lines.push('## Summary');
  lines.push('');
  lines.push(`- Ready forecasts: ${report.summary.readyForecasts}`);
  lines.push(`- Ready without confidence: ${report.summary.readyWithoutConfidence}`);
  lines.push(`- Start time mismatches: ${report.summary.startTimeMismatches}`);
  lines.push(`- Unmatched events: ${report.summary.unmatchedEvents}`);
  lines.push(`- Moneyline sides outside range: ${report.summary.moneylineSidesOutsideRange}`);
  lines.push(`- Spread fields outside range: ${report.summary.spreadSidesOutsideRange}`);
  lines.push(`- Total fields outside range: ${report.summary.totalSidesOutsideRange}`);
  lines.push('');
  lines.push('## Exact Match Rollups');
  lines.push('');
  lines.push(`- Moneyline sides exact: ${report.exactRollups.exactMoneylineSides}`);
  lines.push(`- Spread lines exact: ${report.exactRollups.exactSpreadLineSides}`);
  lines.push(`- Spread odds exact: ${report.exactRollups.exactSpreadOddsSides}`);
  lines.push(`- Total lines exact: ${report.exactRollups.exactTotalLineSides}`);
  lines.push(`- Total odds exact: ${report.exactRollups.exactTotalOddsSides}`);
  lines.push('');
  lines.push('## Start Delta Distribution');
  lines.push('');
  for (const entry of report.startDeltaDistribution) {
    lines.push(`- ${entry.delta} minutes: ${entry.count}`);
  }

  if (issueSamples.length > 0) {
    lines.push('');
    lines.push('## Issue Samples');
    lines.push('');
    for (const event of issueSamples) {
      lines.push(`- ${event.matchup} (${event.league})`);
      lines.push(`  issues: ${event.issues.join(', ')}`);
      if (event.startDeltaMin != null) {
        lines.push(`  start delta: ${event.startDeltaMin} minutes`);
      }
      if (event.forecastStatus === 'ready' && event.confidence == null) {
        lines.push('  confidence: null');
      }
    }
  }

  return `${lines.join('\n')}\n`;
}
