// ABOUTME: Service for observability and monitoring queries
// ABOUTME: Provides data freshness, upcoming races, and timeline views

import { PrismaClient } from '@prisma/client';
import logger from '../utils/logger';
import type { ScrapeType } from './race-service';

// ============================================================================
// Types
// ============================================================================

export interface UpcomingRace {
  id: string;
  name: string;
  meetingName: string;
  startTime: Date;
  minutesUntilStart: number;
  country: string;
  category: string;
  status: string;
  runnerCount: number;
  scratchedCount: number;
  lastScraped: Date | null;
  dataFreshness: DataFreshness | null;
}

export interface DataFreshness {
  ageMinutes: number;
  status: 'fresh' | 'stale' | 'missing';
  hasMorningOdds: boolean;
  hasT60Odds: boolean;
  hasT15Odds: boolean;
}

export interface RaceTimeline {
  date: string;
  races: RaceWithScrapes[];
}

export interface RaceWithScrapes {
  id: string;
  name: string;
  meetingName: string;
  startTime: Date;
  status: string;
  country: string;
  category: string;
  scrapes: ScrapeInfo[];
  expectedScrapes: ExpectedScrape[];
}

export interface ScrapeInfo {
  scrapeType: string;
  scrapedAt: Date;
  success: boolean;
  runnersFound: number;
  changesDetected?: any;
}

export interface ExpectedScrape {
  type: ScrapeType;
  expectedAt: Date;
}

export interface DataFreshnessSummary {
  summary: {
    totalRacesToday: number;
    racesWithFreshData: number;
    racesWithStaleData: number;
    racesMissingData: number;
  };
  breakdown: Record<string, {
    fresh: number;
    stale: number;
    missing: number;
  }>;
}

// ============================================================================
// ObservabilityService Class
// ============================================================================

export class ObservabilityService {
  private readonly FRESH_DATA_THRESHOLD_MINUTES = 15;
  private readonly STALE_DATA_THRESHOLD_MINUTES = 60;

  constructor(private prisma: PrismaClient) {
    logger.info('ObservabilityService initialized');
  }

  /**
   * Get upcoming races within the next N hours with data freshness info
   */
  async getUpcomingRaces(hoursAhead: number = 2): Promise<UpcomingRace[]> {
    const now = new Date();
    const futureTime = new Date(now.getTime() + hoursAhead * 60 * 60 * 1000);

    logger.debug({ hoursAhead, futureTime }, 'Fetching upcoming races');

    const races = await this.prisma.race.findMany({
      where: {
        startTime: {
          gte: now,
          lte: futureTime,
        },
      },
      include: {
        meeting: {
          select: {
            name: true,
            category: true,
          },
        },
        runners: {
          select: {
            id: true,
            scratched: true,
            oddsSnapshots: {
              select: {
                snapshotType: true,
              },
            },
          },
        },
        scrapes: {
          orderBy: {
            scrapedAt: 'desc',
          },
          take: 1,
        },
      },
      orderBy: {
        startTime: 'asc',
      },
    });

    const upcomingRaces: UpcomingRace[] = races.map((race) => {
      const minutesUntilStart = Math.round(
        (race.startTime.getTime() - now.getTime()) / (1000 * 60)
      );

      const runnerCount = race.runners.length;
      const scratchedCount = race.runners.filter((r) => r.scratched).length;

      const lastScrape = race.scrapes[0];
      const lastScraped = lastScrape ? lastScrape.scrapedAt : null;

      const dataFreshness = this.calculateRaceDataFreshness(
        race.runners,
        lastScraped,
        now
      );

      return {
        id: race.id,
        name: race.name,
        meetingName: race.meeting.name,
        startTime: race.startTime,
        minutesUntilStart,
        country: race.country,
        category: race.meeting.category,
        status: race.status,
        runnerCount,
        scratchedCount,
        lastScraped,
        dataFreshness,
      };
    });

    logger.info({ count: upcomingRaces.length }, 'Upcoming races retrieved');
    return upcomingRaces;
  }

  /**
   * Get timeline of all races for a specific date with scrape history
   */
  async getRaceTimeline(date: Date): Promise<RaceTimeline> {
    const dateStart = new Date(date);
    dateStart.setHours(0, 0, 0, 0);
    const dateEnd = new Date(date);
    dateEnd.setHours(23, 59, 59, 999);

    logger.debug({ date, dateStart, dateEnd }, 'Fetching race timeline');

    const races = await this.prisma.race.findMany({
      where: {
        startTime: {
          gte: dateStart,
          lte: dateEnd,
        },
      },
      include: {
        meeting: {
          select: {
            name: true,
            category: true,
          },
        },
        scrapes: {
          orderBy: {
            scrapedAt: 'asc',
          },
        },
        runners: {
          select: {
            id: true,
          },
        },
      },
      orderBy: {
        startTime: 'asc',
      },
    });

    const racesWithScrapes: RaceWithScrapes[] = races.map((race) => {
      const scrapes: ScrapeInfo[] = race.scrapes.map((scrape) => ({
        scrapeType: scrape.scrapeType,
        scrapedAt: scrape.scrapedAt,
        success: scrape.success,
        runnersFound: race.runners.length,
        changesDetected: scrape.changesDetected,
      }));

      const expectedScrapes = this.getExpectedScrapes(race.startTime, race.status);

      return {
        id: race.id,
        name: race.name,
        meetingName: race.meeting.name,
        startTime: race.startTime,
        status: race.status,
        country: race.country,
        category: race.meeting.category,
        scrapes,
        expectedScrapes,
      };
    });

    logger.info({ date, raceCount: racesWithScrapes.length }, 'Race timeline retrieved');

    return {
      date: date.toISOString().split('T')[0],
      races: racesWithScrapes,
    };
  }

