require 'cli/kit'
require 'English'

module CLI
  module Kit
    class Executor
      def initialize(
        tool_name:, command_registry:, error_handler:, log_file: nil
      )
        @tool_name = tool_name
        @command_registry = command_registry
        @error_handler = error_handler
        @log_file = log_file
      end

      def with_logging(&block)
        return yield unless @log_file
        CLI::UI.log_output_to(@log_file, &block)
      end

      def commands_and_aliases
        @command_registry.command_names + @command_registry.aliases.keys
      end

      def trap_signals
        trap('QUIT') do
          z = caller
          CLI::UI.raw do
            STDERR.puts('SIGQUIT: quit')
            STDERR.puts(z)
          end
          exit 1
        end
        trap('INFO') do
          z = caller
          CLI::UI.raw do
            STDERR.puts('SIGINFO:')
            STDERR.puts(z)
            # Thread.list.map { |t| t.backtrace }
          end
        end
      end

      def call(command, command_name, args)
        trap_signals
        with_logging do
          @error_handler.handle_abort do
            if command.nil?
              command_not_found(command_name)
              raise CLI::Kit::AbortSilent # Already output message
            end
            command.call(args, command_name)
            CLI::Kit::EXIT_SUCCESS # unless an exception was raised
          end
        end
      end

      def command_not_found(name)
        CLI::UI::Frame.open("Command not found", color: :red, timing: false) do
          STDERR.puts(CLI::UI.fmt("{{command:#{@tool_name} #{name}}} was not found"))
        end

        cmds = commands_and_aliases
        if cmds.all? { |cmd| cmd.is_a?(String) }
          possible_matches = cmds.min_by(2) do |cmd|
            CLI::Kit::Levenshtein.distance(cmd, name)
          end

          # We don't want to match against any possible command
          # so reject anything that is too far away
          possible_matches.reject! do |possible_match|
            CLI::Kit::Levenshtein.distance(possible_match, name) > 3
          end

          # If we have any matches left, tell the user
          if possible_matches.any?
            CLI::UI::Frame.open("{{bold:Did you mean?}}", timing: false, color: :blue) do
              possible_matches.each do |possible_match|
                STDERR.puts CLI::UI.fmt("{{command:#{@tool_name} #{possible_match}}}")
              end
            end
          end
        end
      end
    end
  end
end