/* eslint-disable no-await-in-loop */
import BigNumber from 'bignumber.js';
import { ExchangeRateProvider } from '../../store/models/exchange-rate-provider';
import logger from '../logger';
import { events } from '../event';
import type { ExchangeRateProviderSnapshot, ExchangeRateResult, IExchangeRateProvider } from './types';
import { SymbolNotSupportedError } from './types';
import { TokenDataProvider } from './token-data-provider';
import { CoinGeckoProvider } from './coingecko-provider';
import { CoinMarketCapProvider } from './coinmarketcap-provider';
import { exchangeRateCacheTTLSeconds } from '../env';

interface CacheEntry {
  data: ExchangeRateResult;
  timestamp: number;
}

// Cache TTL from environment variable, default 10 minutes
const CACHE_TTL_MS = (exchangeRateCacheTTLSeconds || 10 * 60) * 1000;
const MAX_RATE_AGE_MS = 5 * 60 * 1000; // 5 minutes
const MAX_DEVIATION_PERCENT = 5; // 5%
const HISTORY_WINDOW_SIZE = 10;
const MAX_PROVIDER_SPREAD_PERCENT = 5; // 5%
/**
 * Check if an error is a rate limit error (for CoinGecko)
 * Detects rate limit by checking for "rate limit" text or "429" status code
 */
export function isRateLimitError(error: Error): boolean {
  const message = (error.message || '').toLowerCase();
  return message.includes('rate limit') || message.includes('429');
}

const formatRate = (value: BigNumber) => {
  const decimals = value.decimalPlaces();
  return value.toFixed(decimals === null ? 0 : decimals);
};

const getMedianRate = (rates: string[]): string => {
  const sorted = rates
    .map((rate) => new BigNumber(rate))
    .filter((rate) => rate.isFinite())
    .sort((a, b) => a.comparedTo(b) ?? 0);
  if (sorted.length === 0) {
    throw new Error('No rates available for median calculation');
  }
  const mid = Math.floor(sorted.length / 2);
  if (sorted.length % 2 === 1) {
    return formatRate(sorted[mid]!);
  }
  const average = sorted[mid - 1]!.plus(sorted[mid]!).div(2);
  return formatRate(average);
};

const getRateSpreadPercent = (rates: string[], medianRate: string): number => {
  const numericRates = rates.map((rate) => Number(rate)).filter((rate) => Number.isFinite(rate));
  const median = Number(medianRate);
  if (!numericRates.length || !Number.isFinite(median) || median <= 0) {
    return 0;
  }
  const max = Math.max(...numericRates);
  const min = Math.min(...numericRates);
  return (Math.abs(max - min) / median) * 100;
};

/**
 * Generate human-readable provider display string
 * - Single source: "CoinGecko"
 * - Multiple sources: "CoinGecko (2 sources)"
 */
const getProviderDisplay = (providers?: ExchangeRateProviderSnapshot[], fallbackName?: string): string => {
  if (!providers?.length) {
    return fallbackName || '—';
  }

  // Get first provider name as primary
  const primaryProvider = providers[0]?.provider_name;
  if (!primaryProvider) {
    return fallbackName || '—';
  }

  if (providers.length === 1) {
    return primaryProvider;
  }

  return `${primaryProvider} (${providers.length} sources)`;
};

export class ExchangeRateService {
  private cache: Map<string, CacheEntry> = new Map();
  private rateHistory: Map<string, string[]> = new Map();
  private providerInstances: Map<string, IExchangeRateProvider> = new Map();

  // CoinGecko rate limit state
  private coingeckoRateLimitedUntil: Date | null = null;
  private coingeckoRetryCount: number = 0;
  private readonly RATE_LIMIT_INITIAL_DELAY_MS = 10 * 60 * 1000; // 10 minutes
  private readonly RATE_LIMIT_MAX_RETRIES = 5;
  private readonly RATE_LIMIT_BACKOFF_MULTIPLIER = 2;

