// ABOUTME: Service for fetching and storing detailed race data
// ABOUTME: Handles runners, odds snapshots, results, and dividends

import { PrismaClient } from '@prisma/client';
import { TabApiClient } from '../api/tab';
import { trace, SpanStatusCode } from '@opentelemetry/api';
import { Counter, Histogram } from 'prom-client';
import logger from '../utils/logger';

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

export type ScrapeType = 'morning_scrape' | 'pre_race_t60' | 'pre_race_t15' | 'post_race';
export type SnapshotType = 'morning' | 't60' | 't15' | 'final';

export interface FetchRaceResult {
  success: boolean;
  raceId: string;
  runnersProcessed: number;
  oddsSnapshotsCaptured: number;
  resultsProcessed: number;
  dividendsProcessed: number;
  errors: string[];
  durationMs: number;
}

// ============================================================================
// Metrics
// ============================================================================

const tracer = trace.getTracer('race-service');

const runnersProcessedCounter = new Counter({
  name: 'race_runners_processed_total',
  help: 'Total number of runners processed',
  labelNames: ['scrape_type', 'status'],
});

const oddsSnapshotsCounter = new Counter({
  name: 'race_odds_snapshots_total',
  help: 'Total number of odds snapshots captured',
  labelNames: ['snapshot_type'],
});

const dividendsProcessedCounter = new Counter({
  name: 'race_dividends_processed_total',
  help: 'Total number of dividends processed',
  labelNames: ['product_name'],
});

const operationDuration = new Histogram({
  name: 'race_service_operation_duration_seconds',
  help: 'Duration of race service operations',
  labelNames: ['operation'],
  buckets: [0.1, 0.5, 1, 2, 5, 10, 30],
});

const resultsProcessedCounter = new Counter({
  name: 'race_results_processed_total',
  help: 'Total number of race results processed',
});

const resultsCaptureLatency = new Histogram({
  name: 'race_results_capture_latency_seconds',
  help: 'Time from race start to result capture',
  buckets: [300, 600, 900, 1800, 3600, 7200], // 5min to 2hr
});

// Note: This gauge is available for future use to track provisional results
// Currently not implemented as the API doesn't clearly indicate provisional vs final
// export const provisionalResultsGauge = new Gauge({
//   name: 'race_provisional_results_total',
//   help: 'Number of races with provisional results',
// });

const dividendsByProduct = new Counter({
  name: 'race_dividends_by_product_total',
  help: 'Total dividends processed by product and tote',
  labelNames: ['product_name', 'tote'],
});

// ============================================================================
// RaceService Class
// ============================================================================

export class RaceService {
  constructor(
    private apiClient: TabApiClient,
    private prisma: PrismaClient
  ) {
    logger.info('RaceService initialized');
  }

