import * as plugins from '../../plugins.js';
import type { IStockProvider, IProviderConfig } from '../interfaces/provider.js';
import type {
  IStockPrice,
  IStockDataRequest,
  IStockCurrentRequest,
  IStockHistoricalRequest,
  IStockIntradayRequest,
  IStockBatchCurrentRequest
} from '../interfaces/stockprice.js';

/**
 * Custom error for rate limit exceeded responses
 */
class RateLimitError extends Error {
  constructor(
    message: string,
    public waitTime: number,
    public retryAfter?: number
  ) {
    super(message);
    this.name = 'RateLimitError';
  }
}

/**
 * Rate limiter for CoinGecko API
 * Free tier (Demo): 30 requests per minute
 * Without registration: 5-15 requests per minute
 */
class RateLimiter {
  private requestTimes: number[] = [];
  private maxRequestsPerMinute: number;
  private consecutiveRateLimitErrors: number = 0;

  constructor(maxRequestsPerMinute: number = 30) {
    this.maxRequestsPerMinute = maxRequestsPerMinute;
  }

  public async waitForSlot(): Promise<void> {
    const now = Date.now();
    const oneMinuteAgo = now - 60000;

    // Remove requests older than 1 minute
    this.requestTimes = this.requestTimes.filter(time => time > oneMinuteAgo);

    // If we've hit the limit, wait
    if (this.requestTimes.length >= this.maxRequestsPerMinute) {
      const oldestRequest = this.requestTimes[0];
      const waitTime = 60000 - (now - oldestRequest) + 100; // +100ms buffer
      await plugins.smartdelay.delayFor(waitTime);
      return this.waitForSlot(); // Recursively check again
    }

    // Record this request
    this.requestTimes.push(now);
  }

  /**
   * Get time in milliseconds until next request slot is available
   */
  public getTimeUntilNextSlot(): number {
    const now = Date.now();
    const oneMinuteAgo = now - 60000;

    // Clean old requests
    const recentRequests = this.requestTimes.filter(time => time > oneMinuteAgo);

    if (recentRequests.length < this.maxRequestsPerMinute) {
      return 0; // Slot available now
    }

    // Calculate wait time until oldest request expires
    const oldestRequest = recentRequests[0];
    return Math.max(0, 60000 - (now - oldestRequest) + 100);
  }

  /**
   * Handle rate limit error with exponential backoff
   * Returns wait time in milliseconds
   */
  public handleRateLimitError(): number {
    this.consecutiveRateLimitErrors++;

    // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 60s (max)
    const baseWait = 1000; // 1 second
    const exponent = this.consecutiveRateLimitErrors - 1;
    const backoff = Math.min(
      baseWait * Math.pow(2, exponent),
      60000 // max 60 seconds
    );

    // After 3 consecutive 429s, reduce rate limit to 80% as safety measure
    if (this.consecutiveRateLimitErrors >= 3) {
      const newLimit = Math.floor(this.maxRequestsPerMinute * 0.8);
      if (newLimit < this.maxRequestsPerMinute) {
        console.warn(
          `Adjusting rate limit from ${this.maxRequestsPerMinute} to ${newLimit} requests/min due to repeated 429 errors`
        );
        this.maxRequestsPerMinute = newLimit;
      }
    }

    return backoff;
  }

  /**
   * Reset consecutive error count on successful request
   */
  public resetErrors(): void {
    if (this.consecutiveRateLimitErrors > 0) {
      this.consecutiveRateLimitErrors = 0;
    }
  }
}

/**
 * Interface for coin list response
 */
interface ICoinListItem {
  id: string;
  symbol: string;
  name: string;
}

/**
 * CoinGecko Crypto Price Provider
 *
 * Documentation: https://docs.coingecko.com/v3.0.1/reference/endpoint-overview
 *
 * Features:
 * - Current crypto prices (single and batch)
 * - Historical price data with OHLCV
 * - 13M+ tokens, 240+ networks, 1600+ exchanges
 * - Accepts both ticker symbols (BTC, ETH) and CoinGecko IDs (bitcoin, ethereum)
 * - 24/7 market data (crypto never closes)
 *
 * Rate Limits:
 * - Free tier (no key): 5-15 requests/minute
 * - Demo plan (free with registration): ~30 requests/minute, 10,000/month
 * - Paid plans: Higher limits
 *
 * API Authentication:
 * - Optional API key for Demo/paid plans
 * - Header: x-cg-demo-api-key (Demo) or x-cg-pro-api-key (paid)
 */
