# frozen_string_literal: true

require 'faraday'
require 'circuitbox'

class Circuitbox
  class FaradayMiddleware < Faraday::Middleware
    class RequestFailed < StandardError; end

    class NullResponse < Faraday::Response
      attr_reader :original_response, :original_exception

      def initialize(response = nil, exception = nil)
        @original_response  = response
        @original_exception = exception
        super(status: 503, response_headers: {})
      end
    end

    DEFAULT_OPTIONS = {
      open_circuit: lambda do |response|
        # response.status:
        # nil -> connection could not be established, or failed very hard
        # 5xx -> non recoverable server error, opposed to 4xx which are client errors
        response.status.nil? || (response.status >= 500 && response.status <= 599)
      end,
      default_value: ->(service_response, exception) { NullResponse.new(service_response, exception) },
      # It's possible for the URL object to not have a host at the time the middleware
      # is run. To not break circuitbox by creating a circuit with a nil service name
      # we can get the string representation of the URL object and use that as the service name.
      identifier: ->(env) { env[:url].host || env[:url].to_s },
      # default circuit breaker options are merged in during initialization
      circuit_breaker_options: {}
    }.freeze

    DEFAULT_EXCEPTIONS = [
      Faraday::TimeoutError,
      RequestFailed
    ].freeze

    DEFAULT_CIRCUIT_BREAKER_OPTIONS = {
      exceptions: DEFAULT_EXCEPTIONS
    }.freeze

    def initialize(app, opts = {})
      @app = app
      @opts = DEFAULT_OPTIONS.merge(opts)

      @opts[:circuit_breaker_options] = DEFAULT_CIRCUIT_BREAKER_OPTIONS.merge(@opts[:circuit_breaker_options])
      super(app)
    end

    def call(request_env)
      service_response = nil
      circuit(request_env).run do
        @app.call(request_env).on_complete do |env|
          service_response = Faraday::Response.new(env)
          raise RequestFailed if open_circuit?(service_response)
        end
      end
    rescue Circuitbox::Error => e
      circuit_open_value(request_env, service_response, e)
    end

    private

    def call_default_value(response, exception)
      default_value = @opts[:default_value]
      default_value.respond_to?(:call) ? default_value.call(response, exception) : default_value
    end

    def open_circuit?(response)
      @opts[:open_circuit].call(response)
    end

    def circuit_open_value(env, service_response, exception)
      env[:circuit_breaker_default_value] || call_default_value(service_response, exception)
    end

    def circuit(env)
      identifier = @opts[:identifier]
      id = identifier.respond_to?(:call) ? identifier.call(env) : identifier

      Circuitbox.circuit(id, @opts[:circuit_breaker_options])
    end
  end
end

Faraday::Middleware.register_middleware(circuitbox: Circuitbox::FaradayMiddleware)
