# frozen_string_literal: true

module SidekiqUniqueJobs
  # Utility module for reducing the number of uses of logger.
  #
  # @author Mikael Henriksson <mikael@mhenrixon.com>
  module Logging
    def self.included(base)
      base.send(:extend, self)
    end

    #
    # A convenience method for using the configured gem logger
    #
    # @see SidekiqUniqueJobs#.logger
    #
    # @return [Logger]
    #
    def logger
      SidekiqUniqueJobs.logger
    end

    #
    # Logs a message at debug level
    #
    # @param [String, Exception] message_or_exception the message or exception to log
    #
    # @return [void]
    #
    # @yield [String, Exception] the message or exception to use for log message
    #
    def log_debug(message_or_exception = nil, item = nil, &block)
      return unless logging?

      message = build_message(message_or_exception, item)
      logger.debug(message, &block)
      nil
    end

    #
    # Logs a message at info level
    #
    # @param [String, Exception] message_or_exception the message or exception to log
    #
    # @return [void]
    #
    # @yield [String, Exception] the message or exception to use for log message
    #
    def log_info(message_or_exception = nil, item = nil, &block)
      return unless logging?

      message = build_message(message_or_exception, item)
      logger.info(message, &block)
      nil
    end

    #
    # Logs a message at warn level
    #
    # @param [String, Exception] message_or_exception the message or exception to log
    #
    # @return [void]
    #
    # @yield [String, Exception] the message or exception to use for log message
    #
    def log_warn(message_or_exception = nil, item = nil, &block)
      return unless logging?

      message = build_message(message_or_exception, item)
      logger.warn(message, &block)
      nil
    end

    #
    # Logs a message at error level
    #
    # @param [String, Exception] message_or_exception the message or exception to log
    #
    # @return [void]
    #
    # @yield [String, Exception] the message or exception to use for log message
    #
    def log_error(message_or_exception = nil, item = nil, &block)
      return unless logging?

      message = build_message(message_or_exception, item)
      logger.error(message, &block)
      nil
    end

    #
    # Logs a message at fatal level
    #
    # @param [String, Exception] message_or_exception the message or exception to log
    #
    # @return [void]
    #
    # @yield [String, Exception] the message or exception to use for log message
    #
    def log_fatal(message_or_exception = nil, item = nil, &block)
      return unless logging?

      message = build_message(message_or_exception, item)
      logger.fatal(message, &block)

      nil
    end

    #
    # Build a log message
    #
    # @param [String, Exception] message_or_exception an entry to log
    # @param [Hash] item the sidekiq job hash
    #
    # @return [String] a complete log entry
    #
    def build_message(message_or_exception, item = nil)
      return nil if message_or_exception.nil?
      return message_or_exception if item.nil?

      message = message_or_exception.dup
      details = item.slice(LOCK, QUEUE, CLASS, JID, LOCK_DIGEST).each_with_object([]) do |(key, value), memo|
        memo << "#{key}=#{value}"
      end
      message << " ("
      message << details.join(" ")
      message << ")"

      message
    end

    #
    # Wraps the middleware logic with context aware logging
    #
    #
    # @return [void]
    #
    # @yieldreturn [void] yield to the middleware instance
    #
    def with_logging_context
      with_configured_loggers_context do
        return yield
      end

      nil # Need to make sure we don't return anything here
    end

    #
    # Attempt to setup context aware logging for the given logger
    #
    #
    # @return [void]
    #
    # @yield
    #
    def with_configured_loggers_context(&block)
      logger_method.call(logging_context, &block)
    end

    #
    # Setup some variables to add to each log line
    #
    #
    # @return [Hash] the context to use for each log line
    #
    def logging_context
      raise NotImplementedError, "#{__method__} needs to be implemented in #{self.class}"
    end

    private

    #
    # A memoized method to use for setting up a logging context
    #
    #
    # @return [proc] the method to call
    #
    def logger_method
      @logger_method ||= sidekiq_context_method
      @logger_method ||= sidekiq_logger_context_method
      @logger_method ||= sidekiq_logging_context_method
      @logger_method ||= no_sidekiq_context_method
    end

    #
    # Checks if the logger respond to `with_context`.
    #
    # @note only used to remove the need for explicitly ignoring manual dispatch in other places.
    #
    #
    # @return [true,false]
    #
    def logger_respond_to_with_context?
      logger.respond_to?(:with_context)
    end

    #
    # Checks if the logger context takes a hash argument
    #
    # @note only used to remove the need for explicitly ignoring manual dispatch in other places.
    #
    #
    # @return [true,false]
    #
    def logger_context_hash?
      defined?(Sidekiq::Context) || logger_respond_to_with_context?
    end

    def sidekiq_context_method
      Sidekiq::Context.method(:with) if defined?(Sidekiq::Context)
    end

    def sidekiq_logger_context_method
      logger.method(:with_context) if logger_respond_to_with_context?
    end

    def sidekiq_logging_context_method
      Sidekiq::Logging.method(:with_context) if defined?(Sidekiq::Logging)
    end

    def no_sidekiq_context_method
      method(:fake_logger_context)
    end

    def fake_logger_context(_context)
      logger.warn "Don't know how to setup the logging context. Please open a feature request: " \
                  "https://github.com/mhenrixon/sidekiq-unique-jobs/issues/new?template=feature_request.md"

      yield
    end

    def logging?
      SidekiqUniqueJobs.logging?
    end
  end
end