export class CoinGeckoProvider implements IStockProvider {
  public name = 'CoinGecko';
  public priority = 90; // High priority for crypto, between Yahoo (100) and Marketstack (80)
  public readonly requiresAuth = false; // API key is optional
  public readonly rateLimit = {
    requestsPerMinute: 30, // Demo plan default
    requestsPerDay: 10000 // Demo plan monthly quota / 30
  };

  private logger = console;
  private baseUrl = 'https://api.coingecko.com/api/v3';
  private apiKey?: string;
  private rateLimiter: RateLimiter;

  // Coin mapping cache
  private coinMapCache = new Map<string, string>(); // ticker/id -> coingecko id
  private coinListLoadedAt: Date | null = null;
  private readonly coinListCacheTTL = 24 * 60 * 60 * 1000; // 24 hours

  // Priority map for common crypto tickers (to avoid conflicts)
  private readonly priorityTickerMap = new Map<string, string>([
    ['btc', 'bitcoin'],
    ['eth', 'ethereum'],
    ['usdt', 'tether'],
    ['bnb', 'binancecoin'],
    ['sol', 'solana'],
    ['usdc', 'usd-coin'],
    ['xrp', 'ripple'],
    ['ada', 'cardano'],
    ['doge', 'dogecoin'],
    ['trx', 'tron'],
    ['dot', 'polkadot'],
    ['matic', 'matic-network'],
    ['ltc', 'litecoin'],
    ['shib', 'shiba-inu'],
    ['avax', 'avalanche-2'],
    ['link', 'chainlink'],
    ['atom', 'cosmos'],
    ['uni', 'uniswap'],
    ['etc', 'ethereum-classic'],
    ['xlm', 'stellar']
  ]);

  constructor(apiKey?: string, private config?: IProviderConfig) {
    this.apiKey = apiKey;
    this.rateLimiter = new RateLimiter(this.rateLimit.requestsPerMinute);
  }

  /**
   * Unified data fetching method supporting all request types
   */
  public async fetchData(request: IStockDataRequest): Promise<IStockPrice | IStockPrice[]> {
    switch (request.type) {
      case 'current':
        return this.fetchCurrentPrice(request);
      case 'batch':
        return this.fetchBatchCurrentPrices(request);
      case 'historical':
        return this.fetchHistoricalPrices(request);
      case 'intraday':
        return this.fetchIntradayPrices(request);
      default:
        throw new Error(`Unsupported request type: ${(request as any).type}`);
    }
  }

  /**
   * Fetch current price for a single crypto
   */
  private async fetchCurrentPrice(request: IStockCurrentRequest): Promise<IStockPrice> {
    return this.fetchWithRateLimitRetry(async () => {
      // Resolve ticker to CoinGecko ID
      const coinId = await this.resolveCoinId(request.ticker);

      // Build URL
      const params = new URLSearchParams({
        ids: coinId,
        vs_currencies: 'usd',
        include_market_cap: 'true',
        include_24hr_vol: 'true',
        include_24hr_change: 'true',
        include_last_updated_at: 'true'
      });

      const url = `${this.baseUrl}/simple/price?${params}`;

      // Wait for rate limit slot
      await this.rateLimiter.waitForSlot();

      // Make request
      const response = await plugins.smartrequest.SmartRequest.create()
        .url(url)
        .headers(this.buildHeaders())
        .timeout(this.config?.timeout || 10000)
        .get();

      const responseData = await response.json() as any;

      // Check for rate limit error
      if (this.isRateLimitError(responseData)) {
        const waitTime = this.rateLimiter.handleRateLimitError();
        throw new RateLimitError(
          `Rate limit exceeded for ${request.ticker}`,
          waitTime
        );
      }

      if (!responseData[coinId]) {
        throw new Error(`No data found for ${request.ticker} (${coinId})`);
      }

      return this.mapToStockPrice(request.ticker, coinId, responseData[coinId], 'live');
    }, `current price for ${request.ticker}`);
  }