  /**
   * Fetch race details from API and store all data
   */
  async fetchAndStoreRaceDetails(raceId: string, scrapeType: ScrapeType): Promise<FetchRaceResult> {
    const span = tracer.startSpan('race_service.fetch_and_store_race_details');
    const startTime = Date.now();

    const result: FetchRaceResult = {
      success: true,
      raceId,
      runnersProcessed: 0,
      oddsSnapshotsCaptured: 0,
      resultsProcessed: 0,
      dividendsProcessed: 0,
      errors: [],
      durationMs: 0,
    };

    try {
      span.setAttributes({
        'race.id': raceId,
        'scrape.type': scrapeType,
      });

      logger.info({ raceId, scrapeType }, 'Fetching race details from API');

      // Fetch race details from API
      const apiResponse = await this.apiClient.getRaceById(raceId);
      const raceData = apiResponse.data;

      logger.debug({
        raceId,
        runnersCount: raceData.runners.length,
        hasResults: !!raceData.results,
        hasDividends: !!raceData.dividends,
        raceStatus: raceData.race.status,
      }, 'Received race data from API');

      // Process within transaction
      await this.prisma.$transaction(async (tx) => {
        // Process runners
        const runnerResults = await this.processRunners(tx, raceId, raceData.runners, scrapeType);
        result.runnersProcessed = runnerResults.processed;
        result.errors.push(...runnerResults.errors);

        // Capture odds snapshot for non-scratched runners
        const snapshotType = this.mapScrapeToSnapshotType(scrapeType);
        const snapshotResults = await this.captureOddsSnapshots(
          tx,
          raceId,
          raceData.runners,
          snapshotType
        );
        result.oddsSnapshotsCaptured = snapshotResults.captured;

        // Process results and dividends only if race is Final
        if (raceData.race.status === 'Final') {
          if (raceData.results && raceData.results.length > 0) {
            const resultsResult = await this.processResults(
              tx,
              raceId,
              raceData.results,
              new Date(raceData.race.advertised_start * 1000)
            );
            result.resultsProcessed = resultsResult.processed;
            result.errors.push(...resultsResult.errors);
          }

          if (raceData.dividends && raceData.dividends.length > 0) {
            const dividendsResult = await this.processDividends(tx, raceId, raceData.dividends);
            result.dividendsProcessed = dividendsResult.processed;
            result.errors.push(...dividendsResult.errors);
          }
        }
      });

      // Record metrics
      const duration = (Date.now() - startTime) / 1000;
      operationDuration.labels('fetch_and_store_race_details').observe(duration);

      span.setStatus({ code: SpanStatusCode.OK });
      span.setAttribute('runners.processed', result.runnersProcessed);
      span.setAttribute('odds_snapshots.captured', result.oddsSnapshotsCaptured);

      logger.info({
        raceId,
        scrapeType,
        runnersProcessed: result.runnersProcessed,
        oddsSnapshotsCaptured: result.oddsSnapshotsCaptured,
        resultsProcessed: result.resultsProcessed,
        dividendsProcessed: result.dividendsProcessed,
        duration,
      }, 'Race details processed successfully');

    } catch (error) {
      result.success = false;
      const errorMessage = error instanceof Error ? error.message : 'Unknown error';
      result.errors.push(errorMessage);

      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: errorMessage,
      });

