require "rack_reverse_proxy/response_builder"

module RackReverseProxy
  # FIXME: Enable them and fix issues during refactoring
  # rubocop:disable Metrics/ClassLength

  # RoundTrip represents one request-response made by rack-reverse-proxy
  # middleware.
  class RoundTrip
    def initialize(app, env, global_options, rules, response_builder_klass = ResponseBuilder)
      @app = app
      @env = env
      @global_options = global_options
      @rules = rules
      @response_builder_klass = response_builder_klass
    end

    def call
      return app.call(env) if rule.nil?
      return proxy_with_newrelic if new_relic?
      proxy
    end

    private

    attr_reader :app, :env, :global_options, :rules, :response_builder_klass

    def new_relic?
      global_options[:newrelic_instrumentation]
    end

    def proxy_with_newrelic
      perform_action_with_newrelic_trace(:name => action_name, :request => source_request) do
        proxy
      end
    end

    def action_name
      "#{action_path}/#{source_request.request_method}"
    end

    def action_path
      # Rack::ReverseProxy/foo/bar#GET
      source_request.path.gsub(%r{/\d+}, "/:id").gsub(%r{^/}, "")
    end

    def uri
      return @_uri if defined?(@_uri)
      @_uri = rule.get_uri(path, env, headers, source_request)
    end

    def options
      @_options ||= global_options.dup.merge(rule.options)
    end

    def https_redirect
      rewrite_uri(uri, source_request)
      uri.scheme = "https"
      [301, { "Location" => uri.to_s }, [""]]
    end

    def need_https_redirect?
      options[:force_ssl] &&
        options[:replace_response_host] &&
        source_request.scheme == "http"
    end

    def target_request
      @_target_request ||= build_target_request
    end

    def target_request_headers
      @_target_request_headers ||= headers
    end

    def build_target_request
      Net::HTTP.const_get(
        source_request.request_method.capitalize
      ).new(uri.request_uri)
    end

    def preserve_host
      return unless options[:preserve_host]
      target_request_headers["HOST"] = host_header
    end

    def strip_headers
      return unless options[:stripped_headers]
      options[:stripped_headers].each do |header|
        target_request_headers.delete(header)
      end
    end

    def host_header
      return uri.host if uri.port == uri.default_port
      "#{uri.host}:#{uri.port}"
    end

    def set_forwarded_headers
      return unless options[:x_forwarded_headers]
      target_request_headers["X-Forwarded-Host"] = source_request.host
      target_request_headers["X-Forwarded-Port"] = source_request.port.to_s
      target_request_headers["X-Forwarded-Proto"] = source_request.scheme
    end

    def optional_headers
      options[:optional_headers].each do |header,value|
        target_request_headers[header.to_s] = value
      end
    end

    def initialize_http_header
      target_request.initialize_http_header(target_request_headers)
    end

    def set_basic_auth
      return unless need_basic_auth?
      target_request.basic_auth(options[:username], options[:password])
    end

    def need_basic_auth?
      options[:username] && options[:password]
    end

    def setup_body
      return unless can_have_body? && body?
      source_request.body.rewind
      target_request.body_stream = source_request.body
    end

    def can_have_body?
      target_request.request_body_permitted?
    end

    def body?
      source_request.body
    end

    def set_content_length
      target_request.content_length = source_request.content_length || 0
    end

    def set_content_type
      return unless content_type?
      target_request.content_type = source_request.content_type
    end

    def content_type?
      source_request.content_type
    end

    def target_response
      @_target_response ||= response_builder_klass.new(
        target_request,
        uri,
        options
      ).fetch
    end

    def response_headers
      @_response_headers ||= build_response_headers
    end

    def build_response_headers
      ["Transfer-Encoding", "Status"].inject(rack_response_headers) do |acc, header|
        acc.delete(header)
        acc
      end
    end

    def rack_response_headers
      Rack::Utils::HeaderHash.new(
        Rack::Proxy.normalize_headers(
          format_headers(target_response.headers)
        )
      )
    end

    def replace_location_header
      return unless need_replace_location?
      rewrite_uri(response_location, source_request)
      response_headers["Location"] = response_location.to_s
    end

    def response_location
      @_response_location ||= URI(response_headers["Location"])
    end

    def need_replace_location?
      response_headers["Location"] && options[:replace_response_host] && response_location.host
    end

    def setup_request
      preserve_host
      strip_headers
      set_forwarded_headers
      optional_headers
      initialize_http_header
      set_basic_auth
      setup_body
      set_content_length
      set_content_type
    end

    def setup_response_headers
      replace_location_header
    end

    def rack_response
      [target_response.status, response_headers, target_response.body]
    end

    def proxy
      return app.call(env) if uri.nil?
      return https_redirect if need_https_redirect?

      setup_request
      setup_response_headers

      transform_response(rack_response)
    end

    def transform_response(response)
      rule.transform(path, env, response, uri, headers, source_request)
    end

    def format_headers(headers)
      headers.inject({}) do |acc, (key, val)|
        formated_key = key.split("-").map(&:capitalize).join("-")
        acc[formated_key] = Array(val)
        acc
      end
    end

    def request_default_port?(req)
      [["http", 80], ["https", 443]].include?([req.scheme, req.port])
    end

    def rewrite_uri(uri, original_req)
      uri.scheme = original_req.scheme
      uri.host   = original_req.host
      uri.port   = original_req.port unless request_default_port?(original_req)
    end

    def source_request
      @_source_request ||= Rack::Request.new(env)
    end

    def rule
      return @_rule if defined?(@_rule)
      @_rule = find_rule
    end

    def find_rule
      return if matches.empty?
      non_ambiguous_match
      matches.first
    end

    def path
      @_path ||= source_request.fullpath
    end

    def headers
      Rack::Proxy.extract_http_request_headers(source_request.env)
    end

    def matches
      @_matches ||= rules.select do |rule|
        rule.proxy?(path, headers, source_request)
      end
    end

    def non_ambiguous_match
      return unless ambiguous_match?
      raise Errors::AmbiguousMatch.new(path, matches)
    end

    def ambiguous_match?
      matches.length > 1 && global_options[:matching] != :first
    end
  end
end
