// ABOUTME: Service for fetching and storing racing meeting data from TAB API
// ABOUTME: Handles upsert operations, race relationships, and observability metrics

import { PrismaClient, Prisma, Meeting, Race } 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 interface FetchMeetingsOptions {
  category: 'T' | 'H' | 'G';
  country: string;
  date: Date;
}

export interface GetMeetingsOptions {
  country?: string;
  category?: 'T' | 'H' | 'G';
}

export interface FetchResult {
  meetingsProcessed: number;
  racesProcessed: number;
  errors: Array<{ meetingId?: string; error: string }>;
}

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

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

const meetingsProcessedCounter = new Counter({
  name: 'meetings_processed_total',
  help: 'Total number of meetings processed',
  labelNames: ['operation', 'status'],
});

const racesProcessedCounter = new Counter({
  name: 'races_processed_total',
  help: 'Total number of races processed',
  labelNames: ['operation', 'status'],
});

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

// ============================================================================
// MeetingService Class
// ============================================================================

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

  /**
   * Fetch meetings from API and store in database
   */
  async fetchAndStoreMeetings(options: FetchMeetingsOptions): Promise<FetchResult> {
    const span = tracer.startSpan('meeting_service.fetch_and_store_meetings');
    const startTime = Date.now();

    try {
      span.setAttributes({
        'meeting.category': options.category,
        'meeting.country': options.country,
        'meeting.date': options.date.toISOString(),
      });

      logger.info({
        category: options.category,
        country: options.country,
        date: options.date.toISOString().split('T')[0],
      }, 'Fetching and storing meetings');

      // Format date for API
      const dateStr = options.date.toISOString().split('T')[0];

      // Fetch from API
      const apiResponse = await this.apiClient.getMeetings({
        category: options.category,
        country: options.country,
        dateFrom: dateStr,
        dateTo: dateStr,
      });

      const meetings = apiResponse.data.meetings;
      logger.debug({ count: meetings.length }, 'Fetched meetings from API');

      // Process each meeting in a transaction
      const result: FetchResult = {
        meetingsProcessed: 0,
        racesProcessed: 0,
        errors: [],
      };

      for (const meeting of meetings) {
        try {
          await this.processMeeting(meeting);
          result.meetingsProcessed++;
          result.racesProcessed += meeting.races.length;

          meetingsProcessedCounter.labels('fetch_and_store', 'success').inc();
          racesProcessedCounter.labels('fetch_and_store', 'success').inc(meeting.races.length);

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

          logger.error({
            meetingId: meeting.meeting,
            meetingName: meeting.name,
            error: errorMessage,
          }, 'Failed to process meeting');

          meetingsProcessedCounter.labels('fetch_and_store', 'error').inc();
        }
      }

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

      span.setStatus({ code: SpanStatusCode.OK });
      span.setAttribute('meetings.processed', result.meetingsProcessed);
      span.setAttribute('races.processed', result.racesProcessed);
      span.setAttribute('errors.count', result.errors.length);

      logger.info({
        meetingsProcessed: result.meetingsProcessed,
        racesProcessed: result.racesProcessed,
        errors: result.errors.length,
        duration,
      }, 'Completed fetching and storing meetings');

      return result;

    } catch (error) {
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: error instanceof Error ? error.message : 'Unknown error',
      });

      logger.error({
        error: error instanceof Error ? error.message : 'Unknown error',
        category: options.category,
        country: options.country,
        date: options.date.toISOString().split('T')[0],
      }, 'Failed to fetch and store meetings');

      throw error;

    } finally {
      span.end();
    }
  }

  /**
   * Process a single meeting (upsert meeting and races)
   */
  private async processMeeting(apiMeeting: any): Promise<void> {
    await this.prisma.$transaction(async (tx) => {
      // Upsert meeting
      await tx.meeting.upsert({
        where: { id: apiMeeting.meeting },
        create: {
          id: apiMeeting.meeting,
          name: apiMeeting.name,
          date: new Date(apiMeeting.date),
          country: apiMeeting.country,
          state: apiMeeting.state || null,
          category: apiMeeting.category,
          categoryName: apiMeeting.category_name || null,
          trackCondition: apiMeeting.track_condition || null,
          weather: null,
          videoChannels: Prisma.JsonNull,
          quaddie: [],
          earlyQuaddie: [],
          toteMeetingNumber: null,
          toteStatus: null,
          metadata: Prisma.JsonNull,
        },
        update: {
          name: apiMeeting.name,
          trackCondition: apiMeeting.track_condition || null,
          updatedAt: new Date(),
        },
      });

      // Upsert each race
      for (const race of apiMeeting.races) {
        await tx.race.upsert({
          where: {
            meetingId_raceNumber: {
              meetingId: apiMeeting.meeting,
              raceNumber: race.race_number,
            },
          },
          create: {
            id: race.id,
            meetingId: apiMeeting.meeting,
            raceNumber: race.race_number,
            name: race.name,
            startTime: new Date(race.start_time),
            toteStartTime: race.tote_start_time ? this.parseTime(race.tote_start_time) : null,
            distance: race.distance,
            trackCondition: race.track_condition || null,
            weather: race.weather || null,
            status: race.status,
            country: race.country,
            state: race.state || null,
            metadata: Prisma.JsonNull,
          },
          update: {
            name: race.name,
            startTime: new Date(race.start_time),
            trackCondition: race.track_condition || null,
            weather: race.weather || null,
            status: race.status,
            updatedAt: new Date(),
          },
        });
      }
    });
  }

  /**
   * Get a meeting by ID from database
   */
  async getMeetingById(id: string): Promise<(Meeting & { races: Race[] }) | null> {
    const span = tracer.startSpan('meeting_service.get_meeting_by_id');

    try {
      span.setAttribute('meeting.id', id);

      const meeting = await this.prisma.meeting.findUnique({
        where: { id },
        include: { races: true },
      });

      span.setStatus({ code: SpanStatusCode.OK });
      span.setAttribute('meeting.found', meeting !== null);

      return meeting;

    } catch (error) {
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: error instanceof Error ? error.message : 'Unknown error',
      });
      throw error;

    } finally {
      span.end();
    }
  }

  /**
   * Get meetings by date with optional filters
   */
  async getMeetingsByDate(
    date: Date,
    options: GetMeetingsOptions = {}
  ): Promise<Array<Meeting & { races: Race[] }>> {
    const span = tracer.startSpan('meeting_service.get_meetings_by_date');

    try {
      span.setAttributes({
        'meeting.date': date.toISOString(),
        'meeting.country': options.country || 'all',
        'meeting.category': options.category || 'all',
      });

      const meetings = await this.prisma.meeting.findMany({
        where: {
          date,
          ...(options.country && { country: options.country }),
          ...(options.category && { category: options.category }),
        },
        include: { races: true },
        orderBy: { name: 'asc' },
      });

      span.setStatus({ code: SpanStatusCode.OK });
      span.setAttribute('meetings.count', meetings.length);

      return meetings;

    } catch (error) {
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: error instanceof Error ? error.message : 'Unknown error',
      });
      throw error;

    } finally {
      span.end();
    }
  }

  /**
   * Update a meeting from API (refresh data)
   */
  async updateMeeting(id: string): Promise<void> {
    const span = tracer.startSpan('meeting_service.update_meeting');

    try {
      span.setAttribute('meeting.id', id);

      logger.info({ meetingId: id }, 'Updating meeting from API');

      // Fetch latest data from API
      const apiResponse = await this.apiClient.getMeetingById(id);
      const apiMeeting = apiResponse.data.meetings[0];

      // Process the meeting (will upsert)
      await this.processMeeting(apiMeeting);

      span.setStatus({ code: SpanStatusCode.OK });

      logger.info({ meetingId: id }, 'Meeting updated successfully');

    } catch (error) {
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: error instanceof Error ? error.message : 'Unknown error',
      });

      logger.error({
        meetingId: id,
        error: error instanceof Error ? error.message : 'Unknown error',
      }, 'Failed to update meeting');

      throw error;

    } finally {
      span.end();
    }
  }

  // ============================================================================
  // Helper Methods
  // ============================================================================

  /**
   * Parse time string (HH:MM:SS) to Date object
   */
  private parseTime(timeStr: string): Date {
    const [hours, minutes, seconds] = timeStr.split(':').map(Number);
    const date = new Date();
    date.setHours(hours, minutes, seconds, 0);
    return date;
  }
}
