# frozen_string_literal: true

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.
    #
    # Parameters
    #   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
        super
        self.info = 'process is flapping'
      end

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

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

      def process(event, payload)
        return if event != :state_change

        event_from_state, event_to_state = *payload

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

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

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

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

      private

      def retry_mechanism
        return unless retry_in

        @retry_timeline << Time.now

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

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

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

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

            sleep retry_in

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

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