# frozen_string_literal: true

#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

class Enrollment < ActiveRecord::Base
  SIS_TYPES = {
    "TeacherEnrollment" => "teacher",
    "TaEnrollment" => "ta",
    "DesignerEnrollment" => "designer",
    "StudentEnrollment" => "student",
    "ObserverEnrollment" => "observer"
  }.freeze
  WORKFLOW_STATES = %w[active completed deleted invited pending creation_pending rejected inactive"].freeze

  include Workflow

  belongs_to :course, inverse_of: :enrollments
  belongs_to :course_section, inverse_of: :enrollments
  belongs_to :root_account, class_name: "Account", inverse_of: :enrollments
  belongs_to :user, inverse_of: :enrollments
  belongs_to :sis_pseudonym, class_name: "Pseudonym", inverse_of: :sis_enrollments
  belongs_to :associated_user, class_name: "User"
  belongs_to :temporary_enrollment_pairing, inverse_of: :enrollments

  belongs_to :role
  include Role::AssociationHelper

  has_one :enrollment_state, dependent: :destroy, inverse_of: :enrollment

  has_many :role_overrides, as: :context, inverse_of: :context
  has_many :pseudonyms, primary_key: :user_id, foreign_key: :user_id, inverse_of: false
  has_many :course_account_associations, foreign_key: "course_id", primary_key: "course_id", inverse_of: false
  has_many :scores, -> { active }

  validates :user_id, :course_id, :type, :root_account_id, :course_section_id, :workflow_state, :role_id, presence: true
  validates :limit_privileges_to_course_section, inclusion: { in: [true, false] }
  validates :associated_user_id, inclusion: { in: [nil],
                                              unless: ->(enrollment) { enrollment.type == "ObserverEnrollment" },
                                              message: ->(_object, _data) { t("only ObserverEnrollments may have an associated_user_id") } }
  validate :cant_observe_self, if: ->(enrollment) { enrollment.type == "ObserverEnrollment" }
  validate :cant_observe_observer, if: ->(enrollment) { enrollment.type == "ObserverEnrollment" }

  validate :valid_role?
  validate :valid_course?
  validate :not_template_course?
  validate :valid_section?
  validate :not_student_view

  # update bulk destroy if changing or adding an after save
  before_save :assign_uuid
  before_validation :assert_section
  after_save :recalculate_enrollment_state
  after_save :update_user_account_associations_if_necessary
  before_save :audit_groups_for_deleted_enrollments
  before_validation :ensure_role_id
  after_create :create_linked_enrollments
  after_create :create_enrollment_state
  after_save :copy_scores_from_existing_enrollment, if: :need_to_copy_scores?
  after_save :clear_email_caches
  after_save :cancel_future_appointments
  after_save :update_linked_enrollments
  after_save :set_update_cached_due_dates
  after_save :touch_graders_if_needed
  after_save :reset_notifications_cache
  after_save :dispatch_invitations_later
  after_save :add_to_favorites_later
  after_save :delete_student_allocation_rules_if_needed
  after_commit :update_cached_due_dates
  after_save :update_assignment_overrides_if_needed
  after_create :needs_grading_count_updated, if: :active_student?
  after_update :needs_grading_count_updated, if: :active_student_changed?

  after_commit :sync_microsoft_group
  scope :microsoft_sync_relevant, -> { active_or_pending.accepted.not_fake }
  scope :microsoft_sync_irrelevant_but_not_fake, -> { not_fake.where("enrollments.workflow_state IN ('rejected', 'completed', 'inactive', 'invited')") }

  attr_accessor :already_enrolled, :need_touch_user, :skip_touch_user

  scope :current, -> { joins(:course).where(QueryBuilder.new(:active).conditions).readonly(false) }
  scope :current_and_invited, -> { joins(:course).where(QueryBuilder.new(:current_and_invited).conditions).readonly(false) }
  scope :current_and_future, -> { joins(:course).where(QueryBuilder.new(:current_and_future).conditions).readonly(false) }
  scope :concluded, -> { joins(:course).where(QueryBuilder.new(:completed).conditions).readonly(false) }
  scope :current_and_concluded, -> { joins(:course).where(QueryBuilder.new(:current_and_concluded).conditions).readonly(false) }
  scope :horizon, -> { joins(:course).where(courses: { horizon_course: true }) }
  scope :not_horizon, -> { joins(:course).where(courses: { horizon_course: false }) }

  def ensure_role_id
    self.role_id ||= role.id
  end

  def cant_observe_self
    errors.add(:associated_user_id, "Cannot observe yourself") if user_id == associated_user_id
  end

  def cant_observe_observer
    if !deleted? && course.enrollments.where(type: "ObserverEnrollment",
                                             user_id: associated_user_id,
                                             associated_user_id: user_id).exists?
      errors.add(:associated_user_id, "Cannot observe observer observing self")
    end
  end

  def valid_course?
    if !deleted? && course.deleted?
      errors.add(:course_id, "is not a valid course")
    end
  end

  def not_template_course?
    if course.template?
      errors.add(:course_id, "is a template course")
    end
  end

  def valid_section?
    unless deleted? || course_section.active?
      errors.add(:course_section_id, "is not a valid section")
    end
  end

  def not_student_view
    if type != "StudentViewEnrollment" && (new_record? || association(:user).loaded?) &&
       user.fake_student?
      errors.add(:user_id, "cannot add a student view student in a regular role")
    end
  end

  def valid_role?
    return true if deleted? || role.built_in?

    unless role.base_role_type == type
      errors.add(:role_id, "is not valid for the enrollment type")
    end

    unless course.account.valid_role?(role)
      errors.add(:role_id, "is not an available role for this course's account")
    end
  end

  def self.get_built_in_role_for_type(enrollment_type, root_account_id:)
    role = Role.get_built_in_role("StudentEnrollment", root_account_id:) if enrollment_type == "StudentViewEnrollment"
    role ||= Role.get_built_in_role(enrollment_type, root_account_id:)
    role
  end

  def default_canvas_role
    Enrollment.get_built_in_role_for_type(type, root_account_id: course.root_account_id)
  end

  # see #active_student?
  def self.active_student_conditions
    "(enrollments.type IN ('StudentEnrollment', 'StudentViewEnrollment') AND enrollments.workflow_state = 'active')"
  end

  def self.active_or_completed_student_conditions
    "(enrollments.type IN ('StudentEnrollment', 'StudentViewEnrollment') AND enrollments.workflow_state IN ('active', 'completed'))"
  end

  # see .active_student_conditions
  def active_student?(was = false)
    suffix = was ? "_before_last_save" : ""

    %w[StudentEnrollment StudentViewEnrollment].include?(send(:"type#{suffix}")) &&
      send(:"workflow_state#{suffix}") == "active"
  end

  def active_student_changed?
    active_student? != active_student?(:was)
  end

  def clear_needs_grading_count_cache
    Assignment
      .where(context_id: course_id, context_type: "Course")
      .where(Submission.where(user_id:)
               .where("assignment_id=assignments.id")
               .where("#{Submission.needs_grading_conditions} OR
            (workflow_state = 'deleted' AND submission_type IS NOT NULL AND
            (score IS NULL OR NOT grade_matches_current_submission OR
            (submission_type = 'online_quiz' AND quiz_submission_id IS NOT NULL)))")
            .arel.exists)
      .where.not(Enrollment.where(Enrollment.active_student_conditions)
               .where(user_id:, course_id:)
               .where("id<>?", self)
               .arel.exists)
      .clear_cache_keys(:needs_grading)
  end

  def needs_grading_count_updated
    self.class.connection.after_transaction_commit do
      clear_needs_grading_count_cache
    end
  end

  def delete_student_allocation_rules_if_needed
    # Delete allocation rules for this user in the course if the enrollment was concluded, deactivated, or deleted
    if saved_change_to_workflow_state? && %w[completed inactive deleted].include?(workflow_state)
      user_course_rules = AllocationRule.for_user_in_course(user_id, course_id)
      user_course_rules.update_all(workflow_state: "deleted")
    end
  end

  include StickySisFields

  are_sis_sticky :start_at, :end_at

  has_a_broadcast_policy

  set_broadcast_policy do |p|
    p.dispatch :enrollment_invitation
    p.to { user }
    p.whenever do |record|
      !record.self_enrolled &&
        record.course &&
        record.user.registered? &&
        !record.observer? &&
        ((record.invited? && (record.previously_new_record? || record.saved_change_to_workflow_state?)) || @re_send_confirmation)
    end

    p.dispatch :enrollment_registration
    p.to { user.communication_channel }
    p.whenever do |record|
      !record.self_enrolled &&
        record.course &&
        !record.user.registered? &&
        ((record.invited? && (record.previously_new_record? || record.saved_change_to_workflow_state?)) || @re_send_confirmation)
    end

    p.dispatch :enrollment_notification
    p.to { user }
    p.whenever do |record|
      !record.self_enrolled &&
        record.course &&
        !record.course.created? &&
        !record.observer? &&
        record.previously_new_record? && record.active?
    end

    p.dispatch :enrollment_accepted
    p.to do |record|
      course_admins = record.course.participating_admins.restrict_to_sections([record.course_section_id]) - [user]
      if record.root_account.feature_enabled?(:temporary_enrollments)
        user_ids =
          Enrollment.joins(:enrollment_state).where(user_id: course_admins, course_section_id: record.course_section_id)
                    .where("enrollments.temporary_enrollment_source_user_id IS NULL AND enrollments.workflow_state = 'active' OR
                           (enrollments.temporary_enrollment_source_user_id IS NOT NULL AND enrollment_states.state = 'active')")
                    .distinct.pluck(:user_id)
        course_admins = course_admins.select { |admin| Array(user_ids).include?(admin.id) }
      end
      course_admins
    end
    p.whenever do |record|
      record.course &&
        !record.observer? &&
        !record.previously_new_record? && (record.changed_state(:active, :invited) || record.changed_state(:active, :creation_pending))
    end
  end

  def dispatch_invitations_later
    # if in an invited state but not frd "invited?" because of future date restrictions, send it later
    if (previously_new_record? || saved_change_to_workflow_state? || @re_send_confirmation) && workflow_state == "invited" && inactive? && available_at &&
       !self_enrolled && !(observer? && user.registered?)
      # this won't work if they invite them and then change the course/term/section dates _afterwards_ so hopefully people don't do that
      delay(run_at: available_at, singleton: "send_enrollment_invitations_#{global_id}").re_send_confirmation_if_invited!
    end
  end

  scope :active, -> { where("enrollments.workflow_state<>'deleted'") }
  scope :deleted, -> { where(workflow_state: "deleted") }

  scope :admin, lambda {
                  select(:course_id)
                    .joins(:course)
                    .where("enrollments.type IN ('TeacherEnrollment','TaEnrollment', 'DesignerEnrollment') AND (courses.workflow_state IN ('created', 'claimed') OR (enrollments.workflow_state='active' AND courses.workflow_state='available'))")
                }

  scope :instructor, lambda {
                       select(:course_id)
                         .joins(:course)
                         .where("enrollments.type IN ('TeacherEnrollment','TaEnrollment') AND (courses.workflow_state IN ('created', 'claimed') OR (enrollments.workflow_state='active' AND courses.workflow_state='available'))")
                     }

  scope :of_student_type, -> { where(type: "StudentEnrollment") }

  scope :of_admin_type, -> { where(type: %w[TeacherEnrollment TaEnrollment DesignerEnrollment]) }

  scope :of_instructor_type, -> { where(type: ["TeacherEnrollment", "TaEnrollment"]) }

  scope :of_content_admins, -> { where(type: ["TeacherEnrollment", "DesignerEnrollment"]) }

  scope :of_observer_type, -> { where(type: "ObserverEnrollment") }

  scope :not_of_observer_type, -> { where.not(type: "ObserverEnrollment") }

  scope :student, lambda {
                    select(:course_id)
                      .joins(:course)
                      .where(type: "StudentEnrollment", workflow_state: "active", courses: { workflow_state: "available" })
                  }

  scope :student_in_claimed_or_available, lambda {
                                            select(:course_id)
                                              .joins(:course)
                                              .where(type: "StudentEnrollment", workflow_state: "active", courses: { workflow_state: %w[available claimed created] })
                                          }

  scope :all_student, lambda {
                        eager_load(:course)
                          .where("
                            (
                              enrollments.type = 'StudentEnrollment'
                              AND enrollments.workflow_state IN ('invited', 'active', 'completed')
                            )
                              OR
                            (
                              enrollments.type = 'StudentViewEnrollment'
                              AND enrollments.workflow_state = 'active'
                            )
                          ")
                          .merge(Course.active)
                      }

  scope :not_deleted, lambda {
    joins(:course)
      .where("(courses.workflow_state<>'deleted') AND (enrollments.workflow_state<>'deleted')")
  }

  scope :not_fake, -> { where("enrollments.type<>'StudentViewEnrollment'") }

  scope :temporary_enrollment_recipients_for_provider, lambda { |user|
    active.joins(:course).where(temporary_enrollment_source_user_id: user,
                                courses: { workflow_state: %w[available claimed created] })
  }

  scope :temporary_enrollments_for_recipient, lambda { |user|
    active.joins(:course).where(user_id: user, courses: { workflow_state: %w[available claimed created] })
          .where.not(temporary_enrollment_source_user_id: nil)
  }

  def self.readable_types
    # with enough use, even translations can add up
    RequestCache.cache("enrollment_readable_types") do
      {
        "TeacherEnrollment" => t("#enrollment.roles.teacher", "Teacher"),
        "TaEnrollment" => t("#enrollment.roles.ta", "TA"),
        "DesignerEnrollment" => t("#enrollment.roles.designer", "Designer"),
        "StudentEnrollment" => t("#enrollment.roles.student", "Student"),
        "StudentViewEnrollment" => t("#enrollment.roles.student", "Student"),
        "ObserverEnrollment" => t("#enrollment.roles.observer", "Observer")
      }
    end
  end

  def self.readable_type(type)
    readable_types[type] || readable_types["StudentEnrollment"]
  end

  def self.sis_type(type)
    SIS_TYPES[type] || SIS_TYPES["StudentEnrollment"]
  end

  def sis_type
    Enrollment.sis_type(type)
  end

  def sis_role
    (!role.built_in? && role.name) || Enrollment.sis_type(type)
  end

  def self.valid_types
    SIS_TYPES.keys
  end

  def self.valid_type?(type)
    SIS_TYPES.key?(type)
  end

  def reload(options = nil)
    @enrollment_dates = nil
    super
  end

  def should_update_user_account_association?
    new_record? || previously_new_record? || saved_change_to_course_id? || saved_change_to_course_section_id? ||
      saved_change_to_root_account_id? || being_restored?
  end

  def update_user_account_associations_if_necessary
    return if fake_student?

    if previously_new_record? || being_restored?
      return if %w[creation_pending deleted].include?(user.workflow_state)

      associations = User.calculate_account_associations_from_accounts([course.account_id, course_section.course.account_id, course_section.nonxlist_course.try(:account_id)].compact.uniq)
      user.update_account_associations(incremental: true, precalculated_associations: associations)
    elsif should_update_user_account_association?
      user.update_account_associations_later
    end
  end
  protected :update_user_account_associations_if_necessary

  def other_section_enrollment_exists?
    # If other active sessions that the user is enrolled in exist.
    course.student_enrollments.where.not(workflow_state: ["deleted", "rejected"]).for_user(user).where.not(id:).exists?
  end

  def audit_groups_for_deleted_enrollments
    # did the student cease to be enrolled in a non-deleted state in a section?
    had_section = course_section_id_was.present?
    deleted_states = ["deleted", "rejected"]
    was_active = !deleted_states.include?(workflow_state_was)
    is_deleted = deleted_states.include?(workflow_state)
    return unless had_section && was_active &&
                  (course_section_id_changed? || is_deleted)

    # what section the user is abandoning, and the section they're moving to
    # (if it's in the same course and the enrollment's not deleted)
    section = CourseSection.find(course_section_id_was)

    # ok, consider groups the user is in from the abandoned section's course
    user.groups.preload(:group_category).where(
      context_type: "Course", context_id: section.course_id
    ).each do |group|
      # check group deletion criteria if either enrollment is not a deletion
      # or it may be a deletion/unenrollment from a section but not from the course as a whole (still enrolled in another section)
      if !is_deleted || other_section_enrollment_exists?
        # don't bother unless the group's category has section restrictions
        next unless group.group_category&.restricted_self_signup?

        # skip if the user is the only user in the group. there's no one to have
        # a conflicting section.
        next unless group.users.where.not(id: user_id).exists?

        # check if the group has the section the user is abandoning as a common
        # section (from CourseSection#common_to_users? view, the enrollment is
        # still there since it queries the db directly and we haven't saved yet);
        # if not, dropping the section is not necessary
        next unless section.common_to_users?(group.users)
      end

      # at this point, the group is restricted, there's more than one user and
      # it appears that the group is common to the section being left by the user so
      # remove the user from the group. Or the student was only enrolled in one section and
      # by leaving the section he/she is completely leaving the course so remove the
      # user from any group related to the course.
      membership = group.group_memberships.where(user_id:).first
      membership&.destroy
    end

    user.differentiation_tags.preload(:group_category).where(
      context_type: "Course", context_id: section.course_id
    ).find_each do |tag|
      # Only remove differentiation tag memberships if the enrollment is being deleted/rejected
      next unless is_deleted

      next if other_section_enrollment_exists?

      membership = tag.group_memberships.where(user_id:).first
      membership&.destroy
    end
  end
  protected :audit_groups_for_deleted_enrollments

  def observers
    student? ? user.linked_observers.active.linked_through_root_account(root_account) : []
  end

  def create_linked_enrollments
    observers.each do |observer|
      create_linked_enrollment_for(observer)
    end
  end

  def update_linked_enrollments(restore: false)
    restorable_states = %w[inactive deleted completed]
    observers.each do |observer|
      enrollment = restore ? linked_enrollment_for(observer) : active_linked_enrollment_for(observer)
      if enrollment
        enrollment.update_from(self)
      elsif restore || (saved_change_to_workflow_state? && restorable_states.include?(workflow_state_before_last_save))
        create_linked_enrollment_for(observer)
      end
    end
  end

  def create_linked_enrollment_for(observer)
    # we don't want to create a new observer enrollment if one exists
    self.class.unique_constraint_retry do
      enrollment = linked_enrollment_for(observer)
      return true if enrollment && !enrollment.deleted? && !enrollment.inactive?
      return false unless observer.can_be_enrolled_in_course?(course)

      enrollment ||= observer.observer_enrollments.build
      enrollment.associated_user_id = user_id
      enrollment.shard = shard if enrollment.new_record?
      enrollment.update_from(self, !!@skip_broadcasts)
    end
  end

  def linked_enrollment_for(observer)
    observer.observer_enrollments.where(
      associated_user_id: user_id,
      course_section_id: course_section_id_before_last_save || course_section_id
    )
            .shard(Shard.shard_for(course_id)).first
  end

  def active_linked_enrollment_for(observer)
    enrollment = linked_enrollment_for(observer)
    # we don't want to "undelete" observer enrollments that have been
    # explicitly deleted
    return nil if enrollment&.deleted? && workflow_state_before_last_save != "deleted"

    enrollment
  end

  # This is Part 1 of the update_cached_due_dates callback.  It sets @update_cached_due_dates which determines
  # whether or not the update_cached_due_dates after_commit callback runs after this record has been committed.
  # This split allows us to suspend this callback and affect the update_cached_due_dates callback since after_commit
  # callbacks aren't being suspended properly.  We suspend this callback during some bulk operations.
  def set_update_cached_due_dates
    @update_cached_due_dates = saved_change_to_workflow_state? && (student? || fake_student?) && course
  end

  def update_cached_due_dates
    if @update_cached_due_dates
      update_grades = being_restored?(to_state: "active") ||
                      being_restored?(to_state: "inactive") ||
                      previously_new_record?
      SubmissionLifecycleManager.recompute_users_for_course(user_id, course, nil, update_grades:)
    end
  end

  def update_from(other, skip_broadcasts = false)
    self.course_id = other.course_id
    self.workflow_state = if type == "ObserverEnrollment" && other.workflow_state == "invited"
                            "active"
                          else
                            other.workflow_state
                          end
    self.start_at = other.start_at
    self.end_at = other.end_at
    self.course_section_id = other.course_section_id
    self.root_account_id = other.root_account_id
    self.sis_batch_id = other.sis_batch_id unless sis_batch_id.nil?
    self.skip_touch_user = other.skip_touch_user
    if skip_broadcasts
      save_without_broadcasting!
    else
      save!
    end
  end

  def clear_email_caches
    if saved_change_to_workflow_state? && (workflow_state_before_last_save == "invited" || workflow_state == "invited")
      if Enrollment.cross_shard_invitations?
        Shard.birth.activate do
          user.communication_channels.email.unretired.each { |cc| Rails.cache.delete([cc.path, "all_invited_enrollments2"].cache_key) }
        end
      else
        user.communication_channels.email.unretired.each { |cc| Rails.cache.delete([cc.path, "invited_enrollments2"].cache_key) }
      end
    end
  end

  def cancel_future_appointments
    if saved_change_to_workflow_state? && %w[completed deleted].include?(workflow_state) &&
       !course.current_enrollments.where(user_id:).exists? # ignore if they have another still valid enrollment
      course.appointment_participants.active.current.for_context_codes(user.asset_string).update_all(workflow_state: "deleted")
    end
  end

  def conclude
    self.workflow_state = "completed"
    self.completed_at = Time.zone.now
    save
  end

  def unconclude
    self.workflow_state = "active"
    self.completed_at = nil
    save
  end

  def deactivate
    self.workflow_state = "inactive"
    save
  end

  def reactivate
    self.workflow_state = "active"
    save
  end

  def defined_by_sis?
    !!sis_batch_id
  end

  def assigned_observer?
    observer? && associated_user_id
  end

  def participating?
    state_based_on_date == :active
  end

  def participating_student?
    student? && participating?
  end

  def participating_observer?
    observer? && participating?
  end

  def participating_teacher?
    teacher? && participating?
  end

  def participating_ta?
    ta? && participating?
  end

  def participating_instructor?
    instructor? && participating?
  end

  def participating_designer?
    designer? && participating?
  end

  def participating_admin?
    admin? && participating?
  end

  def participating_content_admin?
    content_admin? && participating?
  end

  def associated_user_name
    associated_user&.short_name
  end

  def assert_section
    self.course_section = course.default_section if !course_section_id && course
    self.root_account_id ||= course&.root_account_id
  end

  def course_name(display_user = nil)
    course.nickname_for(display_user) || t("#enrollment.default_course_name", "Course")
  end

  def short_name(length = nil, display_user = nil)
    return @short_name if @short_name

    @short_name = course_section.display_name if course_section && root_account&.show_section_name_as_course_name
    @short_name ||= course_name(display_user)
    @short_name = @short_name[0..length] if length
    @short_name
  end

  def long_name(display_user = nil)
    return @long_name if @long_name

    @long_name = course_name(display_user)
    @long_name = t("#enrollment.with_section", "%{course_name}, %{section_name}", course_name: @long_name, section_name: course_section.display_name) if course_section&.display_name && course_section.display_name != course_name(display_user)
    @long_name
  end

  TYPE_RANKS = {
    default: %w[TeacherEnrollment TaEnrollment DesignerEnrollment StudentEnrollment StudentViewEnrollment ObserverEnrollment],
    student: %w[StudentEnrollment TeacherEnrollment TaEnrollment DesignerEnrollment StudentViewEnrollment ObserverEnrollment]
  }.freeze
  TYPE_RANK_HASHES = TYPE_RANKS.transform_values { |v| rank_hash(v) }
  def self.type_rank_sql(order = :default)
    # don't call rank_sql during class load
    rank_sql(TYPE_RANKS[order], "enrollments.type")
  end

  def rank_sortable(order = :default)
    TYPE_RANK_HASHES[order][self.class.to_s]
  end

  STATE_RANK = ["active", ["invited", "creation_pending"], "completed", "inactive", "rejected", "deleted"].freeze
  STATE_RANK_HASH = rank_hash(STATE_RANK)
  def self.state_rank_sql
    # don't call rank_sql during class load
    @state_rank_sql ||= rank_sql(STATE_RANK, "enrollments.workflow_state")
  end

  def state_sortable
    STATE_RANK_HASH[state.to_s]
  end

  STATE_BY_DATE_RANK = ["active", %w[invited creation_pending pending_active pending_invited], "completed", "inactive", "rejected", "deleted"].freeze
  STATE_BY_DATE_RANK_HASH = rank_hash(STATE_BY_DATE_RANK)
  def self.state_by_date_rank_sql
    @state_by_date_rank_sql ||= Arel.sql(
      rank_sql(STATE_BY_DATE_RANK, "enrollment_states.state")
        .sub(/^CASE/, "CASE WHEN enrollment_states.restricted_access THEN #{STATE_BY_DATE_RANK.index("inactive")}") # pretend restricted access is the same as inactive
    )
  end

  def state_with_date_sortable
    STATE_RANK_HASH[state_based_on_date.to_s]
  end

  def accept!
    res = accept
    raise "can't accept" unless res

    res
  end

  def accept(force = false)
    GuardRail.activate(:primary) do
      return false unless force || invited?

      if update_attribute(:workflow_state, "active")
        if type == "StudentEnrollment"
          Enrollment.recompute_final_score_in_singleton(user_id, course_id)
        end
        true
      end
    end
  end

  def reset_notifications_cache
    if saved_change_to_workflow_state?
      StreamItemCache.invalidate_recent_stream_items(user_id, "Course", course_id)
    end
  end

  def add_to_favorites_later
    if saved_change_to_workflow_state? && workflow_state == "active"
      self.class.connection.after_transaction_commit do
        delay_if_production(priority: Delayed::LOW_PRIORITY).add_to_favorites
      end
    end
  end

  def self.batch_add_to_favorites(enrollment_ids)
    Enrollment.where(id: enrollment_ids).each(&:add_to_favorites)
  end

  def add_to_favorites
    # this method was written by Alan Smithee
    user.shard.activate do
      if user.favorites.where(context_type: "Course").exists? # only add a favorite if they've ever favorited anything even if it's no longer in effect
        Favorite.create_or_find_by(user:, context: course)
      end
    end
  end

  workflow do
    state :invited do
      event :reject, transitions_to: :rejected
      event :complete, transitions_to: :completed
    end

    state :creation_pending do
      event :invite, transitions_to: :invited
    end

    state :active do
      event :reject, transitions_to: :rejected
      event :complete, transitions_to: :completed
    end

    state :deleted
    state :rejected do
      event :unreject, transitions_to: :invited
    end
    state :completed

    # Inactive is a "hard" state, i.e. tuition not paid
    state :inactive
  end

  def enrollment_dates
    Canvas::Builders::EnrollmentDateBuilder.preload([self]) unless @enrollment_dates
    @enrollment_dates
  end

  def enrollment_state
    raise "cannot call enrollment_state on a new record" if new_record?

    result = super
    unless result
      association(:enrollment_state).reload
      result = super
    end
    # ensure we have an enrollment state object present with a reverse association
    result ||= create_enrollment_state
    result.enrollment = self
    result
  end

  def create_enrollment_state
    self.enrollment_state =
      shard.activate do
        GuardRail.activate(:primary) do
          EnrollmentState.unique_constraint_retry do
            EnrollmentState.where(enrollment_id: self).first_or_create
          end
        end
      end
  end

  def recalculate_enrollment_state
    if saved_changes.keys.intersect?(%w[workflow_state start_at end_at])
      @enrollment_dates = nil
      enrollment_state.state_is_current = false
      enrollment_state.is_direct_recalculation = true
    end
    enrollment_state.skip_touch_user ||= skip_touch_user
    enrollment_state.ensure_current_state
  end

  def state_based_on_date
    RequestCache.cache("enrollment_state_based_on_date", self, workflow_state, saved_changes?) do
      if %w[invited active completed].include?(workflow_state)
        enrollment_state.get_effective_state
      else
        workflow_state.to_sym
      end
    end
  end

  def readable_state_based_on_date
    # when view restrictions are in place, the effective state_based_on_date is :inactive, but
    # to admins we should show that they are :completed or :pending
    enrollment_state.get_display_state
  end

  def available_at
    if enrollment_state.pending?
      enrollment_state.state_valid_until
    end
  end

  def view_restrictable?
    (student? && !fake_student?) || observer?
  end

  def restrict_past_view?
    view_restrictable? && RequestCache.cache("restrict_student_past_view", global_course_id) do
      course.restrict_student_past_view?
    end
  end

  def restrict_future_view?
    view_restrictable? && RequestCache.cache("restrict_student_future_view", global_course_id) do
      course.restrict_student_future_view?
    end
  end

  def restrict_future_listing?
    enrollment_state.pending? &&
      (enrollment_state.restricted_access? || (!admin? && course.unpublished?)) &&
      course.account.restrict_student_future_listing[:value]
  end

  def active?
    state_based_on_date == :active
  end

  def inactive?
    state_based_on_date == :inactive
  end

  def hard_inactive?
    workflow_state == "inactive"
  end

  def invited?
    state_based_on_date == :invited
  end

  def accepted?
    state_based_on_date == :accepted
  end

  def completed?
    enrollment_state.get_display_state == :completed
  end

  def explicitly_completed?
    state == :completed
  end

  def completed_at
    if (date = super)
      date
    elsif !new_record? && completed?
      enrollment_state.state_started_at
    end
  end

  alias_method :destroy_permanently!, :destroy
  def destroy
    self.workflow_state = "deleted"
    result = save
    if saved_change_to_workflow_state?
      user.try(:update_account_associations)
      scores.update_all(updated_at: Time.zone.now, workflow_state: :deleted)
      Enrollment.recompute_final_score_in_singleton(
        user.id,
        course.id
      )

      Assignment.remove_user_as_final_grader(user_id, course_id) if remove_user_as_final_grader?
    end
    result
  end

  def restore
    self.workflow_state = "active"
    self.completed_at = nil
    save
    true
  end

  def re_send_confirmation!
    @re_send_confirmation = true
    save
    @re_send_confirmation = false
    true
  end

  def re_send_confirmation_if_invited!
    re_send_confirmation! if invited?
  end

  def has_permission_to?(action)
    @permission_lookup ||= {}
    unless @permission_lookup.key? action
      @permission_lookup[action] = RoleOverride.enabled_for?(course, action, self.role_id, nil)
    end
    @permission_lookup[action].include?(:self)
  end

  def base_role_name
    self.class.to_s
  end

  # Determine if a user has permissions to conclude this enrollment.
  #
  # user    - The user requesting permission to conclude/delete enrollment.
  # context - The current context, e.g. course or section.
  # session - The current user's session (pass nil if not available).
  #
  # return Boolean
  def can_be_concluded_by(user, context, session)
    can_remove = [StudentEnrollment].include?(self.class) &&
                 context.grants_right?(user, session, :manage_students) &&
                 context.id == (context.is_a?(Course) ? course_id : course_section_id)
    can_remove || context.grants_right?(user, session, :allow_course_admin_actions)
  end

  # Determine if a user has permissions to delete this enrollment.
  #
  # user    - The user requesting permission to conclude/delete enrollment.
  # context - The current context, e.g. course or section.
  # session - The current user's session (pass nil if not available).
  #
  # return Boolean
  def can_be_deleted_by(user, context, session)
    return context.grants_right?(user, session, :use_student_view) if fake_student?

    can_remove = can_delete_via_granular(user, session, context)
    can_remove &&= user_id != user.id || context.account.grants_right?(user, session, :allow_course_admin_actions)
    can_remove && context.id == (context.is_a?(Course) ? course_id : course_section_id)
  end

  def pending?
    invited? || creation_pending?
  end

  delegate :email, to: :user

  def user_name
    has_attribute?("user_name") ? self["user_name"] : user.name
  end

  def context
    @context ||= course
  end

  def context_id
    @context_id ||= course_id
  end

  def can_switch_to?(type)
    case type
    when "ObserverEnrollment",
         "StudentEnrollment"
      %w[TeacherEnrollment TaEnrollment DesignerEnrollment].include?(self.type)
    when "TaEnrollment"
      ["TeacherEnrollment"].include?(self.type)
    else
      false
    end
  end

  def self.workflow_readable_type(state)
    case state.to_s
    when "active"
      t("#enrollment.workflow.active", "Active")
    when "completed"
      t("#enrollment.workflow.completed", "Completed")
    when "deleted"
      t("#enrollment.workflow.deleted", "Deleted")
    when "invited"
      t("#enrollment.workflow.invited", "Invited")
    when "pending", "creation_pending"
      t("#enrollment.workflow.pending", "Pending")
    when "rejected"
      t("#enrollment.workflow.rejected", "Rejected")
    when "inactive"
      t("#enrollment.workflow.inactive", "Inactive")
    end
  end

  def readable_role_name
    role.built_in? ? readable_type : role.name
  end

  def readable_type
    Enrollment.readable_type(self.class.to_s)
  end

  # This is called to recompute the users' cached scores for a given course
  # when:
  #
  # * The user is merged with another user; the scores are recomputed for the
  #   new user in each of his/her courses.
  #
  # * An assignment's default grade is changed; all users in the assignment's
  #   course have their scores for that course recomputed.
  #
  # * A course is merged into another, a section is crosslisted/uncrosslisted,
  #   or a section is otherwise moved between courses; scores are recomputed
  #   for all users in the target course.
  #
  # * A course's group_weighting_scheme is changed; scores are recomputed for
  #   all users in the course.
  #
  # * Assignments are reordered (since an assignment may change groups, which
  #   may have weights); scores are recomputed for all users in the associated
  #   course.
  #
  # * An assignment's points_possible is changed; scores are recomputed for all
  #   users in the associated course.
  #
  # * An assignment group's rules or group_weight are changed; scores are
  #   recomputed for all users in the associated course.
  #
  # * A submission's score is changed; scores for the submission owner in the
  #   associated course are recomputed.
  #
  # * An assignment is deleted/undeleted
  #
  # * An enrollment is accepted (to address the scenario where a student
  #   is transferred from one section to another, and final grades need
  #   to be transferred)
  #
  # If some new feature comes up that affects calculation of a user's score,
  # please add appropriate calls to this so that the cached values don't get
  # stale! And once you've added the call, add the condition to the comment
  # here for future enlightenment.

  def self.recompute_final_score(*, **)
    GradeCalculator.recompute_final_score(*, **)
  end

  # This method is intended to not duplicate work for a single user.
  def self.recompute_final_score_in_singleton(user_id, course_id, **opts)
    # Guard against getting more than one user_id
    raise ArgumentError, "Cannot call with more than one user" if Array(user_id).size > 1

    delay_if_production(singleton: "Enrollment.recompute_final_score:#{user_id}:#{course_id}:#{opts[:grading_period_id]}",
                        max_attempts: 10)
      .recompute_final_score(user_id, course_id, **opts)
  end

  def self.recompute_due_dates_and_scores(user_id)
    Course.where(id: StudentEnrollment.where(user_id:).distinct.select(:course_id)).each do |course|
      SubmissionLifecycleManager.recompute_users_for_course([user_id], course, nil, update_grades: true)
    end
  end

  def self.recompute_final_scores(user_id)
    StudentEnrollment.where(user_id:).distinct.pluck(:course_id).each do |course_id|
      recompute_final_score_in_singleton(user_id, course_id)
    end
  end

  def computed_current_grade(id_opts = nil)
    cached_score_or_grade(:current, :grade, :posted, id_opts)
  end

  def computed_final_grade(id_opts = nil)
    cached_score_or_grade(:final, :grade, :posted, id_opts)
  end

  def computed_current_score(id_opts = nil)
    cached_score_or_grade(:current, :score, :posted, id_opts)
  end

  def computed_final_score(id_opts = nil)
    cached_score_or_grade(:final, :score, :posted, id_opts)
  end

  def effective_current_grade(id_opts = nil)
    score = find_score(id_opts)

    if score&.overridden? && course.allow_final_grade_override?
      score.effective_final_grade
    else
      computed_current_grade(id_opts)
    end
  end

  def effective_current_score(id_opts = nil)
    score = find_score(id_opts)

    if score&.overridden? && course.allow_final_grade_override?
      score.effective_final_score
    else
      computed_current_score(id_opts)
    end
  end

  def effective_final_grade(id_opts = nil)
    score = find_score(id_opts)

    if score&.overridden? && course.allow_final_grade_override?
      score.effective_final_grade
    else
      computed_final_grade(id_opts)
    end
  end

  def effective_final_score(id_opts = nil)
    score = find_score(id_opts)

    if score&.overridden? && course.allow_final_grade_override?
      score.effective_final_score
    else
      computed_final_score(id_opts)
    end
  end

  def effective_final_grade_custom_status_id(id_opts = nil)
    score = find_score(id_opts)

    score.custom_grade_status_id if score&.overridden? && course.allow_final_grade_override?
  end

  def override_grade(id_opts = nil)
    return nil unless course.allow_final_grade_override? && course.grading_standard_enabled?

    score = find_score(id_opts)
    score.effective_final_grade if score&.override_score
  end

  def override_score(id_opts = nil)
    return nil unless course.allow_final_grade_override?

    score = find_score(id_opts)
    score&.override_score
  end

  def computed_current_points(id_opts = nil)
    find_score(id_opts)&.current_points
  end

  def computed_final_points(id_opts = nil)
    find_score(id_opts)&.final_points
  end

  def unposted_current_points(id_opts = nil)
    find_score(id_opts)&.unposted_current_points
  end

  def unposted_current_grade(id_opts = nil)
    cached_score_or_grade(:current, :grade, :unposted, id_opts)
  end

  def unposted_final_grade(id_opts = nil)
    cached_score_or_grade(:final, :grade, :unposted, id_opts)
  end

  def unposted_current_score(id_opts = nil)
    cached_score_or_grade(:current, :score, :unposted, id_opts)
  end

  def unposted_final_score(id_opts = nil)
    cached_score_or_grade(:final, :score, :unposted, id_opts)
  end

  def cached_score_or_grade(current_or_final, score_or_grade, posted_or_unposted, id_opts = nil)
    score = find_score(id_opts)
    method = "#{current_or_final}_#{score_or_grade}"
    method.prepend("unposted_") if posted_or_unposted == :unposted
    score&.send(method)
  end
  private :cached_score_or_grade

  def find_score(id_opts = nil)
    id_opts ||= Score.params_for_course
    given_score = id_opts.delete(:score)
    return given_score if given_score

    valid_keys = %i[course_score grading_period grading_period_id assignment_group assignment_group_id]
    return nil if id_opts.except(*valid_keys).any?

    result = if scores.loaded?
               scores.detect { |score| score.attributes >= id_opts.with_indifferent_access }
             else
               scores.where(id_opts).first
             end
    if result
      result.enrollment = self
      # have to go through gymnastics to force-preload a has_one :through without causing a db transaction
      if association(:course).loaded?
        assn = result.association(:course)
        assn.target = course
      end
    end
    result
  end

  def graded_at
    find_score&.updated_at
  end

  def self.typed_enrollment(type)
    return nil unless %w[StudentEnrollment
                         StudentViewEnrollment
                         TeacherEnrollment
                         TaEnrollment
                         ObserverEnrollment
                         DesignerEnrollment].include?(type)

    type.constantize
  end

  # overridden to return true in appropriate subclasses
  def student?
    false
  end

  def fake_student?
    false
  end

  def student_with_conditions?(include_future:, include_fake_student:)
    return false unless student? || fake_student?

    if include_fake_student
      include_future || participating?
    else
      include_future ? student? : participating_student?
    end
  end

  def temporary_enrollment?
    temporary_enrollment_source_user_id.present?
  end

  def temporary_enrollment_source_user
    return nil unless temporary_enrollment?

    User.find(temporary_enrollment_source_user_id)
  end

  def observer?
    false
  end

  def teacher?
    false
  end

  def ta?
    false
  end

  def designer?
    false
  end

  def instructor?
    teacher? || ta?
  end

  def admin?
    instructor? || designer?
  end

  def content_admin?
    teacher? || designer?
  end

  def to_atom
    {
      title: t("#enrollment.title", "%{user_name} in %{course_name}", user_name:, course_name:),
      updated: updated_at,
      published: created_at,
      link: "/courses/#{course.id}/enrollments/#{id}"
    }
  end

  set_policy do
    given { |user, session| course.grants_any_right?(user, session, :manage_students, :allow_course_admin_actions, :read_roster) }
    can :read

    given { |user| self.user == user }
    can :read and can :read_grades

    given do |user, session|
      course.students_visible_to(user, include: :priors).where(id: user_id).exists? &&
        course.grants_any_right?(user, session, :manage_grades, :view_all_grades)
    end
    can :read and can :read_grades

    given { |user| course.observer_enrollments.where(user_id: user, associated_user_id: user_id).exists? }
    can :read and can :read_grades

    given { |user, session| course.grants_right?(user, session, :participate_as_student) && self.user.show_user_services }
    can :read_services

    # read_services says this person has permission to see what web services this enrollment has linked to their account
    given { |user, session| grants_right?(user, session, :read) && self.user.show_user_services }
    can :read_services
  end

  scope :before, lambda { |date|
    where("enrollments.created_at<?", date)
  }

  scope :for_user, ->(user) { where(user_id: user) }

  scope :for_courses_with_user_name, lambda { |courses|
    where(course_id: courses)
      .joins(:user)
      .select("user_id, course_id, users.name AS user_name")
  }
  scope :invited, -> { where(workflow_state: "invited") }
  scope :accepted, -> { where("enrollments.workflow_state<>'invited'") }
  scope :active_or_pending, -> { where("enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted', 'inactive')") }
  scope :all_active_or_pending, -> { where("enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted')") } # includes inactive

  scope :excluding_pending, -> { joins(:enrollment_state).where.not(enrollment_states: { state: EnrollmentState::PENDING_STATES }) }
  scope :active_by_date, -> { joins(:enrollment_state).where("enrollment_states.state = 'active'") }
  scope :invited_by_date, lambda {
                            joins(:enrollment_state).where(enrollment_states: { restricted_access: false })
                                                    .where("enrollment_states.state IN ('invited', 'pending_invited')")
                          }
  scope :active_or_pending_by_date, lambda {
                                      joins(:enrollment_state).where(enrollment_states: { restricted_access: false })
                                                              .where("enrollment_states.state IN ('active', 'invited', 'pending_invited', 'pending_active')")
                                    }
  scope :invited_or_pending_by_date, lambda {
                                       joins(:enrollment_state).where(enrollment_states: { restricted_access: false })
                                                               .where("enrollment_states.state IN ('invited', 'pending_invited', 'pending_active')")
                                     }
  scope :completed_by_date,
        -> { joins(:enrollment_state).where(enrollment_states: { restricted_access: false, state: "completed" }) }
  scope :not_inactive_by_date, lambda {
                                 joins(:enrollment_state).where(enrollment_states: { restricted_access: false })
                                                         .where("enrollment_states.state IN ('active', 'invited', 'completed', 'pending_invited', 'pending_active')")
                               }

  scope :active_or_pending_by_date_ignoring_access, lambda {
                                                      joins(:enrollment_state)
                                                        .where("enrollment_states.state IN ('active', 'invited', 'pending_invited', 'pending_active')")
                                                    }
  scope :not_inactive_by_date_ignoring_access, lambda {
                                                 joins(:enrollment_state)
                                                   .where("enrollment_states.state IN ('active', 'invited', 'completed', 'pending_invited', 'pending_active')")
                                               }
  scope :new_or_active_by_date, lambda {
                                  joins(:enrollment_state)
                                    .where("enrollment_states.state IN ('active', 'invited', 'pending_invited', 'pending_active', 'creation_pending')")
                                }

  scope :currently_online, -> { joins(:pseudonyms).where("pseudonyms.last_request_at>?", 5.minutes.ago) }
  # this returns enrollments for creation_pending users; should always be used in conjunction with the invited scope
  scope :for_email, lambda { |email|
    joins(user: :communication_channels)
      .where("users.workflow_state='creation_pending' AND communication_channels.workflow_state='unconfirmed' AND path_type='email' AND LOWER(path)=LOWER(?)", email)
      .select("enrollments.*")
      .readonly(false)
  }
  def self.cached_temporary_invitations(email)
    if Enrollment.cross_shard_invitations?
      Shard.birth.activate do
        Rails.cache.fetch([email, "all_invited_enrollments2"].cache_key) do
          Shard.with_each_shard(CommunicationChannel.associated_shards(email)) do
            Enrollment.invited.for_email(email).to_a
          end
        end
      end
    else
      Rails.cache.fetch([email, "invited_enrollments2"].cache_key) do
        Enrollment.invited.for_email(email).to_a
      end
    end
  end

  def self.order_by_sortable_name
    clause = User.sortable_name_order_by_clause("users")
    scope = order(clause)
    if scope.select_values.present?
      scope.select(clause)
    else
      scope.select(arel_table[Arel.star])
    end
  end

  def self.top_enrollment_by(key, rank_order = :default)
    raise "top_enrollment_by_user must be scoped" unless all.where_clause.present?

    key = key.to_s
    order(Arel.sql("#{key}, #{type_rank_sql(rank_order)}")).distinct_on(key)
  end

  def assign_uuid
    # DON'T use ||=, because that will cause an immediate save to the db if it
    # doesn't already exist
    self.uuid = CanvasSlug.generate_securish_uuid unless self["uuid"]
  end
  protected :assign_uuid

  def uuid
    unless super
      update_attribute(:uuid, CanvasSlug.generate_securish_uuid)
    end
    super
  end

  def self.limit_privileges_to_course_section!(course, user, limit)
    course.shard.activate do
      Enrollment.where(course_id: course, user_id: user).each do |enrollment|
        enrollment.limit_privileges_to_course_section = !!limit
        enrollment.save!
      end
    end
    user.clear_cache_key(:enrollments)
  end

  def self.course_user_state(course, uuid)
    Rails.cache.fetch(["user_state", course, uuid].cache_key) do
      enrollment = course.enrollments.where(uuid:).first
      if enrollment
        {
          enrollment_state: enrollment.workflow_state,
          user_state: enrollment.user.state,
          is_admin: enrollment.admin?
        }
      else
        nil
      end
    end
  end

  def self.serialization_excludes
    %i[uuid computed_final_score computed_current_score]
  end

  # enrollment term per-section is deprecated; a section's term is inherited from the
  # course it is currently tied to
  delegate :enrollment_term, to: :course

  def effective_start_at
    # try and use the enrollment dates logic first, since it knows about
    # overrides, etc. but if it doesn't find anything, start guessing by
    # looking at the enrollment, section, course, then term. if we still didn't
    # find it, fall back to the section or course creation date.
    enrollment_dates.filter_map(&:first).min ||
      start_at ||
      course_section&.start_at ||
      course.start_at ||
      course.enrollment_term&.start_at ||
      course_section&.created_at ||
      course.created_at
  end

  def effective_end_at
    # try and use the enrollment dates logic first, since it knows about
    # overrides, etc. but if it doesn't find anything, start guessing by
    # looking at the enrollment, section, course, then term.
    enrollment_dates.filter_map(&:last).max ||
      end_at ||
      course_section&.end_at ||
      course.conclude_at ||
      course.enrollment_term&.end_at
  end

  def self.cross_shard_invitations?
    false
  end

  def total_activity_time
    super.to_i
  end

  def touch_graders_if_needed
    if !active_student? && active_student?(:was) && course.submissions.where(user_id:).exists?
      self.class.connection.after_transaction_commit do
        course.admins.clear_cache_keys(:todo_list)
      end
    end
  end

  def update_assignment_overrides_if_needed
    assignment_scope = Assignment.where(context_id: course_id, context_type: "Course")
    override_scope = AssignmentOverrideStudent.where(user_id:)

    if being_deleted? && !enrollments_exist_for_user_in_course?
      return unless (assignment_ids = assignment_scope.pluck(:id)).any?

      # this is handled in after_commit :update_cached_due_dates
      AssignmentOverrideStudent.suspend_callbacks(:update_cached_due_dates) do
        override_scope.where(assignment_id: assignment_ids).find_each(&:destroy)
      end
    end

    if being_accepted?
      return unless ConditionalRelease::Service.enabled_in_context?(course)

      # Deleted student overrides associated with assignments with a Mastery Path override
      releases = override_scope.where(workflow_state: "deleted")
                               .where(assignment: assignment_scope)
                               .joins(assignment: :assignment_overrides)
                               .where(assignment_overrides: {
                                        set_type: AssignmentOverride::SET_TYPE_NOOP,
                                        set_id: AssignmentOverride::NOOP_MASTERY_PATHS,
                                        workflow_state: "active"
                                      }).distinct
      return unless releases.exists?

      # Add parent join to reduce duplication, which are used in both cases below
      releases = releases
                 .joins("INNER JOIN #{AssignmentOverride.quoted_table_name} parent ON assignment_override_students.assignment_override_id = parent.id")
      # Restore student overrides associated with an active assignment override
      releases.where("parent.workflow_state = 'active'").update(workflow_state: "active")
      # Restore student overrides and assignment overrides if assignment override is deleted
      releases.preload(:assignment_override).where("parent.workflow_state = 'deleted'").find_each do |release|
        release.update(workflow_state: "active")
        release.assignment_override.update(workflow_state: "active")
      end
    end
  end

  def section_or_course_date_in_past?
    if course_section&.end_at
      course_section.end_at < Time.zone.now
    elsif course.conclude_at
      course.conclude_at < Time.zone.now
    end
  end

  def student_or_fake_student?
    ["StudentEnrollment", "StudentViewEnrollment"].include?(type)
  end

  def allows_favoriting?
    !(course.elementary_subject_course? || course.elementary_homeroom_course?) || teacher? || ta? || designer? || user.roles(root_account).include?("teacher")
  end

  private

  def enrollments_exist_for_user_in_course?
    Enrollment.active.where(user_id:, course_id:).exists?
  end

  def copy_scores_from_existing_enrollment
    Score.where(enrollment_id: self).each(&:destroy_permanently!)
    other_enrollment_of_same_type_with_score.scores.each { |score| score.dup.update!(enrollment: self) }
  end

  def need_to_copy_scores?
    return false unless previously_new_record? || being_restored?

    student_or_fake_student? && other_enrollment_of_same_type_with_score.present?
  end

  def other_enrollment_of_same_type_with_score
    return @other_enrollment_of_same_type_with_score if defined?(@other_enrollment_of_same_type_with_score)

    @other_enrollment_of_same_type_with_score = other_enrollments_of_type(type).joins(:scores).first
  end

  def other_enrollments_of_type(types)
    Enrollment.where(
      course_id: course,
      user_id: user,
      type: Array.wrap(types)
    ).where.not(id:).where.not(workflow_state: :deleted)
  end

  def can_delete_via_granular(user, session, context)
    (teacher? && context.grants_right?(user, session, :remove_teacher_from_course)) ||
      (ta? && context.grants_right?(user, session, :remove_ta_from_course)) ||
      (designer? && context.grants_right?(user, session, :remove_designer_from_course)) ||
      (observer? && context.grants_right?(user, session, :remove_observer_from_course)) ||
      (student? && context.grants_right?(user, session, :remove_student_from_course))
  end

  def remove_user_as_final_grader?
    instructor? &&
      !other_enrollments_of_type(["TaEnrollment", "TeacherEnrollment"]).exists?
  end

  def being_accepted?
    saved_change_to_workflow_state? && workflow_state == "active" && workflow_state_before_last_save == "invited"
  end

  def being_restored?(to_state: workflow_state)
    saved_change_to_workflow_state? && workflow_state_before_last_save == "deleted" && workflow_state == to_state
  end

  def being_reactivated?
    saved_change_to_workflow_state? && workflow_state != "deleted" && workflow_state_before_last_save == "inactive"
  end

  def being_uncompleted?
    saved_change_to_workflow_state? && workflow_state != "deleted" && workflow_state_before_last_save == "completed"
  end

  def being_deleted?
    workflow_state == "deleted" && workflow_state_before_last_save != "deleted"
  end

  def sync_microsoft_group
    return if type == "StudentViewEnrollment"
    return unless root_account.feature_enabled?(:microsoft_group_enrollments_syncing)
    return unless root_account.settings[:microsoft_sync_enabled]

    MicrosoftSync::Group.not_deleted.find_by(course_id:)&.enqueue_future_partial_sync self
  end
end