  /**
   * Check if CoinGecko provider should be skipped due to rate limiting
   * Returns true if:
   * - Currently in rate limit cooldown period
   * - Max retries exceeded (provider disabled)
   */
  private shouldSkipCoinGecko(): boolean {
    // Check if max retries exceeded (provider disabled)
    if (this.coingeckoRetryCount >= this.RATE_LIMIT_MAX_RETRIES) {
      return true;
    }

    // Check if in rate limit cooldown period
    if (this.coingeckoRateLimitedUntil) {
      if (new Date() < this.coingeckoRateLimitedUntil) {
        return true;
      }
      // Cooldown expired, clear the rate limit state (retry will happen)
      this.coingeckoRateLimitedUntil = null;
    }

    return false;
  }

  /**
   * Handle CoinGecko rate limit error
   * Implements exponential backoff: 10min -> 20min -> 40min -> 80min -> 160min
   * After max retries, provider is disabled
   */
  private handleCoinGeckoRateLimit(): void {
    // Don't increment beyond max
    if (this.coingeckoRetryCount >= this.RATE_LIMIT_MAX_RETRIES) {
      logger.warn('CoinGecko provider disabled due to max rate limit retries', {
        retryCount: this.coingeckoRetryCount,
        maxRetries: this.RATE_LIMIT_MAX_RETRIES,
      });
      return;
    }

    // Increment retry count
    this.coingeckoRetryCount += 1;

    // Calculate backoff delay: initialDelay * (multiplier ^ (retryCount - 1))
    // Retry 1: 10min, Retry 2: 20min, Retry 3: 40min, Retry 4: 80min, Retry 5: 160min
    const backoffMultiplier = this.RATE_LIMIT_BACKOFF_MULTIPLIER ** (this.coingeckoRetryCount - 1);
    const delayMs = this.RATE_LIMIT_INITIAL_DELAY_MS * backoffMultiplier;

    this.coingeckoRateLimitedUntil = new Date(Date.now() + delayMs);

    logger.warn('CoinGecko rate limit detected, scheduling retry', {
      retryCount: this.coingeckoRetryCount,
      maxRetries: this.RATE_LIMIT_MAX_RETRIES,
      delayMinutes: delayMs / 60000,
      rateLimitedUntil: this.coingeckoRateLimitedUntil.toISOString(),
    });
  }

  /**
   * Reset CoinGecko rate limit state after successful request
   * Called when CoinGecko returns a successful response
   */
  private resetCoinGeckoRateLimitState(): void {
    if (this.coingeckoRetryCount > 0 || this.coingeckoRateLimitedUntil) {
      logger.info('CoinGecko rate limit state reset after successful request', {
        previousRetryCount: this.coingeckoRetryCount,
      });
    }
    this.coingeckoRetryCount = 0;
    this.coingeckoRateLimitedUntil = null;
  }

  /**
   * Check if provider is CoinGecko type
   */
  private isCoinGeckoProvider(provider: { type?: string }): boolean {
    return provider.type === 'coingecko';
  }

  /**
   * Get exchange rate for a given symbol
   * @param symbol - Token symbol (e.g., 'ABT', 'ETH')
   * @returns Exchange rate result with provider info
   */
  async getRate(symbol: string): Promise<ExchangeRateResult> {
    const cacheKey = symbol.toUpperCase();

    // Check cache first
    const cached = this.cache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
      logger.debug('Exchange rate cache hit', { symbol, age: Date.now() - cached.timestamp });
      return cached.data;
    }

    // Load active providers from database
    const allProviders = await ExchangeRateProvider.getActiveProviders();
    if (allProviders.length === 0) {
      throw new Error('No active exchange rate providers available');
    }

    // Filter out CoinGecko if rate limited
    const shouldSkipCG = this.shouldSkipCoinGecko();
    const providers = allProviders.filter((provider) => {
      if (this.isCoinGeckoProvider(provider) && shouldSkipCG) {
        logger.info('Skipping CoinGecko provider due to rate limit', {
          rateLimitedUntil: this.coingeckoRateLimitedUntil?.toISOString(),
          retryCount: this.coingeckoRetryCount,
        });
        return false;
      }
      return true;
    });

    if (providers.length === 0) {
      throw new Error('No active exchange rate providers available (all providers rate limited)');
    }

