/**
 * MorningScrapeScheduler
 *
 * Daily morning scrape of all racing meetings
 * Runs at 6 AM AEDT/AEST to fetch today's and tomorrow's meetings
 */

import { PrismaClient } from '@prisma/client';
import { DateTime } from 'luxon';
import logger from '../utils/logger';
import { TabApiClient } from '../api/tab';
import { MeetingService } from '../services/meeting-service';
import { RaceService } from '../services/race-service';
import { BaseScheduler } from './base-scheduler';
import { ScheduleType, JobContext, JobResult } from './types';
import { SCHEDULER_CONFIGS, MORNING_SCRAPE_CONFIG } from './config';

export class MorningScrapeScheduler extends BaseScheduler {
  private meetingService: MeetingService;
  private raceService: RaceService;

  constructor(
    apiClient: TabApiClient,
    prisma: PrismaClient
  ) {
    super(ScheduleType.MORNING_SCRAPE, SCHEDULER_CONFIGS[ScheduleType.MORNING_SCRAPE], prisma);
    this.meetingService = new MeetingService(apiClient, prisma);
    this.raceService = new RaceService(apiClient, prisma);
  }

  protected async executeJob(context: JobContext): Promise<JobResult> {
    const startTime = Date.now();
    const result: JobResult = {
      success: true,
      itemsProcessed: 0,
      durationMs: 0,
      errors: [],
      metadata: {
        meetingsByDay: {},
        racesByDay: {},
        runnersProcessed: 0,
        oddsSnapshotsCaptured: 0,
        failedCombinations: [] as Array<{category: string; country: string; date: string; error: string}>,
        retryScheduled: false,
      },
    };

    logger.info({
      triggeredAt: context.triggeredAt.toISOString(),
      categories: MORNING_SCRAPE_CONFIG.categories,
      countries: MORNING_SCRAPE_CONFIG.countries,
      daysToFetch: MORNING_SCRAPE_CONFIG.daysToFetch,
    }, 'Starting morning scrape');

    try {
      // Generate dates to fetch (today and tomorrow in UTC for consistency)
      const dates = this.generateDatesToFetch();

      // Phase 1: Fetch meetings for each combination of category, country, and date
      for (const category of MORNING_SCRAPE_CONFIG.categories) {
        for (const country of MORNING_SCRAPE_CONFIG.countries) {
          for (const date of dates) {
            try {
              logger.debug({
                category,
                country,
                date: date.toISOString().split('T')[0],
              }, 'Fetching meetings');

              const fetchResult = await this.meetingService.fetchAndStoreMeetings({
                category,
                country,
                date,
              });

              // Track results
              const dateKey = date.toISOString().split('T')[0];
              if (!result.metadata!.meetingsByDay[dateKey]) {
                result.metadata!.meetingsByDay[dateKey] = 0;
                result.metadata!.racesByDay[dateKey] = 0;
              }

              result.metadata!.meetingsByDay[dateKey] += fetchResult.meetingsProcessed;
              result.metadata!.racesByDay[dateKey] += fetchResult.racesProcessed;

              result.itemsProcessed += fetchResult.meetingsProcessed;

              // Collect any errors
              if (fetchResult.errors.length > 0) {
                result.success = false;
                fetchResult.errors.forEach(err => {
                  result.errors.push(`${category}/${country}/${dateKey}/${err.meetingId}: ${err.error}`);
                  result.metadata!.failedCombinations.push({
                    category,
                    country,
                    date: dateKey,
                    error: err.error,
                  });
                });
              }

            } catch (error) {
              result.success = false;
              const errorMessage = error instanceof Error ? error.message : 'Unknown error';
              const dateKey = date.toISOString().split('T')[0];

              result.errors.push(`${category}/${country}/${dateKey}: ${errorMessage}`);
              result.metadata!.failedCombinations.push({
                category,
                country,
                date: dateKey,
                error: errorMessage,
              });

              logger.error({
                category,
                country,
                date: date.toISOString().split('T')[0],
                error: errorMessage,
              }, 'Failed to fetch meetings for combination');

              // Continue with other combinations even if one fails
              continue;
            }
          }
        }
      }

      // Phase 2: Fetch detailed race data (runners, opening odds) for all races
      logger.info('Phase 2: Fetching detailed race data');
      const raceDetailResults = await this.fetchDetailedRaceData(dates);

      result.metadata!.runnersProcessed = raceDetailResults.runnersProcessed;
      result.metadata!.oddsSnapshotsCaptured = raceDetailResults.oddsSnapshotsCaptured;

      if (raceDetailResults.errors.length > 0) {
        result.success = false;
        result.errors.push(...raceDetailResults.errors);
      }

      result.durationMs = Date.now() - startTime;

      // Schedule retry if there were failures and we haven't exceeded max retries
      const retryCount = (context.data?.retryCount as number) || 0;
      if (!result.success && result.metadata!.failedCombinations.length > 0) {
        if (retryCount < MORNING_SCRAPE_CONFIG.maxRetries) {
          // Schedule retry in 30 minutes
          const retryDelayMs = 30 * 60 * 1000; // 30 minutes
          result.metadata!.retryScheduled = true;
          result.metadata!.nextRetryIn = retryDelayMs;

          this.scheduleRetry(retryDelayMs, context.jobRunId, retryCount + 1);

          logger.info({
            retryCount: retryCount + 1,
            failedCombinations: result.metadata!.failedCombinations.length,
            retryInMinutes: 30,
          }, 'Scheduling morning scrape retry');
        } else {
          logger.warn({
            retryCount,
            maxRetries: MORNING_SCRAPE_CONFIG.maxRetries,
            failedCombinations: result.metadata!.failedCombinations.length,
          }, 'Max retries reached for morning scrape');
        }
      }

      logger.info({
        itemsProcessed: result.itemsProcessed,
        meetingsByDay: result.metadata!.meetingsByDay,
        racesByDay: result.metadata!.racesByDay,
        runnersProcessed: result.metadata!.runnersProcessed,
        oddsSnapshotsCaptured: result.metadata!.oddsSnapshotsCaptured,
        errors: result.errors.length,
        durationMs: result.durationMs,
        retryScheduled: result.metadata!.retryScheduled,
      }, 'Morning scrape completed');

      return result;

    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Unknown error';

      logger.error({
        error: errorMessage,
        stack: error instanceof Error ? error.stack : undefined,
      }, 'Morning scrape failed');

      result.success = false;
      result.durationMs = Date.now() - startTime;
      result.errors.push(`Fatal error: ${errorMessage}`);

      return result;
    }
  }

