// ABOUTME: HTTP client for TAB Affiliates API with rate limiting and retry logic
// ABOUTME: Includes Prometheus metrics, OpenTelemetry tracing, and Zod response validation

import axios, { AxiosInstance, AxiosError } from 'axios';
import axiosRetry from 'axios-retry';
import Bottleneck from 'bottleneck';
import { trace, SpanStatusCode } from '@opentelemetry/api';
import { Counter, Histogram, Gauge } from 'prom-client';
import logger from '../../utils/logger';
import {
  TabApiClientConfig,
  GetMeetingsOptions,
  MeetingsListResponse,
  MeetingByIdResponse,
  RaceByIdResponse,
} from './types';
import {
  MeetingsListResponseSchema,
  MeetingByIdResponseSchema,
  RaceByIdResponseSchema,
} from './schemas';

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

const tracer = trace.getTracer('tab-api-client');

const requestDuration = new Histogram({
  name: 'tab_api_request_duration_seconds',
  help: 'Duration of TAB API requests in seconds',
  labelNames: ['endpoint', 'status'],
  buckets: [0.1, 0.5, 1, 2, 5, 10],
});

const requestCounter = new Counter({
  name: 'tab_api_requests_total',
  help: 'Total number of TAB API requests',
  labelNames: ['endpoint', 'status'],
});

const errorCounter = new Counter({
  name: 'tab_api_errors_total',
  help: 'Total number of TAB API errors',
  labelNames: ['endpoint', 'error_type'],
});

const rateLimiterQueueDepth = new Gauge({
  name: 'tab_api_rate_limiter_queue_depth',
  help: 'Number of requests queued in rate limiter',
});

const rateLimiterRunning = new Gauge({
  name: 'tab_api_rate_limiter_running',
  help: 'Number of requests currently executing',
});

// ============================================================================
// TabApiClient Class
// ============================================================================

export class TabApiClient {
  private client: AxiosInstance;
  private limiter: Bottleneck;
  private config: Required<TabApiClientConfig>;

  constructor(config: TabApiClientConfig) {
    // Set defaults
    this.config = {
      baseUrl: config.baseUrl,
      timeout: config.timeout ?? 10000,
      maxRetries: config.maxRetries ?? 3,
      retryDelay: config.retryDelay ?? 1000,
      rateLimitPerMinute: config.rateLimitPerMinute ?? 100,
    };

    // Create axios instance
    this.client = axios.create({
      baseURL: this.config.baseUrl,
      timeout: this.config.timeout,
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
    });

    // Configure retry logic with exponential backoff
    axiosRetry(this.client, {
      retries: this.config.maxRetries,
      retryDelay: (retryCount) => {
        return axiosRetry.exponentialDelay(retryCount, undefined, this.config.retryDelay);
      },
      retryCondition: (error) => {
        // Retry on network errors and 5xx server errors
        return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
               (error.response?.status ?? 0) >= 500;
      },
      onRetry: (retryCount, error, requestConfig) => {
        logger.warn({
          retryCount,
          url: requestConfig.url,
          method: requestConfig.method,
          error: error.message,
        }, 'Retrying TAB API request');
      },
    });

    // Configure rate limiting
    this.limiter = new Bottleneck({
      reservoir: this.config.rateLimitPerMinute,
      reservoirRefreshAmount: this.config.rateLimitPerMinute,
      reservoirRefreshInterval: 60 * 1000, // 1 minute
      maxConcurrent: 10, // Max concurrent requests
      minTime: 600, // Minimum 600ms between requests (100/min)
    });

    // Update rate limiter metrics periodically
    this.limiter.on('queued', () => {
      const counts = this.limiter.counts();
      rateLimiterQueueDepth.set(counts.QUEUED);
      rateLimiterRunning.set(counts.RUNNING);
    });

    this.limiter.on('executing', () => {
      const counts = this.limiter.counts();
      rateLimiterQueueDepth.set(counts.QUEUED);
      rateLimiterRunning.set(counts.RUNNING);
    });

    this.limiter.on('done', () => {
      const counts = this.limiter.counts();
      rateLimiterQueueDepth.set(counts.QUEUED);
      rateLimiterRunning.set(counts.RUNNING);
    });

    logger.info({
      baseUrl: this.config.baseUrl,
      timeout: this.config.timeout,
      maxRetries: this.config.maxRetries,
      rateLimitPerMinute: this.config.rateLimitPerMinute,
    }, 'TabApiClient initialized');
  }

