# typed: true

require 'cli/kit'
require 'English'
require 'fileutils'

module CLI
  module Kit
    class Executor
      extend T::Sig

      sig { params(log_file: String).void }
      def initialize(log_file:)
        FileUtils.mkpath(File.dirname(log_file))
        @log_file = log_file
      end

      sig { params(command: T.class_of(CLI::Kit::BaseCommand), command_name: String, args: T::Array[String]).void }
      def call(command, command_name, args)
        with_traps do
          with_logging do |id|
            command.call(args, command_name)
          rescue => e
            begin
              $stderr.puts "This command ran with ID: #{id}"
              $stderr.puts 'Please include this information in any issues/report along with relevant logs'
            rescue SystemCallError
              # Outputting to stderr is best-effort.  Avoid raising another error when outputting debug info so that
              # we can detect and log the original error, which may even be the source of this error.
              nil
            end
            raise e
          end
        end
      end

      private

      sig do
        type_parameters(:T).params(block: T.proc.params(id: String).returns(T.type_parameter(:T)))
          .returns(T.type_parameter(:T))
      end
      def with_logging(&block)
        CLI::UI.log_output_to(@log_file) do
          CLI::UI::StdoutRouter.with_id(on_streams: [CLI::UI::StdoutRouter.duplicate_output_to].compact) do |id|
            block.call(id)
          end
        end
      end

      sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
      def with_traps(&block)
        twrap('QUIT', method(:quit_handler)) do
          twrap('INFO', method(:info_handler), &block)
        end
      end

      sig do
        type_parameters(:T).params(signal: String, handler: Method,
          block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T))
      end
      def twrap(signal, handler, &block)
        return yield unless Signal.list.key?(signal)

        begin
          begin
            prev_handler = trap(signal, handler)
            installed = true
          rescue ArgumentError
            # If we couldn't install a signal handler because the signal is
            # reserved, remember not to uninstall it later.
            installed = false
          end
          yield
        ensure
          trap(signal, prev_handler) if installed
        end
      end

      sig { params(_sig: T.untyped).void }
      def quit_handler(_sig)
        z = caller
        CLI::UI.raw do
          $stderr.puts('SIGQUIT: quit')
          $stderr.puts(z)
        end
        exit(CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG)
      end

      sig { params(_sig: T.untyped).void }
      def info_handler(_sig)
        z = caller
        CLI::UI.raw do
          $stderr.puts('SIGINFO:')
          $stderr.puts(z)
        end
      end
    end
  end
end