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';

/**
 * Marketstack API v2 Provider - Professional Plan with Intelligent Intraday Support
 * Documentation: https://docs.apilayer.com/marketstack/docs/marketstack-api-v2-v-2-0-0
 *
 * Features:
 * - Intelligent endpoint selection based on market hours
 * - Real-time intraday pricing with multiple intervals (1min, 5min, 10min, 15min, 30min, 1hour)
 * - End-of-Day (EOD) stock prices with historical data
 * - Market state detection (PRE, REGULAR, POST, CLOSED)
 * - Exchange filtering via MIC codes (XNAS, XNYS, XLON, etc.)
 * - Supports 500,000+ tickers across 72+ exchanges worldwide
 * - OHLCV data (Open, High, Low, Close, Volume)
 * - Pagination for large datasets
 * - Automatic fallback from intraday to EOD on errors
 * - Requires API key authentication
 *
 * Intelligent Endpoint Selection:
 * - During market hours (PRE/REGULAR/POST): Uses intraday endpoints for fresh data
 * - After market close (CLOSED): Uses EOD endpoints to save API credits
 * - Automatic fallback to EOD if intraday fails (rate limits, plan restrictions, etc.)
 *
 * Rate Limits:
 * - Free Plan: 100 requests/month (EOD only)
 * - Basic Plan: 10,000 requests/month (EOD only)
 * - Professional Plan: 100,000 requests/month (intraday + EOD)
 *
 * Intraday Access:
 * - Intervals below 15min (1min, 5min, 10min) require Professional Plan or higher
 * - Real-time data from IEX Exchange for US tickers
 * - Symbol formatting: Periods replaced with hyphens for intraday (BRK.B → BRK-B)
 */
export class MarketstackProvider implements IStockProvider {
  public name = 'Marketstack';
  public priority = 90; // Increased from 80 - now supports real-time intraday data during market hours
  public readonly requiresAuth = true;
  public readonly rateLimit = {
    requestsPerMinute: 0, // No per-minute limit specified
    requestsPerDay: undefined // Varies by plan
  };

  private logger = console;
  private baseUrl = 'https://api.marketstack.com/v2';
  private apiKey: string;

  constructor(apiKey: string, private config?: IProviderConfig) {
    if (!apiKey) {
      throw new Error('API key is required for Marketstack provider');
    }

    this.apiKey = apiKey;
  }

  /**
   * 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 'historical':
        return this.fetchHistoricalPrices(request);
      case 'intraday':
        return this.fetchIntradayPrices(request);
      case 'batch':
        return this.fetchBatchCurrentPrices(request);
      default:
        throw new Error(`Unsupported request type: ${(request as any).type}`);
    }
  }

  /**
   * Fetch current/latest price with intelligent endpoint selection
   * Uses intraday during market hours (PRE, REGULAR, POST) for fresh data
   * Uses EOD after market close (CLOSED) to save API credits
   */
  private async fetchCurrentPrice(request: IStockCurrentRequest): Promise<IStockPrice> {
    try {
      // Determine current market state
      const marketState = this.getUsMarketState();
      const useIntraday = this.shouldUseIntradayEndpoint(marketState);

      if (useIntraday) {
        // Use intraday endpoint for fresh data during market hours
        return await this.fetchCurrentPriceIntraday(request, marketState);
      } else {
        // Use EOD endpoint for after-close data
        return await this.fetchCurrentPriceEod(request);
      }
    } catch (error) {
      // If intraday fails, fallback to EOD with warning
      const errorMessage = plugins.getErrorMessage(error);
      if (errorMessage.includes('intraday') || errorMessage.includes('Marketstack API error')) {
        this.logger.warn(`Intraday endpoint failed for ${request.ticker}, falling back to EOD:`, errorMessage);
        try {
          return await this.fetchCurrentPriceEod(request);
        } catch (eodError) {
          // Both failed, throw original error
          throw error;
        }
      }
      throw error;
    }
  }

