class Provider::Eodhd < Provider
  include SecurityConcept, RateLimitable
  extend SslConfigurable

  # Subclass so errors caught in this provider are raised as Provider::Eodhd::Error
  Error = Class.new(Provider::Error)
  InvalidSecurityPriceError = Class.new(Error)
  RateLimitError = Class.new(Error)

  # Minimum delay between requests to avoid rate limiting (in seconds)
  MIN_REQUEST_INTERVAL = 0.5

  # Maximum API calls per day (EODHD free/basic plans are very restrictive)
  MAX_REQUESTS_PER_DAY = 20

  # EODHD free tier provides ~1 year of EOD data
  def max_history_days
    365
  end

  # EODHD uses {SYMBOL}.{EXCHANGE} ticker format with its own exchange codes
  MIC_TO_EODHD_EXCHANGE = {
    "XNYS" => "US", "XNAS" => "US", "XASE" => "US",
    "XLON" => "LSE",
    "XETR" => "XETRA",
    "XTSE" => "TO",
    "XPAR" => "PA",
    "XAMS" => "AS",
    "XSWX" => "SW",
    "XHKG" => "HK",
    "XASX" => "AU",
    "XTKS" => "TSE",
    "XMIL" => "MI",
    "XMAD" => "MC",
    "XOSL" => "OL",
    "XHEL" => "HE",
    "XCSE" => "CO",
    "XSTO" => "ST",
    "XKRX" => "KS",
    "XBOM" => "BSE",
    "XNSE" => "NSE"
  }.freeze

  EODHD_EXCHANGE_TO_MIC = {
    "US" => "XNYS", "LSE" => "XLON", "XETRA" => "XETR",
    "TO" => "XTSE", "PA" => "XPAR", "AS" => "XAMS",
    "SW" => "XSWX", "HK" => "XHKG", "AU" => "XASX",
    "TSE" => "XTKS", "MI" => "XMIL", "MC" => "XMAD",
    "OL" => "XOSL", "HE" => "XHEL", "CO" => "XCSE",
    "ST" => "XSTO", "KS" => "XKRX", "BSE" => "XBOM",
    "NSE" => "XNSE"
  }.freeze

  EODHD_COUNTRY_TO_CODE = {
    "USA" => "US", "UK" => "GB", "Germany" => "DE", "France" => "FR",
    "Netherlands" => "NL", "Switzerland" => "CH", "Canada" => "CA",
    "Japan" => "JP", "Australia" => "AU", "Hong Kong" => "HK",
    "Italy" => "IT", "Spain" => "ES", "Norway" => "NO",
    "Finland" => "FI", "Denmark" => "DK", "Sweden" => "SE",
    "South Korea" => "KR", "India" => "IN"
  }.freeze

  EXCHANGE_CURRENCY = {
    "US" => "USD", "LSE" => "GBP", "XETRA" => "EUR", "TO" => "CAD",
    "PA" => "EUR", "AS" => "EUR", "SW" => "CHF", "HK" => "HKD",
    "AU" => "AUD", "TSE" => "JPY", "MI" => "EUR", "MC" => "EUR",
    "OL" => "NOK", "HE" => "EUR", "CO" => "DKK",
    "ST" => "SEK", "KS" => "KRW", "BSE" => "INR",
    "NSE" => "INR"
  }.freeze

  def initialize(api_key)
    @api_key = api_key # pipelock:ignore
  end

  def healthy?
    with_provider_response do
      response = client.get("#{base_url}/api/user") do |req|
        req.params["api_token"] = api_key
        req.params["fmt"] = "json"
      end

      JSON.parse(response.body).dig("name").present?
    end
  end

  def usage
    with_provider_response do
      response = client.get("#{base_url}/api/user") do |req|
        req.params["api_token"] = api_key
        req.params["fmt"] = "json"
      end

      parsed = JSON.parse(response.body)

      limit = parsed.dig("apiRequests").to_i
      daily_limit = parsed.dig("dailyRateLimit").to_i

      daily_key = daily_cache_key
      used = Rails.cache.read(daily_key).to_i

      UsageData.new(
        used: used,
        limit: daily_limit > 0 ? daily_limit : MAX_REQUESTS_PER_DAY,
        utilization: daily_limit > 0 ? (used.to_f / daily_limit * 100) : (used.to_f / MAX_REQUESTS_PER_DAY * 100),
        plan: parsed.dig("subscriptionType") || "unknown"
      )
    end
  end

  # ================================
  #           Securities
  # ================================

  def search_securities(symbol, country_code: nil, exchange_operating_mic: nil)
    with_provider_response do
      enforce_daily_limit!
      throttle_request

      response = client.get("#{base_url}/api/search/#{CGI.escape(symbol)}") do |req|
        req.params["api_token"] = api_key
        req.params["fmt"] = "json"
      end

      parsed = JSON.parse(response.body)
      check_api_error!(parsed)

      unless parsed.is_a?(Array)
        raise Error, "Unexpected response format from search API"
      end

      parsed.first(25).map do |security|
        eodhd_exchange = security.dig("Exchange")
        mic = EODHD_EXCHANGE_TO_MIC[eodhd_exchange]
        country = EODHD_COUNTRY_TO_CODE[security.dig("Country")]
        code = security.dig("Code")
        currency = security.dig("Currency")

        # Cache the API-returned currency so fetch_security_prices can use it
        if currency.present? && mic.present?
          cache_key = "eodhd:currency:#{code.upcase}:#{mic}"
          Rails.cache.write(cache_key, currency, expires_in: 24.hours)
        end

        Security.new(
          symbol: code,
          name: security.dig("Name"),
          logo_url: nil,
          exchange_operating_mic: mic,
          country_code: country,
          currency: currency
        )
      end
    end
  end

  def fetch_security_info(symbol:, exchange_operating_mic:)
    with_provider_response do
      enforce_daily_limit!
      throttle_request

      ticker = eodhd_symbol(symbol, exchange_operating_mic)

      response = client.get("#{base_url}/api/fundamentals/#{CGI.escape(ticker)}") do |req|
        req.params["api_token"] = api_key
        req.params["fmt"] = "json"
      end

      parsed = JSON.parse(response.body)
      check_api_error!(parsed)

      general = parsed.dig("General") || {}

      SecurityInfo.new(
        symbol: symbol,
        name: general.dig("Name"),
        links: general.dig("WebURL"),
        logo_url: general.dig("LogoURL"),
        description: general.dig("Description"),
        kind: general.dig("Type"),
        exchange_operating_mic: exchange_operating_mic
      )
    end
  end

  def fetch_security_price(symbol:, exchange_operating_mic: nil, date:)
    with_provider_response do
      historical_data = fetch_security_prices(symbol:, exchange_operating_mic:, start_date: date, end_date: date)

      raise historical_data.error if historical_data.error.present?
      raise InvalidSecurityPriceError, "No prices found for security #{symbol} on date #{date}" if historical_data.data.blank?

      historical_data.data.first
    end
  end

  def fetch_security_prices(symbol:, exchange_operating_mic: nil, start_date:, end_date:)
    with_provider_response do
      enforce_daily_limit!
      throttle_request

      ticker = eodhd_symbol(symbol, exchange_operating_mic)

      response = client.get("#{base_url}/api/eod/#{CGI.escape(ticker)}") do |req|
        req.params["api_token"] = api_key
        req.params["fmt"] = "json"
        req.params["from"] = start_date.to_s
        req.params["to"] = end_date.to_s
      end

      parsed = JSON.parse(response.body)
      check_api_error!(parsed)

      unless parsed.is_a?(Array)
        raise InvalidSecurityPriceError, "Unexpected response format from EOD API"
      end

      # Prefer cached currency from search results; fall back to hardcoded map
      cache_key = "eodhd:currency:#{symbol.upcase}:#{exchange_operating_mic}"
      eodhd_exchange = MIC_TO_EODHD_EXCHANGE[exchange_operating_mic]
      currency = Rails.cache.read(cache_key) || EXCHANGE_CURRENCY[eodhd_exchange]

      parsed.map do |resp|
        price = resp.dig("close")
        date = resp.dig("date")

        if price.nil? || price.to_f <= 0
          Rails.logger.warn("#{self.class.name} returned invalid price data for security #{symbol} on: #{date}.  Price data: #{price.inspect}")
          next
        end

        Price.new(
          symbol: symbol,
          date: date.to_date,
          price: price,
          currency: currency,
          exchange_operating_mic: exchange_operating_mic
        )
      end.compact
    end
  end

  private
    attr_reader :api_key

    def base_url
      ENV["EODHD_URL"] || "https://eodhd.com"
    end

    def client
      @client ||= Faraday.new(url: base_url, ssl: self.class.faraday_ssl_options) do |faraday|
        faraday.request(:retry, {
          max: 3,
          interval: 1.0,
          interval_randomness: 0.5,
          backoff_factor: 2,
          exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [ Faraday::ConnectionFailed ]
        })

        faraday.request :json
        faraday.response :raise_error
      end
    end

    # Builds the EODHD ticker format: {SYMBOL}.{EXCHANGE}
    def eodhd_symbol(symbol, exchange_operating_mic)
      eodhd_exchange = MIC_TO_EODHD_EXCHANGE[exchange_operating_mic] if exchange_operating_mic.present?

      if eodhd_exchange.present?
        "#{symbol}.#{eodhd_exchange}"
      elsif exchange_operating_mic.present?
        "#{symbol}.#{exchange_operating_mic}"
      else
        "#{symbol}.US"
      end
    end

    # Cache key for tracking daily API usage
    def daily_cache_key
      "eodhd:daily:#{Date.current}"
    end

    # Enforces the daily rate limit. Raises RateLimitError if the limit is exhausted.
    # Uses atomic increment-then-check to avoid TOCTOU races between concurrent workers.
    def enforce_daily_limit!
      new_count = Rails.cache.increment(daily_cache_key, 1, expires_in: 24.hours).to_i

      if new_count > max_requests_per_day
        raise RateLimitError, "EODHD daily rate limit of #{max_requests_per_day} requests exhausted"
      end
    end

    # throttle_request and min_request_interval provided by RateLimitable

    def max_requests_per_day
      ENV.fetch("EODHD_MAX_REQUESTS_PER_DAY", MAX_REQUESTS_PER_DAY).to_i
    end

    def check_api_error!(parsed)
      return unless parsed.is_a?(Hash) && parsed["error"].present?

      raise Error, "API error: #{parsed["error"]}"
    end
end
