module MeltingPot
  class MeltingPotError               < StandardError  ; end
  class UnsupportedProxyPassTypeError < MeltingPotError; end

  class ProxyController
    def initialize(rest_api_proxy_interactor, any_proxy_interactor, static_dir = "/app/static", logger = Logger.new(STDERR))
      @rest_api_proxy_interactor = rest_api_proxy_interactor
      @any_proxy_interactor = any_proxy_interactor
      @static_dir = static_dir
      @logger = logger
    end
    attr_reader :rest_api_proxy_interactor, :any_proxy_interactor, :static_dir, :logger

    def proxy_pass(req)
      ProxyControllerAdapter[proxy_controller_type(req)].new(self).proxy_pass(req)
    end

    def proxy_controller_type(req)
      _is_json?(req) || _has_bearer?(req) ? :rest_api : :any
    end

    def _is_json?(req)
      header = req.get_header("HTTP_CONTENT_TYPE")
      if header.blank?
        return false
      end
      header = content_type.downcase
      header.include?("application/json")
    end

    def _has_bearer?(req)
      header = req.get_header("HTTP_AUTHORIZATION")
      if header.blank?
        return false
      end
      header = header.downcase
      header.include?("bearer") || header.include?("mac")
    end
  end

  class ProxyControllerAdapter
    class << self
      def register(k, v)
        adapters[k] = v
      end

      def [](k)
        adapter = adapters[k]
        if ! adapter
          raise UnsupportedProxyPassTypeError, "unsupported proxy pass type: #{ k }"
        end
        adapter
      end

      def adapters
        @adapters ||= {}
      end
    end

    def initialize(proxy_controller)
      @proxy_controller = proxy_controller
    end

    def proxy_pass(req)
      raise NotImplementedError
    end

    def make_headers(req)
      # see RFC 3875 - The Common Gateway Interface(CGI) <https://tools.ietf.org/html/rfc3876> 
      headers = Rack::Utils::HeaderHash[req.env.select { |k, _| k[0, 5] == "HTTP_" }.map { |k, v| [k[5, k.length].gsub("_", "-"), v] }]

      len = req.content_length
      if len
        headers["CONTENT-LENGTH"] = len
      end
      type = req.content_type
      if type
        headers["CONTENT-TYPE"] = type
      end

      remote_addr = req.get_header("REMOTE_ADDR") || "127.0.0.1"
      if headers["X-FORWARDED-FOR"]
        headers["X-FORWARDED-FOR"] << ", #{remote_addr}"
      else
        headers["X-FORWARDED-FOR"] = remote_addr
      end

      # XXX, sanitizing headers for passing them to the upstream
      headers.delete("AUTHORIZATION")
      headers.delete("HOST")
      headers.delete("X-USER-ID")

      headers
    end
  end

  # this is input boundary
  class RestApiProxyContext
    BEARER_PATTERN = /(Bearer)\s+([\w\d\-._~+\/]+)/i

    def self.from_request(req)
      if req.has_header?("HTTP_AUTHORIZATION")
        _, token_type, access_token = *(req.get_header("HTTP_AUTHORIZATION").match(BEARER_PATTERN))
        new(token_type, access_token)
      else
        new
      end
    end

    def initialize(token_type = nil, access_token = nil)
      @token_type = token_type
      @access_token = access_token
    end
    attr_reader :token_type, :access_token
  end

  class RestApiProxyController < ProxyControllerAdapter
    def proxy_pass(req)
      ctx = RestApiProxyContext.from_request(req)
      result = @proxy_controller.rest_api_proxy_interactor.proxy_pass(ctx, req.url, req.request_method, make_headers(req), req.body)

      # set x-fallthru-set for logging
      result.headers["x-fallthru-set-X-USER-ID"] = result.user_id ? result.user_id.to_s : "n/a"
      result.headers["x-fallthru-set-RTT"] = result.rtt ? result.rtt.to_s : "-1"

      [result.status, result.headers, result.body]
    rescue UnauthorizedError => err
      @proxy_controller.logger.error(err)
      [
        401,
        { 
          "Content-Type" => "application/json; charset=utf-8",
          "Cache-Control" => "no-store",
          "Pragma" => "no-cache"
        },
        [
          JSON.stringify({ :message => "Unauthorized" })
        ]
      ]
    rescue NotFoundError => err
      @proxy_controller.logger.error(err)
      [
        404,
        { 
          "Content-Type" => "application/json; charset=utf-8",
        },
        [
          JSON.stringify({ :message => "Not Found" })
        ]
      ]
    rescue => err
      @proxy_controller.logger.error(err)
      [
        500,
        { 
          "Content-Type" => "application/json; charset=utf-8",
        },
        [
          JSON.stringify({ :message => "Internal Server Error" })
        ]
      ]
    end
  end
  ProxyControllerAdapter.register :rest_api, RestApiProxyController

  class AnyProxyContext
    def self.from_request(req)
      if req.session.key?("user_id")
        new(req.session["user_id"], nil, true)
      elsif req.post?
        new(req.params["username"], req.params["password"], false)
      else
        new
      end
    end

    def initialize(username = nil, password = nil, session_started = false)
      @username = username
      @password = password
      @session_started = session_started
    end
    attr_reader :username, :password, :session_started

    def session_started?
      @session_started
    end
  end

  class AnyProxyController < ProxyControllerAdapter
    def proxy_pass(req)
      ctx = AnyProxyContext.from_request(req)
      result = @proxy_controller.any_proxy_interactor.proxy_pass(ctx, req.url, req.request_method, make_headers(req), req.body)

      # set session
      req.session["user_id"] = result.user_id if result.user_id
      # set x-fallthru-set for logging
      result.headers["x-fallthru-set-X-USER-ID"] = result.user_id ? result.user_id.to_s : "n/a"
      result.headers["x-fallthru-set-RTT"] = result.rtt ? result.rtt.to_s : "-1"

      [result.status, result.headers, result.body]
    rescue BadRequestError => err
      @proxy_controller.logger.error(err)
      [
        400,
        {
          "Cache-Control" => "no-store",
          "Pragma" => "no-cache",
          "x-reproxy-url" => File.join(@proxy_controller.static_dir, "/400.html")
        },
        []
      ]
    rescue UnauthorizedError => err
      @proxy_controller.logger.error(err)
      [
        401,
        {
          "Cache-Control" => "no-store",
          "Pragma" => "no-cache",
          "x-reproxy-url" => File.join(@proxy_controller.static_dir, "/401.html")
        },
        []
      ]
    rescue NotFoundError => err
      @proxy_controller.logger.error(err)
      [404, { "x-reproxy-url" => File.join(@proxy_controller.static_dir, "/404.html") }, []]
    rescue => err
      @proxy_controller.logger.error(err)
      [500, { "x-reproxy-url" => File.join(@proxy_controller.static_dir, "/500.html") }, []]
    end
  end
  ProxyControllerAdapter.register :any, AnyProxyController
end