  /**
   * Fetch batch current prices for multiple cryptos
   */
  private async fetchBatchCurrentPrices(request: IStockBatchCurrentRequest): Promise<IStockPrice[]> {
    return this.fetchWithRateLimitRetry(async () => {
      // Resolve all tickers to CoinGecko IDs
      const coinIds = await Promise.all(
        request.tickers.map(ticker => this.resolveCoinId(ticker))
      );

      // Build URL with comma-separated IDs
      const params = new URLSearchParams({
        ids: coinIds.join(','),
        vs_currencies: 'usd',
        include_market_cap: 'true',
        include_24hr_vol: 'true',
        include_24hr_change: 'true',
        include_last_updated_at: 'true'
      });

      const url = `${this.baseUrl}/simple/price?${params}`;

      // Wait for rate limit slot
      await this.rateLimiter.waitForSlot();

      // Make request
      const response = await plugins.smartrequest.SmartRequest.create()
        .url(url)
        .headers(this.buildHeaders())
        .timeout(this.config?.timeout || 15000)
        .get();

      const responseData = await response.json() as any;

      // Check for rate limit error
      if (this.isRateLimitError(responseData)) {
        const waitTime = this.rateLimiter.handleRateLimitError();
        throw new RateLimitError(
          `Rate limit exceeded for batch request`,
          waitTime
        );
      }

      const prices: IStockPrice[] = [];

      // Map responses back to original tickers
      for (let i = 0; i < request.tickers.length; i++) {
        const ticker = request.tickers[i];
        const coinId = coinIds[i];

        if (responseData[coinId]) {
          try {
            prices.push(this.mapToStockPrice(ticker, coinId, responseData[coinId], 'live'));
          } catch (error) {
            this.logger.warn(`Failed to parse data for ${ticker}:`, error);
          }
        } else {
          this.logger.warn(`No data returned for ${ticker} (${coinId})`);
        }
      }

      if (prices.length === 0) {
        throw new Error('No valid price data received from batch request');
      }

      return prices;
    }, `batch prices for ${request.tickers.length} tickers`);
  }

  /**
   * Fetch historical prices with OHLCV data
   */
  private async fetchHistoricalPrices(request: IStockHistoricalRequest): Promise<IStockPrice[]> {
    return this.fetchWithRateLimitRetry(async () => {
      const coinId = await this.resolveCoinId(request.ticker);

      // Calculate days between dates
      const days = Math.ceil((request.to.getTime() - request.from.getTime()) / (1000 * 60 * 60 * 24));

      // Build URL
      const params = new URLSearchParams({
        vs_currency: 'usd',
        days: days.toString(),
        interval: 'daily' // Explicit daily granularity for historical data
      });

      const url = `${this.baseUrl}/coins/${coinId}/market_chart?${params}`;

      // Wait for rate limit slot
      await this.rateLimiter.waitForSlot();

      // Make request
      const response = await plugins.smartrequest.SmartRequest.create()
        .url(url)
        .headers(this.buildHeaders())
        .timeout(this.config?.timeout || 20000)
        .get();

      const responseData = await response.json() as any;

      // Check for rate limit error
      if (this.isRateLimitError(responseData)) {
        const waitTime = this.rateLimiter.handleRateLimitError();
        throw new RateLimitError(
          `Rate limit exceeded for historical ${request.ticker}`,
          waitTime
        );
      }

      if (!responseData.prices || !Array.isArray(responseData.prices)) {
        this.logger.error(`Invalid API response for ${request.ticker}:`, JSON.stringify(responseData).substring(0, 500));
        throw new Error(`Invalid response format for ${request.ticker}: ${JSON.stringify(responseData).substring(0, 200)}`);
      }

      const prices: IStockPrice[] = [];
      const priceData = responseData.prices;
      const marketCapData = responseData.market_caps || [];
      const volumeData = responseData.total_volumes || [];

      // Warn if processing large amount of historical data
      const maxRecords = this.config?.maxRecords || 10000;
      if (priceData.length > maxRecords) {
        this.logger.warn(
          `Historical request for ${request.ticker} returned ${priceData.length} records, ` +
          `which exceeds maxRecords limit of ${maxRecords}. Processing first ${maxRecords} only.`
        );
      }

      // Process each data point (up to maxRecords)
      const recordsToProcess = Math.min(priceData.length, maxRecords);
      for (let i = 0; i < recordsToProcess; i++) {
        const [timestamp, price] = priceData[i];
        const date = new Date(timestamp);

        // Filter by date range
        if (date < request.from || date > request.to) continue;

        const marketCap = marketCapData[i]?.[1];
        const volume = volumeData[i]?.[1];

        // Calculate previous close for change calculation
        const previousClose = i > 0 ? priceData[i - 1][1] : price;
        const change = price - previousClose;
        const changePercent = previousClose !== 0 ? (change / previousClose) * 100 : 0;

        prices.push({
          ticker: request.ticker.toUpperCase(),
          price: price,
          currency: 'USD',
          change: change,
          changePercent: changePercent,
          previousClose: previousClose,
          timestamp: date,
          provider: this.name,
          marketState: 'REGULAR', // Crypto markets are always open

          // OHLCV data (note: market_chart doesn't provide OHLC, only close prices)
          volume: volume,

          dataType: 'eod',
          fetchedAt: new Date(),

          companyName: coinId.charAt(0).toUpperCase() + coinId.slice(1)
        });
      }

      return prices;
    }, `historical prices for ${request.ticker}`);
  }

