# frozen_string_literal: true

module Cron
  #
  # Value object for a cron tab entry
  #
  class Tab
    include StandardModel
    include SearchAble
    #
    # Constants
    #
    WILDCARD = '*' unless defined? WILDCARD
    COMMA_DELIM = ',' unless defined? COMMA_DELIM
    SLASH_DEMO = '/' unless defined? SLASH_DEMO
    TIME_UNITS = %i[min hour wday mday month].freeze unless defined? TIME_UNITS
    #
    # Fields
    #
    field :name, type: String
    field :enabled, type: Boolean, default: true
    field :min, type: String, default: 0
    field :hour, type: String, default: 0
    field :wday, type: String, default: WILDCARD
    field :mday, type: String, default: WILDCARD
    field :month, type: String, default: WILDCARD
    field :last_run_at, type: Time
    #
    # Validations
    #
    validates :name, presence: true, uniqueness: true
    validate :valid_values

    #
    # The method might look a bit obtuse, but basically we want to compare the time values for
    #  * Minute
    #  * Hour
    #  * Day of Week
    #  * Day of Month
    #  * Month
    #
    def time_to_run?(time)
      enabled? && TIME_UNITS.collect { |unit| valid_time?(time, unit, send(unit)) }.all?
    end

    #
    # For completion of class, but must be implemented by child class
    #
    def run
      set({ last_run_at: Time.now.utc })
    end

    private

    #
    # Check that all values are within the range
    #
    def valid_values
      valid_range :min, 0..59
      valid_range :hour, 0..23
      valid_range :month, 0..11
      valid_range :wday, 0..6
      valid_range :mday, 0..30
    end

    def valid_range(field, range)
      value = send(field)
      valid = case value
              when WILDCARD
                true
              when Integer
                range.include?(value)
              else
                if value.include?(SLASH_DEMO)
                  (numerator, divisor) = value.split(SLASH_DEMO)
                  range.include?(divisor.to_i) && numerator.eql?(WILDCARD)
                elsif value.include?(COMMA_DELIM)
                  options = value.split(COMMA_DELIM)
                  options.collect { |o| range.include?(o.to_i) }.all?
                else
                  range.include?(value.to_i)
                end
              end
      errors.add(field, "Invalid value, allowed range: #{range}") unless valid
    end

    #
    # Test if the target value matches the time unit or the wild card, or the comma separated list
    # 0 - matches the zero value
    # * - Wildcard, any value matches
    # */15 - matches any value where dividing by 15 is even, or the modulus is zero
    # 5,10,15 - matches on 5, 10 and 15 values
    #
    def valid_time?(time, unit, target)
      case target
      when WILDCARD
        true
      when Integer
        time.send(unit).eql?(target)
      else
        if target.include?(SLASH_DEMO)
          divisor = target.split(SLASH_DEMO).last.to_i
          (time.send(unit) % divisor).zero?
        elsif target.include?(COMMA_DELIM)
          options = target.split(COMMA_DELIM)
          options.collect { |o| time.send(unit.eql?(o.to_i)) }.any?
        else
          time.send(unit).eql?(target.to_i)
        end
      end
    end
  end
end