class Provider::Synth < Provider
  include ExchangeRate::Provideable
  include Security::Provideable

  def initialize(api_key)
    @api_key = api_key
  end

  def healthy?
    provider_response do
      response = client.get("#{base_url}/user")
      JSON.parse(response.body).dig("id").present?
    end
  end

  def usage
    provider_response do
      response = client.get("#{base_url}/user")

      parsed = JSON.parse(response.body)

      remaining = parsed.dig("api_calls_remaining")
      limit = parsed.dig("api_limit")
      used = limit - remaining

      UsageData.new(
        used: used,
        limit: limit,
        utilization: used.to_f / limit * 100,
        plan: parsed.dig("plan"),
      )
    end
  end

  # ================================
  #          Exchange Rates
  # ================================

  def fetch_exchange_rate(from:, to:, date:)
    provider_response retries: 2 do
      response = client.get("#{base_url}/rates/historical") do |req|
        req.params["date"] = date.to_s
        req.params["from"] = from
        req.params["to"] = to
      end

      rates = JSON.parse(response.body).dig("data", "rates")

      ExchangeRate::Provideable::FetchRateData.new(
        rate: ExchangeRate.new(
          from_currency: from,
          to_currency: to,
          date: date,
          rate: rates.dig(to)
        )
      )
    end
  end

  def fetch_exchange_rates(from:, to:, start_date:, end_date:)
    provider_response retries: 1 do
      data = paginate(
        "#{base_url}/rates/historical-range",
        from: from,
        to: to,
        date_start: start_date.to_s,
        date_end: end_date.to_s
      ) do |body|
        body.dig("data")
      end

      ExchangeRate::Provideable::FetchRatesData.new(
        rates: data.paginated.map do |exchange_rate|
          ExchangeRate.new(
            from_currency: from,
            to_currency: to,
            date: exchange_rate.dig("date"),
            rate: exchange_rate.dig("rates", to)
          )
        end
      )
    end
  end

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

  def search_securities(symbol, country_code: nil, exchange_operating_mic: nil)
    provider_response do
      response = client.get("#{base_url}/tickers/search") do |req|
        req.params["name"] = symbol
        req.params["dataset"] = "limited"
        req.params["country_code"] = country_code if country_code.present?
        req.params["exchange_operating_mic"] = exchange_operating_mic if exchange_operating_mic.present?
        req.params["limit"] = 25
      end

      parsed = JSON.parse(response.body)

      Security::Provideable::Search.new(
        securities: parsed.dig("data").map do |security|
          Security.new(
            ticker: security.dig("symbol"),
            name: security.dig("name"),
            logo_url: security.dig("logo_url"),
            exchange_acronym: security.dig("exchange", "acronym"),
            exchange_mic: security.dig("exchange", "mic_code"),
            exchange_operating_mic: security.dig("exchange", "operating_mic_code"),
            country_code: security.dig("exchange", "country_code")
          )
        end
      )
    end
  end

  def fetch_security_info(security)
    provider_response do
      response = client.get("#{base_url}/tickers/#{security.ticker}") do |req|
        req.params["mic_code"] = security.exchange_mic if security.exchange_mic.present?
        req.params["operating_mic"] = security.exchange_operating_mic if security.exchange_operating_mic.present?
      end

      data = JSON.parse(response.body).dig("data")

      Security::Provideable::SecurityInfo.new(
        ticker: security.ticker,
        name: data.dig("name"),
        links: data.dig("links"),
        logo_url: data.dig("logo_url"),
        description: data.dig("description"),
        kind: data.dig("kind")
      )
    end
  end

  def fetch_security_price(security, date:)
    provider_response do
      historical_data = fetch_security_prices(security, start_date: date, end_date: date)

      raise ProviderError, "No prices found for security #{security.ticker} on date #{date}" if historical_data.data.prices.empty?

      Security::Provideable::PriceData.new(
        price: historical_data.data.prices.first
      )
    end
  end

  def fetch_security_prices(security, start_date:, end_date:)
    provider_response retries: 1 do
      params = {
        start_date: start_date,
        end_date: end_date
      }

      params[:operating_mic_code] = security.exchange_operating_mic if security.exchange_operating_mic.present?

      data = paginate(
        "#{base_url}/tickers/#{security.ticker}/open-close",
        params
      ) do |body|
        body.dig("prices")
      end

      currency = data.first_page.dig("currency")
      country_code = data.first_page.dig("exchange", "country_code")
      exchange_mic = data.first_page.dig("exchange", "mic_code")
      exchange_operating_mic = data.first_page.dig("exchange", "operating_mic_code")

      Security::Provideable::PricesData.new(
        prices: data.paginated.map do |price|
          Security::Price.new(
            security: security,
            date: price.dig("date"),
            price: price.dig("close") || price.dig("open"),
            currency: currency
          )
        end
      )
    end
  end

  # ================================
  #           Transactions
  # ================================

  def enrich_transaction(description, amount: nil, date: nil, city: nil, state: nil, country: nil)
    provider_response do
      params = {
        description: description,
        amount: amount,
        date: date,
        city: city,
        state: state,
        country: country
      }.compact

      response = client.get("#{base_url}/enrich", params)

      parsed = JSON.parse(response.body)

      TransactionEnrichmentData.new(
        name: parsed.dig("merchant"),
        icon_url: parsed.dig("icon"),
        category: parsed.dig("category")
      )
    end
  end

  private
    attr_reader :api_key

    TransactionEnrichmentData = Data.define(:name, :icon_url, :category)

    def retryable_errors
      [
        Faraday::TimeoutError,
        Faraday::ConnectionFailed,
        Faraday::SSLError,
        Faraday::ClientError,
        Faraday::ServerError
      ]
    end

    def base_url
      ENV["SYNTH_URL"] || "https://api.synthfinance.com"
    end

    def app_name
      "maybe_app"
    end

    def app_type
      Rails.application.config.app_mode
    end

    def client
      @client ||= Faraday.new(url: base_url) do |faraday|
        faraday.response :raise_error
        faraday.headers["Authorization"] = "Bearer #{api_key}"
        faraday.headers["X-Source"] = app_name
        faraday.headers["X-Source-Type"] = app_type
      end
    end

    def fetch_page(url, page, params = {})
      client.get(url, params.merge(page: page))
    end

    def paginate(url, params = {})
      results = []
      page = 1
      current_page = 0
      total_pages = 1
      first_page = nil

      while current_page < total_pages
        response = fetch_page(url, page, params)

        body = JSON.parse(response.body)
        first_page = body unless first_page
        page_results = yield(body)
        results.concat(page_results)

        current_page = body.dig("paging", "current_page")
        total_pages = body.dig("paging", "total_pages")

        page += 1
      end

      PaginatedData.new(
        paginated: results,
        first_page: first_page,
        total_pages: total_pages
      )
    end
end
