module Group::Destroy
  ##
  # Instead of firing of separate queries for each and every Member and MemberRole
  # instance upon group deletion this implementation does most of the deletion
  # in a hand full of aggregate queries.
  #
  # Instead of doing
  #
  #   Member:
  #     before_destroy :remove_from_category_assignments
  #     after_destroy :unwatch_from_permission_change
  #     after_destroy :destroy_notification
  #
  #   MemberRole:
  #     after_destroy :remove_role_from_group_users
  #
  # for every row all relevant roles are deleted within 5 mass delete queries
  # + 1 query for each member instance for each group itself + number of watchers
  # among the users in the deleted group.
  #
  # Example:
  #
  # Given: 150 projects and 1 group with 20 users which is member in every project
  #
  # That makes 150 * 20 3000 Member rows and also 3000 MemberRole rows.
  #
  # Without this patch this would result in at least 4 queries for each member
  # (the callbacks mentioned above + the deletion of the member) and 2 queries
  # for each MemberRole (callback mentioned above + deletion of the actual MemberRole).
  # Altogether that makes:
  #
  # num_queries_pre_patch = 3000 * 4 + 3000 * 2 + W = 18000 + W
  #
  # Where W is the number of watchers among the users in the destroyed group.
  # The actual number is actually even higher as for the callbacks a bunch of read queries
  # (loading the project, the user, etc.) are triggered, too.
  #
  # With this patch the number of queries is reduced to the 5 + 1 for each group member
  # as explained above, making it:
  #
  # num_queries_post_patch = 5 + 150 + W = 155 + W
  #
  def destroy
    MemberRole.transaction do
      members = Member.table_name
      member_roles = MemberRole.table_name

      # Store all project/user combinations for later watcher pruning
      # See: Member#unwatch_from_permission_change
      user_id_and_project_id = Member
                               .joins(
                                 "INNER JOIN #{member_roles} umr
                                    ON #{members}.id = umr.member_id
                                  INNER JOIN #{member_roles} gmr
                                    ON umr.inherited_from = gmr.id
                                  INNER JOIN #{members} gm
                                    ON gm.id = gmr.member_id AND gm.user_id = #{id}"
                               )
                               .distinct
                               .pluck(:user_id, :project_id)

      user_ids, project_ids = user_id_and_project_id.each_with_object([[], []]) do |element, array|
        array.first << element.first
        array.last << element.last
      end

      users = User.find(user_ids)

      # Delete all MemberRoles created through this group for each user within it.
      MemberRole
        .joins("INNER JOIN #{member_roles} b on #{member_roles}.inherited_from = b.id")
        .joins("INNER JOIN #{members} on #{members}.id = b.member_id")
        .where("#{members}.user_id" => self.id) # group ID
        .delete_all

      # Delete all MemberRoles associating this group itself with a project.
      MemberRole
        .joins("INNER JOIN #{members} on #{members}.id = #{member_roles}.member_id")
        .where("#{members}.user_id" => self.id)
        .delete_all

      Watcher.prune(user: users, project_id: project_ids)

      # Destroy member instances for this group itself to trigger
      # member destroyed notifications.
      Member
        .where(user_id: self.id)
        .destroy_all

      # Remove category based auto assignments for this member.
      # See: Member#remove_from_category_assignments
      Category
        .joins("INNER JOIN #{members}
                ON #{members}.project_id = categories.project_id
                AND #{members}.user_id = categories.assigned_to_id")
        .where("#{members}.user_id" => self.id)
        .update_all "assigned_to_id = NULL"

      self.users.delete_all # remove all users from this group
      self.reload # so associated member instances are not destroyed again

      super # destroy the actual, now empty group
    end
  end
end
