# frozen_string_literal: true

require 'vmpooler/util/parsing'

module Vmpooler
  class API
  class V1 < Sinatra::Base
    api_version = '1'
    api_prefix  = "/api/v#{api_version}"

    helpers do
      include Vmpooler::API::Helpers
    end

    def backend
      Vmpooler::API.settings.redis
    end

    def metrics
      Vmpooler::API.settings.metrics
    end

    def config
      Vmpooler::API.settings.config[:config]
    end

    def pools
      Vmpooler::API.settings.config[:pools]
    end

    def pool_exists?(template)
      Vmpooler::API.settings.config[:pool_names].include?(template)
    end

    def need_auth!
      validate_auth(backend)
    end

    def need_token!
      validate_token(backend)
    end

    def checkoutlock
      Vmpooler::API.settings.checkoutlock
    end

    def get_template_aliases(template)
      result = []
      aliases = Vmpooler::API.settings.config[:alias]
      if aliases
        result += aliases[template] if aliases[template].is_a?(Array)
        template_backends << aliases[template] if aliases[template].is_a?(String)
      end
      result
    end

    def get_pool_weights(template_backends)
      pool_index = pool_index(pools)
      weighted_pools = {}
      template_backends.each do |t|
        next unless pool_index.key? t

        index = pool_index[t]
        clone_target = pools[index]['clone_target'] || config['clone_target']
        next unless config.key?('backend_weight')

        weight = config['backend_weight'][clone_target]
        if weight
          weighted_pools[t] = weight
        end
      end
      weighted_pools
    end

    def count_selection(selection)
      result = {}
      selection.uniq.each do |poolname|
        result[poolname] = selection.count(poolname)
      end
      result
    end

    def evaluate_template_aliases(template, count)
      template_backends = []
      template_backends << template if backend.sismember('vmpooler__pools', template)
      selection = []
      aliases = get_template_aliases(template)
      if aliases
        template_backends += aliases
        weighted_pools = get_pool_weights(template_backends)

        if weighted_pools.count > 1 && weighted_pools.count == template_backends.count
          pickup = Pickup.new(weighted_pools)
          count.to_i.times do
            selection << pickup.pick
          end
        else
          count.to_i.times do
            selection << template_backends.sample
          end
        end
      end

      count_selection(selection)
    end

    def fetch_single_vm(template)
      template_backends = [template]
      aliases = Vmpooler::API.settings.config[:alias]
      if aliases
        template_backends += aliases[template] if aliases[template].is_a?(Array)
        template_backends << aliases[template] if aliases[template].is_a?(String)
        pool_index = pool_index(pools)
        weighted_pools = {}
        template_backends.each do |t|
          next unless pool_index.key? t

          index = pool_index[t]
          clone_target = pools[index]['clone_target'] || config['clone_target']
          next unless config.key?('backend_weight')

          weight = config['backend_weight'][clone_target]
          if weight
            weighted_pools[t] = weight
          end
        end

        if weighted_pools.count == template_backends.count
          pickup = Pickup.new(weighted_pools)
          selection = pickup.pick
          template_backends.delete(selection)
          template_backends.unshift(selection)
        else
          first = template_backends.sample
          template_backends.delete(first)
          template_backends.unshift(first)
        end
      end

      checkoutlock.synchronize do
        template_backends.each do |template_backend|
          vms = backend.smembers("vmpooler__ready__#{template_backend}")
          next if vms.empty?

          vms.reverse.each do |vm|
            ready = vm_ready?(vm, config['domain'])
            if ready
              smoved = backend.smove("vmpooler__ready__#{template_backend}", "vmpooler__running__#{template_backend}", vm)
              if smoved
                return [vm, template_backend, template]
              else
                metrics.increment("checkout.smove.failed.#{template_backend}")
                return [nil, nil, nil]
              end
            else
              backend.smove("vmpooler__ready__#{template_backend}", "vmpooler__completed__#{template_backend}", vm)
              metrics.increment("checkout.nonresponsive.#{template_backend}")
            end
          end
        end
        [nil, nil, nil]
      end
    end

    def return_vm_to_ready_state(template, vm)
      backend.srem("vmpooler__migrating__#{template}", vm)
      backend.hdel("vmpooler__active__#{template}", vm)
      backend.hdel("vmpooler__vm__#{vm}", 'checkout', 'token:token', 'token:user')
      backend.smove("vmpooler__running__#{template}", "vmpooler__ready__#{template}", vm)
    end

    def account_for_starting_vm(template, vm)
      user = backend.hget("vmpooler__token__#{request.env['HTTP_X_AUTH_TOKEN']}", 'user')
      has_token_result = has_token?
      backend.sadd("vmpooler__migrating__#{template}", vm)
      backend.hset("vmpooler__active__#{template}", vm, Time.now)
      backend.hset("vmpooler__vm__#{vm}", 'checkout', Time.now)

      if Vmpooler::API.settings.config[:auth] and has_token_result
        backend.hset("vmpooler__vm__#{vm}", 'token:token', request.env['HTTP_X_AUTH_TOKEN'])
        backend.hset("vmpooler__vm__#{vm}", 'token:user', user)

        if config['vm_lifetime_auth'].to_i > 0
          backend.hset("vmpooler__vm__#{vm}", 'lifetime', config['vm_lifetime_auth'].to_i)
        end
      end
    end

    def update_result_hosts(result, template, vm)
      result[template] ||= {}
      if result[template]['hostname']
        result[template]['hostname'] = Array(result[template]['hostname'])
        result[template]['hostname'].push(vm)
      else
        result[template]['hostname'] = vm
      end
    end

    def atomically_allocate_vms(payload)
      result = { 'ok' => false }
      failed = false
      vms = []

      validate_token(backend) if Vmpooler::API.settings.config[:auth] and has_token?

      payload.each do |requested, count|
        count.to_i.times do |_i|
          vmname, vmpool, vmtemplate = fetch_single_vm(requested)
          if vmname
            account_for_starting_vm(vmpool, vmname)
            vms << [vmpool, vmname, vmtemplate]
            metrics.increment("checkout.success.#{vmpool}")
            update_user_metrics('allocate', vmname) if Vmpooler::API.settings.config[:config]['usage_stats']
          else
            failed = true
            metrics.increment("checkout.empty.#{requested}")
            break
          end
        end
      end

      if failed
        vms.each do |(vmpool, vmname, _vmtemplate)|
          return_vm_to_ready_state(vmpool, vmname)
        end
        status 503
      else
        vms.each do |(_vmpool, vmname, vmtemplate)|
          update_result_hosts(result, vmtemplate, vmname)
        end

        result['ok'] = true
        result['domain'] = config['domain'] if config['domain']
      end

      result
    end

    def update_user_metrics(operation, vmname)
      backend.multi
      backend.hget("vmpooler__vm__#{vmname}", 'tag:jenkins_build_url')
      backend.hget("vmpooler__vm__#{vmname}", 'token:user')
      backend.hget("vmpooler__vm__#{vmname}", 'template')
      jenkins_build_url, user, poolname = backend.exec

      if user
        user = user.gsub('.', '_')
      else
        user = 'unauthenticated'
      end
      metrics.increment("user.#{user}.#{operation}.#{poolname}")

      if jenkins_build_url
        if jenkins_build_url.include? 'litmus'
          # Very simple filter for Litmus jobs - just count them coming through for the moment.
          metrics.increment("usage_litmus.#{user}.#{operation}.#{poolname}")
          return
        end

        url_parts = jenkins_build_url.split('/')[2..-1]
        jenkins_instance = url_parts[0].gsub('.', '_')
        value_stream_parts = url_parts[2].split('_')
        value_stream_parts = value_stream_parts.map { |s| s.gsub('.', '_') }
        value_stream = value_stream_parts.shift
        branch = value_stream_parts.pop
        project = value_stream_parts.shift
        job_name = value_stream_parts.join('_')
        build_metadata_parts = url_parts[3]
        component_to_test = component_to_test('RMM_COMPONENT_TO_TEST_NAME', build_metadata_parts)

        metrics.increment("usage_jenkins_instance.#{jenkins_instance}.#{value_stream}.#{operation}.#{poolname}")
        metrics.increment("usage_branch_project.#{branch}.#{project}.#{operation}.#{poolname}")
        metrics.increment("usage_job_component.#{job_name}.#{component_to_test}.#{operation}.#{poolname}")
      end
    rescue StandardError => e
      puts 'd', "[!] [#{poolname}] failed while evaluating usage labels on '#{vmname}' with an error: #{e}"
    end

    def update_pool_size(payload)
      result = { 'ok' => false }

      pool_index = pool_index(pools)
      pools_updated = 0
      sync_pool_sizes

      payload.each do |poolname, size|
        unless pools[pool_index[poolname]]['size'] == size.to_i
          pools[pool_index[poolname]]['size'] = size.to_i
          backend.hset('vmpooler__config__poolsize', poolname, size)
          pools_updated += 1
          status 201
        end
      end
      status 200 unless pools_updated > 0
      result['ok'] = true
      result
    end

    def update_pool_template(payload)
      result = { 'ok' => false }

      pool_index = pool_index(pools)
      pools_updated = 0
      sync_pool_templates

      payload.each do |poolname, template|
        unless pools[pool_index[poolname]]['template'] == template
          pools[pool_index[poolname]]['template'] = template
          backend.hset('vmpooler__config__template', poolname, template)
          pools_updated += 1
          status 201
        end
      end
      status 200 unless pools_updated > 0
      result['ok'] = true
      result
    end

    def reset_pool(payload)
      result = { 'ok' => false }

      payload.each do |poolname, _count|
        backend.sadd('vmpooler__poolreset', poolname)
      end
      status 201
      result['ok'] = true
      result
    end

    def update_clone_target(payload)
      result = { 'ok' => false }

      pool_index = pool_index(pools)
      pools_updated = 0
      sync_clone_targets

      payload.each do |poolname, clone_target|
        unless pools[pool_index[poolname]]['clone_target'] == clone_target
          pools[pool_index[poolname]]['clone_target'] = clone_target
          backend.hset('vmpooler__config__clone_target', poolname, clone_target)
          pools_updated += 1
          status 201
        end
      end
      status 200 unless pools_updated > 0
      result['ok'] = true
      result
    end

    def sync_pool_templates
      pool_index = pool_index(pools)
      template_configs = backend.hgetall('vmpooler__config__template')
      template_configs&.each do |poolname, template|
        next unless pool_index.include? poolname

        pools[pool_index[poolname]]['template'] = template
      end
    end

    def sync_pool_sizes
      pool_index = pool_index(pools)
      poolsize_configs = backend.hgetall('vmpooler__config__poolsize')
      poolsize_configs&.each do |poolname, size|
        next unless pool_index.include? poolname

        pools[pool_index[poolname]]['size'] = size.to_i
      end
    end

    def sync_clone_targets
      pool_index = pool_index(pools)
      clone_target_configs = backend.hgetall('vmpooler__config__clone_target')
      clone_target_configs&.each do |poolname, clone_target|
        next unless pool_index.include? poolname

        pools[pool_index[poolname]]['clone_target'] = clone_target
      end
    end

    def too_many_requested?(payload)
      payload&.each do |poolname, count|
        next unless count.to_i > config['max_ondemand_instances_per_request']

        metrics.increment("ondemandrequest_fail.toomanyrequests.#{poolname}")
        return true
      end
      false
    end

    def generate_ondemand_request(payload)
      result = { 'ok': false }

      requested_instances = payload.reject { |k, _v| k == 'request_id' }
      if too_many_requested?(requested_instances)
        result['message'] = "requested amount of instances exceeds the maximum #{config['max_ondemand_instances_per_request']}"
        status 403
        return result
      end

      score = Time.now.to_i
      request_id = payload['request_id']
      request_id ||= generate_request_id
      result['request_id'] = request_id

      if backend.exists?("vmpooler__odrequest__#{request_id}")
        result['message'] = "request_id '#{request_id}' has already been created"
        status 409
        metrics.increment('ondemandrequest_generate.duplicaterequests')
        return result
      end

      status 201

      platforms_with_aliases = []
      requested_instances.each do |poolname, count|
        selection = evaluate_template_aliases(poolname, count)
        selection.map { |selected_pool, selected_pool_count| platforms_with_aliases << "#{poolname}:#{selected_pool}:#{selected_pool_count}" }
      end
      platforms_string = platforms_with_aliases.join(',')

      return result unless backend.zadd('vmpooler__provisioning__request', score, request_id)

      backend.hset("vmpooler__odrequest__#{request_id}", 'requested', platforms_string)
      if Vmpooler::API.settings.config[:auth] and has_token?
        backend.hset("vmpooler__odrequest__#{request_id}", 'token:token', request.env['HTTP_X_AUTH_TOKEN'])
        backend.hset("vmpooler__odrequest__#{request_id}", 'token:user',
                     backend.hget("vmpooler__token__#{request.env['HTTP_X_AUTH_TOKEN']}", 'user'))
      end

      result['domain'] = config['domain'] if config['domain']
      result[:ok] = true
      metrics.increment('ondemandrequest_generate.success')
      result
    end

    def generate_request_id
      SecureRandom.uuid
    end

    get '/' do
      sync_pool_sizes
      redirect to('/dashboard/')
    end

    # Provide run-time statistics
    #
    # Example:
    #
    # {
    #   "boot": {
    #     "duration": {
    #       "average": 163.6,
    #       "min": 65.49,
    #       "max": 830.07,
    #       "total": 247744.71000000002
    #     },
    #     "count": {
    #       "total": 1514
    #     }
    #   },
    #   "capacity": {
    #     "current": 968,
    #     "total": 975,
    #     "percent": 99.3
    #   },
    #   "clone": {
    #     "duration": {
    #       "average": 17.0,
    #       "min": 4.66,
    #       "max": 637.96,
    #       "total": 25634.15
    #     },
    #     "count": {
    #       "total": 1507
    #     }
    #   },
    #   "queue": {
    #     "pending": 12,
    #     "cloning": 0,
    #     "booting": 12,
    #     "ready": 968,
    #     "running": 367,
    #     "completed": 0,
    #     "total": 1347
    #   },
    #   "pools": {
    #     "ready": 100,
    #     "running": 120,
    #     "pending": 5,
    #     "max": 250,
    #   }
    #   "status": {
    #     "ok": true,
    #     "message": "Battle station fully armed and operational.",
    #     "empty": [ # NOTE: would not have 'ok: true' w/ "empty" pools
    #       "redhat-7-x86_64",
    #       "ubuntu-1404-i386"
    #     ],
    #     "uptime": 179585.9
    # }
    #
    # If the query parameter 'view' is provided, it will be used to select which top level
    # element to compute and return. Select them by specifying them in a comma separated list.
    # For example /status?view=capacity,boot
    # would return only the "capacity" and "boot" statistics. "status" is always returned

    get "#{api_prefix}/status/?" do
      content_type :json

      if params[:view]
        views = params[:view].split(",")
      end

      result = {
        status: {
          ok: true,
          message: 'Battle station fully armed and operational.'
        }
      }

      sync_pool_sizes

      result[:capacity] = get_capacity_metrics(pools, backend) unless views and not views.include?("capacity")
      result[:queue] = get_queue_metrics(pools, backend) unless views and not views.include?("queue")
      result[:clone] = get_task_metrics(backend, 'clone', Date.today.to_s) unless views and not views.include?("clone")
      result[:boot] = get_task_metrics(backend, 'boot', Date.today.to_s) unless views and not views.include?("boot")

      # Check for empty pools
      result[:pools] = {} unless views and not views.include?("pools")
      ready_hash = get_list_across_pools_redis_scard(pools, 'vmpooler__ready__', backend)
      running_hash = get_list_across_pools_redis_scard(pools, 'vmpooler__running__', backend)
      pending_hash = get_list_across_pools_redis_scard(pools, 'vmpooler__pending__', backend)
      lastBoot_hash = get_list_across_pools_redis_hget(pools, 'vmpooler__lastboot', backend)

      unless views and not views.include?("pools")
        pools.each do |pool|
          # REMIND: move this out of the API and into the back-end
          ready    = ready_hash[pool['name']]
          running  = running_hash[pool['name']]
          pending  = pending_hash[pool['name']]
          max      = pool['size']
          lastBoot = lastBoot_hash[pool['name']]
          aka      = pool['alias']

          result[:pools][pool['name']] = {
            ready: ready,
            running: running,
            pending: pending,
            max: max,
            lastBoot: lastBoot
          }

          if aka
            result[:pools][pool['name']][:alias] = aka
          end

          # for backwards compatibility, include separate "empty" stats in "status" block
          if ready == 0 && max != 0
            result[:status][:empty] ||= []
            result[:status][:empty].push(pool['name'])

            result[:status][:ok] = false
            result[:status][:message] = "Found #{result[:status][:empty].length} empty pools."
          end
        end
      end

      result[:status][:uptime] = (Time.now - Vmpooler::API.settings.config[:uptime]).round(1) if Vmpooler::API.settings.config[:uptime]

      JSON.pretty_generate(Hash[result.sort_by { |k, _v| k }])
    end

    # request statistics for specific pools by passing parameter 'pool'
    # with a coma separated list of pools we want to query ?pool=ABC,DEF
    # returns the ready, max numbers and the aliases (if set)
    get "#{api_prefix}/poolstat/?" do
      content_type :json

      result = {}

      poolscopy = []

      if params[:pool]
        subpool = params[:pool].split(",")
        poolscopy = pools.select do |p|
          if subpool.include?(p['name'])
            true
          elsif !p['alias'].nil?
            if p['alias'].instance_of?(Array)
              (p['alias'] & subpool).any?
            elsif p['alias'].instance_of?(String)
              subpool.include?(p['alias'])
            end
          end
        end
      end

      result[:pools] = {}

      poolscopy.each do |pool|
        result[:pools][pool['name']] = {}

        max      = pool['size']
        aka      = pool['alias']

        result[:pools][pool['name']][:max] = max

        if aka
          result[:pools][pool['name']][:alias] = aka
        end
      end

      ready_hash = get_list_across_pools_redis_scard(poolscopy, 'vmpooler__ready__', backend)

      ready_hash.each { |k, v| result[:pools][k][:ready] = v }

      JSON.pretty_generate(Hash[result.sort_by { |k, _v| k }])
    end

    # requests the total number of running VMs
    get "#{api_prefix}/totalrunning/?" do
      content_type :json
      queue = {
          running: 0
      }

      queue[:running] = get_total_across_pools_redis_scard(pools, 'vmpooler__running__', backend)

      JSON.pretty_generate(queue)
    end

    get "#{api_prefix}/summary/?" do
      content_type :json

      result = {
        daily: []
      }

      from_param = params[:from] || Date.today.to_s
      to_param   = params[:to]   || Date.today.to_s

      # Validate date formats
      [from_param, to_param].each do |param|
        if !validate_date_str(param.to_s)
          halt 400, "Invalid date format '#{param}', must match YYYY-MM-DD."
        end
      end

      from_date, to_date = Date.parse(from_param), Date.parse(to_param)

      if to_date < from_date
        halt 400, 'Date range is invalid, \'to\' cannot come before \'from\'.'
      elsif from_date > Date.today
        halt 400, 'Date range is invalid, \'from\' must be in the past.'
      end

      boot = get_task_summary(backend, 'boot', from_date, to_date, :bypool => true)
      clone = get_task_summary(backend, 'clone', from_date, to_date, :bypool => true)
      tag = get_tag_summary(backend, from_date, to_date)

      result[:boot] = boot[:boot]
      result[:clone] = clone[:clone]
      result[:tag] = tag[:tag]

      daily = {}

      boot[:daily].each do |day|
        daily[day[:date]] ||= {}
        daily[day[:date]][:boot] = day[:boot]
      end

      clone[:daily].each do |day|
        daily[day[:date]] ||= {}
        daily[day[:date]][:clone] = day[:clone]
      end

      tag[:daily].each do |day|
        daily[day[:date]] ||= {}
        daily[day[:date]][:tag] = day[:tag]
      end

      daily.each_key do |day|
        result[:daily].push({
          date: day,
          boot: daily[day][:boot],
          clone: daily[day][:clone],
          tag: daily[day][:tag]
        })
      end

      JSON.pretty_generate(result)
    end

    get "#{api_prefix}/summary/:route/?:key?/?" do
      content_type :json

      result = {}

      from_param = params[:from] || Date.today.to_s
      to_param   = params[:to]   || Date.today.to_s

      # Validate date formats
      [from_param, to_param].each do |param|
        if !validate_date_str(param.to_s)
          halt 400, "Invalid date format '#{param}', must match YYYY-MM-DD."
        end
      end

      from_date, to_date = Date.parse(from_param), Date.parse(to_param)

      if to_date < from_date
        halt 400, 'Date range is invalid, \'to\' cannot come before \'from\'.'
      elsif from_date > Date.today
        halt 400, 'Date range is invalid, \'from\' must be in the past.'
      end

      case params[:route]
        when 'boot'
          result = get_task_summary(backend, 'boot', from_date, to_date, :bypool => true, :only => params[:key])
        when 'clone'
          result = get_task_summary(backend, 'clone', from_date, to_date, :bypool => true, :only => params[:key])
        when 'tag'
          result = get_tag_summary(backend, from_date, to_date, :only => params[:key])
        else
          halt 404, JSON.pretty_generate({ 'ok' => false })
      end

      JSON.pretty_generate(result)
    end

    get "#{api_prefix}/token/?" do
      content_type :json

      status 404
      result = { 'ok' => false }

      if Vmpooler::API.settings.config[:auth]
        status 401

        need_auth!

        backend.keys('vmpooler__token__*').each do |key|
          data = backend.hgetall(key)

          if data['user'] == Rack::Auth::Basic::Request.new(request.env).username
            token = key.split('__').last

            result[token] ||= {}

            result[token]['created'] = data['created']
            result[token]['last'] = data['last'] || 'never'

            result['ok'] = true
          end
        end

        if result['ok']
          status 200
        else
          status 404
        end
      end

      JSON.pretty_generate(result)
    end

    get "#{api_prefix}/token/:token/?" do
      content_type :json

      status 404
      result = { 'ok' => false }

      if Vmpooler::API.settings.config[:auth]
        token = backend.hgetall("vmpooler__token__#{params[:token]}")

        if not token.nil? and not token.empty?
          status 200

          pools.each do |pool|
            backend.smembers("vmpooler__running__#{pool['name']}").each do |vm|
              if backend.hget("vmpooler__vm__#{vm}", 'token:token') == params[:token]
                token['vms'] ||= {}
                token['vms']['running'] ||= []
                token['vms']['running'].push(vm)
              end
            end
          end

          result = { 'ok' => true, params[:token] => token }
        end
      end

      JSON.pretty_generate(result)
    end

    delete "#{api_prefix}/token/:token/?" do
      content_type :json

      status 404
      result = { 'ok' => false }

      if Vmpooler::API.settings.config[:auth]
        status 401

        need_auth!

        if backend.del("vmpooler__token__#{params[:token]}").to_i > 0
          status 200
          result['ok'] = true
        end
      end

      JSON.pretty_generate(result)
    end

    post "#{api_prefix}/token" do
      content_type :json

      status 404
      result = { 'ok' => false }

      if Vmpooler::API.settings.config[:auth]
        status 401

        need_auth!

        o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten
        result['token'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join

        backend.hset("vmpooler__token__#{result['token']}", 'user', @auth.username)
        backend.hset("vmpooler__token__#{result['token']}", 'created', Time.now)

        status 200
        result['ok'] = true
      end

      JSON.pretty_generate(result)
    end

    get "#{api_prefix}/vm/?" do
      content_type :json

      result = []

      pools.each do |pool|
        result.push(pool['name'])
      end

      JSON.pretty_generate(result)
    end

    post "#{api_prefix}/ondemandvm/?" do
      content_type :json
      metrics.increment('http_requests_vm_total.post.ondemand.requestid')

      need_token! if Vmpooler::API.settings.config[:auth]

      result = { 'ok' => false }

      begin
        payload = JSON.parse(request.body.read)

        if payload
          invalid = invalid_templates(payload.reject { |k, _v| k == 'request_id' })
          if invalid.empty?
            result = generate_ondemand_request(payload)
          else
            result[:bad_templates] = invalid
            invalid.each do |bad_template|
              metrics.increment("ondemandrequest_fail.invalid.#{bad_template}")
            end
            status 404
          end
        else
          metrics.increment('ondemandrequest_fail.invalid.unknown')
          status 404
        end
      rescue JSON::ParserError
        status 400
        result = {
          'ok' => false,
          'message' => 'JSON payload could not be parsed'
        }
      end

      JSON.pretty_generate(result)
    end

    post "#{api_prefix}/ondemandvm/:template/?" do
      content_type :json
      result = { 'ok' => false }
      metrics.increment('http_requests_vm_total.delete.ondemand.template')

      need_token! if Vmpooler::API.settings.config[:auth]

      payload = extract_templates_from_query_params(params[:template])

      if payload
        invalid = invalid_templates(payload.reject { |k, _v| k == 'request_id' })
        if invalid.empty?
          result = generate_ondemand_request(payload)
        else
          result[:bad_templates] = invalid
          invalid.each do |bad_template|
            metrics.increment("ondemandrequest_fail.invalid.#{bad_template}")
          end
          status 404
        end
      else
        metrics.increment('ondemandrequest_fail.invalid.unknown')
        status 404
      end

      JSON.pretty_generate(result)
    end

    get "#{api_prefix}/ondemandvm/:requestid/?" do
      content_type :json
      metrics.increment('http_requests_vm_total.get.ondemand.request')

      status 404
      result = check_ondemand_request(params[:requestid])

      JSON.pretty_generate(result)
    end

    delete "#{api_prefix}/ondemandvm/:requestid/?" do
      content_type :json
      need_token! if Vmpooler::API.settings.config[:auth]
      metrics.increment('http_requests_vm_total.delete.ondemand.request')

      status 404
      result = delete_ondemand_request(params[:requestid])

      JSON.pretty_generate(result)
    end

    post "#{api_prefix}/vm/?" do
      content_type :json
      result = { 'ok' => false }
      metrics.increment('http_requests_vm_total.post.vm.checkout')

      payload = JSON.parse(request.body.read)

      if payload
        invalid = invalid_templates(payload)
        if invalid.empty?
          result = atomically_allocate_vms(payload)
        else
          invalid.each do |bad_template|
            metrics.increment("checkout.invalid.#{bad_template}")
          end
          status 404
        end
      else
        metrics.increment('checkout.invalid.unknown')
        status 404
      end

      JSON.pretty_generate(result)
    end

    def extract_templates_from_query_params(params)
      payload = {}

      params.split('+').each do |template|
        payload[template] ||= 0
        payload[template] += 1
      end

      payload
    end

    def invalid_templates(payload)
      invalid = []
      payload.keys.each do |template|
        invalid << template unless pool_exists?(template)
      end
      invalid
    end

    def invalid_template_or_size(payload)
      invalid = []
      payload.each do |pool, size|
        invalid << pool unless pool_exists?(pool)
        unless is_integer?(size)
          invalid << pool
          next
        end
        invalid << pool unless Integer(size) >= 0
      end
      invalid
    end

    def invalid_template_or_path(payload)
      invalid = []
      payload.each do |pool, template|
        invalid << pool unless pool_exists?(pool)
        invalid << pool unless template.include? '/'
        invalid << pool if template[0] == '/'
        invalid << pool if template[-1] == '/'
      end
      invalid
    end

    def invalid_pool(payload)
      invalid = []
      payload.each do |pool, _clone_target|
        invalid << pool unless pool_exists?(pool)
      end
      invalid
    end

    def check_ondemand_request(request_id)
      result = { 'ok' => false }
      request_hash = backend.hgetall("vmpooler__odrequest__#{request_id}")
      if request_hash.empty?
        result['message'] = "no request found for request_id '#{request_id}'"
        return result
      end

      result['request_id'] = request_id
      result['ready'] = false
      result['ok'] = true
      status 202

      case request_hash['status']
      when 'ready'
        result['ready'] = true
        Parsing.get_platform_pool_count(request_hash['requested']) do |platform_alias, pool, _count|
          instances = backend.smembers("vmpooler__#{request_id}__#{platform_alias}__#{pool}")

          if result.key?(platform_alias)
            result[platform_alias][:hostname] = result[platform_alias][:hostname] + instances
          else
            result[platform_alias] = { 'hostname': instances }
          end
        end
        result['domain'] = config['domain'] if config['domain']
        status 200
      when 'failed'
        result['message'] = "The request failed to provision instances within the configured ondemand_request_ttl '#{config['ondemand_request_ttl']}'"
        status 200
      when 'deleted'
        result['message'] = 'The request has been deleted'
        status 200
      else
        Parsing.get_platform_pool_count(request_hash['requested']) do |platform_alias, pool, count|
          instance_count = backend.scard("vmpooler__#{request_id}__#{platform_alias}__#{pool}")
          instances_pending = count.to_i - instance_count.to_i

          if result.key?(platform_alias) && result[platform_alias].key?(:ready)
            result[platform_alias][:ready] = (result[platform_alias][:ready].to_i + instance_count).to_s
            result[platform_alias][:pending] = (result[platform_alias][:pending].to_i + instances_pending).to_s
          else
            result[platform_alias] = {
              'ready': instance_count.to_s,
              'pending': instances_pending.to_s
            }
          end
        end
      end

      result
    end

    def delete_ondemand_request(request_id)
      result = { 'ok' => false }

      platforms = backend.hget("vmpooler__odrequest__#{request_id}", 'requested')
      unless platforms
        result['message'] = "no request found for request_id '#{request_id}'"
        return result
      end

      if backend.hget("vmpooler__odrequest__#{request_id}", 'status') == 'deleted'
        result['message'] = 'the request has already been deleted'
      else
        backend.hset("vmpooler__odrequest__#{request_id}", 'status', 'deleted')

        Parsing.get_platform_pool_count(platforms) do |platform_alias, pool, _count|
          backend.smembers("vmpooler__#{request_id}__#{platform_alias}__#{pool}")&.each do |vm|
            backend.smove("vmpooler__running__#{pool}", "vmpooler__completed__#{pool}", vm)
          end
          backend.del("vmpooler__#{request_id}__#{platform_alias}__#{pool}")
        end
        backend.expire("vmpooler__odrequest__#{request_id}", 129_600_0)
      end
      status 200
      result['ok'] = true
      result
    end

    post "#{api_prefix}/vm/:template/?" do
      content_type :json
      result = { 'ok' => false }
      metrics.increment('http_requests_vm_total.get.vm.template')

      payload = extract_templates_from_query_params(params[:template])

      if payload
        invalid = invalid_templates(payload)
        if invalid.empty?
          result = atomically_allocate_vms(payload)
        else
          invalid.each do |bad_template|
            metrics.increment("checkout.invalid.#{bad_template}")
          end
          status 404
        end
      else
        metrics.increment('checkout.invalid.unknown')
        status 404
      end

      JSON.pretty_generate(result)
    end

    get "#{api_prefix}/vm/:hostname/?" do
      content_type :json
      metrics.increment('http_requests_vm_total.get.vm.hostname')

      result = {}

      status 404
      result['ok'] = false

      params[:hostname] = hostname_shorten(params[:hostname], config['domain'])

      rdata = backend.hgetall("vmpooler__vm__#{params[:hostname]}")
      unless rdata.empty?
        status 200
        result['ok'] = true

        result[params[:hostname]] = {}

        result[params[:hostname]]['template'] = rdata['template']
        result[params[:hostname]]['lifetime'] = (rdata['lifetime'] || config['vm_lifetime']).to_i

        if rdata['destroy']
          result[params[:hostname]]['running'] = ((Time.parse(rdata['destroy']) - Time.parse(rdata['checkout'])) / 60 / 60).round(2) if rdata['checkout']
          result[params[:hostname]]['state'] = 'destroyed'
        elsif rdata['checkout']
          result[params[:hostname]]['running'] = ((Time.now - Time.parse(rdata['checkout'])) / 60 / 60).round(2)
          result[params[:hostname]]['remaining'] = ((Time.parse(rdata['checkout']) + rdata['lifetime'].to_i*60*60 - Time.now) / 60 / 60).round(2)
          result[params[:hostname]]['start_time'] = Time.parse(rdata['checkout']).to_datetime.rfc3339
          result[params[:hostname]]['end_time'] = (Time.parse(rdata['checkout']) + rdata['lifetime'].to_i*60*60).to_datetime.rfc3339
          result[params[:hostname]]['state'] = 'running'
        elsif rdata['check']
          result[params[:hostname]]['state'] = 'ready'
        else
          result[params[:hostname]]['state'] = 'pending'
        end

        rdata.keys.each do |key|
          if key.match('^tag\:(.+?)$')
            result[params[:hostname]]['tags'] ||= {}
            result[params[:hostname]]['tags'][$1] = rdata[key]
          end

          if key.match('^snapshot\:(.+?)$')
            result[params[:hostname]]['snapshots'] ||= []
            result[params[:hostname]]['snapshots'].push($1)
          end
        end

        if rdata['disk']
          result[params[:hostname]]['disk'] = rdata['disk'].split(':')
        end

        # Look up IP address of the hostname
        begin
          ipAddress = TCPSocket.gethostbyname(params[:hostname])[3]
        rescue StandardError
          ipAddress = ""
        end

        result[params[:hostname]]['ip'] = ipAddress

        if config['domain']
          result[params[:hostname]]['domain'] = config['domain']
        end

        result[params[:hostname]]['host'] = rdata['host'] if rdata['host']
        result[params[:hostname]]['migrated'] = rdata['migrated'] if rdata['migrated']

      end

      JSON.pretty_generate(result)
    end

    delete "#{api_prefix}/vm/:hostname/?" do
      content_type :json
      metrics.increment('http_requests_vm_total.delete.vm.hostname')

      result = {}

      status 404
      result['ok'] = false

      params[:hostname] = hostname_shorten(params[:hostname], config['domain'])

      rdata = backend.hgetall("vmpooler__vm__#{params[:hostname]}")
      unless rdata.empty?
        need_token! if rdata['token:token']

        if backend.srem("vmpooler__running__#{rdata['template']}", params[:hostname])
          backend.sadd("vmpooler__completed__#{rdata['template']}", params[:hostname])

          status 200
          result['ok'] = true
          metrics.increment('delete.success')
          update_user_metrics('destroy', params[:hostname]) if Vmpooler::API.settings.config[:config]['usage_stats']
        else
          metrics.increment('delete.failed')
        end
      end

      JSON.pretty_generate(result)
    end

    put "#{api_prefix}/vm/:hostname/?" do
      content_type :json
      metrics.increment('http_requests_vm_total.put.vm.modify')

      status 404
      result = { 'ok' => false }

      failure = []

      params[:hostname] = hostname_shorten(params[:hostname], config['domain'])

      if backend.exists?("vmpooler__vm__#{params[:hostname]}")
        begin
          jdata = JSON.parse(request.body.read)
        rescue StandardError
          halt 400, JSON.pretty_generate(result)
        end

        # Validate data payload
        jdata.each do |param, arg|
          case param
            when 'lifetime'
              need_token! if Vmpooler::API.settings.config[:auth]

              # in hours, defaults to one week
              max_lifetime_upper_limit = config['max_lifetime_upper_limit']
              if max_lifetime_upper_limit
                max_lifetime_upper_limit = max_lifetime_upper_limit.to_i
                if arg.to_i >= max_lifetime_upper_limit
                  failure.push("You provided a lifetime (#{arg}) that exceeds the configured maximum of #{max_lifetime_upper_limit}.")
                end
              end

              # validate lifetime is within boundaries
              unless arg.to_i > 0
                failure.push("You provided a lifetime (#{arg}) but you must provide a positive number.")
              end

            when 'tags'
              failure.push("You provided tags (#{arg}) as something other than a hash.") unless arg.is_a?(Hash)
              failure.push("You provided unsuppored tags (#{arg}).") if config['allowed_tags'] && !(arg.keys - config['allowed_tags']).empty?
            else
              failure.push("Unknown argument #{arg}.")
          end
        end

        if !failure.empty?
          status 400
          result['failure'] = failure
        else
          jdata.each do |param, arg|
            case param
              when 'lifetime'
                need_token! if Vmpooler::API.settings.config[:auth]

                arg = arg.to_i

                backend.hset("vmpooler__vm__#{params[:hostname]}", param, arg)
              when 'tags'
                filter_tags(arg)
                export_tags(backend, params[:hostname], arg)
            end
          end

          status 200
          result['ok'] = true
        end
      end

      JSON.pretty_generate(result)
    end

    post "#{api_prefix}/vm/:hostname/disk/:size/?" do
      content_type :json
      metrics.increment('http_requests_vm_total.post.vm.disksize')

      need_token! if Vmpooler::API.settings.config[:auth]

      status 404
      result = { 'ok' => false }

      params[:hostname] = hostname_shorten(params[:hostname], config['domain'])

      if ((params[:size].to_i > 0 )and (backend.exists?("vmpooler__vm__#{params[:hostname]}")))
        result[params[:hostname]] = {}
        result[params[:hostname]]['disk'] = "+#{params[:size]}gb"

        backend.sadd('vmpooler__tasks__disk', "#{params[:hostname]}:#{params[:size]}")

        status 202
        result['ok'] = true
      end

      JSON.pretty_generate(result)
    end

    post "#{api_prefix}/vm/:hostname/snapshot/?" do
      content_type :json
      metrics.increment('http_requests_vm_total.post.vm.snapshot')

      need_token! if Vmpooler::API.settings.config[:auth]

      status 404
      result = { 'ok' => false }

      params[:hostname] = hostname_shorten(params[:hostname], config['domain'])

      if backend.exists?("vmpooler__vm__#{params[:hostname]}")
        result[params[:hostname]] = {}

        o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten
        result[params[:hostname]]['snapshot'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join

        backend.sadd('vmpooler__tasks__snapshot', "#{params[:hostname]}:#{result[params[:hostname]]['snapshot']}")

        status 202
        result['ok'] = true
      end

      JSON.pretty_generate(result)
    end

    post "#{api_prefix}/vm/:hostname/snapshot/:snapshot/?" do
      content_type :json
      metrics.increment('http_requests_vm_total.post.vm.disksize')

      need_token! if Vmpooler::API.settings.config[:auth]

      status 404
      result = { 'ok' => false }

      params[:hostname] = hostname_shorten(params[:hostname], config['domain'])

      unless backend.hget("vmpooler__vm__#{params[:hostname]}", "snapshot:#{params[:snapshot]}").to_i.zero?
        backend.sadd('vmpooler__tasks__snapshot-revert', "#{params[:hostname]}:#{params[:snapshot]}")

        status 202
        result['ok'] = true
      end

      JSON.pretty_generate(result)
    end

    post "#{api_prefix}/config/poolsize/?" do
      content_type :json
      result = { 'ok' => false }

      if config['experimental_features']
        need_token! if Vmpooler::API.settings.config[:auth]

        payload = JSON.parse(request.body.read)

        if payload
          invalid = invalid_template_or_size(payload)
          if invalid.empty?
            result = update_pool_size(payload)
          else
            invalid.each do |bad_template|
              metrics.increment("config.invalid.#{bad_template}")
            end
            result[:not_configured] = invalid
            status 400
          end
        else
          metrics.increment('config.invalid.unknown')
          status 404
        end
      else
        status 405
      end

      JSON.pretty_generate(result)
    end

    post "#{api_prefix}/config/pooltemplate/?" do
      content_type :json
      result = { 'ok' => false }

      if config['experimental_features']
        need_token! if Vmpooler::API.settings.config[:auth]

        payload = JSON.parse(request.body.read)

        if payload
          invalid = invalid_template_or_path(payload)
          if invalid.empty?
            result = update_pool_template(payload)
          else
            invalid.each do |bad_template|
              metrics.increment("config.invalid.#{bad_template}")
            end
            result[:bad_templates] = invalid
            status 400
          end
        else
          metrics.increment('config.invalid.unknown')
          status 404
        end
      else
        status 405
      end

      JSON.pretty_generate(result)
    end

    post "#{api_prefix}/poolreset/?" do
      content_type :json
      result = { 'ok' => false }

      if config['experimental_features']
        need_token! if Vmpooler::API.settings.config[:auth]

        begin
          payload = JSON.parse(request.body.read)
          if payload
            invalid = invalid_templates(payload)
            if invalid.empty?
              result = reset_pool(payload)
            else
              invalid.each do |bad_pool|
                metrics.increment("poolreset.invalid.#{bad_pool}")
              end
              result[:bad_pools] = invalid
              status 400
            end
          else
            metrics.increment('poolreset.invalid.unknown')
            status 404
          end
        rescue JSON::ParserError
          status 400
          result = {
            'ok' => false,
            'message' => 'JSON payload could not be parsed'
          }
        end
      else
        status 405
      end

      JSON.pretty_generate(result)
    end

    post "#{api_prefix}/config/clonetarget/?" do
      content_type :json
      result = { 'ok' => false }

      if config['experimental_features']
        need_token! if Vmpooler::API.settings.config[:auth]

        payload = JSON.parse(request.body.read)

        if payload
          invalid = invalid_pool(payload)
          if invalid.empty?
            result = update_clone_target(payload)
          else
            invalid.each do |bad_template|
              metrics.increment("config.invalid.#{bad_template}")
            end
            result[:bad_templates] = invalid
            status 400
          end
        else
          metrics.increment('config.invalid.unknown')
          status 404
        end
      else
        status 405
      end

      JSON.pretty_generate(result)
    end

    get "#{api_prefix}/config/?" do
      content_type :json
      result = { 'ok' => false }
      status 404

      if pools
        sync_pool_sizes
        sync_pool_templates

        pool_configuration = []
        pools.each do |pool|
          pool['template_ready'] = template_ready?(pool, backend)
          pool_configuration << pool
        end

        result = {
          pool_configuration: pool_configuration,
          status: {
            ok: true
          }
        }

        status 200
      end
      JSON.pretty_generate(result)
    end
  end
  end
end