require "cfoundry/trace_helpers"
require "net/https"
require "net/http/post/multipart"
require "multi_json"
require "fileutils"
require "forwardable"

module CFoundry
  class BaseClient # :nodoc:
    include CFoundry::ProxyOptions

    extend Forwardable

    attr_reader :rest_client
    attr_accessor :client_id, :client_secret, :skip_ssl_validation

    def_delegators :rest_client, :target, :target=, :token,
      :trace, :backtrace, :backtrace=, :log, :log=

    def initialize(target, token = nil, options = {})
      @rest_client = CFoundry::RestClient.new(target, token)
      self.client_id = options[:client_id]
      self.client_secret = options[:client_secret]
      self.skip_ssl_validation = options[:skip_ssl_validation]
      self.trace = false
      self.backtrace = false
      self.log = false
    end

    def uaa
      @uaa ||= begin
        endpoint = info[:authorization_endpoint]

        if endpoint
          uaa = CFoundry::UAAClient.new(endpoint,
                                        client_id || "cf",
                                        client_secret: client_secret,
                                        skip_ssl_validation: skip_ssl_validation)
          uaa.trace = trace
          uaa.token = token
          uaa
        else
          nil
        end
      end
    end

    def password_score(password)
      uaa ? uaa.password_score(password) : :unknown
    end

    def token=(token)
      if token.is_a?(String)
        token = CFoundry::AuthToken.new(token)
      end

      @rest_client.token = token
      @uaa.token = token if @uaa
    end

    def trace=(trace)
      @rest_client.trace = trace
      @uaa.trace = trace if @uaa
    end

    # Cloud metadata
    def info
      get("info", :accept => :json)
    end

    def get(*args)
      request("GET", *args)
    end

    def delete(*args)
      request("DELETE", *args)
    end

    def post(*args)
      request("POST", *args)
    end

    def put(*args)
      request("PUT", *args)
    end

    def request(method, *args)
      if needs_token_refresh?
        token.auth_header = nil
        refresh_token!
      end

      path, options = normalize_arguments(args)
      request, response = request_raw(method, path, options)
      handle_response(response, options, request)
    end

    def request_raw(method, path, options)
      @rest_client.request(method, path, options)
    end

    def refresh_token!
      self.token = uaa.try_to_refresh_token!
    end

    def stream_url(url, &blk)
      uri = URI.parse(url)

      opts = {}
      
      if uri.scheme == "https"
        opts[:use_ssl] = true
        opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE 
      end

      Net::HTTP.start(uri.host, uri.port, *proxy_options_for(uri), opts) do |http|
        http.read_timeout = 5

        req = Net::HTTP::Get.new(uri.request_uri)
        req["Authorization"] = token.auth_header if token

        http.request(req) do |response|
          case response
          when Net::HTTPOK
            response.read_body(&blk)
          when Net::HTTPNotFound
            raise CFoundry::NotFound.new(response.body, 404)
          when Net::HTTPForbidden
            raise CFoundry::Denied.new(response.body, 403)
          when Net::HTTPUnauthorized
            raise CFoundry::Unauthorized.new(response.body, 401)
          else
            raise CFoundry::BadResponse.new(response.body, response.code)
          end
        end
      end
    end

    private

    def needs_token_refresh?
      token && token.auth_header && token.refresh_token && \
        token.expires_soon?
    end

    def status_is_successful?(code)
      (code >= 200) && (code < 400)
    end

    def handle_response(response, options, request)
      if status_is_successful?(response[:status].to_i)
        handle_successful_response(response, options)
      else
        handle_error_response(response, request)
      end
    end

    def handle_successful_response(response, options)
      if options[:return_response]
        response
      elsif options[:accept] == :json
        parse_json(response[:body])
      else
        response[:body]
      end
    end

    def handle_error_response(response, request)
      body_json = parse_json(response[:body])
      body_code = body_json && body_json[:code]
      code = body_code || response[:status].to_i

      if body_code
        error_class = CFoundry::APIError.error_classes[body_code] || CFoundry::APIError
        raise error_class.new(body_json[:description], body_code, request, response)
      end

      case code
        when 404
          raise CFoundry::NotFound.new(nil, code, request, response)
        when 403
          raise CFoundry::Denied.new(nil, code, request, response)
        when 401
          raise CFoundry::Unauthorized.new(nil, code, request, response)
        else
          raise CFoundry::BadResponse.new(nil, code, request, response)
      end
    end

    def normalize_arguments(args)
      if args.last.is_a?(Hash)
        options = args.pop
      else
        options = {}
      end

      [normalize_path(args), options]
    end

    URI_ENCODING_PATTERN = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")

    def normalize_path(segments)
      if segments.size == 1 && segments.first =~ /^\//
        segments.first
      else
        segments.flatten.collect { |x|
          URI.encode(x.to_s, URI_ENCODING_PATTERN)
        }.join("/")
      end
    end

    def parse_json(x)
      if x.nil? || x.empty?
        raise MultiJson::DecodeError.new("Empty JSON string")
      else
        MultiJson.load(x, :symbolize_keys => true)
      end
    rescue MultiJson::DecodeError
      nil
    end
  end
end
