module MCollective
  module RPC
    # Various utilities for the RPC system
    class Helpers
      # Parse JSON output as produced by printrpc or puppet query
      # and extract the "sender" / "certname" of each entry
      #
      # The simplist valid JSON based data would be:
      #
      # [
      #  {"sender" => "example.com"},
      #  {"sender" => "another.com"}
      # ]
      #
      # or
      #
      # [
      #  {"certname" => "example.com"},
      #  {"certname" => "another.com"}
      # ]
      def self.extract_hosts_from_json(json)
        hosts = JSON.parse(json)

        raise "JSON hosts list is not an array" unless hosts.is_a?(Array)

        hosts.map do |host|
          raise "JSON host list is not an array of Hashes" unless host.is_a?(Hash)

          unless host.include?("sender") || host.include?("certname")
            raise "JSON host list does not have senders in it"
          end

          host["sender"] || host["certname"]
        end.uniq
      end

      # Given an array of something, make sure each is a string
      # chomp off any new lines and return just the array of hosts
      def self.extract_hosts_from_array(hosts)
        [hosts].flatten.map do |host|
          raise "#{host} should be a string" unless host.is_a?(String)
          host.chomp
        end
      end

      # Returns a blob of text representing the results in a standard way
      #
      # It tries hard to do sane things so you often
      # should not need to write your own display functions
      #
      # If the agent you are getting results for has a DDL
      # it will use the hints in there to do the right thing specifically
      # it will look at the values of display in the DDL to choose
      # when to show results
      #
      # If you do not have a DDL you can pass these flags:
      #
      #    printrpc exim.mailq, :flatten => true
      #    printrpc exim.mailq, :verbose => true
      #
      # If you've asked it to flatten the result it will not print sender
      # hostnames, it will just print the result as if it's one huge result,
      # handy for things like showing a combined mailq.
      def self.rpcresults(result, flags = {})
        flags = {:verbose => false, :flatten => false, :format => :console, :force_display_mode => false}.merge(flags)

        result_text = ""
        ddl = nil

        # if running in verbose mode, just use the old style print
        # no need for all the DDL helpers obfuscating the result
        if flags[:format] == :json
          if STDOUT.tty?
            result_text = JSON.pretty_generate(result)
          else
            result_text = result.to_json
          end
        else
          if flags[:verbose]
            result_text = old_rpcresults(result, flags)
          else
            [result].flatten.each do |r|
              begin
                ddl ||= DDL.new(r.agent).action_interface(r.action.to_s)

                sender = r[:sender]
                status = r[:statuscode]
                message = r[:statusmsg]
                result = r[:data]

                if flags[:force_display_mode]
                  display = flags[:force_display_mode]
                else
                  display = ddl[:display]
                end

                # appand the results only according to what the DDL says
                case display
                  when :ok
                    if status == 0
                      result_text << text_for_result(sender, status, message, result, ddl)
                    end

                  when :failed
                    if status > 0
                      result_text << text_for_result(sender, status, message, result, ddl)
                    end

                  when :always
                    result_text << text_for_result(sender, status, message, result, ddl)

                  when :flatten
                    Log.warn("The display option :flatten is being deprecated and will be removed in the next minor release")
                    result_text << text_for_flattened_result(status, result)

                end
              rescue Exception => e
                # no DDL so just do the old style print unchanged for
                # backward compat
                result_text = old_rpcresults(result, flags)
              end
            end
          end
        end

        result_text
      end

      # Return text representing a result
      def self.text_for_result(sender, status, msg, result, ddl)
        statusses = ["",
                     Util.colorize(:red, "Request Aborted"),
                     Util.colorize(:yellow, "Unknown Action"),
                     Util.colorize(:yellow, "Missing Request Data"),
                     Util.colorize(:yellow, "Invalid Request Data"),
                     Util.colorize(:red, "Unknown Request Status")]

        result_text = "%-40s %s\n" % [sender, statusses[status]]
        result_text << "   %s\n" % [Util.colorize(:yellow, msg)] unless msg == "OK"

        # only print good data, ignore data that results from failure
        if status == 0
          if result.is_a?(Hash)
            # figure out the lengths of the display as strings, we'll use
            # it later to correctly justify the output
            lengths = result.keys.map do |k|
              begin
                ddl[:output][k][:display_as].size
              rescue
                k.to_s.size
              end
            end

            result.keys.sort_by{|k| k}.each do |k|
              # get all the output fields nicely lined up with a
              # 3 space front padding
              begin
                display_as = ddl[:output][k][:display_as]
              rescue
                display_as = k.to_s
              end

              display_length = display_as.size
              padding = lengths.max - display_length + 3
              result_text << " " * padding

              result_text << "#{display_as}:"

              if [String, Numeric].include?(result[k].class)
                lines = result[k].to_s.split("\n")

                if lines.empty?
                  result_text << "\n"
                else
                  lines.each_with_index do |line, i|
                    i == 0 ? padtxt = " " : padtxt = " " * (padding + display_length + 2)

                    result_text << "#{padtxt}#{line}\n"
                  end
                end
              else
                padding = " " * (lengths.max + 5)
                result_text << " " << result[k].pretty_inspect.split("\n").join("\n" << padding) << "\n"
              end
            end
          elsif status == 1
            # for status 1 we dont want to show half baked
            # data by default since the DDL will supply all the defaults
            # it just doesnt look right
          else
            result_text << "\n\t" + result.pretty_inspect.split("\n").join("\n\t")
          end
        end

        result_text << "\n"
        result_text
      end

      # Returns text representing a flattened result of only good data
      def self.text_for_flattened_result(status, result)
        result_text = ""

        if status <= 1
          unless result.is_a?(String)
            result_text << result.pretty_inspect
          else
            result_text << result
          end
        end
      end

      # Backward compatible display block for results without a DDL
      def self.old_rpcresults(result, flags = {})
        result_text = ""

        if flags[:flatten]
          result.each do |r|
            if r[:statuscode] <= 1
              data = r[:data]

              unless data.is_a?(String)
                result_text << data.pretty_inspect
              else
                result_text << data
              end
            else
              result_text << r.pretty_inspect
            end
          end

          result_text << ""
        else
          [result].flatten.each do |r|
            if flags[:verbose]
              result_text << "%-40s: %s\n" % [r[:sender], r[:statusmsg]]

              if r[:statuscode] <= 1
                r[:data].pretty_inspect.split("\n").each {|m| result_text += "    #{m}"}
                result_text << "\n\n"
              elsif r[:statuscode] == 2
                # dont print anything, no useful data to display
                # past what was already shown
              elsif r[:statuscode] == 3
                # dont print anything, no useful data to display
                # past what was already shown
              elsif r[:statuscode] == 4
                # dont print anything, no useful data to display
                # past what was already shown
              else
                result_text << "    #{r[:statusmsg]}"
              end
            else
              unless r[:statuscode] == 0
                result_text << "%-40s %s\n" % [r[:sender], Util.colorize(:red, r[:statusmsg])]
              end
            end
          end
        end

        result_text << ""
      end

      # Add SimpleRPC common options
      def self.add_simplerpc_options(parser, options)
        parser.separator ""
        parser.separator "RPC Options"

        # add SimpleRPC specific options to all clients that use our library
        parser.on('--np', '--no-progress', 'Do not show the progress bar') do |v|
          options[:progress_bar] = false
        end

        parser.on('--one', '-1', 'Send request to only one discovered nodes') do |v|
          options[:mcollective_limit_targets] = 1
        end

        parser.on('--batch SIZE', 'Do requests in batches') do |v|
          # validate batch string. Is it x% where x > 0 or is it an integer
          if ((v =~ /^(\d+)%$/ && Integer($1) != 0) || v =~ /^(\d+)$/)
            options[:batch_size] = v
          else
            raise(::OptionParser::InvalidArgument.new(v))
          end
        end

        parser.on('--batch-sleep SECONDS', Float, 'Sleep time between batches') do |v|
          options[:batch_sleep_time] = v
        end

        parser.on('--limit-seed NUMBER', Integer, 'Seed value for deterministic random batching') do |v|
          options[:limit_seed] = v
        end

        parser.on('--limit-nodes COUNT', '--ln', '--limit', 'Send request to only a subset of nodes, can be a percentage') do |v|
          raise "Invalid limit specified: #{v} valid limits are /^\d+%*$/" unless v =~ /^\d+%*$/

          if v =~ /^\d+$/
            options[:mcollective_limit_targets] = v.to_i
          else
            options[:mcollective_limit_targets] = v
          end
        end

        parser.on('--json', '-j', 'Produce JSON output') do |v|
          options[:progress_bar] = false
          options[:output_format] = :json
        end

        parser.on('--display MODE', 'Influence how results are displayed. One of ok, all or failed') do |v|
          if v == "all"
            options[:force_display_mode] = :always
          else
            options[:force_display_mode] = v.intern
          end

          raise "--display has to be one of 'ok', 'all' or 'failed'" unless [:ok, :failed, :always].include?(options[:force_display_mode])
        end
      end
    end
  end
end