  /**
   * Fetch intraday prices with hourly intervals
   */
  private async fetchIntradayPrices(request: IStockIntradayRequest): Promise<IStockPrice[]> {
    return this.fetchWithRateLimitRetry(async () => {
      const coinId = await this.resolveCoinId(request.ticker);

      // Map interval to days parameter (CoinGecko auto-granularity)
      // For hourly data, request 1-7 days
      let days = 1;
      switch (request.interval) {
        case '1min':
        case '5min':
        case '10min':
        case '15min':
        case '30min':
          throw new Error('CoinGecko only supports hourly intervals in market_chart. Use interval: "1hour"');
        case '1hour':
          days = 1; // Last 24 hours with hourly granularity
          break;
      }

      // Build URL (omit interval param for automatic granularity based on days)
      const params = new URLSearchParams({
        vs_currency: 'usd',
        days: days.toString()
      });

      const url = `${this.baseUrl}/coins/${coinId}/market_chart?${params}`;

      // Wait for rate limit slot
      await this.rateLimiter.waitForSlot();

      // Make request
      const response = await plugins.smartrequest.SmartRequest.create()
        .url(url)
        .headers(this.buildHeaders())
        .timeout(this.config?.timeout || 15000)
        .get();

      const responseData = await response.json() as any;

      // Check for rate limit error
      if (this.isRateLimitError(responseData)) {
        const waitTime = this.rateLimiter.handleRateLimitError();
        throw new RateLimitError(
          `Rate limit exceeded for intraday ${request.ticker}`,
          waitTime
        );
      }

      if (!responseData.prices || !Array.isArray(responseData.prices)) {
        this.logger.error(`Invalid API response for ${request.ticker}:`, JSON.stringify(responseData).substring(0, 500));
        throw new Error(`Invalid response format for ${request.ticker}: ${JSON.stringify(responseData).substring(0, 200)}`);
      }

      const prices: IStockPrice[] = [];
      const priceData = responseData.prices;
      const marketCapData = responseData.market_caps || [];
      const volumeData = responseData.total_volumes || [];

      // Apply default limit if user didn't specify one (performance optimization)
      const effectiveLimit = request.limit || this.config?.defaultIntradayLimit || 1000;

      // Warn if fetching large amount of data without explicit limit
      if (!request.limit && priceData.length > effectiveLimit) {
        this.logger.warn(
          `Intraday request for ${request.ticker} returned ${priceData.length} records but no limit specified. ` +
          `Applying default limit of ${effectiveLimit}. Consider adding a limit to the request for better performance.`
        );
      }

      // Apply limit (take most recent data)
      const limit = Math.min(effectiveLimit, priceData.length);
      const dataToProcess = priceData.slice(-limit);

      for (let i = 0; i < dataToProcess.length; i++) {
        const actualIndex = priceData.length - limit + i;
        const [timestamp, price] = dataToProcess[i];
        const date = new Date(timestamp);

        const marketCap = marketCapData[actualIndex]?.[1];
        const volume = volumeData[actualIndex]?.[1];

        const previousClose = i > 0 ? dataToProcess[i - 1][1] : price;
        const change = price - previousClose;
        const changePercent = previousClose !== 0 ? (change / previousClose) * 100 : 0;

        prices.push({
          ticker: request.ticker.toUpperCase(),
          price: price,
          currency: 'USD',
          change: change,
          changePercent: changePercent,
          previousClose: previousClose,
          timestamp: date,
          provider: this.name,
          marketState: 'REGULAR',

          volume: volume,

          dataType: 'intraday',
          fetchedAt: new Date(),

          companyName: coinId.charAt(0).toUpperCase() + coinId.slice(1)
        });
      }

      return prices;
    }, `intraday prices for ${request.ticker}`);
  }

