require "base64"
require "cgi"
require "excon"
require "multi_json"
require "securerandom"
require "uri"
require "zlib"

__LIB_DIR__ = File.expand_path(File.join(File.dirname(__FILE__), ".."))
unless $LOAD_PATH.include?(__LIB_DIR__)
  $LOAD_PATH.unshift(__LIB_DIR__)
end

require "heroku/api/errors"
require "heroku/api/mock"
require "heroku/api/version"

require "heroku/api/addons"
require "heroku/api/apps"
require "heroku/api/attachments"
require "heroku/api/buildpacks"
require "heroku/api/collaborators"
require "heroku/api/config_vars"
require "heroku/api/domains"
require "heroku/api/features"
require "heroku/api/keys"
require "heroku/api/login"
require "heroku/api/logs"
require "heroku/api/processes"
require "heroku/api/releases"
require "heroku/api/ssl_endpoints"
require "heroku/api/stacks"
require "heroku/api/user"

srand

module Heroku
  class API

    HEADERS = {
      'Accept'                => 'application/json',
      'Accept-Encoding'       => 'gzip',
      #'Accept-Language'       => 'en-US, en;q=0.8',
      'User-Agent'            => "heroku-rb/#{Heroku::API::VERSION}",
      'X-Ruby-Version'        => RUBY_VERSION,
      'X-Ruby-Platform'       => RUBY_PLATFORM
    }

    OPTIONS = {
      :headers  => {},
      :host     => 'api.heroku.com',
      :nonblock => false,
      :scheme   => 'https'
    }

    attr_accessor :second_factor

    def initialize(options={})
      options = OPTIONS.merge(options)

      @api_key = options.delete(:api_key) || ENV['HEROKU_API_KEY']
      if !@api_key && options.has_key?(:username) && options.has_key?(:password)
        username = options.delete(:username)
        password = options.delete(:password)
        @connection = Excon.new("#{options[:scheme]}://#{options[:host]}", options.merge(:headers => HEADERS))
        @api_key = self.post_login(username, password).body["api_key"]
      end

      user_pass = ":#{@api_key}"
      options[:headers] = HEADERS.merge({
        'Authorization' => "Basic #{Base64.encode64(user_pass).gsub("\n", '')}",
      }).merge(options[:headers])

      @connection = Excon.new("#{options[:scheme]}://#{options[:host]}", options)
    end

    def request(params, &block)
      begin
        if @second_factor
          params[:headers] ||= {}
          params[:headers]['Heroku-Two-Factor-Code'] = @second_factor
          @second_factor = nil # don't use the token again
        end
        response = @connection.request(params, &block)
      rescue Excon::Errors::HTTPStatusError => error
        klass = case error.response.status
          when 401 then Heroku::API::Errors::Unauthorized
          when 402 then Heroku::API::Errors::VerificationRequired
          when 403 then Heroku::API::Errors::Forbidden
          when 404
            if error.request[:path].match /\/apps\/\/.*/
              Heroku::API::Errors::NilApp
            else
              Heroku::API::Errors::NotFound
            end
          when 408 then Heroku::API::Errors::Timeout
          when 422 then Heroku::API::Errors::RequestFailed
          when 423 then Heroku::API::Errors::Locked
          when 429 then Heroku::API::Errors::RateLimitExceeded
          when /50./ then Heroku::API::Errors::RequestFailed
          else Heroku::API::Errors::ErrorWithResponse
        end

        decompress_response!(error.response)
        reerror = klass.new(error.message, error.response)
        reerror.set_backtrace(error.backtrace)
        raise(reerror)
      end

      if response.body && !response.body.empty?
        decompress_response!(response)
        begin
          response.body = MultiJson.load(response.body)
        rescue
          if response.headers['Content-Type'] === 'application/json'
            raise
          end
          # leave non-JSON body as is
        end
      end

      # reset (non-persistent) connection
      @connection.reset

      response
    end

    private

    def decompress_response!(response)
      return unless response.headers['Content-Encoding'] == 'gzip'
      response.body = Zlib::GzipReader.new(StringIO.new(response.body)).read
    end

    def app_params(params)
      app_params = {}
      params.each do |key, value|
        app_params["app[#{key}]"] = value
      end
      app_params
    end

    def addon_params(params)
      params.inject({}) do |accum, (key, value)|
        accum["config[#{key}]"] = value
        accum
      end
    end

    def escape(string)
      CGI.escape(string).gsub('.', '%2E')
    end

    def ps_options(params)
      if ps_env = params.delete(:ps_env) || params.delete('ps_env')
        ps_env.each do |key, value|
          params["ps_env[#{key}]"] = value
        end
      end
      params
    end

  end
end
