# frozen_string_literal: true

class UxToolkitRecurringIssueRule < ApplicationRecord
  INTERVAL_ONCE = 'once'
  INTERVAL_DAILY = 'daily'
  INTERVAL_WEEKLY = 'weekly'
  INTERVAL_MONTHLY = 'monthly'
  INTERVAL_YEARLY = 'yearly'
  INTERVALS = [
    INTERVAL_ONCE,
    INTERVAL_DAILY,
    INTERVAL_WEEKLY,
    INTERVAL_MONTHLY,
    INTERVAL_YEARLY
  ].freeze

  LOCK_TIMEOUT = 15.minutes
  MESSAGE_LIMIT = 1000

  belongs_to :project, optional: true
  belongs_to :tracker, optional: true
  belongs_to :priority, class_name: 'IssuePriority', optional: true
  belongs_to :assigned_to, class_name: 'Principal', optional: true
  belongs_to :category, class_name: 'IssueCategory', optional: true
  belongs_to :created_by, class_name: 'User', optional: true
  belongs_to :updated_by, class_name: 'User', optional: true

  validates :project_id, :tracker_id, :priority_id, :interval, :next_run_on, presence: true
  validates :subject, presence: true, length: {maximum: 255}
  validates :interval, inclusion: {in: INTERVALS}
  validate :tracker_belongs_to_project
  validate :category_belongs_to_project
  validate :assignee_is_assignable

  scope :enabled, -> { where(enabled: true) }
  scope :due_on, ->(date) { enabled.where("#{table_name}.next_run_on <= ?", date) }
  scope :ordered, -> { joins(:project).order("#{Project.table_name}.name ASC", :subject, :id) }

  def self.manageable_by(user)
    return all if user&.admin?

    project_ids = Project.visible(user).active.to_a.select do |project|
      user.allowed_to?(:manage_ux_toolkit_recurring_issues, project)
    end.map(&:id)

    where(project_id: project_ids)
  end

  def self.interval_label_key(interval)
    :"label_ux_toolkit_recurring_interval_#{interval}"
  end

  def next_run_after(date)
    return next_run_on if interval == INTERVAL_ONCE

    current = date || next_run_on || Date.current

    loop do
      current =
        case interval
        when INTERVAL_DAILY
          current + 1
        when INTERVAL_WEEKLY
          current + 7
        when INTERVAL_MONTHLY
          current >> 1
        when INTERVAL_YEARLY
          current >> 12
        else
          current + 1
        end

      return current if current > Date.current
    end
  end

  def claim!(now = Time.current)
    updated_rows = self.class
                       .where(id: id)
                       .where('locked_until IS NULL OR locked_until <= ?', now)
                       .where("#{self.class.table_name}.enabled = ? AND #{self.class.table_name}.next_run_on <= ?", true, Date.current)
                       .update_all(
                         locked_until: now + LOCK_TIMEOUT,
                         last_status: 'running',
                         last_message: nil,
                         updated_at: now
                       )

    updated_rows == 1
  end

  def release_success!(run_date, message)
    attributes = {
      last_run_on: run_date,
      next_run_on: next_run_after(run_date),
      last_status: 'success',
      last_message: truncate_message(message),
      locked_until: nil
    }
    attributes[:enabled] = false if interval == INTERVAL_ONCE

    update!(attributes)
  end

  def release_failure!(message)
    update!(
      last_status: 'failed',
      last_message: truncate_message(message),
      locked_until: nil
    )
  end

  def build_issue(author)
    Issue.new(
      project: project,
      tracker: tracker,
      status: tracker&.default_status || IssueStatus.where(is_closed: false).first,
      subject: subject,
      description: description,
      priority: priority,
      assigned_to: assigned_to,
      category: category,
      author: author
    )
  end

  private

  def tracker_belongs_to_project
    return if project.blank? || tracker.blank?
    return if project.trackers.include?(tracker)

    errors.add(:tracker_id, :invalid)
  end

  def category_belongs_to_project
    return if category.blank? || project.blank?
    return if category.project_id == project.id

    errors.add(:category_id, :invalid)
  end

  def assignee_is_assignable
    return if assigned_to.blank? || project.blank?

    assignable_users = project.assignable_users(tracker)
    return if assignable_users.include?(assigned_to)

    errors.add(:assigned_to_id, :invalid)
  end

  def truncate_message(message)
    message.to_s.truncate(MESSAGE_LIMIT)
  end
end
