require 'clamp'

class Kontena::Command < Clamp::Command

  attr_accessor :arguments
  attr_reader :result
  attr_reader :exit_code

  module Finalizer
    def self.extended(obj)
      # Tracepoint is used to trigger finalize once the command is completely
      # loaded. If done through def self.inherited the finalizer and
      # after_load callbacks would run before the options are defined.
      TracePoint.trace(:end) do |t|
        if obj == t.self
          obj.finalize
          t.disable
        end
      end
    end

    def finalize
      return if self.has_subcommands?
      return if self.callback_matcher

      name_parts = self.name.split('::')[-2, 2]

      unless name_parts.compact.empty?
        # 1: Remove trailing 'Command' from for example AuthCommand
        # 2: Convert the string from CamelCase to under_score
        # 3: Convert the string into a symbol
        #
        # In comes: ['ExternalRegistry', 'UseCommand']
        # Out goes: [:external_registry, :use]
        name_parts = name_parts.map { |np|
          np.gsub(/Command$/, '').
          gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
          gsub(/([a-z\d])([A-Z])/,'\1_\2').
          tr("-", "_").
          downcase.
          to_sym
        }
        self.callback_matcher(*name_parts)
      end

      # Run all #after_load callbacks for this command.
      [name_parts.last, :all].compact.uniq.each do |cmd_type|
        [name_parts.first, :all].compact.uniq.each do |cmd_class|
          if Kontena::Callback.callbacks.fetch(cmd_class, {}).fetch(cmd_type, nil)
            Kontena::Callback.callbacks[cmd_class][cmd_type].each do |cb|
              if cb.instance_methods.include?(:after_load)
                cb.new(self).after_load
              end
            end
          end
        end
      end
    end
  end

  def self.inherited(where)
    where.extend Finalizer
  end

  def self.callback_matcher(cmd_class = nil, cmd_type = nil)
    unless cmd_class
      if @command_class.nil?
        return nil
      else
        return [@command_class, @command_type]
      end
    end
    @command_class = cmd_class.to_sym
    @command_type = cmd_type.to_sym
    [@command_class, @command_type]
  end

  def run_callbacks(state)
    if self.class.respond_to?(:callback_matcher) && !self.class.callback_matcher.nil? && !self.class.callback_matcher.compact.empty?
      Kontena::Callback.run_callbacks(self.class.callback_matcher, state, self)
    end
  end

  # Overwrite Clamp's banner command. Calling banner multiple times 
  # will now add lines to the banner message instead of overwriting
  # the whole message. This is useful if callbacks add banner messages.
  #
  # @param [String] message
  def self.banner(msg, extra_feed = true)
    self.description = [self.description, extra_feed ? "\n#{msg}" : msg].compact.join("\n")
  end

  def self.requires_current_master
    unless Kontena::Cli::Config.current_master
      banner "#{Kontena.pastel.green("Requires current master")}: This command requires that you have selected a current master using 'kontena master login' or 'kontena master use'. You can also use the environment variable KONTENA_URL to specify the master address or KONTENA_MASTER=master_name to override the current_master setting."
    end
    @requires_current_master = true
  end

  def self.requires_current_grid
    unless Kontena::Cli::Config.current_grid
      banner "#{Kontena.pastel.green("Requires current grid")}: This command requires that you have selected a grid as the current grid using 'kontena grid use' or by setting KONTENA_GRID environment variable."
    end
    @requires_current_grid = true
  end

  def self.requires_current_account_token
    unless Kontena::Cli::Config.current_account && Kontena::Cli::Config.current_account.token && Kontena::Cli::Config.current_account.token.access_token
      banner "#{Kontena.pastel.green("Requires account authentication")}: This command requires that you have authenticated to Kontena Cloud using 'kontena cloud auth'"
    end
    @requires_current_account_token = true
  end


  def self.requires_current_master?
    @requires_current_master ||= false
  end

  def verify_current_master
    Kontena::Cli::Config.instance.require_current_master if self.class.requires_current_master?
  end

  def self.requires_current_grid?
    @requires_current_grid ||= false
  end

  def verify_current_grid
    Kontena::Cli::Config.instance.require_current_grid if self.class.requires_current_grid?
  end

  def self.requires_current_account_token?
    @requires_current_account_token ||= false
  end

  def verify_current_account_token
    retried ||= false
    Kontena::Cli::Config.instance.require_current_account_token if self.class.requires_current_account_token?
  end

  def self.requires_current_master_token
    @requires_current_master_token = true
  end

  def self.requires_current_master_token?
    @requires_current_master_token ||= false
  end

  def verify_current_master_token
    return nil unless self.class.requires_current_master_token?
    retried ||= false
    Kontena::Cli::Config.instance.require_current_master_token
  rescue Kontena::Cli::Config::TokenExpiredError
    success = Kontena::Client.new(
      Kontena::Cli::Config.instance.current_master,
      Kontena::Cli::Config.instance.current_master.token
    ).refresh_token
    if success && !retried
      retried = true
      retry
    else
      raise Kontena::Cli::Config::TokenExpiredError, "The access token has expired and refresh failed. Try authenticating again, use: kontena master login"
    end
  end

  def help_requested?
    return true if @arguments.include?('--help')
    return true if @arguments.include?('-h')
    false
  end

  def run(arguments)
    ENV["DEBUG"] && puts("Running #{self} -- callback matcher = '#{self.class.callback_matcher.nil? ? "nil" : self.class.callback_matcher.map(&:to_s).join(' ')}'")
    @arguments = arguments

    run_callbacks :before_parse unless help_requested?

    parse @arguments

    unless help_requested?
      verify_current_master
      verify_current_master_token
      verify_current_grid
      run_callbacks :before
    end

    begin
      @result = execute
      @exit_code = @result.kind_of?(FalseClass) ? 1 : 0
    rescue SystemExit => exc
      @result = exc.status == 0
      @exit_code = exc.status
    end
    run_callbacks :after unless help_requested?
    exit(@exit_code) if @exit_code.to_i > 0
    @result
  end
end

require_relative 'callback'