require 'fog/joyent'
require 'fog/joyent/errors'
require 'fog/compute'

module Fog
  module Compute
    class Joyent < Fog::Service
      requires :joyent_username

      recognizes :joyent_password
      recognizes :joyent_url
      recognizes :joyent_keyname
      recognizes :joyent_keyfile

      model_path 'fog/joyent/models/compute'
      request_path 'fog/joyent/requests/compute'

      request :list_datacenters
      # request :get_datacenter

      # Keys
      collection :keys
      model :key

      request :list_keys
      request :get_key
      request :create_key
      request :delete_key

      # Images
      collection :images
      model :image
      request :list_datasets
      request :get_dataset

      # Flavors
      collection :flavors
      model :flavor
      request :list_packages
      request :get_package

      # Servers
      collection :servers
      model :server
      request :list_machines
      request :get_machine
      request :create_machine
      request :start_machine
      request :stop_machine
      request :reboot_machine
      request :resize_machine
      request :delete_machine

      # Snapshots
      collection :snapshots
      model :snapshot
      request :create_machine_snapshot
      request :start_machine_from_snapshot
      request :list_machine_snapshots
      request :get_machine_snapshot
      request :delete_machine_snapshot
      request :update_machine_metadata
      request :get_machine_metadata
      request :delete_machine_metadata
      request :delete_all_machine_metadata

      # MachineTags
      request :add_machine_tags
      request :list_machine_tags
      request :get_machine_tag
      request :delete_machine_tag
      request :delete_all_machine_tags



      class Mock
        def self.data
          @data ||= Hash.new do |hash, key|
            hash[key] = {}
          end
        end

        def data
          self.class.data
        end

        def initialize(options = {})
          @joyent_username = options[:joyent_username] || Fog.credentials[:joyent_username]
          @joyent_password = options[:joyent_password] || Fog.credentials[:joyent_password]
        end

        def request(opts)
          raise "Not Implemented"
        end
      end # Mock

      class Real
        def initialize(options = {})
          @connection_options = options[:connection_options] || {}
          @persistent = options[:persistent] || false

          @joyent_url = options[:joyent_url] || 'https://us-sw-1.api.joyentcloud.com'
          @joyent_version = options[:joyent_version] || '~6.5'

          @joyent_username = options[:joyent_username]

          unless @joyent_username
            raise ArgumentError, "options[:joyent_username] required"
          end

          if options[:joyent_keyname] && options[:joyent_keyfile]
            if File.exists?(options[:joyent_keyfile])
              @joyent_keyname = options[:joyent_keyname]
              @joyent_key = File.read(options[:joyent_keyfile])

              if @joyent_key.lines.first.include?('-----BEGIN DSA PRIVATE KEY-----')
                @key = OpenSSL::PKey::DSA.new(@joyent_key)
              elsif @joyent_key.lines.first.include?('-----BEGIN RSA PRIVATE KEY-----')
                @key = OpenSSL::PKey::RSA.new(@joyent_key)
              else
                raise ArgumentError, "options[joyent_keyfile] provided must be an RSA or DSA private key"
              end


              @header_method = method(:header_for_signature_auth)
            else
              raise ArgumentError, "options[:joyent_keyfile] provided does not exist."
            end

          elsif options[:joyent_password]
            @joyent_password = options[:joyent_password]

            @header_method = method(:header_for_basic_auth)
          else
            raise ArgumentError, "Must provide either a joyent_password or joyent_keyname and joyent_keyfile pair"
          end

          @connection = Fog::Connection.new(
            @joyent_url,
            @persistent,
            @connection_options
          )
        end

        def request(request = {})
          request[:headers] = {
            "X-Api-Version" => @joyent_version,
            "Content-Type" => "application/json",
            "Accept" => "application/json"
          }.merge(request[:headers] || {}).merge(@header_method.call) 

          if request[:body]
            request[:body] = Fog::JSON.encode(request[:body])
          end

          response = @connection.request(request)

          if response.headers["Content-Type"] == "application/json"
            response.body = json_decode(response.body)
          end

          raise_if_error!(request, response)

          response
        end

        private

        def json_decode(body)
          parsed = Fog::JSON.decode(body)
          decode_time_attrs(parsed)
        end

        def header_for_basic_auth
          {
            "Authorization" => "Basic #{Base64.encode64("#{@joyent_username}:#{@joyent_password}").delete("\r\n")}"
          }
        end

        def header_for_signature_auth
          date = Time.now.utc.httpdate
          begin
            signature = Base64.encode64(@key.sign("sha256", date)).delete("\r\n")
          rescue OpenSSL::PKey::PKeyError => e
            if e.message == 'wrong public key type'
              puts 'ERROR: Your version of ruby/openssl does not suport DSA key signing'
              puts 'see: http://bugs.ruby-lang.org/issues/4734'
              puts 'workaround: Please use an RSA key instead'
            end
            raise
          end
          key_id = "/#{@joyent_username}/keys/#{@joyent_keyname}"

          {
            "Date" => date,
            "Authorization" => "Signature keyId=\"#{key_id}\",algorithm=\"rsa-sha256\" #{signature}"
          }
        end

        def decode_time_attrs(obj)
          if obj.kind_of?(Hash)
            obj["created"] = Time.parse(obj["created"]) if obj["created"]
            obj["updated"] = Time.parse(obj["updated"]) if obj["updated"]
          elsif obj.kind_of?(Array)
            obj.map do |o|
              decode_time_attrs(o)
            end
          end

          obj
        end

        def raise_if_error!(request, response)
          case response.status
          when 401 then
            raise Errors::Unauthorized.new('Invalid credentials were used', request, response)
          when 403 then
            raise Errors::Forbidden.new('No permissions to the specified resource', request, response)
          when 404 then
            raise Errors::NotFound.new('Requested resource was not found', request, response)
          when 405 then
            raise Errors::MethodNotAllowed.new('Method not supported for the given resource', request, response)
          when 406 then
            raise Errors::NotAcceptable.new('Try sending a different Accept header', request, response)
          when 409 then
            raise Errors::Conflict.new('Most likely invalid or missing parameters', request, response)
          when 414 then
            raise Errors::RequestEntityTooLarge.new('You sent too much data', request, response)
          when 415 then
            raise Errors::UnsupportedMediaType.new('You encoded your request in a format we don\'t understand', request, response)
          when 420 then
            raise Errors::PolicyNotForfilled.new('You are sending too many requests', request, response)
          when 449 then
            raise Errors::RetryWith.new('Invalid API Version requested; try with a different API Version', request, response)
          when 503 then
            raise Errors::ServiceUnavailable.new('Either there\'s no capacity in this datacenter, or we\'re in a maintenance window', request, response)
          end
        end

      end # Real
    end
  end
end
