# frozen_string_literal: true

require "optparse"

module PlatformosCheck
  class Cli
    class Abort < StandardError; end

    FORMATS = %i[text json]

    attr_accessor :path

    def initialize
      @path = "."
      @command = :check
      @include_categories = []
      @exclude_categories = []
      @auto_correct = false
      @update_docs = false
      @config_path = nil
      @fail_level = :error
      @format = :text
    end

    def option_parser(parser = OptionParser.new, help: true)
      return @option_parser if defined?(@option_parser)

      @option_parser = parser
      @option_parser.banner = "Usage: platformos-check [options] [/path/to/your/platformos_app]"

      @option_parser.separator("")
      @option_parser.separator("Basic Options:")
      @option_parser.on(
        "-C", "--config PATH",
        "Use the config provided, overriding .platformos-check.yml if present",
        "Use :platformos_app_app_extension to use default checks for app extensions"
      ) { |path| @config_path = path }
      @option_parser.on(
        "-o", "--output FORMAT", FORMATS,
        "The output format to use. (text|json, default: text)"
      ) { |format| @format = format.to_sym }
      @option_parser.on(
        "-c", "--category CATEGORY", Check::CATEGORIES, "Only run this category of checks",
        "Runs checks matching all categories when specified more than once"
      ) { |category| @include_categories << category.to_sym }
      @option_parser.on(
        "-x", "--exclude-category CATEGORY", Check::CATEGORIES, "Exclude this category of checks",
        "Excludes checks matching any category when specified more than once"
      ) { |category| @exclude_categories << category.to_sym }
      @option_parser.on(
        "-a", "--auto-correct",
        "Automatically fix offenses"
      ) { @auto_correct = true }
      @option_parser.on(
        "--fail-level SEVERITY", [:crash] + Check::SEVERITIES,
        "Minimum severity (error|suggestion|style) for exit with error code"
      ) do |severity|
        @fail_level = severity.to_sym
      end

      @option_parser.separator("")
      @option_parser.separator("Miscellaneous:")
      @option_parser.on(
        "--init",
        "Generate a .platformos-check.yml file"
      ) { @command = :init }
      @option_parser.on(
        "--print",
        "Output active config to STDOUT"
      ) { @command = :print }
      @option_parser.on(
        "--update-docs",
        "Update PlatformOS Check docs (objects, filters, and tags)"
      ) { @update_docs = true }
      @option_parser.on(
        "-h", "--help",
        "Show this. Hi!"
      ) { @command = :help } if help
      @option_parser.on(
        "-l", "--list",
        "List enabled checks"
      ) { @command = :list }
      @option_parser.on(
        "-v", "--version",
        "Print PlatformOS Check version"
      ) { @command = :version }

      if ENV["PLATFORMOS_CHECK_DEBUG"]
        @option_parser.separator("")
        @option_parser.separator("Debugging:")
        @option_parser.on(
          "--profile",
          "Output a profile to STDOUT compatible with FlameGraph."
        ) { @command = :profile }
      end

      @option_parser.separator("")
      @option_parser.separator(<<~EOS)
        Description:
            PlatformOS Check helps you follow platformOS best practices by analyzing the
            Liquid & JSON inside your app.

            You can configure checks in the .platformos-check.yml file of your platformos_app root directory.
      EOS

      @option_parser
    end

    def parse(argv)
      @path = option_parser.parse(argv).first || "."
    rescue OptionParser::InvalidArgument => e
      abort(e.message)
    end

    def run!
      unless %i[version init help].include?(@command)
        @config = if @config_path
                    PlatformosCheck::Config.new(
                      root: @path,
                      configuration: PlatformosCheck::Config.load_config(@config_path)
                    )
                  else
                    PlatformosCheck::Config.from_path(@path)
                  end
        @config.include_categories = @include_categories unless @include_categories.empty?
        @config.exclude_categories = @exclude_categories unless @exclude_categories.empty?
        @config.auto_correct = @auto_correct
      end

      send(@command)
    end

    def run
      run!
      exit(0)
    rescue Abort => e
      if e.message.empty?
        exit(1)
      else
        abort(e.message)
      end
    rescue PlatformosCheckError => e
      warn(e.message)
      exit(2)
    end

    def self.parse_and_run!(argv)
      cli = new
      cli.parse(argv)
      cli.run!
    end

    def self.parse_and_run(argv)
      cli = new
      cli.parse(argv)
      cli.run
    end

    def list
      puts @config.enabled_checks
    end

    def version
      puts PlatformosCheck::VERSION
    end

    def init
      dotfile_path = PlatformosCheck::Config.find(@path)
      raise Abort, "#{PlatformosCheck::Config::DOTFILE} already exists at #{@path}" unless dotfile_path.nil?

      config_name = @config_path || "default"
      File.write(
        File.join(@path, PlatformosCheck::Config::DOTFILE),
        File.read(PlatformosCheck::Config.bundled_config_path(config_name))
      )

      puts "Writing new #{PlatformosCheck::Config::DOTFILE} to #{@path}"
    end

    def print
      puts YAML.dump(@config.to_h)
    end

    def help
      puts option_parser
    end

    def check(out_stream = STDOUT)
      update_docs

      warn "Checking #{@config.root}:"
      storage = PlatformosCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
      raise Abort, "No platformos_app files found." if storage.platformos_app.all.empty?

      analyzer = PlatformosCheck::Analyzer.new(storage.platformos_app, @config.enabled_checks, @config.auto_correct)
      analyzer.analyze_platformos_app
      analyzer.correct_offenses
      print_with_format(storage.platformos_app, analyzer, out_stream)
      # corrections are committed after printing so that the
      # source_excerpts are still pointing to the uncorrected source.
      analyzer.write_corrections
      raise Abort, "" if analyzer.uncorrectable_offenses.any? do |offense|
        offense.check.severity_value <= Check.severity_value(@fail_level)
      end
    end

    def update_docs
      return unless @update_docs

      warn 'Updating documentation...'

      PlatformosCheck::PlatformosLiquid::SourceManager.download
    end

    def profile
      require 'ruby-prof-flamegraph'

      result = RubyProf.profile do
        check(STDERR)
      end

      # Print a graph profile to text
      printer = RubyProf::FlameGraphPrinter.new(result)
      printer.print(STDOUT, {})
    rescue LoadError
      warn "Profiling is only available in development"
    end

    def print_with_format(platformos_app, analyzer, out_stream)
      case @format
      when :text
        PlatformosCheck::Printer.new(out_stream).print(platformos_app, analyzer.offenses, @config.auto_correct)
      when :json
        PlatformosCheck::JsonPrinter.new(out_stream).print(analyzer.offenses)
      end
    end
  end
end