  /**
   * Get list of meetings with optional filters
   */
  async getMeetings(options: GetMeetingsOptions = {}): Promise<MeetingsListResponse> {
    const endpoint = '/affiliates/v1/racing/meetings';

    return this.limiter.schedule(async () => {
      const span = tracer.startSpan('tab_api.get_meetings');
      const startTime = Date.now();

      try {
        span.setAttributes({
          'http.method': 'GET',
          'http.url': endpoint,
          'tab.category': options.category ?? 'all',
          'tab.country': options.country ?? 'all',
        });

        logger.debug({
          endpoint,
          options,
        }, 'Fetching meetings from TAB API');

        // Build query parameters
        const params: Record<string, any> = {
          enc: 'json',
        };

        if (options.category) params.category = options.category;
        if (options.country) params.country = options.country;
        if (options.dateFrom) params.date_from = options.dateFrom;
        if (options.dateTo) params.date_to = options.dateTo;
        if (options.futures !== undefined) params.futures = options.futures;
        if (options.limit !== undefined) params.limit = options.limit;
        if (options.offset !== undefined) params.offset = options.offset;

        // Make request
        const response = await this.client.get(endpoint, { params });

        // Validate response
        const validatedData = MeetingsListResponseSchema.parse(response.data);

        // Record metrics
        const duration = (Date.now() - startTime) / 1000;
        requestDuration.labels(endpoint, 'success').observe(duration);
        requestCounter.labels(endpoint, 'success').inc();

        span.setStatus({ code: SpanStatusCode.OK });
        span.setAttribute('http.status_code', 200);
        span.setAttribute('tab.meetings_count', validatedData.data.meetings.length);

        logger.info({
          endpoint,
          duration,
          meetingsCount: validatedData.data.meetings.length,
        }, 'Successfully fetched meetings from TAB API');

        return validatedData;

      } catch (error) {
        const duration = (Date.now() - startTime) / 1000;
        const errorType = this.getErrorType(error);

        errorCounter.labels(endpoint, errorType).inc();
        requestDuration.labels(endpoint, 'error').observe(duration);
        requestCounter.labels(endpoint, 'error').inc();

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

        if (axios.isAxiosError(error)) {
          span.setAttribute('http.status_code', error.response?.status ?? 0);
          this.handleAxiosError(error, endpoint);
        }

        logger.error({
          endpoint,
          error: error instanceof Error ? error.message : 'Unknown error',
          duration,
          options,
        }, 'Failed to fetch meetings from TAB API');

        throw error;

      } finally {
        span.end();
      }
    });
  }

  /**
   * Get a specific meeting by ID
   */
  async getMeetingById(id: string): Promise<MeetingByIdResponse> {
    // Validate UUID format
    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
    if (!uuidRegex.test(id)) {
      const error = new Error(`Invalid meeting ID format: ${id}. Expected UUID.`);
      errorCounter.labels('/affiliates/v1/racing/meetings/:id', 'validation_error').inc();
      throw error;
    }

    const endpoint = `/affiliates/v1/racing/meetings/${id}`;

    return this.limiter.schedule(async () => {
      const span = tracer.startSpan('tab_api.get_meeting_by_id');
      const startTime = Date.now();

      try {
        span.setAttributes({
          'http.method': 'GET',
          'http.url': endpoint,
          'tab.meeting_id': id,
        });

        logger.debug({
          endpoint,
          meetingId: id,
        }, 'Fetching meeting by ID from TAB API');

        // Make request
        const response = await this.client.get(endpoint, {
          params: { enc: 'json' },
        });

        // Validate response
        const validatedData = MeetingByIdResponseSchema.parse(response.data);

        // Record metrics
        const duration = (Date.now() - startTime) / 1000;
        requestDuration.labels(endpoint, 'success').observe(duration);
        requestCounter.labels(endpoint, 'success').inc();

        span.setStatus({ code: SpanStatusCode.OK });
        span.setAttribute('http.status_code', 200);
        span.setAttribute('tab.races_count', validatedData.data.meetings[0].races.length);

        logger.info({
          endpoint,
          duration,
          meetingId: id,
          racesCount: validatedData.data.meetings[0].races.length,
        }, 'Successfully fetched meeting by ID from TAB API');

        return validatedData;

      } catch (error) {
        const duration = (Date.now() - startTime) / 1000;
        const errorType = this.getErrorType(error);

        errorCounter.labels(endpoint, errorType).inc();
        requestDuration.labels(endpoint, 'error').observe(duration);
        requestCounter.labels(endpoint, 'error').inc();

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

        if (axios.isAxiosError(error)) {
          span.setAttribute('http.status_code', error.response?.status ?? 0);
          this.handleAxiosError(error, endpoint);
        }

        logger.error({
          endpoint,
          meetingId: id,
          error: error instanceof Error ? error.message : 'Unknown error',
          duration,
        }, 'Failed to fetch meeting by ID from TAB API');

        throw error;

      } finally {
        span.end();
      }
    });
  }

