# frozen_string_literal: true

require 'oxr/version'
require 'oxr/configuration'

require 'date'
require 'json'
require 'open-uri'

##
# All use of OXR takes place through module methods.
#
# Before you can make API calls, you must configure OXR with your Open Exchange
# Rates App ID. Visit https://openexchangerates.org to sign up and generate one.
#
#   OXR.configure do |config|
#     config.app_id = 'XXXX'
#   end
#
# Optionally you may also specify the base currency for the conversion rates.
# The default base currency is USD.
#
#   OXR.configure do |config|
#     config.app_id = 'XXXX'
#     config.base = 'JPY'
#   end
#
# If you will be using OXR in a test environment, you may want to test the
# substitute local files for the API endpoints to provide deterministic results
# and avoid running over your Open Exchange Rates API request quota.
#
#   class ThisIsATest < Minitest::Test
#     def setup
#       OXR.configure do |config|
#         config.latest = 'test/fixtures/sample.json'
#       end
#     end
#
#     def teardown
#       OXR.reset_sources
#     end
#   end
#
# To quickly get an exchange rate from the base currency to a given currency,
# use `OXR.get_rate`.
#
#   OXR.get_rate 'GBP' # => 0.703087
#   OXR.get_rate 'JPY' # => 111.7062
#
# This method is aliased as `OXR.[]`.
#
#   OXR['GBP'] # => 0.703087
#
# Pass the `:on` keyword argument to get an historical exchange rate on a given
# date.
#
#   OXR.get_rate 'GBP', on: Date.new(2015, 6, 14) # => 0.642607
#
module OXR
  class Error < StandardError
  end

  ##
  # ApiError is raised when OXR encounters an error connecting to the Open
  # Exchange Rates API server.
  class ApiError < Error
    def message
      cause.message
    end

    def description
      response['description']
    end

    def response
      @response ||= JSON.parse cause.io.read
    end
  end

  class << self
    ##
    # DEPRECATED: TO BE REMOVED IN 0.7.
    #
    # OXR should no longer be instantiated explicitly.
    #
    # Call OXR.configure to specify the application ID and use the module
    # methods below to access the different API endpoints.
    def new(app_id)
      warn '[DEPRECATION] OXR.new is deprecated and will be removed from 0.7.' \
           " Use OXR class methods instead (from #{caller(1..1).first})."

      configure do |config|
        config.app_id = app_id
      end
      self
    end

    ##
    # Get the latest exchange rate for currency identified by +code+.
    #
    # If the optional +on+ keyword is provided, it instead returns the
    # historical exchange rate on the given date.
    def get_rate(code, on: nil)
      data = if on
               historical on: on
             else
               latest
             end
      data['rates'][code.to_s]
    end

    alias [] get_rate

    ##
    # Returns a Hash mapping currency codes to full currency names.
    def currencies
      call configuration.currencies
    end

    ##
    # Returns a Hash mapping currency codes to their historical exchange rate
    # on the date given by the +on+ keyword argument.
    #
    # The exchange rate is relative to the configured base currency.
    def historical(on:)
      call configuration.historical on
    end

    ##
    # Returns a Hash mapping currency codes to their latest exchange rate.
    #
    # The exchange rate is relative to the configured base currency.
    def latest
      call configuration.latest
    end

    ##
    # Returns a Hash containinng details about the Open Exchange Rates account
    # and current usage statistics.
    def usage
      call configuration.usage
    end

    ##
    # Resets API endpoints to their defaults.
    def reset_sources
      configure(&:reset_sources)
    end

    ##
    # Returns the OXR configuration struct.
    #
    # If given a block, the configuration will be yielded to the block.
    def configure
      yield configuration if block_given?
      configuration
    end

    ##
    # Returns the OXR configuration struct.
    def configuration
      @configuration ||= Configuration.new
    end

    private

    def call(endpoint)
      # This method makes it possible for a user to provide an endpoint path as
      # either a String, Pathname, or URI. We will massage it until we have
      # something that responds to #read.
      endpoint = URI.parse(endpoint.to_s) unless endpoint.respond_to?(:read)
      endpoint = File.open(endpoint.path) unless endpoint.respond_to?(:read)

      JSON.parse endpoint.read
    rescue OpenURI::HTTPError => e
      raise ApiError, e
    end
  end
end