  /**
   * Check if CoinGecko API is available
   */
  public async isAvailable(): Promise<boolean> {
    try {
      const url = `${this.baseUrl}/simple/price?ids=bitcoin&vs_currencies=usd`;

      await this.rateLimiter.waitForSlot();

      const response = await plugins.smartrequest.SmartRequest.create()
        .url(url)
        .headers(this.buildHeaders())
        .timeout(5000)
        .get();

      const responseData = await response.json() as any;
      return responseData.bitcoin?.usd !== undefined;
    } catch (error) {
      this.logger.warn('CoinGecko provider is not available:', error);
      return false;
    }
  }

  /**
   * Check if a market/network is supported
   * CoinGecko supports 240+ networks
   */
  public supportsMarket(market: string): boolean {
    // CoinGecko has extensive crypto network coverage
    const supportedNetworks = [
      'CRYPTO', 'BTC', 'ETH', 'BSC', 'POLYGON', 'AVALANCHE',
      'SOLANA', 'ARBITRUM', 'OPTIMISM', 'BASE'
    ];
    return supportedNetworks.includes(market.toUpperCase());
  }

  /**
   * Check if a ticker format is supported
   * Supports both ticker symbols (BTC) and CoinGecko IDs (bitcoin)
   */
  public supportsTicker(ticker: string): boolean {
    // Accept alphanumeric with hyphens (for coin IDs like 'wrapped-bitcoin')
    return /^[A-Za-z0-9\-]{1,50}$/.test(ticker);
  }

  /**
   * Resolve ticker symbol or CoinGecko ID to canonical CoinGecko ID
   * Supports both formats: "BTC" -> "bitcoin", "bitcoin" -> "bitcoin"
   */
  private async resolveCoinId(tickerOrId: string): Promise<string> {
    const normalized = tickerOrId.toLowerCase();

    // Check priority map first (for common cryptos)
    if (this.priorityTickerMap.has(normalized)) {
      const coinId = this.priorityTickerMap.get(normalized)!;
      this.coinMapCache.set(normalized, coinId);
      return coinId;
    }

    // Check cache
    if (this.coinMapCache.has(normalized)) {
      return this.coinMapCache.get(normalized)!;
    }

    // Check if it's already a valid CoinGecko ID (contains hyphens or is all lowercase with original case)
    if (normalized.includes('-') || normalized === tickerOrId) {
      // Assume it's a CoinGecko ID, cache it
      this.coinMapCache.set(normalized, normalized);
      return normalized;
    }

    // Load coin list if needed
    if (!this.coinListLoadedAt ||
        Date.now() - this.coinListLoadedAt.getTime() > this.coinListCacheTTL) {
      await this.loadCoinList();
    }

    // Try to find in cache after loading
    if (this.coinMapCache.has(normalized)) {
      return this.coinMapCache.get(normalized)!;
    }

    // Not found - return as-is and let API handle the error
    this.logger.warn(`Could not resolve ticker ${tickerOrId} to CoinGecko ID, using as-is`);
    return normalized;
  }

