module God
  module Conditions

    # Condition Symbol :flapping
    # Type: Trigger
    #
    # Trigger when a Task transitions to or from a state or states a given number
    # of times within a given period.
    #
    # Paramaters
    #   Required
    #     +times+ is the number of times that the Task must transition before
    #             triggering.
    #     +within+ is the number of seconds within which the Task must transition
    #              the specified number of times before triggering. You may use
    #              the sugar methods #seconds, #minutes, #hours, #days to clarify
    #              your code (see examples).
    #     --one or both of--
    #     +from_state+ is the state (as a Symbol) from which the transition must occur.
    #     +to_state is the state (as a Symbol) to which the transition must occur.
    #
    #   Optional:
    #     +retry_in+ is the number of seconds after which to re-monitor the Task after
    #                it has been disabled by the condition.
    #     +retry_times+ is the number of times after which to permanently unmonitor
    #                   the Task.
    #     +retry_within+ is the number of seconds within which
    #
    # Examples
    #
    # Trigger if
    class Flapping < TriggerCondition
      attr_accessor :times,
                    :within,
                    :from_state,
                    :to_state,
                    :retry_in,
                    :retry_times,
                    :retry_within

      def initialize
        self.info = "process is flapping"
      end

      def prepare
        @timeline = Timeline.new(self.times)
        @retry_timeline = Timeline.new(self.retry_times)
      end

      def valid?
        valid = true
        valid &= complain("Attribute 'times' must be specified", self) if self.times.nil?
        valid &= complain("Attribute 'within' must be specified", self) if self.within.nil?
        valid &= complain("Attributes 'from_state', 'to_state', or both must be specified", self) if self.from_state.nil? && self.to_state.nil?
        valid
      end

      def process(event, payload)
        begin
          if event == :state_change
            event_from_state, event_to_state = *payload

            from_state_match = !self.from_state || self.from_state && Array(self.from_state).include?(event_from_state)
            to_state_match = !self.to_state || self.to_state && Array(self.to_state).include?(event_to_state)

            if from_state_match && to_state_match
              @timeline << Time.now

              concensus = (@timeline.size == self.times)
              duration = (@timeline.last - @timeline.first) < self.within

              if concensus && duration
                @timeline.clear
                trigger
                retry_mechanism
              end
            end
          end
        rescue => e
          puts e.message
          puts e.backtrace.join("\n")
        end
      end

      private

      def retry_mechanism
        if self.retry_in
          @retry_timeline << Time.now

          concensus = (@retry_timeline.size == self.retry_times)
          duration = (@retry_timeline.last - @retry_timeline.first) < self.retry_within

          if concensus && duration
            # give up
            Thread.new do
              sleep 1

              # log
              msg = "#{self.watch.name} giving up"
              applog(self.watch, :info, msg)
            end
          else
            # try again later
            Thread.new do
              sleep 1

              # log
              msg = "#{self.watch.name} auto-reenable monitoring in #{self.retry_in} seconds"
              applog(self.watch, :info, msg)

              sleep self.retry_in

              # log
              msg = "#{self.watch.name} auto-reenabling monitoring"
              applog(self.watch, :info, msg)

              if self.watch.state == :unmonitored
                self.watch.monitor
              end
            end
          end
        end
      end
    end

  end
end