  /**
   * Get a specific race/event by ID with full details
   * Returns runners, odds, results, and dividends
   */
  async getRaceById(id: string): Promise<RaceByIdResponse> {
    // Validate UUID format
    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
    if (!uuidRegex.test(id)) {
      const error = new Error(`Invalid race ID format: ${id}. Expected UUID.`);
      errorCounter.labels('/affiliates/v1/racing/events/:id', 'validation_error').inc();
      throw error;
    }

    const endpoint = `/affiliates/v1/racing/events/${id}`;

    return this.limiter.schedule(async () => {
      const span = tracer.startSpan('tab_api.get_race_by_id');
      const startTime = Date.now();

      try {
        span.setAttributes({
          'http.method': 'GET',
          'http.url': endpoint,
          'tab.race_id': id,
        });

        logger.debug({
          endpoint,
          raceId: id,
        }, 'Fetching race by ID from TAB API');

        // Make request
        const response = await this.client.get(endpoint, {
          params: { enc: 'json' },
        });

        // Validate response
        const validatedData = RaceByIdResponseSchema.parse(response.data);

        // Record metrics
        const duration = (Date.now() - startTime) / 1000;
        requestDuration.labels(endpoint, 'success').observe(duration);
        requestCounter.labels(endpoint, 'success').inc();

        span.setStatus({ code: SpanStatusCode.OK });
        span.setAttribute('http.status_code', 200);
        span.setAttribute('tab.runners_count', validatedData.data.runners.length);
        span.setAttribute('tab.race_status', validatedData.data.race.status);

        logger.info({
          endpoint,
          duration,
          raceId: id,
          runnersCount: validatedData.data.runners.length,
          raceStatus: validatedData.data.race.status,
        }, 'Successfully fetched race by ID from TAB API');

        return validatedData;

      } catch (error) {
        const duration = (Date.now() - startTime) / 1000;
        const errorType = this.getErrorType(error);

        errorCounter.labels(endpoint, errorType).inc();
        requestDuration.labels(endpoint, 'error').observe(duration);
        requestCounter.labels(endpoint, 'error').inc();

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

        if (axios.isAxiosError(error)) {
          span.setAttribute('http.status_code', error.response?.status ?? 0);
          this.handleAxiosError(error, endpoint);
        }

        logger.error({
          endpoint,
          raceId: id,
          error: error instanceof Error ? error.message : 'Unknown error',
          duration,
        }, 'Failed to fetch race by ID from TAB API');

        throw error;

      } finally {
        span.end();
      }
    });
  }

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

  private handleAxiosError(error: AxiosError, endpoint: string): never {
    if (error.response) {
      // Server responded with error status
      const status = error.response.status;
      const data = error.response.data as any;

      const errorMessage = data?.error?.message ?? `HTTP ${status} error`;
      const enhancedError = new Error(errorMessage);

      logger.error({
        endpoint,
        status,
        errorCode: data?.error?.code,
        errorMessage,
      }, 'TAB API returned error response');

      throw enhancedError;
    } else if (error.request) {
      // Request made but no response received
      const timeoutError = new Error(
        error.code === 'ECONNABORTED'
          ? `Request timeout after ${this.config.timeout}ms`
          : 'No response received from TAB API'
      );

      logger.error({
        endpoint,
        errorCode: error.code,
        message: error.message,
      }, 'No response received from TAB API');

      throw timeoutError;
    } else {
      // Error setting up the request
      throw error;
    }
  }

  private getErrorType(error: unknown): string {
    if (axios.isAxiosError(error)) {
      if (error.response) {
        return `http_${error.response.status}`;
      } else if (error.code === 'ECONNABORTED') {
        return 'timeout';
      } else if (error.code) {
        return error.code.toLowerCase();
      }
      return 'network_error';
    }

    if (error instanceof Error && error.message.includes('validation')) {
      return 'validation_error';
    }

    return 'unknown_error';
  }

  /**
   * Get current rate limiter status (for monitoring)
   */
  getRateLimitStatus() {
    const counts = this.limiter.counts();
    return {
      running: counts.RUNNING,
      queued: counts.QUEUED,
    };
  }
}