    // Fetch all providers and compute consensus (median)
    let lastError: Error | null = null;
    const results = await Promise.all(
      providers.map(async (provider) => {
        try {
          const result = await this.fetchWithProvider(provider, symbol);

          // Reset CoinGecko rate limit state on success
          if (this.isCoinGeckoProvider(provider)) {
            this.resetCoinGeckoRateLimitState();
          }

          return { ok: true as const, result };
        } catch (error: any) {
          lastError = error;

          // Handle CoinGecko rate limit errors specially
          if (this.isCoinGeckoProvider(provider) && isRateLimitError(error)) {
            this.handleCoinGeckoRateLimit();
            logger.warn('CoinGecko rate limit error handled', {
              provider: provider.name,
              symbol,
              error: error.message,
              nextRetryAt: this.coingeckoRateLimitedUntil?.toISOString(),
            });
            return { ok: false as const, error };
          }

          // SymbolNotSupportedError is a data issue, not a service issue
          // Don't record it as a provider failure to avoid pausing healthy providers
          if (error instanceof SymbolNotSupportedError) {
            logger.info('Symbol not supported by provider', {
              provider: provider.name,
              symbol,
              error: error.message,
            });
          } else {
            logger.warn('Provider failed during consensus fetch', {
              provider: provider.name,
              symbol,
              error: error.message,
            });
            // Only record failure for actual service errors
            provider.recordFailure(error.message).catch((err) => {
              logger.error('Failed to record provider failure', { error: err.message });
            });
          }
          return { ok: false as const, error };
        }
      })
    );

    const successResults = results.filter((item) => item.ok).map((item) => item.result);
    if (successResults.length === 0) {
      const errorMessage =
        lastError && typeof lastError === 'object' && 'message' in lastError
          ? String((lastError as any).message)
          : 'All providers failed';

      // Alert admin that all providers are unavailable
      this.emitProvidersUnavailableAlert(symbol, errorMessage);

      throw new Error(`Failed to fetch exchange rate for ${symbol}: ${errorMessage}`);
    }

    const providerSnapshots: ExchangeRateProviderSnapshot[] = successResults.map((result) => ({
      provider_id: result.provider_id,
      provider_name: result.provider_name,
      rate: result.rate,
      timestamp_ms: result.timestamp_ms,
      degraded: result.degraded,
      degraded_reason: result.degraded_reason,
    }));

    const consensusRate = getMedianRate(providerSnapshots.map((p) => p.rate));
    const consensusTimestamp = Math.max(...providerSnapshots.map((p) => p.timestamp_ms));
    const spreadPercent = getRateSpreadPercent(
      providerSnapshots.map((p) => p.rate),
      consensusRate
    );

    const degradedReasons: string[] = [];
    if (providers.length >= 2 && providerSnapshots.length < 2) {
      degradedReasons.push(`Only ${providerSnapshots.length} provider returned a rate`);
    }
    if (providerSnapshots.length < providers.length) {
      degradedReasons.push(`Some providers failed (${providerSnapshots.length}/${providers.length})`);
    }
    if (providerSnapshots.some((snapshot) => snapshot.degraded)) {
      degradedReasons.push('One or more providers are degraded');
    }
    if (spreadPercent > MAX_PROVIDER_SPREAD_PERCENT) {
      degradedReasons.push(`Rate spread ${spreadPercent.toFixed(2)}% exceeds threshold`);

      // Emit alert for high rate spread between providers
      this.emitRateSpreadAlert(symbol, spreadPercent, providerSnapshots);
    }

    logger.info('Exchange rate consensus computed', {
      symbol,
      providers_total: providers.length,
      providers_success: providerSnapshots.length,
      consensus_rate: consensusRate,
      spread_percent: spreadPercent.toFixed(2),
      degraded: degradedReasons.length > 0,
      degraded_reason: degradedReasons.join('; ') || null,
    });

    const fetchedAt = Date.now();
    const consensusResult: ExchangeRateResult = {
      rate: consensusRate,
      timestamp_ms: consensusTimestamp,
      fetched_at: fetchedAt,
      provider_id: 'consensus',
      provider_name: 'median',
      provider_display: getProviderDisplay(providerSnapshots),
      degraded: degradedReasons.length > 0,
      degraded_reason: degradedReasons.length > 0 ? degradedReasons.join('; ') : undefined,
      providers: providerSnapshots,
      consensus_method: 'median',
    };