  /**
   * Fetch detailed race data (runners, opening odds) for races in the given dates
   */
  private async fetchDetailedRaceData(dates: Date[]): Promise<{
    runnersProcessed: number;
    oddsSnapshotsCaptured: number;
    errors: string[];
  }> {
    const results = {
      runnersProcessed: 0,
      oddsSnapshotsCaptured: 0,
      errors: [] as string[],
    };

    // Find all races for the given dates that need runner data
    const dateStart = DateTime.fromJSDate(dates[0]).startOf('day').toJSDate();
    const dateEnd = DateTime.fromJSDate(dates[dates.length - 1]).endOf('day').toJSDate();

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

    logger.info({
      raceCount: races.length,
      dateRange: `${dateStart.toISOString()} to ${dateEnd.toISOString()}`,
    }, 'Fetching detailed data for races');

    for (const race of races) {
      try {
        const raceResult = await this.raceService.fetchAndStoreRaceDetails(
          race.id,
          'morning_scrape'
        );

        if (raceResult.success) {
          results.runnersProcessed += raceResult.runnersProcessed;
          results.oddsSnapshotsCaptured += raceResult.oddsSnapshotsCaptured;
        } else {
          results.errors.push(...raceResult.errors.map(e => `${race.id}: ${e}`));
        }

      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown error';
        results.errors.push(`${race.id}: ${errorMessage}`);

        logger.error({
          raceId: race.id,
          meetingId: race.meetingId,
          raceNumber: race.raceNumber,
          error: errorMessage,
        }, 'Failed to fetch race details');
      }
    }

    return results;
  }

  /**
   * Generate array of dates to fetch (today and tomorrow in UTC for consistent date boundaries)
   */
  private generateDatesToFetch(): Date[] {
    const now = DateTime.now().toUTC();
    const dates: Date[] = [];

    for (let i = 0; i < MORNING_SCRAPE_CONFIG.daysToFetch; i++) {
      const date = now.plus({ days: i }).startOf('day').toJSDate();
      dates.push(date);
    }

    return dates;
  }

  /**
   * Manually trigger the morning scrape (useful for testing)
   */
  async triggerManually(): Promise<JobResult> {
    logger.info('Manually triggering morning scrape');

    const context: JobContext = {
      type: ScheduleType.MORNING_SCRAPE,
      triggeredAt: new Date(),
      jobRunId: 'manual-trigger', // Will be replaced by actual JobRun ID
      data: { manual: true },
    };

    return this.executeJob(context);
  }
}
