require 'forwardable'
require 'kontena_cli'

module Kontena
  autoload :Client, 'kontena/client'

  module Cli
    autoload :ShellSpinner, 'kontena/cli/spinner'
    autoload :Spinner, 'kontena/cli/spinner'
    autoload :Config, 'kontena/cli/config'

    module Common
      extend Forwardable

      def_delegators :prompt, :ask, :yes?
      def_delegators :config,
        :current_grid=, :require_current_grid, :current_master,
        :current_master=, :require_current_master, :require_current_account,
        :current_account
      def_delegator :config, :config_filename, :settings_filename
      def_delegator :client, :server_version, :api_url_version

      def logger
        Kontena.logger
      end

      def prompt
        Kontena.prompt
      end

      def pastel
        Kontena.pastel
      end

      def spinner(msg, &block)
         Kontena::Cli::Spinner.spin(msg, &block)
      end

      def config
        Kontena::Cli::Config.instance
      end

      def debug?
        Kontena.debug?
      end

      # Read from STDIN. If stdin is a console, use prompt to ask.
      # @param [String] message
      # @param [Symbol] mode (prompt method: :ask, :multiline, etc)
      def stdin_input(message = nil, mode = :ask)
        if $stdin.tty?
          Array(prompt.send(mode, message)).join.chomp
        elsif !$stdin.eof?
          $stdin.read.chomp
        else
          exit_with_error 'Missing input'
        end
      end

      def running_silent?
        self.respond_to?(:silent?) && self.silent?
      end

      def running_verbose?
        self.respond_to?(:verbose?) && self.verbose?
      end

      def running_quiet?
        self.respond_to?(:quiet?) && self.quiet?
      end

      # Puts that puts even when self.silent?
      def sputs(*msgs)
        ::Kernel.puts(*msgs)
      end

      # Print that prints even when self.silent?
      def sprint(*msgs)
        ::Kernel.print(*msgs)
      end

      # Print that doesn't print if self.silent?
      def print(*msgs)
        if running_silent?
          logger.debug(msgs.join)
        else
          super(*msgs)
        end
      end

      # Puts that doesn't puts if self.silent?
      def puts(*msgs)
        if running_silent?
          msgs.compact.each { |msg| logger.debug(msg) }
        elsif Thread.main['spinners'] && !Thread.main['spinners'].empty?
          Thread.main['spinner_msgs'] ||= []
          msgs.each { |msg| Thread.main['spinner_msgs'] << msg }
        else
          super(*msgs)
        end
      end

      # Only output message if in verbose mode
      def vputs(msg = nil)
        if running_verbose?
          puts msg
        elsif debug? && msg
          logger.debug msg
        end
      end

      # Only show spinner when in verbose mode
      def vspinner(msg, &block)
        return vfakespinner(msg) unless block_given?

        if running_verbose? && $stdout.tty?
          spinner(msg, &block)
        else
          logger.debug { msg }
          yield
        end
      end

      # Output a message like: "> Reading foofoo .."
      # @param message [String] the message to display
      # @param dots [TrueClass,FalseClass] set to false if you don't want to add ".." after the message
      def caret(msg, dots: true)
        puts "#{pastel.green('>')} #{msg}#{" #{pastel.green('..')}" if dots}"
      end

      # Run a spinner with a message for the block if a truthy value or a proc returns true.
      # @example
      #   spin_if(proc { prompt.yes?("for real?") }, "Doing as requested") do
      #     # doing stuff
      #   end
      #   spin_if(a == 1, "Value of 'a' is 1, so let's do this") do
      #     # doing stuff
      #   end
      # @param obj_or_proc [Object,Proc] something that responds to .call or is truthy/falsey
      # @param message [String] the message to display
      # @return anything the block returns
      def spin_if(obj_or_proc, message, &block)
        if (obj_or_proc.respond_to?(:call) && obj_or_proc.call) || obj_or_proc
          spinner(message, &block)
        else
          logger.debug { message }
          yield
        end
      end

      # Like vspinner but without actually running any block
      def vfakespinner(msg, success: true)
        if !running_verbose?
          logger.debug { msg }
          return
        end
        puts " [#{ success ? pastel.green('done') : pastel.red('fail')}] #{msg}"
      end

      def warning(msg)
        warning = pastel.yellow('warn')
        $stderr.puts " [#{warning}] #{msg}"
      end

      def exit_with_error(msg, code = 1)
        error = pastel.red('error')
        $stderr.puts " [#{error}] #{msg}"
        exit code
      end
      module_function :exit_with_error

      def require_api_url
        config.require_current_master.url
      end

      def require_token
        retried ||= false
        config.require_current_master_token
      rescue Kontena::Cli::Config::TokenExpiredError
        if retried
          raise ArgumentError, "Current master access token has expired and refresh failed."
        else
          logger.debug "Access token expired, trying to refresh"
          retried = true
          client.refresh_token && retry
        end
      end

      ## Invalidate refresh_token
      # @param [Kontena::Cli::Config::Server] server
      def use_refresh_token(server)
        return unless server.token
        return unless server.token.refresh_token
        return if server.token.expired?
        client = Kontena::Client.new(server.url, server.token,
          ssl_cert_path: server.ssl_cert_path,
          ssl_subject_cn: server.ssl_subject_cn,
        )
        logger.debug "Trying to invalidate refresh token on #{server.name}"
        client.refresh_token
      rescue => ex
        logger.debug "Refreshing failed: #{ex.class.name} : #{ex.message}"
      end

      def kontena_account
        @kontena_account ||= config.current_account
      end

      def cloud_auth?
        return false unless kontena_account
        return false unless kontena_account.token
        return false unless kontena_account.token.access_token
        true
      end

      def cloud_client
        @cloud_client ||= Kontena::Client.new(kontena_account.url, kontena_account.token, prefix: '/')
      end

      def reset_cloud_client
        @cloud_client = nil
      end

      def client(token = nil, api_url = nil)
        return @client if @client

        if token.kind_of?(String)
          token = Kontena::Cli::Config::Token.new(access_token: token)
        end

        @client = Kontena::Client.new(
          api_url || require_current_master.url,
          token || require_current_master.token,
          ssl_cert_path: require_current_master.ssl_cert_path,
          ssl_subject_cn: require_current_master.ssl_subject_cn,
        )
      end

      def reset_client
        @client = nil
      end

      def api_url
        config.require_current_master.url
      end

      def clear_current_grid
        current_master.delete_field(:grid) if require_current_master.respond_to?(:grid)
        config.write
      end

      def current_grid
        (self.respond_to?(:grid) ? self.grid : nil) || config.current_grid
      end

      def current_master_index
        config.find_server_index(require_current_master.name)
      end

      def error(message = "Error")
        prompt.error(message)
        exit(1)
      end

      def confirm_command(name, message = nil)
        if self.respond_to?(:force?) && self.force?
          return
        end
        puts message if message
        exit_with_error 'Command requires --force' unless $stdout.tty? && $stdin.tty?
        puts "Destructive command. To proceed, type \"#{name}\" or re-run this command with --force option."

        ask("Enter '#{name}' to confirm: ") == name.to_s || error("Confirmation did not match #{name}. Aborted command.")
      end

      def confirm(message = 'Destructive command. You can skip this prompt by running this command with --force option. Are you sure?')
        if self.respond_to?(:force?) && self.force?
          return
        end
        exit_with_error 'Command requires --force' unless $stdout.tty? && $stdin.tty?
        prompt.yes?(message) || error('Aborted command.')
      end

      def api_url=(api_url)
        config.current_master.url = api_url
        config.write
      end

      def access_token=(token)
        require_current_master.token.access_token = token
        config.write
      end

      def add_master(server_name, master_info)
        config.add_server(master_info.merge('name' => server_name))
      end

      def any_key_to_continue_with_timeout(timeout=9)
        return nil if running_silent?
        return nil unless $stdout.tty?
        prompt.keypress("Press any key to continue or ctrl-c to cancel (Automatically continuing in :countdown seconds) ...", timeout: timeout)
      end

      def any_key_to_continue(timeout = nil)
        return nil if running_silent?
        return nil unless $stdout.tty?
        return any_key_to_continue_with_timeout(timeout) if timeout
        prompt.keypress("Press any key to continue or ctrl-c to cancel.. ")
      end

      def display_account_login_info
        if kontena_account
          if kontena_account.token && kontena_account.token.access_token
            begin
              puts [
                pastel.green("Authenticated to Kontena Cloud at"),
                pastel.yellow(kontena_account.url),
                pastel.green("as"),
                pastel.yellow(kontena_account.username)
              ].join(' ')
            rescue
            end
          else
            puts pastel.cyan("Not authenticated to Kontena Cloud")
          end
        end
      end

      def display_master_login_info
        server = config.current_master
        if server
          if server.token && server.token.access_token
            puts [
              pastel.green('Authenticated to Kontena Master'),
              pastel.yellow(server.name),
              pastel.green('at'),
              pastel.yellow(server.url),
              pastel.green('as'),
              pastel.yellow(server.token.username || server.username)
            ].join(' ')
          else
            puts pastel.cyan("Not authenticated to current master #{server.name}")
          end
        else
          puts pastel.cyan("Current master not selected")
        end
      end

      def display_login_info(only: nil)
        display_master_login_info  unless only == :account
        display_account_login_info unless only == :master
      end

      def display_logo
        puts File.read(File.expand_path('../../../../LOGO', __FILE__))
      end
      module_function :display_logo
    end
  end
end