    this.cache.set(cacheKey, {
      data: consensusResult,
      timestamp: fetchedAt,
    });

    return consensusResult;
  }

  /**
   * Fetch rate from a specific provider with validation
   */
  private async fetchWithProvider(providerModel: ExchangeRateProvider, symbol: string): Promise<ExchangeRateResult> {
    // Get provider instance
    let provider = this.providerInstances.get(providerModel.name);
    if (!provider) {
      // Decrypt sensitive config data (API keys) before creating provider instance
      const decryptedConfig = ExchangeRateProvider.decryptConfig(providerModel.config || {}) || {};
      const type = providerModel.type || 'token-data';
      switch (type) {
        case 'token-data':
          provider = new TokenDataProvider(decryptedConfig);
          break;
        case 'coingecko':
          provider = new CoinGeckoProvider(decryptedConfig);
          break;
        case 'coinmarketcap':
          provider = new CoinMarketCapProvider(decryptedConfig);
          break;
        default:
          logger.warn(`Unknown provider type: ${type}, falling back to token-data`);
          provider = new TokenDataProvider(decryptedConfig);
          break;
      }
      this.providerInstances.set(providerModel.name, provider);
    }

    // Fetch rate
    const rateData = await provider.fetch(symbol);

    // Validate the rate
    const validation = this.validateRate(symbol, rateData.rate, rateData.timestamp_ms, providerModel);

    // Record success in database (fire-and-forget to avoid SQLITE_BUSY)
    providerModel.recordSuccess().catch((err) => {
      logger.error('Failed to record provider success', { error: err.message });
    });

    return {
      ...rateData,
      provider_id: providerModel.id,
      provider_name: providerModel.name,
      provider_display: providerModel.name, // Single source: just the name
      degraded: validation.degraded,
      degraded_reason: validation.degraded_reason,
    };
  }

  /**
   * Validate exchange rate data
   */
  private validateRate(
    symbol: string,
    rateStr: string,
    timestampMs: number,
    providerModel?: ExchangeRateProvider
  ): { degraded: boolean; degraded_reason?: string } {
    const rate = parseFloat(rateStr);

    // 1. Check rate is positive and finite
    if (rate <= 0 || !Number.isFinite(rate)) {
      throw new Error(`Invalid rate value: ${rateStr}`);
    }

    // 2. Check timestamp freshness (≤ 5 minutes)
    const rateAge = Date.now() - timestampMs;
    if (rateAge > MAX_RATE_AGE_MS) {
      logger.warn('Rate timestamp is stale', {
        symbol,
        provider: providerModel?.name,
        provider_id: providerModel?.id,
        rate_age_sec: Math.floor(rateAge / 1000),
        max_age_sec: MAX_RATE_AGE_MS / 1000,
      });
      return {
        degraded: true,
        degraded_reason: `Rate is ${Math.floor(rateAge / 1000)}s old (max: ${MAX_RATE_AGE_MS / 1000}s)`,
      };
    }

    // 3. Check deviation from historical average
    const history = this.rateHistory.get(symbol) || [];
    if (history.length >= 3) {
      const historicalRates = history.map((r) => parseFloat(r));
      const sum = historicalRates.reduce((acc, r) => acc + r, 0);
      const avg = sum / history.length;
      const deviationPercent = (Math.abs(rate - avg) / avg) * 100;

      if (deviationPercent > MAX_DEVIATION_PERCENT) {
        logger.warn('Rate deviation exceeds threshold', {
          symbol,
          provider: providerModel?.name,
          provider_id: providerModel?.id,
          rate: rateStr,
          average: avg.toFixed(8),
          deviation: `${deviationPercent.toFixed(2)}%`,
        });

        return {
          degraded: true,
          degraded_reason: `Rate deviates ${deviationPercent.toFixed(2)}% from historical average`,
        };
      }
    }

    // Update history
    history.push(rateStr);
    if (history.length > HISTORY_WINDOW_SIZE) {
      history.shift();
    }
    this.rateHistory.set(symbol, history);

    return { degraded: false };
  }

  /**
   * Clear cache for a specific symbol or all symbols
   */
  clearCache(symbol?: string): void {
    if (symbol) {
      this.cache.delete(symbol.toUpperCase());
    } else {
      this.cache.clear();
    }
  }

  // Alert state to prevent duplicate alerts
  private rateSpreadAlertHistory: Map<string, number> = new Map();
  private providersUnavailableAlertHistory: Map<string, number> = new Map();
  private readonly ALERT_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes

  /**
   * Emit alert when rate spread between providers exceeds threshold
   * Rate-limited to prevent alert fatigue
   */
  private emitRateSpreadAlert(
    symbol: string,
    spreadPercent: number,
    providerSnapshots: ExchangeRateProviderSnapshot[]
  ): void {
    const alertKey = `spread-${symbol}`;
    const lastAlert = this.rateSpreadAlertHistory.get(alertKey);
    const now = Date.now();

    // Rate limit alerts
    if (lastAlert && now - lastAlert < this.ALERT_COOLDOWN_MS) {
      return;
    }

    this.rateSpreadAlertHistory.set(alertKey, now);

    const alertData = {
      severity: 'warning',
      type: 'rate_spread_exceeded',
      symbol,
      spreadPercent: spreadPercent.toFixed(2),
      threshold: MAX_PROVIDER_SPREAD_PERCENT,
      timestamp: new Date().toISOString(),
      providers: providerSnapshots.map((p) => ({
        id: p.provider_id,
        name: p.provider_name,
        rate: p.rate,
      })),
      impact: 'Exchange rate consensus may be less reliable',
      action: 'Review provider configurations and check for data discrepancies',
    };

    logger.warn('Exchange rate spread alert', alertData);

    // Emit internal event to trigger email notification to admin
    events.emit('exchange_rate.spread_exceeded', {
      alertType: 'spread_exceeded' as const,
      symbol,
      severity: 'warning',
      timestamp: new Date().toISOString(),
      spreadPercent: spreadPercent.toFixed(2),
      threshold: MAX_PROVIDER_SPREAD_PERCENT,
      providers: providerSnapshots.map((p) => ({
        id: p.provider_id,
        name: p.provider_name,
        rate: p.rate,
      })),
    });
  }

  /**
   * Emit alert when all exchange rate providers are unavailable
   * Rate-limited to prevent alert fatigue
   */
  private emitProvidersUnavailableAlert(symbol: string, errorMessage: string): void {
    const alertKey = `unavailable-${symbol}`;
    const lastAlert = this.providersUnavailableAlertHistory.get(alertKey);
    const now = Date.now();

    // Rate limit alerts
    if (lastAlert && now - lastAlert < this.ALERT_COOLDOWN_MS) {
      return;
    }

    this.providersUnavailableAlertHistory.set(alertKey, now);

    const alertData = {
      severity: 'critical',
      type: 'providers_unavailable',
      symbol,
      errorMessage,
      timestamp: new Date().toISOString(),
      impact: 'Users cannot make purchases requiring exchange rate conversion',
      action: 'Check provider configurations and network connectivity immediately',
    };

    logger.error('All exchange rate providers unavailable', alertData);

    // Emit internal event to trigger email notification to admin
    events.emit('exchange_rate.providers_unavailable', {
      alertType: 'providers_unavailable' as const,
      symbol,
      severity: 'critical',
      timestamp: new Date().toISOString(),
      providers: [],
    });
  }
}

// Singleton instance
let serviceInstance: ExchangeRateService | null = null;

export function getExchangeRateService(): ExchangeRateService {
  if (!serviceInstance) {
    serviceInstance = new ExchangeRateService();
    logger.info('Exchange rate service initialized', {
      cache_ttl_seconds: CACHE_TTL_MS / 1000,
      cache_ttl_source: process.env.EXCHANGE_RATE_CACHE_TTL_SECONDS ? 'env' : 'default',
    });
  }
  return serviceInstance;
}
