# frozen_string_literal: true

require 'slim_lint'
require 'slim_lint/options'

require 'sysexits'

module SlimLint
  # Command line application interface.
  class CLI # rubocop:disable Metrics/ClassLength
    # Create a CLI that outputs to the specified logger.
    #
    # @param logger [SlimLint::Logger]
    def initialize(logger)
      @log = logger
    end

    # Parses the given command-line arguments and executes appropriate logic
    # based on those arguments.
    #
    # @param args [Array<String>] command line arguments
    # @return [Integer] exit status code
    def run(args)
      options = SlimLint::Options.new.parse(args)
      act_on_options(options)
    rescue StandardError => ex
      handle_exception(ex)
    end

    private

    attr_reader :log

    # Given the provided options, execute the appropriate command.
    #
    # @return [Integer] exit status code
    def act_on_options(options)
      log.color_enabled = options.fetch(:color, log.tty?)

      if options[:help]
        print_help(options)
        Sysexits::EX_OK
      elsif options[:version] || options[:verbose_version]
        print_version(options)
        Sysexits::EX_OK
      elsif options[:show_linters]
        print_available_linters
        Sysexits::EX_OK
      elsif options[:show_reporters]
        print_available_reporters
        Sysexits::EX_OK
      else
        scan_for_lints(options)
      end
    end

    # Outputs a message and returns an appropriate error code for the specified
    # exception.
    def handle_exception(exception)
      case exception
      when SlimLint::Exceptions::ConfigurationError
        log.error exception.message
        Sysexits::EX_CONFIG
      when SlimLint::Exceptions::InvalidCLIOption
        log.error exception.message
        log.log "Run `#{APP_NAME}` --help for usage documentation"
        Sysexits::EX_USAGE
      when SlimLint::Exceptions::InvalidFilePath
        log.error exception.message
        Sysexits::EX_NOINPUT
      when SlimLint::Exceptions::NoLintersError
        log.error exception.message
        Sysexits::EX_NOINPUT
      else
        print_unexpected_exception(exception)
        Sysexits::EX_SOFTWARE
      end
    end

    # Scans the files specified by the given options for lints.
    #
    # @return [Integer] exit status code
    def scan_for_lints(options)
      report = Runner.new.run(options)
      print_report(report, options)
      report.failed? ? Sysexits::EX_DATAERR : Sysexits::EX_OK
    end

    # Outputs a report of the linter run using the specified reporter.
    def print_report(report, options)
      reporter = options.fetch(:reporter,
                               SlimLint::Reporter::DefaultReporter).new(log)
      reporter.display_report(report)
    end

    # Outputs a list of all currently available linters.
    def print_available_linters
      log.info 'Available linters:'

      linter_names = SlimLint::LinterRegistry.linters.map do |linter|
        linter.name.split('::').last
      end

      linter_names.sort.each do |linter_name|
        log.log " - #{linter_name}"
      end
    end

    # Outputs a list of currently available reporters.
    def print_available_reporters
      log.info 'Available reporters:'

      reporter_names = SlimLint::Reporter.descendants.map do |reporter|
        reporter.name.split('::').last.sub(/Reporter$/, '').downcase
      end

      reporter_names.sort.each do |reporter_name|
        log.log " - #{reporter_name}"
      end
    end

    # Outputs help documentation.
    def print_help(options)
      log.log options[:help]
    end

    # Outputs the application name and version.
    def print_version(options)
      log.log "#{SlimLint::APP_NAME} #{SlimLint::VERSION}"

      if options[:verbose_version]
        log.log "slim #{Gem.loaded_specs['slim'].version}"
        log.log "rubocop #{Gem.loaded_specs['rubocop'].version}"
        log.log RUBY_DESCRIPTION
      end
    end

    # Outputs the backtrace of an exception with instructions on how to report
    # the issue.
    def print_unexpected_exception(exception) # rubocop:disable Metrics/AbcSize
      log.bold_error exception.message
      log.error exception.backtrace.join("\n")
      log.warning 'Report this bug at ', false
      log.info SlimLint::BUG_REPORT_URL
      log.newline
      log.success 'To help fix this issue, please include:'
      log.log '- The above stack trace'
      log.log '- Slim-Lint version: ', false
      log.info SlimLint::VERSION
      log.log '- RuboCop version: ', false
      log.info Gem.loaded_specs['rubocop'].version
      log.log '- Ruby version: ', false
      log.info RUBY_VERSION
    end
  end
end