  /**
   * Fetch current price using intraday endpoint (during market hours)
   * Uses 1min interval for most recent data
   */
  private async fetchCurrentPriceIntraday(
    request: IStockCurrentRequest,
    marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED'
  ): Promise<IStockPrice> {
    const formattedSymbol = this.formatSymbolForIntraday(request.ticker);
    let url = `${this.baseUrl}/tickers/${formattedSymbol}/intraday/latest?access_key=${this.apiKey}`;
    url += `&interval=1min`; // Use 1min for most recent data

    // Add exchange filter if specified
    if (request.exchange) {
      url += `&exchange=${request.exchange}`;
    }

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

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

    // Check for API errors
    if (responseData.error) {
      throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
    }

    if (!responseData || !responseData.close) {
      throw new Error(`No intraday data found for ticker ${request.ticker}`);
    }

    return this.mapToStockPrice(responseData, 'intraday', marketState);
  }

  /**
   * Fetch current price using EOD endpoint (after market close)
   */
  private async fetchCurrentPriceEod(request: IStockCurrentRequest): Promise<IStockPrice> {
    let url = `${this.baseUrl}/tickers/${request.ticker}/eod/latest?access_key=${this.apiKey}`;

    // Add exchange filter if specified
    if (request.exchange) {
      url += `&exchange=${request.exchange}`;
    }

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

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

    // Check for API errors
    if (responseData.error) {
      throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
    }

    // For single ticker endpoint, response is direct object (not wrapped in data field)
    if (!responseData || !responseData.close) {
      throw new Error(`No data found for ticker ${request.ticker}`);
    }

    return this.mapToStockPrice(responseData, 'eod');
  }

  /**
   * Fetch historical EOD prices for a ticker with date range
   */
  private async fetchHistoricalPrices(request: IStockHistoricalRequest): Promise<IStockPrice[]> {
    try {
      const allPrices: IStockPrice[] = [];
      let offset = request.offset || 0;
      const limit = request.limit || 1000; // Max per page
      const maxRecords = this.config?.maxRecords || 10000; // Safety limit (configurable)

      while (true) {
        let url = `${this.baseUrl}/eod?access_key=${this.apiKey}`;
        url += `&symbols=${request.ticker}`;
        url += `&date_from=${this.formatDate(request.from)}`;
        url += `&date_to=${this.formatDate(request.to)}`;
        url += `&limit=${limit}`;
        url += `&offset=${offset}`;

        if (request.exchange) {
          url += `&exchange=${request.exchange}`;
        }

        if (request.sort) {
          url += `&sort=${request.sort}`;
        }

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

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

        // Check for API errors
        if (responseData.error) {
          throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
        }

        if (!responseData?.data || !Array.isArray(responseData.data)) {
          throw new Error('Invalid response format from Marketstack API');
        }

        // Map data to stock prices
        for (const data of responseData.data) {
          try {
            allPrices.push(this.mapToStockPrice(data, 'eod'));
          } catch (error) {
            this.logger.warn(`Failed to parse historical data for ${data.symbol}:`, error);
          }
        }

        // Check if we have more pages
        const pagination = responseData.pagination;
        const hasMore = pagination && offset + limit < pagination.total;

        // Safety check: don't fetch more than maxRecords
        if (!hasMore || allPrices.length >= maxRecords) {
          break;
        }

        offset += limit;
      }

      return allPrices;
    } catch (error) {
      this.logger.error(`Failed to fetch historical prices for ${request.ticker}:`, error);
      throw new Error(`Marketstack: Failed to fetch historical prices for ${request.ticker}: ${plugins.getErrorMessage(error)}`);
    }
  }

