require 'vultr'
require 'vagrant/util/retryable'
require 'limiter'


module VagrantPlugins
  module Vultr
    module Helpers
      module Client
        def client
          @client ||= ApiClient.new(@machine.provider_config.token)
        end
      end


      class ApiClient
        include Vagrant::Util::Retryable

        TimeoutError = Class.new(StandardError)

        TIMEOUT = 300

        def initialize(token)
          ::Vultr.api_key = token
        end

        def servers
          servers = request { ::Vultr::Server.list }
          servers = servers.values if servers.any?

          servers
        end

        def server(sub_id)
          servers.find { |server| server['SUBID'] == sub_id }
        end

        def create_server(attributes)
          params = {
            DCID: region_id(attributes[:region]),
            VPSPLANID: vps_plan_id(attributes[:plan]),
            SSHKEYID: ssh_key_id(attributes[:ssh_key_name]),
          }

          [:enable_ipv6, :enable_private_network, :label, :tag, :hostname].each do |k|
            params[k] = attributes[k]
          end

          if attributes[:snapshot]
            params.merge!(OSID: os_id('Snapshot'), SNAPSHOTID: attributes[:snapshot])
          else
            params.merge!(OSID: os_id(attributes[:os]))
          end

          request { ::Vultr::Server.create(params) }['SUBID']
        end

        def start_server(sub_id)
          request { ::Vultr::Server.start(SUBID: sub_id) }
        end

        def reboot_server(sub_id)
          request { ::Vultr::Server.reboot(SUBID: sub_id) }
        end

        def stop_server(sub_id)
          request { ::Vultr::Server.halt(SUBID: sub_id) }
        end

        def destroy_server(sub_id)
          request { ::Vultr::Server.destroy(SUBID: sub_id) }
        end

        def oses
          request { ::Vultr::OS.list }.values
        end

        def os_id(os)
          oses.find { |o| o['name'] == os }['OSID']
        end

        def regions
          request { ::Vultr::Regions.list }.values
        end

        def region_id(region)
          regions.find { |r| r['name'] == region }['DCID']
        end

        def vps_plan_id(plan)
          plans = request { ::Vultr::Plans.list }
          plans.values.find { |p| p['name'] == plan }['VPSPLANID']
        end

        def ssh_keys
          ssh_keys = request { ::Vultr::SSHKey.list }
          ssh_keys = ssh_keys.values if ssh_keys.any?

          ssh_keys
        end

        def ssh_key_id(ssh_key_name)
          key = ssh_keys.find { |s| s['name'] == ssh_key_name }
          key['SSHKEYID'] if key
        end

        def create_ssh_key(name, key)
          request { ::Vultr::SSHKey.create(name: name, ssh_key: key) }['SSHKEYID']
        end

        def wait_to_activate(sub_id)
          wait_until do
            # it might be not shown in API for some reason
            server = server(sub_id)
            server && server['status'] == 'active'
          end
        end

        def wait_to_power_on(sub_id)
          wait_until do
            # it might be not shown in API for some reason
            server = server(sub_id)
            server && server['power_status'] == 'running'
          end
        end

        def wait_to_destroy(sub_id)
          wait_until { !server(sub_id) }
        end

        # @todo Fix the case when SSH key is not ready so it asks for password
        # @todo Extract away from client?
        def wait_for_ssh(machine)
          # SSH may be unreachable after server is started
          wait_until(Errno::ENETUNREACH) do
            machine.communicate.wait_for_ready(Helpers::ApiClient::TIMEOUT)
          end
        end

        private

        # FIXME this should be an instance variable
        QUEUE = Limiter::RateQueue.new(2, interval: 1)

        def request
          # interval = ENV.fetch('VULTR_RATE_LIMIT_INTERVAL_MS', 500)
          # $stdout.print("sleeping\n")
          # sleep interval.to_f / 1000
          QUEUE.shift
          response = yield
          if response[:status] != 200
            raise "API request failed: #{response[:result]}."
          else
            response[:result]
          end
        end

        def wait_until(exception = TimeoutError)
          retryable(tries: TIMEOUT, sleep: 1, on: exception) do
            yield or raise exception
          end
        end
      end
    end
  end
end
