module MCollective
  module RPC
    # Class to wrap all the stats and to keep track of some timings
    class Stats
      attr_accessor :noresponsefrom, :starttime, :discoverytime, :blocktime, :responses, :totaltime
      attr_accessor :discovered, :discovered_nodes, :okcount, :failcount, :noresponsefrom, :responsesfrom
      attr_accessor :requestid, :aggregate_summary, :ddl

      def initialize
        reset
      end

      # Resets stats, if discovery time is set we keep it as it was
      def reset
        @noresponsefrom = []
        @responsesfrom = []
        @responses = 0
        @starttime = Time.now.to_f
        @discoverytime = 0 unless @discoverytime
        @blocktime = 0
        @totaltime = 0
        @discovered = 0
        @discovered_nodes = []
        @okcount = 0
        @failcount = 0
        @noresponsefrom = []
        @requestid = nil
        @aggregate_summary = []
      end

      # returns a hash of our stats
      def to_hash
        {:noresponsefrom    => @noresponsefrom,
         :starttime         => @starttime,
         :discoverytime     => @discoverytime,
         :blocktime         => @blocktime,
         :responses         => @responses,
         :totaltime         => @totaltime,
         :discovered        => @discovered,
         :discovered_nodes  => @discovered_nodes,
         :noresponsefrom    => @noresponsefrom,
         :okcount           => @okcount,
         :requestid         => @requestid,
         :failcount         => @failcount,
         :aggregate_summary => @aggregate_summary}
      end

      # Fake hash access to keep things backward compatible
      def [](key)
        to_hash[key]
      rescue
        nil
      end

      # increment the count of ok hosts
      def ok
        @okcount += 1
      rescue
        @okcount = 1
      end

      # increment the count of failed hosts
      def fail
        @failcount += 1
      rescue
        @failcount = 1
      end

      # Re-initializes the object with stats from the basic client
      def client_stats=(stats)
        @noresponsefrom = stats[:noresponsefrom]
        @responses = stats[:responses]
        @starttime = stats[:starttime]
        @blocktime = stats[:blocktime]
        @totaltime = stats[:totaltime]
        @requestid = stats[:requestid]
        @discoverytime = stats[:discoverytime] if @discoverytime == 0
      end

      # Utility to time discovery from :start to :end
      def time_discovery(action)
        if action == :start
          @discovery_start = Time.now.to_f
        elsif action == :end
          @discoverytime = Time.now.to_f - @discovery_start
        else
          raise("Uknown discovery action #{action}")
        end
      rescue
        @discoverytime = 0
      end

      # helper to time block execution time
      def time_block_execution(action)
        if action == :start
          @block_start = Time.now.to_f
        elsif action == :end
          @blocktime += Time.now.to_f - @block_start
        else
          raise("Uknown block action #{action}")
        end
      rescue
        @blocktime = 0
      end

      # Update discovered and discovered_nodes based on
      # discovery results
      def discovered_agents(agents)
        @discovered_nodes = agents
        @discovered = agents.size
      end

      # Helper to calculate total time etc
      def finish_request
        @totaltime = @blocktime + @discoverytime

        # figures out who we had no responses from
        dhosts = @discovered_nodes.clone
        @responsesfrom.each {|r| dhosts.delete(r)}
        @noresponsefrom = dhosts
      rescue
        @totaltime = 0
        @noresponsefrom = []
      end

      # Helper to keep track of who we received responses from
      def node_responded(node)
        @responsesfrom << node
      rescue
        @responsesfrom = [node]
      end

      def text_for_aggregates
        result = StringIO.new

        @aggregate_summary.each do |aggregate|
          output_item = aggregate.result[:output]

          begin
            action_interface = @ddl.action_interface(aggregate.action)
            display_as = action_interface[:output][output_item][:display_as]
          rescue
            display_as = output_item
          end

          result.puts Util.colorize(:bold, "Summary of %s:" % display_as)
          result.puts
          result.puts aggregate.to_s
          result.puts
        end

        result.string
      end

      # Returns a blob of text representing the request status based on the
      # stats contained in this class
      def report(caption = "rpc stats", summarize = true, verbose = false)
        result_text = []

        if verbose
            if @aggregate_summary.size > 0 && summarize
              result_text << text_for_aggregates
            else
              result_text << ""
            end

          result_text << Util.colorize(:yellow, "---- #{caption} ----")

          if @discovered
            @responses < @discovered ? color = :red : color = :reset
            result_text << "           Nodes: %s / %s" % [ Util.colorize(color, @discovered), Util.colorize(color, @responses) ]
          else
            result_text << "           Nodes: #{@responses}"
          end

          @failcount < 0 ? color = :red : color = :reset

          result_text << "     Pass / Fail: %s / %s" % [Util.colorize(color, @okcount), Util.colorize(color, @failcount) ]
          result_text << "      Start Time: %s"      % [Time.at(@starttime)]
          result_text << "  Discovery Time: %.2fms"  % [@discoverytime * 1000]
          result_text << "      Agent Time: %.2fms"  % [@blocktime * 1000]
          result_text << "      Total Time: %.2fms"  % [@totaltime * 1000]
        else
          if @discovered
            @responses < @discovered ? color = :red : color = :green

            if @aggregate_summary.size > 0 && summarize
              result_text << text_for_aggregates
            else
              result_text << ""
            end

            result_text << "Finished processing %s / %s hosts in %.2f ms" % [Util.colorize(color, @responses), Util.colorize(color, @discovered), @blocktime * 1000]
          else
            result_text << "Finished processing %s hosts in %.2f ms" % [Util.colorize(:bold, @responses), @blocktime * 1000]
          end
        end

        if no_response_report != ""
          result_text << "" << no_response_report
        end

        result_text.join("\n")
      end

      # Returns a blob of text indicating what nodes did not respond
      def no_response_report
        result_text = StringIO.new

        if @noresponsefrom.size > 0
          result_text.puts
          result_text.puts Util.colorize(:red, "No response from:")
          result_text.puts

          @noresponsefrom.sort.in_groups_of(3) do |c|
            result_text.puts "   %-30s%-30s%-30s" % c
          end

          result_text.puts
        end

        result_text.string
      end
    end
  end
end
