# frozen_string_literal: true

# Fix for https://github.com/erikhuda/thor/issues/398
class Thor
  module Shell
    class Basic
      def print_wrapped(message, options = {})
        indent = (options[:indent] || 0).to_i
        if indent.zero?
          stdout.puts message
        else
          message.each_line do |message_line|
            stdout.print ' ' * indent
            stdout.puts message_line.chomp
          end
        end
      end
    end
  end
end

module Terradactyl
  # rubocop:disable Metrics/ClassLength
  class CLI < Thor
    include Common
    def self.exit_on_failure?
      true
    end

    def initialize(*args)
      # Hook ensures abort on stack errors
      at_exit { abort if Stacks.error? }
      super
    end

    # rubocop:disable Metrics/BlockLength
    no_commands do
      # Monkey-patch Thor internal method to break out of nested calls
      def invoke_command(command, *args)
        catch(:error) { super }
      end

      def validate_smartplan(stacks)
        if stacks.empty?
          print_message 'No Stacks Modified ...'
          print_line 'Did you forget to `git add` your selected changes?'
        end
        stacks
      end

      def validate_planpr(stacks)
        if stacks.empty?
          print_message 'No Stacks Modified ...'
          print_line 'Skipping plan ...'
        end
        stacks
      end

      def generate_report(report)
        data_file = "#{config.base_folder}.audit.json"
        print_warning "Writing Report: #{data_file} ..."
        report[:error] = Stacks.error.map { |s| "#{config.base_folder}/#{s.name}" }.sort
        File.write data_file, JSON.pretty_generate(report)
        print_ok 'Done!'
      end

      def terraform_latest
        Terradactyl::Terraform::VersionManager.latest
      end
    end
    # rubocop:enable Metrics/BlockLength

    #################################################################
    # GENERIC TASKS
    # * These tasks are used regularly against stacks, by name.
    #################################################################

    desc 'defaults', 'Print the compiled configuration'
    def defaults
      puts config.to_h.to_yaml
    end

    desc 'stacks', 'List the stacks'
    def stacks
      print_ok 'Stacks:'
      Stacks.load.each do |name|
        print_dot name.to_s
      end
    end

    desc 'version', 'Print version'
    def version
      print_message format('version: %<semver>s', semver: Terradactyl::VERSION)
    end

    #################################################################
    # SPECIAL TASKS
    # * These tasks are related to Git state and PR planning ops.
    # * Some are useful only in pipelines. These are hidden.
    #################################################################

    desc 'planpr', 'Plan stacks against origin/HEAD (used for PRs)', hide: true
    def planpr
      print_header 'SmartPlanning PR ...'
      stacks = Stacks.load(filter: StacksPlanFilterGitDiffOriginBranch.new)
      validate_planpr(stacks).each do |name|
        clean(name)
        init(name)
        plan(name)
        @stack = nil
      end
    end

    desc 'smartplan', 'Plan any stacks that differ from Git HEAD'
    def smartplan
      print_header 'SmartPlanning Stacks ...'
      stacks = Stacks.load(filter: StacksPlanFilterGitDiffHead.new)
      validate_smartplan(stacks).each do |name|
        clean(name)
        init(name)
        plan(name)
        @stack = nil
      end
    end

    desc 'smartapply', 'Apply any stacks that contain plan files', hide: true
    def smartapply
      print_header 'SmartApplying Stacks ...'
      stacks = Stacks.load(filter: StacksApplyFilterPrePlanned.new)
      print_warning 'No stacks contain plan files ...' unless stacks.any?
      stacks.each do |name|
        apply(name)
        @stack = nil
      end
      print_message "Total Stacks Modified: #{stacks.size}"
    end

    desc 'smartrefresh', 'Refresh any stacks that contain plan files', hide: true
    def smartrefresh
      print_header 'SmartRefreshing Stacks ...'
      stacks = Stacks.load(filter: StacksApplyFilterPrePlanned.new)
      print_warning 'No stacks contain plan files ...' unless stacks.any?
      stacks.each do |name|
        refresh(name)
        @stack = nil
      end
      print_message "Total Stacks Refreshed: #{stacks.size}"
    end

    #################################################################
    # META-STACK TASKS
    # * These tasks are used regularly against groups of stacks, but
    # the `quickplan` task is an exception to this rule.
    #################################################################

    desc 'quickplan NAME', 'Clean, init and plan a stack, by name'
    def quickplan(name)
      print_header "Quick planning #{name} ..."
      clean(name)
      init(name)
      plan(name)
    end

    desc 'clean-all', 'Clean all stacks'
    def clean_all
      print_header 'Cleaning ALL Stacks ...'
      Stacks.load.each do |name|
        clean(name)
        @stack = nil
      end
    end

    desc 'plan-all', 'Plan all stacks'
    def plan_all
      print_header 'Planning ALL Stacks ...'
      Stacks.load.each do |name|
        catch(:error) do
          clean(name)
          init(name)
          plan(name)
        end
        @stack = nil
      end
    end

    desc 'audit-all', 'Audit all stacks'
    options report: :optional
    method_option :report, type: :boolean
    # rubocop:disable Metrics/AbcSize
    def audit_all
      report = { start: Time.now.to_json }
      print_header 'Auditing ALL Stacks ...'
      Stacks.load.each do |name|
        catch(:error) do
          clean(name)
          init(name)
          audit(name)
        end
        @stack = nil
      end
      report[:finish] = Time.now.to_json
      if options[:report]
        print_header 'Audit Report ...'
        generate_report(report)
      end
    end
    # rubocop:enable Metrics/AbcSize

    desc 'validate-all', 'Validate all stacks'
    def validate_all
      print_header 'Validating ALL Stacks ...'
      Stacks.load.each do |name|
        catch(:error) do
          clean(name)
          init(name)
          validate(name)
        end
        @stack = nil
      end
    end

    #################################################################
    # TARGETED STACK TASKS
    # * These tasks are used regularly against stacks, by name.
    #################################################################

    desc 'lint NAME', 'Lint an individual stack, by name'
    def lint(name)
      @stack ||= Stack.new(name)
      print_ok "Linting: #{@stack.name}"
      if @stack.lint.zero?
        print_ok "Formatting OK: #{@stack.name}"
      else
        Stacks.error!(@stack)
        print_warning "Bad Formatting: #{@stack.name}"
      end
    end

    desc 'fmt NAME', 'Format an individual stack, by name'
    def fmt(name)
      @stack ||= Stack.new(name)
      print_warning "Formatting: #{@stack.name}"
      if @stack.fmt.zero?
        print_ok "Formatted: #{@stack.name}"
      else
        Stacks.error!(@stack)
        print_crit "Formatting failed: #{@stack.name}"
      end
    end

    desc 'init NAME', 'Init an individual stack, by name'
    def init(name)
      @stack ||= Stack.new(name)
      print_ok "Initializing: #{@stack.name}"
      if @stack.init.zero?
        print_ok "Initialized: #{@stack.name}"
      else
        Stacks.error!(@stack)
        print_crit "Initialization failed: #{@stack.name}"
        throw :error
      end
    end

    desc 'plan NAME', 'Plan an individual stack, by name'
    # rubocop:disable Metrics/AbcSize
    def plan(name)
      @stack ||= Stack.new(name)
      print_ok "Planning: #{@stack.name}"
      case @stack.plan
      when 0
        print_ok "No changes: #{@stack.name}"
      when 1
        Stacks.error!(@stack)
        print_crit "Plan failed: #{@stack.name}"
        @stack.print_error
        throw :error
      when 2
        Stacks.dirty!(@stack)
        print_warning "Changes detected: #{@stack.name}"
        @stack.print_plan
      else
        raise
      end
    end
    # rubocop:enable Metrics/AbcSize

    desc 'audit NAME', 'Audit an individual stack, by name'
    def audit(name)
      plan(name)
      if (@stack = Stacks.dirty?(name))
        Stacks.error!(@stack)
        print_crit "Dirty stack: #{@stack.name}"
      end
    end

    desc 'validate NAME', 'Validate an individual stack, by name'
    def validate(name)
      @stack ||= Stack.new(name)
      print_ok "Validating: #{@stack.name}"
      if @stack.validate.zero?
        print_ok "Validated: #{@stack.name}"
      else
        Stacks.error!(@stack)
        print_crit "Validation failed: #{@stack.name}"
        throw :error
      end
    end

    desc 'clean NAME', 'Clean an individual stack, by name'
    def clean(name)
      @stack ||= Stack.new(name)
      print_warning "Cleaning: #{@stack.name}"
      @stack.clean
      print_ok "Cleaned: #{@stack.name}"
    end

    desc 'apply NAME', 'Apply an individual stack, by name'
    def apply(name)
      @stack ||= Stack.new(name)
      print_warning "Applying: #{@stack.name}"
      if @stack.apply.zero?
        print_ok "Applied: #{@stack.name}"
      else
        Stacks.error!(@stack)
        print_crit "Failed to apply changes: #{@stack.name}"
      end
    end

    desc 'refresh NAME', 'Refresh state on an individual stack, by name'
    def refresh(name)
      @stack ||= Stack.new(name)
      print_crit "Refreshing: #{@stack.name}"
      if @stack.refresh.zero?
        print_warning "Refreshed: #{@stack.name}"
      else
        Stacks.error!(@stack)
        print_crit "Failed to refresh stack: #{@stack.name}"
      end
    end

    desc 'destroy NAME', 'Destroy an individual stack, by name'
    def destroy(name)
      @stack ||= Stack.new(name)
      print_crit "Destroying: #{@stack.name}"
      if @stack.destroy.zero?
        print_warning "Destroyed: #{@stack.name}"
      else
        Stacks.error!(@stack)
        print_crit "Failed to apply changes: #{@stack.name}"
      end
    end

    #################################################################
    # PROJECT-LEVEL UTILITY TASKS
    # * These tasks are managing project-wide characteristics or
    # invoking useful commands.
    #################################################################

    desc 'install COMPONENT', 'Installs specified component'
    long_desc <<~LONGDESC
       The `terradactyl install COMPONENT` subcommand perfoms installations of
      prerequisties. At present, only Terraform binaries are supported.
       Here are a few examples:
       # Install latest
      `terradactyl install terraform`
       # Install pessimistic version
      `terradactyl install terraform --version="~> 0.13.0"`
       # Install ranged version
      `terradactyl install terraform --version=">= 0.14.5, <= 0.14.7"`
       # Install explicit version
      `terradactyl install terraform --version=0.15.0-beta2`

    LONGDESC
    option :version, type: :string, default: 'latest'
    # rubocop:disable Metrics/AbcSize
    def install(component)
      case component.to_sym
      when :terraform
        print_warning "Installing: #{component}, version: #{options[:version]}"
        version = options[:version] == 'latest' ? terraform_latest : options[:version]
        Terradactyl::Terraform::VersionManager.reset!
        Terradactyl::Terraform::VersionManager.version = version
        Terradactyl::Terraform::VersionManager.install
        if Terradactyl::Terraform::VersionManager.binary
          print_ok "Installed: #{Terradactyl::Terraform::VersionManager.binary}"
        end
      else
        msg = %(Operation not supported -- I don't know how to install: #{component})
        print_crit msg
        exit 1
      end
    end
    # rubocop:enable Metrics/AbcSize

    desc 'upgrade NAME', 'Upgrade an individual stack, by name'
    def upgrade(name)
      @stack ||= Stack.new(name)
      print_warning "Upgrading: #{@stack.name}"
      if @stack.upgrade.zero?
        print_ok "Upgraded: #{@stack.name}"
      else
        Stacks.error!(@stack)
        print_crit "Failed to upgrade: #{@stack.name}"
      end
    rescue Terradactyl::Terraform::Commands::UnsupportedCommandError => e
      print_crit "Error: #{e.message}"
      exit 1
    end
  end
  # rubocop:enable Metrics/ClassLength
end