  /**
   * Load complete coin list from CoinGecko API
   */
  private async loadCoinList(): Promise<void> {
    try {
      const url = `${this.baseUrl}/coins/list`;

      await this.rateLimiter.waitForSlot();

      const response = await plugins.smartrequest.SmartRequest.create()
        .url(url)
        .headers(this.buildHeaders())
        .timeout(10000)
        .get();

      const coinList = await response.json() as ICoinListItem[];

      // Clear cache before rebuilding to prevent memory leak
      // Keep only entries that are in priorityTickerMap
      const priorityEntries = new Map<string, string>();
      for (const [key, value] of this.priorityTickerMap) {
        priorityEntries.set(key, value);
      }
      this.coinMapCache.clear();

      // Restore priority mappings
      for (const [key, value] of priorityEntries) {
        this.coinMapCache.set(key, value);
      }

      // Build mapping: symbol -> id
      for (const coin of coinList) {
        const symbol = coin.symbol.toLowerCase();
        const id = coin.id.toLowerCase();

        // Don't overwrite priority mappings
        if (!this.priorityTickerMap.has(symbol)) {
          this.coinMapCache.set(symbol, id);
        }
        // Always cache the ID mapping (id -> id for when users pass CoinGecko IDs directly)
        this.coinMapCache.set(id, id);
      }

      this.coinListLoadedAt = new Date();
      this.logger.info(`Loaded ${coinList.length} coins from CoinGecko (cache: ${this.coinMapCache.size} entries)`);
    } catch (error) {
      this.logger.error('Failed to load coin list from CoinGecko:', error);
      // Don't throw - we can still work with direct IDs
    }
  }

  /**
   * Map CoinGecko simple/price response to IStockPrice
   */
  private mapToStockPrice(
    ticker: string,
    coinId: string,
    data: any,
    dataType: 'live' | 'eod' | 'intraday'
  ): IStockPrice {
    const price = data.usd;
    const change24h = data.usd_24h_change || 0;

    // Calculate previous close from 24h change
    const changePercent = change24h;
    const change = (price * changePercent) / 100;
    const previousClose = price - change;

    // Parse last updated timestamp
    const timestamp = data.last_updated_at
      ? new Date(data.last_updated_at * 1000)
      : new Date();

    return {
      ticker: ticker.toUpperCase(),
      price: price,
      currency: 'USD',
      change: change,
      changePercent: changePercent,
      previousClose: previousClose,
      timestamp: timestamp,
      provider: this.name,
      marketState: 'REGULAR', // Crypto markets are 24/7

      // Volume and market cap
      volume: data.usd_24h_vol,

      dataType: dataType,
      fetchedAt: new Date(),

      // Company identification (use coin name)
      companyName: coinId.charAt(0).toUpperCase() + coinId.slice(1),
      companyFullName: `${coinId.charAt(0).toUpperCase() + coinId.slice(1)} (${ticker.toUpperCase()})`
    };
  }

  /**
   * Build HTTP headers with optional API key
   */
  private buildHeaders(): Record<string, string> {
    const headers: Record<string, string> = {
      'Accept': 'application/json'
    };

    if (this.apiKey) {
      // Use Demo or Pro API key header
      // CoinGecko accepts both x-cg-demo-api-key and x-cg-pro-api-key
      headers['x-cg-demo-api-key'] = this.apiKey;
    }

    return headers;
  }

  /**
   * Check if response indicates a rate limit error (429)
   */
  private isRateLimitError(responseData: any): boolean {
    return responseData?.status?.error_code === 429;
  }

  /**
   * Wrapper for fetch operations with automatic rate limit retry and exponential backoff
   */
  private async fetchWithRateLimitRetry<T>(
    fetchFn: () => Promise<T>,
    operationName: string,
    maxRetries: number = 3
  ): Promise<T> {
    let lastError: Error | undefined;

    for (let attempt = 0; attempt < maxRetries; attempt++) {
      try {
        const result = await fetchFn();
        this.rateLimiter.resetErrors();
        return result;
      } catch (error) {
        lastError = error as Error;

        if (error instanceof RateLimitError) {
          const attemptInfo = `${attempt + 1}/${maxRetries}`;
          this.logger.warn(
            `Rate limit hit for ${operationName}, waiting ${error.waitTime}ms before retry ${attemptInfo}`
          );

          if (attempt < maxRetries - 1) {
            await plugins.smartdelay.delayFor(error.waitTime);
            continue;
          } else {
            this.logger.error(`Max retries (${maxRetries}) exceeded for ${operationName} due to rate limiting`);
            throw error;
          }
        }

        // Non-rate-limit errors: throw immediately
        throw error;
      }
    }

    throw lastError!;
  }
}
