require 'httparty'
require './core/utils/logger_utils'
require './core/exceptions/heimdall_exceptions'
require 'smoca_common/utils/twitch_utils'

class HeimdallUtils

  @@api_url = 'http://qe-heimdall.us-west2.justin.tv'

  include LoggerUtils

  class << self
    # Returns the first available user from our Heimdall server for a given user_type, and also locks it.
    # @param user_type [UserType] The user type object to search for
    # @param opts [Hash] Optional parameters
    # @option opts [Int] :timeout The duration to wait for a user to become available
    # @return [String] The username to use
    def get_user(user_type, opts = {})
      defaults = {timeout: 0}
      defaults = defaults.merge(opts)
      timeout = defaults[:timeout]

      duration = 0
      while duration <= timeout do
        user = query_avail_user(user_type)
        if user
          logger.debug "[HeimdallUtils] [Available] User Type #{user_type.type}. Returned user: #{user}."
          return user
        else
          logger.debug "[HeimdallUtils] [Unavailable] User Type #{user_type.type}. Trying again."\
                       " Duration until Timeout: #{timeout - duration}"
          sleep(1) # Wait a second before making next request, so as not to spam server with API requests.
          duration += 1
        end
      end
      raise HeimdallExceptions::UserNotAvailable.new("Timed out querying Heimdall for #{user_type.type} after #{timeout}s", user_type)
    end

    # Locks a particular user_type -> user and returns a bool indicating if it was a success.
    # @param user_type [String] The type of user
    # @param user [String] The name of the user
    # @option opts [Int] :timeout The duration to wait for a user to become available
    # @return [Boolean] Whether lock was a success
    def lock_user(user_type, user, opts = {})
      return set_user_lock(user_type, user, false, opts)
    end

    # Unlocks a particular user_type -> user and returns a bool indicating if it was a success.
    # @param user_type [String] The type of user
    # @param user [String] The name of the user
    # @option opts [Int] :timeout The duration to wait for a user to become available
    # @return [Boolean] Whether lock was a success
    def unlock_user(user_type, user, opts = {})
      return set_user_lock(user_type, user, false, opts)
    end

    # @return [Boolean] Whether user specified they want Heimdall enabled
    def enabled?
      return ENV['HEIMDALL'] == 'true'
    end

    private

    # @return [String] The ENV Name for the Heimdall Endpoint
    def env_name
      prod_string    = 'production'
      staging_string = 'staging'
      environment_production? ? prod_string : staging_string
    end

    def query_avail_user(user_type)
      uuid = generate_uuid
      path = "#{@@api_url}/types/#{user_type.type}/available/#{env_name}?request_uuid=#{uuid}"

      logger.debug "[HeimdallUtils] -> #{path}"
      response = rescue_from_timeouts { HTTParty.put(path, body: build_metadata(user_type)) }
      logger.debug "[HeimdallUtils] <- (#{response.code}) | #{path}\n\t Response Body: #{response.body}"
      case response.code
        when 200
          data = parsed_response_body(response)
          if uuid_matches?(uuid, response) == false
            raise HeimdallExceptions::UUIDMismatch.new(uuid, uuid_response(response))
          end
          return data['username']
        when 404
          raise HeimdallExceptions::UserTypeNotFound.new("The User Type #{user_type.type} was not found in Heimdall."\
          ' Contact #qe-automation for assistance', user_type)
        else
          return nil
      end
    end

    # @param user [String]
    # @return [Boolean] Lock change was successful
    def set_user_lock(user_type, user, lock, opts = {})
      defaults = {timeout: 0}
      defaults = defaults.merge(opts)
      timeout = defaults[:timeout]
      duration = 0

      while duration <= timeout do
        begin
          response = put_user_lock(user_type, user, lock)
        rescue HeimdallExceptions::UUIDMismatch => e
          logger.warn("Encountered #{e.class}: #{e.message} while trying to unlock #{user_type}/#{user}. Retrying...")
          sleep(1)
          duration += 1
          next
        end

        return true if response.code == 200
        if response.code == 422 && user_already_unlocked?(parsed_response_body(response))
          logger.warn "#{user} was already unlocked when trying to unlock it"
          return true
        end

        logger.warn "[HeimdallUtils] Received #{response.code} while trying to unlock #{user_type}/#{user}. Retrying..."\
          " Duration: #{duration}s Timeout: #{timeout}s"
        sleep(1) # Wait a second before making next request, so as not to spam server with API requests.
        duration += 1
      end
      raise Exception.new("Timed out updating Heimdall for #{user_type}.#{user}.lock=#{lock} after #{timeout}s")
    end

    # @return [HTTParty::Response]
    def put_user_lock(user_type, user, lock)
      lock ? lock_str = 'lock' : lock_str = 'unlock'
      uuid = generate_uuid
      path = "#{@@api_url}/types/#{user_type}/users/#{user}/#{env_name}/#{lock_str}?request_uuid=#{uuid}"

      logger.info "[HeimdallUtils] Toggling #{lock_str} for #{user_type}/#{user}"
      logger.debug "[HeimdallUtils] -> #{path}"
      response = rescue_from_timeouts { HTTParty.put(path) }
      logger.debug "[HeimdallUtils] <- (#{response.code}) | #{path}\n\t Response Body: #{response.body}"

      if (response.code == 200 || response.code == 422) && !uuid_matches?(uuid, response)
        raise HeimdallExceptions::UUIDMismatch.new(uuid, uuid_response(response))
      end

      return response
    end

    # Checks a body to see if the user was already unlocked
    # @param body [Hash] The response body from the Heimdall lock endpoint
    # @return [Boolean] If the account was already unlocked
    def user_already_unlocked?(body)
      if body.has_key?('message')
        return body['message'] == 'Lock failed: User already unlocked'
      else
        logger.warn "[HeimdallUtils] Body did not have message key. #{body}"
        return false
      end
    end

    # Rescues from connection timeouts
    # @param block [Proc] The HTTParty request to be made
    # @return [HTTParty::Response] The response of the connection
    def rescue_from_timeouts(&block)
      attempts = 0
      max_attempts = 10

      begin
        attempts += 1
        return block.call
      rescue SocketError, Net::OpenTimeout => e
        if attempts < max_attempts
          logger.warn "[HeimdallUtils] Caught #{e.class}: #{e.message}. Attempt \##{attempts} Trying again in 30 seconds."
          sleep(30)
          retry
        else
          raise e
        end
      end
    end

    # @param user_type [UserType] The UserType to retrieve metadata for
    # @return [Hash] Metadata for Heimdall to record to the lock object
    def build_metadata(user_type)
      return {build_id: ENV['BUILD_NUMBER'],
              job_name: ENV['JOB_NAME'],
              thread_id: TwitchUtils.thread_number,
              test_id: user_type.rspec_id}
    end

    # For use in passing a unique ID to Heimdall requests. Helpful to trace nginx and rails logs
    # @return [String] A unique ID
    private def generate_uuid
      require 'securerandom'
      return SecureRandom.uuid
    end

    private def uuid_matches?(uuid, response)
      return uuid_response(response) == uuid
    end

    private def uuid_response(response)
      return response.dig('request_uuid')
    end

    # Cross compatible. Changes are being made to wrap the body contents in a body key
    private def parsed_response_body(response)
      response_body = response.parsed_response
      return response_body.has_key?('body') ? response_body['body'] : response_body
    end
  end
end
