# frozen_string_literal: true

require "faraday"
require "i18n"
require "redis-namespace"

require_relative "../configurable"
require_relative "../configuration_builder"
require_relative "../errors"
require_relative "../namespace"
require_relative "../template"
require_relative "../template_resolver"
require_relative "../timer"
require_relative "../version"

module Lita
  class Handler
    # Methods included in any class that includes at least one type of router.
    # @since 4.0.0
    module Common
      # Adds common functionality to the class and initializes the handler's configuration builder.
      def self.included(klass)
        klass.extend(ClassMethods)
        klass.extend(Configurable)
        klass.extend(Namespace)
        klass.configuration_builder = ConfigurationBuilder.new
      end

      # Common class-level methods for all handlers.
      module ClassMethods
        # Gets (and optionally sets) the directory where the handler's templates are stored.
        # @param path [String] If provided, sets the template root to this value.
        # @return [String] The template root path.
        # @raise [MissingTemplateRootError] If accessed without setting a value first.
        def template_root(path = nil)
          @template_root = path if path

          if defined?(@template_root)
            @template_root
          else
            raise MissingTemplateRootError
          end
        end

        # Returns the translation for a key, automatically namespaced to the handler.
        # @param key [String] The key of the translation.
        # @param hash [Hash] An optional hash of values to be interpolated in the string.
        # @return [String] The translated string.
        # @since 3.0.0
        def translate(key, hash = {})
          I18n.translate("lita.handlers.#{namespace}.#{key}", **hash)
        end

        alias t translate

        # Logs an error raised by a plugin.
        def log_error(robot, error, metadata = {})
          error_handler = robot.config.robot.error_handler

          if error_handler.arity == 2
            error_handler.call(error, metadata.merge(robot: robot))
          else
            error_handler.call(error)
          end

          robot.logger.error I18n.t(
            "lita.handler.exception",
            handler: name,
            message: error.message,
            backtrace: error.backtrace.join("\n")
          )

          raise error if Lita.test_mode?
        end
      end

      # A Redis::Namespace scoped to the handler.
      # @return [Redis::Namespace]
      attr_reader :redis

      # The running {Robot} instance.
      # @return [Robot]
      attr_reader :robot

      # @param robot [Robot] The currently running robot.
      def initialize(robot)
        @robot = robot
        @redis = Redis::Namespace.new(redis_namespace, redis: robot.redis)
      end

      # Invokes the given block after the given number of seconds.
      # @param interval [Integer] The number of seconds to wait before invoking the block.
      # @yieldparam timer [Timer] The current {Timer} instance.
      # @return [void]
      # @since 3.0.0
      def after(interval, &block)
        robot.run_concurrently { Timer.new(interval: interval, &block).start }
      end

      # The handler's configuration object.
      # @return [Configuration, Config] The handler's configuration object.
      # @since 3.2.0
      def config
        if robot.config.handlers.respond_to?(self.class.namespace)
          robot.config.handlers.public_send(self.class.namespace)
        end
      end

      # Invokes the given block repeatedly, waiting the given number of seconds between each
      # invocation.
      # @param interval [Integer] The number of seconds to wait before each invocation of the block.
      # @yieldparam timer [Timer] The current {Timer} instance.
      # @return [void]
      # @note The block should call {Timer#stop} at a terminating condition to avoid infinite
      #   recursion.
      # @since 3.0.0
      def every(interval, &block)
        robot.run_concurrently { Timer.new(interval: interval, recurring: true, &block).start }
      end

      # Creates a new +Faraday::Connection+ for making HTTP requests.
      # @param options [Hash] A set of options passed on to Faraday.
      # @yield [builder] A Faraday builder object for adding middleware.
      # @return [Faraday::Connection] The new connection object.
      def http(options = {}, &block)
        options = default_faraday_options.merge(options)

        if block
          Faraday::Connection.new(nil, options, &block)
        else
          Faraday::Connection.new(nil, options)
        end
      end

      # The +Logger+ for the currently running robot.
      # @return [Logger] The Lita logger.
      # @since 3.2.0
      def log
        robot.logger
      end

      # Render an ERB template to a string, with any provided variables made available to the
      # template.
      # @param template_name [String] The name of the ERB template file.
      # @param variables [Hash] An optional hash of variables to make available within the
      #   template. Hash keys become instance variable names with the hash values as the instance
      #   variables' values.
      # @return [String] The rendered template.
      # @since 4.2.0
      def render_template(template_name, variables = {})
        Template.from_file(file_for_template(template_name)).render(variables)
      end

      # Renders an ERB template to a string, like {#render_template}, but also takes an array of
      # modules with helper methods to be made available to the template.
      # @param template_name [String] The name of the ERB template file.
      # @param helpers [Array<Module>] An array of modules whose methods should be added to the
      #   template evaluation context.
      # @param variables [Hash] An optional hash of variables to make available within the
      #   template. Hash keys become instance variable names with the hash values as the instance
      #   variables' values.
      # @return [String] The rendered template.
      # @since 4.5.0
      def render_template_with_helpers(template_name, helpers, variables = {})
        template = Template.from_file(file_for_template(template_name))
        helpers.each { |helper| template.add_helper(helper) }
        template.render(variables)
      end

      # A hash-like object that can be used to persist data between instances of the current
      # handler. Useful for storing objects that are expensive to create and that can be safely
      # reused between instances.
      # @return [Store] The handler's shared store.
      # @since 5.0.0
      def store
        robot.store[self.class.namespace]
      end

      # @see .translate
      def translate(*args)
        self.class.translate(*args)
      end

      alias t translate

      private

      # Default options for new Faraday connections. Sets the user agent to the
      # current version of Lita.
      def default_faraday_options
        { headers: { "User-Agent" => "Lita v#{VERSION}" } }
      end

      def file_for_template(template_name)
        TemplateResolver.new(
          self.class.template_root,
          template_name,
          robot.config.robot.adapter
        ).resolve
      end

      # The handler's namespace for Redis.
      def redis_namespace
        "handlers:#{self.class.namespace}"
      end
    end
  end
end