  /**
   * Fetch intraday prices with specified interval
   * Supports intervals: 1min, 5min, 10min, 15min, 30min, 1hour
   * Note: Intervals below 15min require Professional Plan or higher
   */
  private async fetchIntradayPrices(request: IStockIntradayRequest): Promise<IStockPrice[]> {
    try {
      const allPrices: IStockPrice[] = [];
      let offset = 0;
      const limit = 1000; // Max per page for intraday
      const maxRecords = this.config?.maxRecords || 10000; // Safety limit (configurable)

      // 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 && effectiveLimit > 1000) {
        this.logger.warn(
          `Intraday request for ${request.ticker} without explicit limit will fetch up to ${effectiveLimit} records. ` +
          `Consider adding a limit to the request for better performance.`
        );
      }

      // Format symbol for intraday endpoint (replace . with -)
      const formattedSymbol = this.formatSymbolForIntraday(request.ticker);

      while (true) {
        let url = `${this.baseUrl}/tickers/${formattedSymbol}/intraday?access_key=${this.apiKey}`;
        url += `&interval=${request.interval}`;
        url += `&limit=${limit}`;
        url += `&offset=${offset}`;

        // Add date filter if specified
        if (request.date) {
          url += `&date_from=${this.formatDate(request.date)}`;
          url += `&date_to=${this.formatDate(request.date)}`;
        }

        // Add exchange filter if specified
        if (request.exchange) {
          url += `&exchange=${request.exchange}`;
        }

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

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

        // Check for API errors
        if (responseData.error) {
          throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
        }

        if (!responseData?.data || !Array.isArray(responseData.data)) {
          throw new Error('Invalid response format from Marketstack API');
        }

        // Map data to stock prices
        for (const data of responseData.data) {
          try {
            allPrices.push(this.mapToStockPrice(data, 'intraday'));
          } catch (error) {
            this.logger.warn(`Failed to parse intraday data for ${data.symbol}:`, error);
          }
        }

        // Check if we have more pages
        const pagination = responseData.pagination;
        const hasMore = pagination && offset + limit < pagination.total;

        // Honor effective limit or safety maxRecords
        if (!hasMore || allPrices.length >= effectiveLimit || allPrices.length >= maxRecords) {
          break;
        }

        offset += limit;
      }

      // Apply effective limit
      if (allPrices.length > effectiveLimit) {
        return allPrices.slice(0, effectiveLimit);
      }

      return allPrices;
    } catch (error) {
      this.logger.error(`Failed to fetch intraday prices for ${request.ticker}:`, error);
      throw new Error(`Marketstack: Failed to fetch intraday prices for ${request.ticker}: ${plugins.getErrorMessage(error)}`);
    }
  }

  /**
   * Fetch current prices for multiple tickers with intelligent endpoint selection
   * Uses intraday during market hours (PRE, REGULAR, POST) for fresh data
   * Uses EOD after market close (CLOSED) to save API credits
   */
  private async fetchBatchCurrentPrices(request: IStockBatchCurrentRequest): Promise<IStockPrice[]> {
    try {
      // Determine current market state
      const marketState = this.getUsMarketState();
      const useIntraday = this.shouldUseIntradayEndpoint(marketState);

      if (useIntraday) {
        return await this.fetchBatchCurrentPricesIntraday(request, marketState);
      } else {
        return await this.fetchBatchCurrentPricesEod(request);
      }
    } catch (error) {
      // Fallback to EOD if intraday fails
      const errorMessage = plugins.getErrorMessage(error);
      if (errorMessage.includes('intraday') || errorMessage.includes('Marketstack API error')) {
        this.logger.warn(`Intraday batch endpoint failed, falling back to EOD:`, errorMessage);
        try {
          return await this.fetchBatchCurrentPricesEod(request);
        } catch (eodError) {
          // Both failed, throw original error
          throw error;
        }
      }
      throw error;
    }
  }

  /**
   * Fetch batch current prices using intraday endpoint
   */
  private async fetchBatchCurrentPricesIntraday(
    request: IStockBatchCurrentRequest,
    marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED'
  ): Promise<IStockPrice[]> {
    // Format symbols for intraday (replace . with -)
    const formattedSymbols = request.tickers.map(t => this.formatSymbolForIntraday(t)).join(',');

    let url = `${this.baseUrl}/intraday/latest?access_key=${this.apiKey}`;
    url += `&symbols=${formattedSymbols}`;
    url += `&interval=1min`; // Use 1min for most recent data

    if (request.exchange) {
      url += `&exchange=${request.exchange}`;
    }

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

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

    if (responseData.error) {
      throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
    }

    if (!responseData?.data || !Array.isArray(responseData.data)) {
      throw new Error('Invalid response format from Marketstack API');
    }

    const prices: IStockPrice[] = [];

    for (const data of responseData.data) {
      try {
        prices.push(this.mapToStockPrice(data, 'intraday', marketState));
      } catch (error) {
        this.logger.warn(`Failed to parse intraday data for ${data.symbol}:`, error);
      }
    }

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

    return prices;
  }

  /**
   * Fetch batch current prices using EOD endpoint
   */
  private async fetchBatchCurrentPricesEod(request: IStockBatchCurrentRequest): Promise<IStockPrice[]> {
    const symbols = request.tickers.join(',');
    let url = `${this.baseUrl}/eod/latest?access_key=${this.apiKey}&symbols=${symbols}`;

    if (request.exchange) {
      url += `&exchange=${request.exchange}`;
    }

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

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

    // Check for API errors
    if (responseData.error) {
      throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
    }

    if (!responseData?.data || !Array.isArray(responseData.data)) {
      throw new Error('Invalid response format from Marketstack API');
    }

    const prices: IStockPrice[] = [];

    for (const data of responseData.data) {
      try {
        prices.push(this.mapToStockPrice(data, 'eod'));
      } catch (error) {
        this.logger.warn(`Failed to parse data for ${data.symbol}:`, error);
        // Continue processing other tickers
      }
    }

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

    return prices;
  }

  /**
   * Check if the Marketstack API is available and accessible
   */
  public async isAvailable(): Promise<boolean> {
    try {
      // Test with a well-known ticker
      const url = `${this.baseUrl}/tickers/AAPL/eod/latest?access_key=${this.apiKey}`;

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

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

      // Check if we got valid data (not an error)
      // Single ticker endpoint returns direct object, not wrapped in data field
      return !responseData.error && responseData.close !== undefined;
    } catch (error) {
      this.logger.warn('Marketstack provider is not available:', error);
      return false;
    }
  }

  /**
   * Check if a market is supported
   * Marketstack supports 72+ exchanges worldwide
   */
  public supportsMarket(market: string): boolean {
    // Marketstack has broad international coverage including:
    // US, UK, DE, FR, JP, CN, HK, AU, CA, IN, etc.
    const supportedMarkets = [
      'US', 'UK', 'GB', 'DE', 'FR', 'JP', 'CN', 'HK', 'AU', 'CA',
      'IN', 'BR', 'MX', 'IT', 'ES', 'NL', 'SE', 'CH', 'NO', 'DK'
    ];
    return supportedMarkets.includes(market.toUpperCase());
  }

  /**
   * Check if a ticker format is supported
   */
  public supportsTicker(ticker: string): boolean {
    // Basic validation - Marketstack supports most standard ticker formats
    return /^[A-Z0-9\.\-]{1,10}$/.test(ticker.toUpperCase());
  }

  /**
   * Map Marketstack API response to IStockPrice interface
   * @param data - API response data
   * @param dataType - Type of data (eod, intraday, live)
   * @param explicitMarketState - Override market state (used for intraday data fetched during known market hours)
   */
  private mapToStockPrice(
    data: any,
    dataType: 'eod' | 'intraday' | 'live' = 'eod',
    explicitMarketState?: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED'
  ): IStockPrice {
    if (!data.close) {
      throw new Error('Missing required price data');
    }

    // Calculate change and change percent
    // EOD data: previous close is typically open price of the same day
    // For better accuracy, we'd need previous day's close, but that requires another API call
    const currentPrice = data.close;
    const previousClose = data.open || currentPrice;
    const change = currentPrice - previousClose;
    const changePercent = previousClose !== 0 ? (change / previousClose) * 100 : 0;

    // Parse timestamp
    const timestamp = data.date ? new Date(data.date) : new Date();
    const fetchedAt = new Date();

    // Determine market state intelligently
    let marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED';

    if (explicitMarketState) {
      // Use provided market state (for intraday data fetched during known market hours)
      marketState = explicitMarketState;
    } else if (dataType === 'eod') {
      // EOD data is always for closed markets
      marketState = 'CLOSED';
    } else if (dataType === 'intraday') {
      // For intraday data without explicit state, determine from timestamp
      marketState = this.getUsMarketState(timestamp.getTime());
    } else {
      // Default fallback
      marketState = 'CLOSED';
    }

    const stockPrice: IStockPrice = {
      ticker: data.symbol.toUpperCase(),
      price: currentPrice,
      currency: data.price_currency || 'USD',
      change: change,
      changePercent: changePercent,
      previousClose: previousClose,
      timestamp: timestamp,
      provider: this.name,
      marketState: marketState, // Now dynamic based on data type, timestamp, and explicit state
      exchange: data.exchange,
      exchangeName: data.exchange_code || data.name,

      // Phase 1 enhancements: OHLCV data
      volume: data.volume,
      open: data.open,
      high: data.high,
      low: data.low,
      adjusted: data.adj_close !== undefined, // If adj_close exists, price is adjusted
      dataType: dataType,
      fetchedAt: fetchedAt,

      // Company identification
      companyName: data.company_name || data.name || undefined,
      companyFullName: this.buildCompanyFullName(data)
    };

    return stockPrice;
  }

  /**
   * Build full company name with exchange and ticker information
   * Example: "Apple Inc (NASDAQ:AAPL)"
   */
  private buildCompanyFullName(data: any): string | undefined {
    // Check if API already provides full name
    if (data.full_name || data.long_name) {
      return data.full_name || data.long_name;
    }

    // Build from available data
    const companyName = data.company_name || data.name;
    const exchangeCode = data.exchange_code; // e.g., "NASDAQ"
    const symbol = data.symbol; // e.g., "AAPL"

    if (!companyName) {
      return undefined;
    }

    // If we have exchange and symbol, build full name: "Apple Inc (NASDAQ:AAPL)"
    if (exchangeCode && symbol) {
      return `${companyName} (${exchangeCode}:${symbol})`;
    }

    // If we only have symbol: "Apple Inc (AAPL)"
    if (symbol) {
      return `${companyName} (${symbol})`;
    }

    // Otherwise just return company name
    return companyName;
  }

  /**
   * Format date to YYYY-MM-DD for API requests
   */
  private formatDate(date: Date): string {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  }

  /**
   * Get US market state based on Eastern Time
   * Regular hours: 9:30 AM - 4:00 PM ET
   * Pre-market: 4:00 AM - 9:30 AM ET
   * After-hours: 4:00 PM - 8:00 PM ET
   *
   * @param timestampMs - Optional timestamp in milliseconds (defaults to current time)
   * @returns Market state: PRE, REGULAR, POST, or CLOSED
   */
  private getUsMarketState(timestampMs?: number): 'PRE' | 'REGULAR' | 'POST' | 'CLOSED' {
    const now = timestampMs ? new Date(timestampMs) : new Date();

    // Convert to ET (UTC-5 or UTC-4 depending on DST)
    // For simplicity, we'll use a rough approximation
    // TODO: Add proper timezone library for production use
    const etOffset = -5; // Standard time, adjust for DST if needed
    const etTime = new Date(now.getTime() + (etOffset * 60 * 60 * 1000));

    // Get day of week (0 = Sunday, 6 = Saturday)
    const dayOfWeek = etTime.getUTCDay();

    // Check if weekend
    if (dayOfWeek === 0 || dayOfWeek === 6) {
      return 'CLOSED';
    }

    // Get hour and minute in ET
    const hours = etTime.getUTCHours();
    const minutes = etTime.getUTCMinutes();
    const timeInMinutes = hours * 60 + minutes;

    // Define market hours in minutes
    const preMarketStart = 4 * 60; // 4:00 AM
    const regularMarketStart = 9 * 60 + 30; // 9:30 AM
    const regularMarketEnd = 16 * 60; // 4:00 PM
    const afterHoursEnd = 20 * 60; // 8:00 PM

    if (timeInMinutes >= preMarketStart && timeInMinutes < regularMarketStart) {
      return 'PRE';
    } else if (timeInMinutes >= regularMarketStart && timeInMinutes < regularMarketEnd) {
      return 'REGULAR';
    } else if (timeInMinutes >= regularMarketEnd && timeInMinutes < afterHoursEnd) {
      return 'POST';
    } else {
      return 'CLOSED';
    }
  }

  /**
   * Determine if intraday endpoint should be used based on market state
   * Uses intraday for PRE, REGULAR, and POST market states
   * Uses EOD for CLOSED state to save API credits
   *
   * @param marketState - Current market state
   * @returns true if intraday endpoint should be used
   */
  private shouldUseIntradayEndpoint(marketState: 'PRE' | 'REGULAR' | 'POST' | 'CLOSED'): boolean {
    return marketState !== 'CLOSED';
  }

  /**
   * Format ticker symbol for intraday endpoints
   * Marketstack intraday API requires periods to be replaced with hyphens
   * Example: BRK.B → BRK-B
   *
   * @param symbol - Original ticker symbol
   * @returns Formatted symbol for intraday endpoints
   */
  private formatSymbolForIntraday(symbol: string): string {
    return symbol.replace(/\./g, '-');
  }
}
