# frozen_string_literal: true

class TopicTimer < ActiveRecord::Base
  MAX_DURATION_MINUTES = 20.years.to_i / 60

  self.ignored_columns = [
    "duration" # TODO(2021-06-01): remove
  ]

  include Trashable

  belongs_to :user
  belongs_to :topic
  belongs_to :category

  validates :user_id, presence: true
  validates :topic_id, presence: true
  validates :execute_at, presence: true
  validates :status_type, presence: true
  validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at] }, if: :public_type?
  validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at, :user_id] }, if: :private_type?
  validates :category_id, presence: true, if: :publishing_to_category?

  validate :executed_at_in_future?
  validate :duration_in_range?

  scope :scheduled_bump_topics, -> { where(status_type: TopicTimer.types[:bump], deleted_at: nil).pluck(:topic_id) }
  scope :pending_timers, ->(before_time = Time.now.utc) do
    where("execute_at <= :before_time AND deleted_at IS NULL", before_time: before_time)
  end

  before_save do
    self.created_at ||= Time.zone.now if execute_at
    self.public_type = self.public_type?

    if (will_save_change_to_execute_at? &&
       !attribute_in_database(:execute_at).nil?) ||
       will_save_change_to_user_id?

      # TODO(martin - 2021-05-01) - Remove this backwards compatability for outstanding
      # jobs once they have all been run and after Jobs::TopicTimerEnqueuer is in place
      self.send("cancel_auto_#{self.class.types[status_type]}_job")
    end
  end

  # These actions are in place to make sure the topic is in the correct
  # state at the point in time where the timer is saved. It does not
  # guarantee that the topic will be in the correct state when the timer
  # job is executed, but each timer job handles deleted topics etc. gracefully.
  #
  # This is also important for the Open Temporarily and Close Temporarily timers,
  # which change the topic's status straight away and set a timer to do the
  # opposite action in the future.
  after_save do
    if (saved_change_to_execute_at? || saved_change_to_user_id?)
      if status_type == TopicTimer.types[:silent_close] || status_type == TopicTimer.types[:close]
        topic.update_status('closed', false, user) if topic.closed?
      end
      if status_type == TopicTimer.types[:open]
        topic.update_status('closed', true, user) if topic.open?
      end
    end
  end

  def status_type_name
    self.class.types[status_type]
  end

  def enqueue_typed_job(time: nil)
    return if typed_job_scheduled?
    self.send("schedule_auto_#{status_type_name}_job")
  end

  # TODO(martin - 2021-05-01) - Remove this backwards compatability for outstanding
  # jobs once they have all been run and after Jobs::TopicTimerEnqueuer is in place
  def typed_job_scheduled?
    scheduled = Jobs.scheduled_for(
      TopicTimer.type_job_map[status_type_name], topic_timer_id: id
    ).any?

    if [:close, :silent_close, :open].include?(status_type_name)
      return scheduled || Jobs.scheduled_for(:toggle_topic_closed, topic_timer_id: id).any?
    end

    scheduled
  end

  def self.type_job_map
    {
      close: :close_topic,
      open: :open_topic,
      publish_to_category: :publish_topic_to_category,
      delete: :delete_topic,
      reminder: :topic_reminder,
      bump: :bump_topic,
      delete_replies: :delete_replies,
      silent_close: :close_topic,
      clear_slow_mode: :clear_slow_mode
    }
  end

  def self.types
    @types ||= Enum.new(
      close: 1,
      open: 2,
      publish_to_category: 3,
      delete: 4,
      reminder: 5,
      bump: 6,
      delete_replies: 7,
      silent_close: 8,
      clear_slow_mode: 9
    )
  end

  def self.public_types
    @_public_types ||= types.except(:reminder, :clear_slow_mode)
  end

  def self.private_types
    @_private_types ||= types.only(:reminder, :clear_slow_mode)
  end

  def public_type?
    !!self.class.public_types[self.status_type]
  end

  def private_type?
    !!self.class.private_types[self.status_type]
  end

  def runnable?
    return false if deleted_at.present?
    return false if execute_at > Time.zone.now
    true
  end

  private

  def duration_in_range?
    return if duration_minutes.blank?

    if duration_minutes.to_i <= 0
      errors.add(:duration_minutes, I18n.t(
        'activerecord.errors.models.topic_timer.attributes.duration_minutes.cannot_be_zero'
      ))
    end

    if duration_minutes.to_i > MAX_DURATION_MINUTES
      errors.add(:duration_minutes, I18n.t(
        'activerecord.errors.models.topic_timer.attributes.duration_minutes.exceeds_maximum'
      ))
    end
  end

  def executed_at_in_future?
    return if created_at.blank? || (execute_at > created_at)

    errors.add(:execute_at, I18n.t(
      'activerecord.errors.models.topic_timer.attributes.execute_at.in_the_past'
    ))
  end

  def publishing_to_category?
    self.status_type.to_i == TopicTimer.types[:publish_to_category]
  end

  # TODO(martin - 2021-05-01) - Remove cancels for toggle_topic_closed once topic timer revamp completed.
  def cancel_auto_close_job
    Jobs.cancel_scheduled_job(:toggle_topic_closed, topic_timer_id: id)
    Jobs.cancel_scheduled_job(:close_topic, topic_timer_id: id)
  end

  # TODO(martin - 2021-05-01) - Remove cancels for toggle_topic_closed once topic timer revamp completed.
  def cancel_auto_open_job
    Jobs.cancel_scheduled_job(:toggle_topic_closed, topic_timer_id: id)
    Jobs.cancel_scheduled_job(:open_topic, topic_timer_id: id)
  end

  # TODO(martin - 2021-05-01) - Remove cancels for toggle_topic_closed once topic timer revamp completed.
  def cancel_auto_silent_close_job
    Jobs.cancel_scheduled_job(:toggle_topic_closed, topic_timer_id: id)
    Jobs.cancel_scheduled_job(:close_topic, topic_timer_id: id)
  end

  def cancel_auto_publish_to_category_job
    Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:publish_to_category], topic_timer_id: id)
  end

  def cancel_auto_delete_job
    Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:delete], topic_timer_id: id)
  end

  def cancel_auto_reminder_job
    Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:reminder], topic_timer_id: id)
  end

  def cancel_auto_bump_job
    Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:bump], topic_timer_id: id)
  end

  def cancel_auto_delete_replies_job
    Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:delete_replies], topic_timer_id: id)
  end

  def cancel_auto_clear_slow_mode_job
    Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:clear_slow_mode], topic_timer_id: id)
  end

  def schedule_auto_delete_replies_job
    Jobs.enqueue(TopicTimer.type_job_map[:delete_replies], topic_timer_id: id)
  end

  def schedule_auto_bump_job
    Jobs.enqueue(TopicTimer.type_job_map[:bump], topic_timer_id: id)
  end

  def schedule_auto_open_job
    Jobs.enqueue(TopicTimer.type_job_map[:open], topic_timer_id: id)
  end

  def schedule_auto_close_job
    Jobs.enqueue(TopicTimer.type_job_map[:close], topic_timer_id: id)
  end

  def schedule_auto_silent_close_job
    Jobs.enqueue(TopicTimer.type_job_map[:close], topic_timer_id: id, silent: true)
  end

  def schedule_auto_publish_to_category_job
    Jobs.enqueue(TopicTimer.type_job_map[:publish_to_category], topic_timer_id: id)
  end

  def schedule_auto_delete_job
    Jobs.enqueue(TopicTimer.type_job_map[:delete], topic_timer_id: id)
  end

  def schedule_auto_reminder_job
    # noop, TODO(martin 2021-03-11): Remove this after timers migrated and outstanding jobs cancelled
  end

  def schedule_auto_clear_slow_mode_job
    Jobs.enqueue(TopicTimer.type_job_map[:clear_slow_mode], topic_timer_id: id)
  end
end

# == Schema Information
#
# Table name: topic_timers
#
#  id                 :integer          not null, primary key
#  execute_at         :datetime         not null
#  status_type        :integer          not null
#  user_id            :integer          not null
#  topic_id           :integer          not null
#  based_on_last_post :boolean          default(FALSE), not null
#  deleted_at         :datetime
#  deleted_by_id      :integer
#  created_at         :datetime         not null
#  updated_at         :datetime         not null
#  category_id        :integer
#  public_type        :boolean          default(TRUE)
#  duration_minutes   :integer
#
# Indexes
#
#  idx_topic_id_public_type_deleted_at  (topic_id) UNIQUE WHERE ((public_type = true) AND (deleted_at IS NULL))
#  index_topic_timers_on_user_id        (user_id)
#
