#!/usr/bin/env ruby

# Formats entity/check data for presentation by the API methods in Flapjack::Gateways::API.

require 'sinatra/base'

require 'flapjack/data/entity_check'

module Flapjack

  module Gateways

    class API < Sinatra::Base

      class EntityCheckPresenter

        def initialize(entity_check)
          @entity_check = entity_check
        end

        def status
          {'name'                              => @entity_check.check,
           'state'                             => @entity_check.state,
           'enabled'                           => @entity_check.enabled?,
           'summary'                           => @entity_check.summary,
           'details'                           => @entity_check.details,
           'in_unscheduled_maintenance'        => @entity_check.in_unscheduled_maintenance?,
           'in_scheduled_maintenance'          => @entity_check.in_scheduled_maintenance?,
           'last_update'                       => @entity_check.last_update,
           'last_problem_notification'         => @entity_check.last_notification_for_state(:problem)[:timestamp],
           'last_recovery_notification'        => @entity_check.last_notification_for_state(:recovery)[:timestamp],
           'last_acknowledgement_notification' => @entity_check.last_notification_for_state(:acknowledgement)[:timestamp]}
        end

        def outages(start_time, end_time, options = {})
          # hist_states is an array of hashes, with [state, timestamp, summary] keys
          hist_states = @entity_check.historical_states(start_time, end_time)
          return hist_states if hist_states.empty?

          initial = @entity_check.historical_state_before(hist_states.first[:timestamp])
          hist_states.unshift(initial) if initial

          # TODO the following works, but isn't the neatest
          num_states = hist_states.size

          index = 0
          result = []
          obj = nil

          while index < num_states do
            last_obj = obj
            obj = hist_states[index]
            index += 1

            next if obj[:state] == 'ok'

            if last_obj && (last_obj[:state] == obj[:state])
              # TODO maybe build up arrays of these instead, and leave calling
              # classes to join them together if needed?
              result.last[:summary] << " / #{obj[:summary]}"
              result.last[:details] << " / #{obj[:details]}"
              next
            end

            ts = obj[:timestamp]

            obj_st  = (last_obj || !start_time) ? ts : [ts, start_time].max

            next_ts_obj = hist_states[index..-1].detect {|hs| hs[:state] != obj[:state] }
            obj_et  = next_ts_obj ? next_ts_obj[:timestamp] : end_time

            obj_dur = obj_et ? obj_et - obj_st : nil

            result << {:state      => obj[:state],
                       :start_time => obj_st,
                       :end_time   => obj_et,
                       :duration   => obj_dur,
                       :summary    => obj[:summary] || '',
                       :details    => obj[:details] || ''
                      }
          end

          result
        end

        def unscheduled_maintenances(start_time, end_time)
          # unsched_maintenance is an array of hashes, with [duration, timestamp, summary] keys
          unsched_maintenance = @entity_check.maintenances(start_time, end_time,
            :scheduled => false)

          # to see if we start in an unscheduled maintenance period, we must check all unscheduled
          # maintenances before the period and their durations
          start_in_unsched = start_time.nil? ? [] :
            @entity_check.maintenances(nil, start_time, :scheduled => false).select {|pu|
              pu[:end_time] >= start_time
            }

          start_in_unsched + unsched_maintenance
        end

        def scheduled_maintenances(start_time, end_time)
          # sched_maintenance is an array of hashes, with [duration, timestamp, summary] keys
          sched_maintenance = @entity_check.maintenances(start_time, end_time,
            :scheduled => true)

          # to see if we start in a scheduled maintenance period, we must check all scheduled
          # maintenances before the period and their durations
          start_in_sched = start_time.nil? ? [] :
            @entity_check.maintenances(nil, start_time, :scheduled => true).select {|ps|
              ps[:end_time] >= start_time
            }

          start_in_sched + sched_maintenance
        end

        # TODO test whether the below overlapping logic is prone to off-by-one
        # errors; the numbers may line up more neatly if we consider outages to
        # start one second after the maintenance period ends.
        #
        # TODO test performance with larger data sets
        def downtime(start_time, end_time)
          sched_maintenances = scheduled_maintenances(start_time, end_time)

          outs = outages(start_time, end_time)

          total_secs  = {}
          percentages = {}

          outs.collect {|obj| obj[:state]}.uniq.each do |st|
            total_secs[st]  = 0
            percentages[st] = (start_time.nil? || end_time.nil?) ? nil : 0
          end

          unless outs.empty?

            # Initially we need to check for cases where a scheduled
            # maintenance period is fully covered by an outage period.
            # We then create two new outage periods to cover the time around
            # the scheduled maintenance period, and remove the original.

            sched_maintenances.each do |sm|

              split_outs = []

              outs.each { |o|
                next unless o[:end_time] && (o[:start_time] < sm[:start_time]) &&
                  (o[:end_time] > sm[:end_time])
                o[:delete] = true
                split_outs += [{:state => o[:state],
                                :start_time => o[:start_time],
                                :end_time => sm[:start_time],
                                :duration => sm[:start_time] - o[:start_time],
                                :summary => "#{o[:summary]} [split start]"},
                               {:state => o[:state],
                                :start_time => sm[:end_time],
                                :end_time => o[:end_time],
                                :duration => o[:end_time] - sm[:end_time],
                                :summary => "#{o[:summary]} [split finish]"}]
              }

              outs.reject! {|o| o[:delete]}
              outs += split_outs
              # not strictly necessary to keep the data sorted, but
              # will make more sense while debgging
              outs.sort! {|a,b| a[:start_time] <=> b[:start_time]}
            end

            sched_maintenances.each do |sm|

              outs.each do |o|
                next unless o[:end_time] && (sm[:start_time] < o[:end_time]) &&
                  (sm[:end_time] > o[:start_time])

                if sm[:start_time] <= o[:start_time] &&
                  sm[:end_time] >= o[:end_time]

                  # outage is fully overlapped by the scheduled maintenance
                  o[:delete] = true

                elsif sm[:start_time] <= o[:start_time]
                  # partially overlapping on the earlier side
                  o[:start_time] = sm[:end_time]
                  o[:duration] = o[:end_time] - o[:start_time]
                elsif sm[:end_time] >= o[:end_time]
                  # partially overlapping on the later side
                  o[:end_time] = sm[:start_time]
                  o[:duration] = o[:end_time] - o[:start_time]
                end
              end

              outs.reject! {|o| o[:delete]}
            end

            total_secs = outs.inject(total_secs) {|ret, o|
              ret[o[:state]] += o[:duration] if o[:duration]
              ret
            }

            unless (start_time.nil? || end_time.nil?)
              total_secs.each_pair do |st, ts|
                percentages[st] = (total_secs[st] * 100.0) / (end_time.to_f - start_time.to_f)
              end
              total_secs['ok'] = (end_time - start_time) - total_secs.values.reduce(:+)
              percentages['ok'] = 100 - percentages.values.reduce(:+)
            end
          end

          {:total_seconds => total_secs, :percentages => percentages, :downtime => outs}
        end

      end

    end

  end

end