module ForemanTasks
  require 'parse-cron'

  class RecurringLogic < ApplicationRecord
    include Authorizable

    belongs_to :task_group
    belongs_to :triggering

    has_many :tasks, :through => :task_group
    has_many :task_groups, -> { distinct }, :through => :tasks

    scoped_search :on => :id, :complete_value => false, :validator => ScopedSearch::Validators::INTEGER
    scoped_search :on => :max_iteration, :complete_value => false, :rename => :iteration_limit
    scoped_search :on => :iteration, :complete_value => false
    scoped_search :on => :cron_line, :complete_value => true
    scoped_search :on => :state, :complete_value => true

    before_create do
      task_group.save
    end

    def self.allowed_states
      %w[active disabled finished cancelled failed]
    end

    def enabled=(value)
      task = tasks.find_by(:state => :scheduled)
      if task
        ForemanTasks.dynflow.world.persistence.set_delayed_plan_frozen(task.execution_plan.id, !value, next_occurrence_time)
        if value
          task.update!(:start_at => next_occurrence_time) if task.start_at < Time.zone.now
          update(:state => 'active')
        else
          update(:state => 'disabled')
        end
      else
        raise RecurringLogicCancelledException
      end
    end

    def enabled?
      state != 'disabled'
    end

    def disabled?
      !enabled?
    end

    def start(action_class, *args)
      start_after(action_class, Time.zone.now, *args)
    end

    def start_after(action_class, time, *args)
      self.state = 'active'
      save!
      trigger_repeat_after(time, action_class, *args)
    end

    def trigger_repeat_after(time, action_class, *args)
      return if cancelled?
      if can_continue?(time)
        self.iteration += 1
        save!
        ::ForemanTasks.delay action_class,
                             generate_delay_options(time),
                             *args
      else
        self.state = 'finished'
        save!
        nil
      end
    end

    def trigger_repeat(action_class, *args)
      trigger_repeat_after(Time.zone.now, action_class, *args)
    end

    def cancel
      self.state = 'cancelled'
      save!
      tasks.active.each(&:cancel)
    end

    def next_occurrence_time(time = Time.zone.now)
      @parser ||= CronParser.new(cron_line, Time.zone)
      # @parser.next(start_time) is not inclusive of the start_time hence stepping back one run to include checking start_time for the first run.
      before_next = @parser.next(@parser.last(time))
      return before_next if before_next >= time && tasks.count == 0
      @parser.next(time)
    end

    def generate_delay_options(time = Time.zone.now, options = {})
      {
        :start_at => next_occurrence_time(time),
        :start_before => options['start_before'],
        :recurring_logic_id => id,
        :frozen => disabled?
      }
    end

    def valid?(*_)
      cron_line.present? && valid_cronline? && !state.nil? || can_start?
    end

    def valid_cronline?
      !!next_occurrence_time
    rescue ArgumentError => _
      false
    end

    def can_start?(time = Time.zone.now)
      (end_time.nil? || next_occurrence_time(time) < end_time) &&
        (max_iteration.nil? || iteration < max_iteration)
    end

    def can_continue?(time = Time.zone.now)
      %w[active disabled].include?(state) && can_start?(time)
    end

    def finished?
      state == 'finished'
    end

    def cancelled?
      state == 'cancelled'
    end

    def done?
      %w[cancelled finished].include?(state)
    end

    def humanized_state
      case state
      when 'active'
        N_('Active')
      when 'cancelled'
        N_('Cancelled')
      when 'finished'
        N_('Finished')
      when 'disabled'
        N_('Disabled')
      else
        N_('N/A')
      end
    end

    def self.assemble_cronline(hash)
      hash.values_at(:minutes, :hours, :days, :months, :days_of_week)
          .map { |value| value.nil? || value.blank? ? '*' : value }
          .join(' ')
    end

    def self.new_from_cronline(cronline)
      new.tap do |logic|
        logic.cron_line = cronline
        logic.task_group = ::ForemanTasks::TaskGroups::RecurringLogicTaskGroup.new
      end
    end

    def self.new_from_triggering(triggering)
      cronline = if triggering.input_type == :cronline
                   triggering.cronline
                 else
                   ::ForemanTasks::RecurringLogic.assemble_cronline(cronline_hash(triggering.input_type, triggering.time, triggering.days, triggering.days_of_week))
                 end
      ::ForemanTasks::RecurringLogic.new_from_cronline(cronline).tap do |manager|
        manager.end_time = triggering.end_time if triggering.end_time_limited.present?
        manager.max_iteration = triggering.max_iteration if triggering.max_iteration.present?
        manager.triggering = triggering
      end
    end

    def self.cronline_hash(recurring_type, time_hash, days, days_of_week_hash)
      hash = Hash[[:years, :months, :days, :hours, :minutes].zip(time_hash.values)]
      days_of_week = days_of_week_hash.select { |_key, value| value == '1' }.keys.join(',')
      hash.update :days_of_week => days_of_week, :days => days
      allowed_keys = case recurring_type
                     when :monthly
                       [:minutes, :hours, :days]
                     when :weekly
                       [:minutes, :hours, :days_of_week]
                     when :daily
                       [:minutes, :hours]
                     when :hourly
                       [:minutes]
                     end
      hash.select { |key, _| allowed_keys.include? key }
    end
  end
end