# frozen_string_literal: true

require 'clamp'
require 'highline'
require 'open3'


module Litany
  module CLI
    # Abstract base class for CLI commands.  Will provide any needed helpers as well as global options that should be
    # supported on all commands.
    #
    # @abstract
    class Base < Clamp::Command

      # @group Input Methods

      # Allows easy and consistent yes or no questions.
      # @param question [String] the question to be asked, should be a yes or no question.
      # @param confirm: [Boolean] if set to true, the question will be followed up with "Are you certain?".
      #   Only returns true if both the original question and the confirmation are responded to with "yes".
      #   Forces the default: of `false` on the confirmation question.
      # @param default: [Boolean, nil] if set to a Boolean value it will allow the user to hit enter with no response to select that choice.
      # @return [Boolean]
      # @raise [ArgumentError] behavior becomes unpredictable if `default:` is not one of `true`, `false`, or `nil`.  As such we will error if we receive any other value.
      def agree(question, confirm: false, default: nil)
        raise ArgumentError, 'You must provide either `true`, `false`, or `nil` to `default:`.' unless [true, false, nil].include?(default)

        checker = proc { |a| [true, false].include?(a) ? a : a.downcase[0] == 'y' }

        prompt = '[Y/n]' if default
        prompt = '[y/N]' unless default
        prompt = '[y/n]' if default.nil?

        answer = cli.ask("#{line_banner} #{question} #{prompt}", checker) do |q|
          q.default = default unless default.nil?

          q.validate = proc { |a| [true, false].include?(a) || /^(?i)(?:ye?(?:(?<=e)s)?|no?)$/.match?(a) }
          q.responses[:not_valid] = 'Please enter "yes" or "no".'
          q.responses[:ask_on_error] = :question
        end

        confirm && answer ? agree('Are you certain?', confirm: false, default: false) : answer
      end

      # Asks a question of the user and allows for arbitrary, if validated, input.
      # @param question [String] the question to ask the end user.  Will be appended with "Default: #{default}" if a default is provided.
      # @param confirm: [Boolean] if true the user will be asked to confirm their input. If the confirmation is negative
      #   the user will be prompted to reenter the answer for the original question. This re-prompting will then be confirmed again.
      # @param default: [String, nil] a default value for the result.  Used if the user just hits return.
      # @param secure: [Boolean] if true this disabled echoing of input to the terminal.
      # @param validator: [nil, Regex] if set will ensure that input matches the regex before the value is returned where
      #   nil allows any input including empty strings.  The default regex does not allow whitespace.
      # @param validation_error: [nil, String] displayed to the user if the validator doesn't match.
      # @return [String]
      def ask(question, confirm: false, default: nil, secure: false, validator: /^(?i)(\S)+$/, validation_error: nil) # rubocop:disable Metrics/ParameterLists
        answer = cli.ask("#{line_banner} #{question}") do |q|
          q.default = default unless default.nil?
          q.echo = !secure

          q.validate = validator unless validator.nil?
          q.responses[:ask_on_error] = :question
          q.responses[:not_valid] = validation_error unless validation_error.nil?
        end

        return answer unless confirm
        agree('Are you certain?', confirm: false, default: false) ? answer : ask(question, confirm: confirm, default: default)
      end

      # @endgroup
      # @group Output Methods

      # Prints a debug level message to the terminal.
      # @param message [String] the message to display.
      # @return [void]
      def debug(message)
        cli.say("#{line_banner(color: :green)} #{message}")
      end

      # Prints an info level message to the terminal.
      # @param message [String] the message to display.
      # @return [void]
      def info(message)
        cli.say("#{line_banner(color: :cyan)} #{message}")
      end

      # Prints a warning level message to the terminal.
      # @param message [String] the message to display.
      # @return [void]
      def warn(message)
        cli.say("#{line_banner(color: :yellow)} #{message}")
      end

      # Prints an error level message to the terminal.
      # @param message [String] the message to display.
      # @return [void]
      def error(message)
        cli.say("#{line_banner(color: :red)} #{message}")
      end

      # Prints a fatal level message to the terminal.
      # @param message [String] the message to display.
      # @return [void]
      def fatal(message)
        cli.say("#{line_banner(color: :magenta)} #{message}")
      end

      # Prints an unknown level message to the terminal.
      # @param message [String] the message to display.
      # @return [void]
      def unknown(message)
        cli.say("#{line_banner(color: :black)} #{message}")
      end

      # Prints a bannerless message to the terminal.
      # @param message [String] the message to display.
      # @return [void]
      def say(message)
        cli.say(message)
      end

      # Prints a waiting statement and then will follow it up by dots on subsequent calls.
      #   Meant for use with AWS's waiters API.
      # @param step [Integer] how many times we've waited for this particular waiter
      # @return [void]
      def waiting(step)
        $stdout.print "#{line_banner(color: :cyan)} #{'Waiting..'.indent(1)}" if step.zero?
        $stdout.print '.'
        $stdout.flush
      end

      # Displays a message to the terminal in such a way as to not show up in logs or scroll back.
      # @param message [String] the message to display.
      # @todo investigate ways of output that are cross platform.
      # @todo look to prevent these from showing up in the process list.
      def secure(message)
        contents = []
        contents << '# a note from Litany:'
        contents << '# this message is being displayed in a way to avoid leaking into logs and scroll back.'
        contents << ''
        contents << message
        contents << ''
        contents << "# please press 'q' to continue..."

        system "echo '#{contents.join('\n')}' | less"
      end

      # @endgroup

      # Returns a singleton instance of the HighLine library for quick and easy reference.
      # @return [HighLine]
      private def cli
        @cli ||= HighLine.new
      end

      # Returns a standard formatted line banner to allow for consistent usage in the CLI.
      # @param level: [Symbol] What message level this is.
      # @return [String]
      private def line_banner(color: :cyan)
        @line_banners ||= {}
        @line_banners[color] ||= cli.color('litany >', color, :bold)
      end

      # Returns a string colorized for console output.
      # @param text [String]
      # @param styles... [Symbol]
      # @return [String]
      private def color(text, *styles)
        cli.color(text, styles)
      end
    end
  end
end
