# frozen_string_literal: true

require 'active_support/inflector'
require 'json'
require 'pathname'
require 'rest-client'

require 'jabber_admin/exceptions'
require 'jabber_admin/configuration'
require 'jabber_admin/commands'
require 'jabber_admin/api_call'
require 'jabber_admin/version'

# jabber_admin
#
# This gem allows making API calls to the ejabberd RESTful admin backend.  We
# support a bunch of predefined commands out of the box, just have a look at
# the +lib/jabber_admin/commands/+ directory or the readme file.
#
# All predefined commands can be called via +JabberAdmin.COMMAND!+ or via
# +JabberAdmin.COMMAND+.  The bang variant checks the result of the command
# (successful, or not) and raise a subclass of +JabberAdmin::Exception+ in case
# of issues. The non-bang variant just sends the commands in a fire and forget
# manner.
#
# When you're missing a command you want to use, you can use the
# +JabberAdmin::ApiCall+ class directly. It allows you to easily fulfill your
# custom needs with the power of error handling (if you like).
#
# You can also use your custom command directly on the +JabberAdmin+ module, in
# both banged and non-banged versions and we pass them as a shortcut to a new
# +JabberAdmin::ApiCall+ instance.
#
# @example Configure jabber_admin gem
#   JabberAdmin.configure do |config|
#     # The ejabberd REST API endpoint as a full URL.
#     # Take care of the path part, because this is individually
#     # configured on ejabberd. (See: https://bit.ly/2rBxatJ)
#     config.url = 'http://jabber.local/api'
#     # Provide here the full user JID in order to authenticate as
#     # a administrator.
#     config.username = 'admin@jabber.local'
#     # The password of the administrator account.
#     config.password = 'password'
#   end
#
# @example Restart the ejabberd service
#   JabberAdmin.restart!
#
# @example Register a new user to the XMPP service
#   JabberAdmin.register! user: 'peter@example.com', password: '123'
#
# @example Delete a user from the XMPP service, in fire and forget manner
#   JabberAdmin.unregister user: 'peter@example.com'
module JabberAdmin
  class << self
    attr_writer :configuration
  end

  # A simple getter to the global JabberAdmin configuration structure.
  #
  # @return [JabberAdmin::Configuration] the global JabberAdmin configuration
  def self.configuration
    @configuration ||= Configuration.new
  end

  # Class method to set and change the global configuration. This is just a
  # tapped variant of the +.configuration+ method.
  #
  # @yield [configuration]
  # @yieldparam [JabberAdmin::Configuration] configuration
  def self.configure
    yield(configuration)
  end

  # Allow an easy to use DSL on the +JabberAdmin+ module. We support predefined
  # (known) commands and unknown ones in bang and non-bang variants. This
  # allows maximum flexibility to the user. The bang versions perform the
  # response checks and raise in case of issues. The non-bang versions skip
  # this checks. For unknown commands the +JabberAdmin::ApiCall+ is directly
  # utilized with the method name as command. (Without the trailling bang, when
  # it is present)
  #
  # @param method [Symbol, String, #to_s] the name of the command to run
  # @param args all additional payload to pass down to the API call
  # @return [RestClient::Response] the actual response of the command
  def self.method_missing(method, *args)
    predefined_command(method).call(predefined_callable(method), *args)
  rescue NameError
    predefined_callable(method).call(method.to_s.chomp('!'), *args)
  end

  # Try to find the given name as a predefined command. When there is no such
  # predefined command, we raise a +NameError+.
  #
  # @param name [Symbol, String, #to_s] the command name to lookup
  # @return [Class] the predefined command class constant
  def self.predefined_command(name)
    # Remove bangs and build the camel case variant
    "JabberAdmin::Commands::#{name.to_s.chomp('!').camelize}".constantize
  end

  # Generate a matching API call wrapper for the given command name. When we
  # have to deal with a bang version, we pass the bang down to the API call
  # instance. Otherwise we just run the regular +#perform+ method on the API
  # call instance.
  #
  # @param name [Symbol, String, #to_s] the command name to match
  # @return [Proc] the API call wrapper
  def self.predefined_callable(name)
    method = name.to_s.end_with?('!') ? 'perform!' : 'perform'
    proc { |*args| ApiCall.send(method, *args) }
  end

  # Determine if a room exists. This is a convenience method for the
  # +JabberAdmin::Commands::GetRoomAffiliations+ command, which can be used
  # to reliably determine whether a room exists or not.
  #
  # @param room [String] the name of the room to check
  # @return [Boolean] whether the room exists or not
  def self.room_exist?(room)
    get_room_affiliations!(room: room)
    true
  rescue JabberAdmin::CommandError => e
    raise e unless /room does not exist/.match? e.response.body

    false
  end

  # We support all methods if you ask for. This is our dynamic command approach
  # here to support predefined and custom commands in the same namespace.
  #
  # @param method [String] the method to lookup
  # @param include_private [Boolean] allow the lookup of private methods
  # @return [Boolean] always +true+
  def self.respond_to_missing?(_method, _include_private = false)
    true
  end
end