  /**
   * Calculate data freshness summary for all races today
   */
  async calculateDataFreshness(): Promise<DataFreshnessSummary> {
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    const tomorrow = new Date(today);
    tomorrow.setDate(tomorrow.getDate() + 1);

    const now = new Date();

    logger.debug({ today, tomorrow }, 'Calculating data freshness summary');

    const races = await this.prisma.race.findMany({
      where: {
        meeting: {
          date: today,
        },
      },
      include: {
        meeting: {
          select: {
            category: true,
          },
        },
        runners: {
          select: {
            id: true,
            scratched: true,
            oddsSnapshots: {
              select: {
                snapshotType: true,
              },
            },
          },
        },
        scrapes: {
          orderBy: {
            scrapedAt: 'desc',
          },
          take: 1,
        },
      },
    });

    const summary = {
      totalRacesToday: races.length,
      racesWithFreshData: 0,
      racesWithStaleData: 0,
      racesMissingData: 0,
    };

    const breakdown: Record<string, { fresh: number; stale: number; missing: number }> = {};

    for (const race of races) {
      const key = `${race.country}_${race.meeting.category}`;

      if (!breakdown[key]) {
        breakdown[key] = { fresh: 0, stale: 0, missing: 0 };
      }

      const lastScrape = race.scrapes[0];
      const lastScraped = lastScrape ? lastScrape.scrapedAt : null;

      const dataFreshness = this.calculateRaceDataFreshness(
        race.runners,
        lastScraped,
        now
      );

      if (dataFreshness) {
        if (dataFreshness.status === 'fresh') {
          summary.racesWithFreshData++;
          breakdown[key].fresh++;
        } else if (dataFreshness.status === 'stale') {
          summary.racesWithStaleData++;
          breakdown[key].stale++;
        } else {
          summary.racesMissingData++;
          breakdown[key].missing++;
        }
      } else {
        summary.racesMissingData++;
        breakdown[key].missing++;
      }
    }

    logger.info({ summary }, 'Data freshness summary calculated');

    return {
      summary,
      breakdown,
    };
  }

  /**
   * Calculate data freshness for a single race
   */
  private calculateRaceDataFreshness(
    runners: Array<{
      id: string;
      scratched: boolean;
      oddsSnapshots: Array<{ snapshotType: string }>;
    }>,
    lastScraped: Date | null,
    now: Date
  ): DataFreshness | null {
    if (!lastScraped) {
      return {
        ageMinutes: 0,
        status: 'missing',
        hasMorningOdds: false,
        hasT60Odds: false,
        hasT15Odds: false,
      };
    }

    const ageMinutes = Math.round((now.getTime() - lastScraped.getTime()) / (1000 * 60));

    let status: 'fresh' | 'stale' | 'missing';
    if (ageMinutes <= this.FRESH_DATA_THRESHOLD_MINUTES) {
      status = 'fresh';
    } else if (ageMinutes <= this.STALE_DATA_THRESHOLD_MINUTES) {
      status = 'stale';
    } else {
      status = 'missing';
    }

    const allSnapshots = runners.flatMap((r) => r.oddsSnapshots);
    const hasMorningOdds = allSnapshots.some((s) => s.snapshotType === 'morning');
    const hasT60Odds = allSnapshots.some((s) => s.snapshotType === 't60');
    const hasT15Odds = allSnapshots.some((s) => s.snapshotType === 't15');

    return {
      ageMinutes,
      status,
      hasMorningOdds,
      hasT60Odds,
      hasT15Odds,
    };
  }

  /**
   * Get expected scrapes for a race based on its start time and status
   */
  private getExpectedScrapes(startTime: Date, status: string): ExpectedScrape[] {
    const expectedScrapes: ExpectedScrape[] = [];
    const now = new Date();

    // Morning scrape happens at 6 AM daily
    const morningTime = new Date(startTime);
    morningTime.setHours(6, 0, 0, 0);
    if (morningTime < startTime) {
      expectedScrapes.push({
        type: 'morning_scrape',
        expectedAt: morningTime,
      });
    }

    // T-60: 60 minutes before race start
    const t60Time = new Date(startTime.getTime() - 60 * 60 * 1000);
    if (t60Time > now && status !== 'Final' && status !== 'Abandoned') {
      expectedScrapes.push({
        type: 'pre_race_t60',
        expectedAt: t60Time,
      });
    }

    // T-15: 15 minutes before race start
    const t15Time = new Date(startTime.getTime() - 15 * 60 * 1000);
    if (t15Time > now && status !== 'Final' && status !== 'Abandoned') {
      expectedScrapes.push({
        type: 'pre_race_t15',
        expectedAt: t15Time,
      });
    }

    // Post-race: 5 minutes after start
    const postRaceTime = new Date(startTime.getTime() + 5 * 60 * 1000);
    if (postRaceTime > now && status !== 'Final' && status !== 'Abandoned') {
      expectedScrapes.push({
        type: 'post_race',
        expectedAt: postRaceTime,
      });
    }

    return expectedScrapes;
  }
}