      logger.error({
        raceId,
        scrapeType,
        error: errorMessage,
      }, 'Failed to fetch and store race details');

    } finally {
      result.durationMs = Date.now() - startTime;
      span.end();
    }

    return result;
  }

  /**
   * Process and upsert runners
   */
  private async processRunners(
    tx: any,
    raceId: string,
    runners: any[],
    scrapeType: ScrapeType
  ): Promise<{ processed: number; errors: string[] }> {
    const errors: string[] = [];
    let processed = 0;

    for (const runner of runners) {
      try {
        // Find or create horse record
        const horseId = await this.findOrCreateHorse(tx, runner);

        // Check if this is a new race for this horse (not a re-scrape)
        const existingRunner = await tx.runner.findFirst({
          where: { horseId, raceId },
        });
        const isNewRace = !existingRunner;

        await tx.runner.upsert({
          where: {
            raceId_runnerNumber: {
              raceId,
              runnerNumber: runner.runner_number,
            },
          },
          create: {
            raceId,
            runnerNumber: runner.runner_number,
            horseId,  // Link to horses table
            entrantId: runner.entrant_id,
            horseName: runner.name,  // Backward compatibility
            barrier: runner.barrier,
            jockeyName: runner.jockey || null,
            trainerName: runner.trainer_name || null,
            trainerLocation: runner.trainer_location || null,
            age: runner.age || null,
            silkColours: runner.silk_colours || null,
            lastTwentyStarts: runner.last_twenty_starts || null,
            scratched: runner.is_scratched,
            scratchedAt: runner.is_scratched && runner.scratch_time > 0
              ? new Date(runner.scratch_time * 1000)
              : null,
            openingOdds: scrapeType === 'morning_scrape' ? runner.odds?.fixed_win : null,
            currentOdds: runner.odds?.fixed_win || null,
          },
          update: {
            horseId,  // Update link on each scrape
            entrantId: runner.entrant_id,
            barrier: runner.barrier,
            jockeyName: runner.jockey || null,
            trainerName: runner.trainer_name || null,
            trainerLocation: runner.trainer_location || null,
            age: runner.age || null,
            silkColours: runner.silk_colours || null,
            lastTwentyStarts: runner.last_twenty_starts || null,
            scratched: runner.is_scratched,
            scratchedAt: runner.is_scratched && runner.scratch_time > 0
              ? new Date(runner.scratch_time * 1000)
              : null,
            currentOdds: runner.odds?.fixed_win || null,
            updatedAt: new Date(),
          },
        });

        // Update horse metadata
        await tx.horse.update({
          where: { id: horseId },
          data: {
            lastSeen: new Date(),
            raceCount: isNewRace ? { increment: 1 } : undefined,  // Only increment for new races
          },
        });

        processed++;
        runnersProcessedCounter.labels(scrapeType, 'success').inc();

      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown error';
        errors.push(`Runner ${runner.runner_number}: ${errorMessage}`);
        runnersProcessedCounter.labels(scrapeType, 'error').inc();
      }
    }

    return { processed, errors };
  }

  /**
   * Capture odds snapshot for non-scratched runners
   */
  private async captureOddsSnapshots(
    tx: any,
    raceId: string,
    runners: any[],
    snapshotType: SnapshotType
  ): Promise<{ captured: number }> {
    let captured = 0;

    for (const runner of runners) {
      // Skip scratched runners
      if (runner.is_scratched) continue;

      // Skip runners without odds
      if (!runner.odds) continue;

      try {
        // Find the runner in database to get their ID
        const dbRunner = await tx.runner.findFirst({
          where: {
            raceId,
            runnerNumber: runner.runner_number,
          },
        });

        if (!dbRunner) {
          logger.warn({
            raceId,
            runnerNumber: runner.runner_number,
          }, 'Runner not found for odds snapshot');
          continue;
        }

        await tx.oddsSnapshot.upsert({
          where: {
            runnerId_snapshotType: {
              runnerId: dbRunner.id,
              snapshotType,
            },
          },
          create: {
            runnerId: dbRunner.id,
            snapshotType,
            fixedWin: runner.odds.fixed_win || null,
            fixedPlace: runner.odds.fixed_place || null,
            poolWin: runner.odds.pool_win || null,
            poolPlace: runner.odds.pool_place || null,
          },
          update: {
            fixedWin: runner.odds.fixed_win || null,
            fixedPlace: runner.odds.fixed_place || null,
            poolWin: runner.odds.pool_win || null,
            poolPlace: runner.odds.pool_place || null,
            capturedAt: new Date(),
          },
        });

        captured++;
        oddsSnapshotsCounter.labels(snapshotType).inc();

      } catch (error) {
        logger.error({
          raceId,
          runnerNumber: runner.runner_number,
          error: error instanceof Error ? error.message : 'Unknown error',
        }, 'Failed to capture odds snapshot');
      }
    }

    return { captured };
  }

  /**
   * Process race results
   */
  private async processResults(
    tx: any,
    raceId: string,
    results: any[],
    raceStartTime: Date
  ): Promise<{ processed: number; errors: string[] }> {
    const errors: string[] = [];
    let processed = 0;

    // Calculate latency from race start to now
    const captureTime = new Date();
    const latencySeconds = (captureTime.getTime() - raceStartTime.getTime()) / 1000;
    resultsCaptureLatency.observe(latencySeconds);

    for (const result of results) {
      try {
        // Find runner by entrant_id
        const runner = await tx.runner.findFirst({
          where: {
            raceId,
            entrantId: result.entrant_id,
          },
        });

        if (!runner) {
          errors.push(`Runner not found for result: entrant_id=${result.entrant_id}`);
          continue;
        }

        await tx.result.upsert({
          where: {
            raceId_runnerId: {
              raceId,
              runnerId: runner.id,
            },
          },
          create: {
            raceId,
            runnerId: runner.id,
            finishPosition: result.position,
            margin: result.margin_length,
            official: true,
          },
          update: {
            finishPosition: result.position,
            margin: result.margin_length,
            official: true,
          },
        });

        processed++;
        resultsProcessedCounter.inc();

      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown error';
        errors.push(`Result position ${result.position}: ${errorMessage}`);
      }
    }

    return { processed, errors };
  }

  /**
   * Process dividends
   */
  private async processDividends(
    tx: any,
    raceId: string,
    dividends: any[]
  ): Promise<{ processed: number; errors: string[] }> {
    const errors: string[] = [];
    let processed = 0;

    for (const dividend of dividends) {
      try {
        await tx.dividend.upsert({
          where: {
            raceId_tote_productName: {
              raceId,
              tote: dividend.tote,
              productName: dividend.product_name,
            },
          },
          create: {
            raceId,
            tote: dividend.tote,
            productName: dividend.product_name,
            status: dividend.status,
            dividend: dividend.dividend,
            poolSize: dividend.pool_size || null,
            jackpotSize: dividend.jackpot_size || null,
            positions: dividend.positions,
            description: dividend.description || null,
          },
          update: {
            status: dividend.status,
            dividend: dividend.dividend,
            poolSize: dividend.pool_size || null,
            jackpotSize: dividend.jackpot_size || null,
            positions: dividend.positions,
            description: dividend.description || null,
          },
        });

        processed++;
        dividendsProcessedCounter.labels(dividend.product_name).inc();
        dividendsByProduct.labels(dividend.product_name, dividend.tote).inc();

      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown error';
        errors.push(`Dividend ${dividend.product_name}: ${errorMessage}`);
      }
    }

    return { processed, errors };
  }

  /**
   * Map scrape type to snapshot type
   */
  private mapScrapeToSnapshotType(scrapeType: ScrapeType): SnapshotType {
    const mapping: Record<ScrapeType, SnapshotType> = {
      morning_scrape: 'morning',
      pre_race_t60: 't60',
      pre_race_t15: 't15',
      post_race: 'final',
    };
    return mapping[scrapeType];
  }

  /**
   * Find or create a horse record using 6-priority matching strategy
   * Priority: tabEntrantId → tabHorseId → harnessNzHorseId → breeding → name → create new
   */
  private async findOrCreateHorse(tx: any, runnerData: any): Promise<string> {
    // Priority 1: Match by entrant_id (TAB)
    if (runnerData.entrant_id) {
      const horse = await tx.horse.findUnique({
        where: { tabEntrantId: runnerData.entrant_id },
      });
      if (horse) return horse.id;
    }

    // Priority 2: Match by horse_id (TAB)
    if (runnerData.horse_id) {
      const horse = await tx.horse.findUnique({
        where: { tabHorseId: runnerData.horse_id },
      });
      if (horse) return horse.id;
    }

    // Priority 3: Match by harness_nz_horse_id (Harness Racing NZ imports)
    if (runnerData.harness_nz_horse_id) {
      const horse = await tx.horse.findUnique({
        where: { harnessNzHorseId: BigInt(runnerData.harness_nz_horse_id) },
      });
      if (horse) return horse.id;
    }

    // Priority 4: Match by breeding (name + sire + dam)
    if (runnerData.sire && runnerData.dam) {
      const normalized = this.normalizeHorseName(runnerData.name);
      const horse = await tx.horse.findFirst({
        where: {
          normalizedName: normalized,
          sire: runnerData.sire,
          dam: runnerData.dam,
        },
      });
      if (horse) return horse.id;
    }

    // Priority 5: Fuzzy match by name only (with warning)
    const normalized = this.normalizeHorseName(runnerData.name);
    const horse = await tx.horse.findFirst({
      where: { normalizedName: normalized },
    });

    if (horse) {
      logger.warn({
        existingHorse: horse.id,
        newRunner: runnerData,
      }, 'Matched horse by name only - potential duplicate');
      return horse.id;
    }

    // Priority 6: Create new horse
    return await this.createHorse(tx, runnerData);
  }

  /**
   * Create a new horse record
   */
  private async createHorse(tx: any, runnerData: any): Promise<string> {
    const horse = await tx.horse.create({
      data: {
        tabEntrantId: runnerData.entrant_id || null,
        tabHorseId: runnerData.horse_id || null,
        harnessNzHorseId: runnerData.harness_nz_horse_id ? BigInt(runnerData.harness_nz_horse_id) : null,
        name: runnerData.name,  // Original name, untouched
        normalizedName: this.normalizeHorseName(runnerData.name),
        sire: runnerData.sire || null,
        dam: runnerData.dam || null,
        damSire: runnerData.dam_sire || null,
        sex: runnerData.sex || null,
        colour: runnerData.colour || null,
        country: runnerData.country || null,
        raceCount: 1,  // First race for this new horse
      },
    });

    return horse.id;
  }

  /**
   * Normalize horse name for matching
   * - Lowercase
   * - Remove apostrophes
   * - Normalize whitespace
   */
  private normalizeHorseName(name: string): string {
    return name
      .toLowerCase()
      .replace(/['']/g, '')    // Remove apostrophes (both types)
      .replace(/\s+/g, ' ')     // Normalize whitespace
      .trim();
  }
}
