# frozen_string_literal: true

class GroupsController < ApplicationController
  requires_login only: %i[
                   set_notifications
                   mentionable
                   messageable
                   check_name
                   update
                   histories
                   request_membership
                   search
                   new
                   test_email_settings
                 ]

  skip_before_action :preload_json, :check_xhr, only: %i[posts_feed mentions_feed]
  skip_before_action :check_xhr, only: [:show]
  after_action :add_noindex_header

  TYPE_FILTERS = {
    my:
      Proc.new do |groups, user|
        raise Discourse::NotFound unless user
        Group.member_of(groups, user)
      end,
    owner:
      Proc.new do |groups, user|
        raise Discourse::NotFound unless user
        Group.owner_of(groups, user)
      end,
    public: Proc.new { |groups| groups.where(public_admission: true, automatic: false) },
    close: Proc.new { |groups| groups.where(public_admission: false, automatic: false) },
    automatic: Proc.new { |groups| groups.where(automatic: true) },
    non_automatic: Proc.new { |groups| groups.where(automatic: false) },
  }
  ADD_MEMBERS_LIMIT = 1000

  def index
    unless SiteSetting.enable_group_directory? || current_user&.staff?
      raise Discourse::InvalidAccess.new(:enable_group_directory)
    end

    order = %w[name user_count].delete(params[:order])
    dir = params[:asc].to_s == "true" ? "ASC" : "DESC"
    sort = order ? "#{order} #{dir}" : nil
    groups = Group.visible_groups(current_user, sort)
    type_filters = TYPE_FILTERS.keys

    if (username = params[:username]).present?
      raise Discourse::NotFound unless user = User.find_by_username(username)
      groups = TYPE_FILTERS[:my].call(groups.members_visible_groups(current_user, sort), user)
      type_filters = type_filters - %i[my owner]
    end

    if (filter = params[:filter]).present?
      groups = Group.search_groups(filter, groups: groups)
    end

    if !guardian.is_staff?
      # hide automatic groups from all non stuff to de-clutter page
      groups =
        groups.where("groups.automatic IS FALSE OR groups.id = ?", Group::AUTO_GROUPS[:moderators])
      type_filters.delete(:automatic)
    end

    if Group.preloaded_custom_field_names.present?
      Group.preload_custom_fields(groups, Group.preloaded_custom_field_names)
    end

    if type = params[:type]&.to_sym
      raise Discourse::InvalidParameters.new(:type) unless callback = TYPE_FILTERS[type]
      groups = callback.call(groups, current_user)
    end

    if current_user
      group_users = GroupUser.where(group: groups, user: current_user)
      user_group_ids = group_users.pluck(:group_id)
      owner_group_ids = group_users.where(owner: true).pluck(:group_id)
    else
      type_filters = type_filters - %i[my owner]
    end

    groups = DiscoursePluginRegistry.apply_modifier(:groups_index_query, groups, self)

    type_filters.delete(:non_automatic)

    # count the total before doing pagination
    total = groups.count

    page = fetch_int_from_params(:page, default: 0)
    page_size = MobileDetection.mobile_device?(request.user_agent) ? 15 : 36
    groups = groups.offset(page * page_size).limit(page_size)

    render_json_dump(
      groups:
        serialize_data(
          groups,
          BasicGroupSerializer,
          user_group_ids: user_group_ids || [],
          owner_group_ids: owner_group_ids || [],
        ),
      extras: {
        type_filters: type_filters,
      },
      total_rows_groups: total,
      load_more_groups:
        groups_path(page: page + 1, type: type, order: order, asc: params[:asc], filter: filter),
    )
  end

  def show
    respond_to do |format|
      group = find_group(:id)

      format.html do
        @title = group.full_name.present? ? group.full_name.capitalize : group.name
        @full_title = "#{@title} - #{SiteSetting.title}"
        @description_meta =
          group.bio_cooked.present? ? PrettyText.excerpt(group.bio_cooked, 300) : @title
        render :show
      end

      format.json do
        groups = Group.visible_groups(current_user)
        if !guardian.is_staff?
          groups =
            groups.where(
              "groups.automatic IS FALSE OR groups.id = ?",
              Group::AUTO_GROUPS[:moderators],
            )
        end

        render_json_dump(
          group: serialize_data(group, GroupShowSerializer, root: nil),
          extras: {
            visible_group_names: groups.pluck(:name),
          },
        )
      end
    end
  end

  def new
  end

  def edit
  end

  def update
    group = Group.find(params[:id])
    guardian.ensure_can_edit!(group) if !guardian.can_admin_group?(group)

    group_attributes = group_params(automatic: group.automatic)
    reset_group_email_settings_if_disabled!(group, group_attributes)

    categories, tags = []
    if !group.automatic || current_user.admin
      notification_level, categories, tags = user_default_notifications(group, group_attributes)

      if params[:update_existing_users].blank?
        user_count = count_existing_users(group.group_users, notification_level, categories, tags)
        if user_count > 0
          return(
            render status: 422,
                   json: {
                     user_count: user_count,
                     errors: [I18n.t("invalid_params", message: :update_existing_users)],
                   }
          )
        end
      end
    end

    if group.update(group_attributes)
      GroupActionLogger.new(current_user, group).log_change_group_settings
      group.record_email_setting_changes!(current_user)
      group.expire_imap_mailbox_cache
      if params[:update_existing_users] == "true"
        update_existing_users(group.group_users, notification_level, categories, tags)
      end
      AdminDashboardData.clear_found_problem("group_#{group.id}_email_credentials")

      # Redirect user to groups index page if they can no longer see the group
      return redirect_with_client_support groups_path if !guardian.can_see?(group)

      render json: success_json
    else
      render_json_error(group)
    end
  end

  def posts
    group = find_group(:group_id)
    guardian.ensure_can_see_group_members!(group)

    posts = group.posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(20)
    render_serialized posts.to_a, GroupPostSerializer
  end

  def posts_feed
    group = find_group(:group_id)
    guardian.ensure_can_see_group_members!(group)

    @posts = group.posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(50)
    @title =
      "#{SiteSetting.title} - #{I18n.t("rss_description.group_posts", group_name: group.name)}"
    @link = Discourse.base_url
    @description = I18n.t("rss_description.group_posts", group_name: group.name)
    render "posts/latest", formats: [:rss]
  end

  def mentions
    raise Discourse::NotFound unless SiteSetting.enable_mentions?
    group = find_group(:group_id)
    posts =
      group.mentioned_posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(20)
    render_serialized posts.to_a, GroupPostSerializer
  end

  def mentions_feed
    raise Discourse::NotFound unless SiteSetting.enable_mentions?
    group = find_group(:group_id)
    @posts =
      group.mentioned_posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(50)
    @title =
      "#{SiteSetting.title} - #{I18n.t("rss_description.group_mentions", group_name: group.name)}"
    @link = Discourse.base_url
    @description = I18n.t("rss_description.group_mentions", group_name: group.name)
    render "posts/latest", formats: [:rss]
  end

  MEMBERS_MAX_PAGE_SIZE = 1_000
  MEMBERS_DEFAULT_PAGE_SIZE = 50

  def members
    group = find_group(:group_id)

    guardian.ensure_can_see_group_members!(group)

    limit = fetch_limit_from_params(default: MEMBERS_DEFAULT_PAGE_SIZE, max: MEMBERS_MAX_PAGE_SIZE)
    offset = params[:offset].to_i

    raise Discourse::InvalidParameters.new(:offset) if offset < 0

    dir = (params[:asc] && params[:asc].present?) ? "ASC" : "DESC"
    order = "NOT group_users.owner"

    if params[:requesters]
      guardian.ensure_can_edit!(group)

      users = group.requesters
      total = users.count

      if (filter = params[:filter]).present?
        filter = filter.split(",") if filter.include?(",")

        if current_user&.admin
          users = users.filter_by_username_or_email(filter)
        else
          users = users.filter_by_username(filter)
        end
      end

      users =
        users
          .select("users.*, group_requests.reason, group_requests.created_at requested_at")
          .order(params[:order] == "requested_at" ? "group_requests.created_at #{dir}" : "")
          .order(username_lower: dir)
          .limit(limit)
          .offset(offset)

      return(
        render json: {
                 members: serialize_data(users, GroupRequesterSerializer),
                 meta: {
                   total: total,
                   limit: limit,
                   offset: offset,
                 },
               }
      )
    end

    if params[:order] && %w[last_posted_at last_seen_at].include?(params[:order])
      order = "#{params[:order]} #{dir} NULLS LAST"
    elsif params[:order] == "added_at"
      order = "group_users.created_at #{dir}"
    end

    users = group.users.human_users
    total = users.count

    if (filter = params[:filter]).present?
      filter = filter.split(",") if filter.include?(",")

      if current_user&.admin
        users = users.filter_by_username_or_email(filter)
      else
        users = users.filter_by_username(filter)
      end
    end

    users =
      users
        .includes(:primary_group)
        .includes(:user_option)
        .select("users.*, group_users.created_at as added_at")
        .order(order)
        .order(username_lower: dir)

    members = users.limit(limit).offset(offset)
    owners = users.where("group_users.owner")

    group_members_serializer =
      params[:include_custom_fields] ? GroupUserWithCustomFieldsSerializer : GroupUserSerializer

    render json: {
             members: serialize_data(members, group_members_serializer),
             owners: serialize_data(owners, GroupUserSerializer),
             meta: {
               total: total,
               limit: limit,
               offset: offset,
             },
           }
  end

  def add_members
    group = Group.find(params[:id])
    guardian.ensure_can_edit!(group)

    users = users_from_params.to_a
    emails = []
    if params[:emails]
      params[:emails]
        .split(",")
        .each do |email|
          existing_user = User.find_by_email(email)
          existing_user.present? ? users.push(existing_user) : emails.push(email)
        end
    end

    guardian.ensure_can_invite_to_forum!([group]) if emails.present?

    if users.empty? && emails.empty?
      raise Discourse::InvalidParameters.new(I18n.t("groups.errors.usernames_or_emails_required"))
    end

    if users.length > ADD_MEMBERS_LIMIT
      return(
        render_json_error(I18n.t("groups.errors.adding_too_many_users", count: ADD_MEMBERS_LIMIT))
      )
    end

    usernames_already_in_group = group.users.where(id: users.map(&:id)).pluck(:username)
    if usernames_already_in_group.present? && usernames_already_in_group.length == users.length &&
         emails.blank?
      render_json_error(
        I18n.t(
          "groups.errors.member_already_exist",
          username: usernames_already_in_group.sort.join(", "),
          count: usernames_already_in_group.size,
        ),
      )
    else
      notify = params[:notify_users]&.to_s == "true"
      uniq_users = users.uniq
      uniq_users.each { |user| add_user_to_group(group, user, notify) }

      emails.each do |email|
        begin
          Invite.generate(current_user, email: email, group_ids: [group.id])
        rescue RateLimiter::LimitExceeded => e
          return(
            render_json_error(
              I18n.t(
                "invite.rate_limit",
                count: SiteSetting.max_invites_per_day,
                time_left: e.time_left,
              ),
            )
          )
        end
      end

      render json: success_json.merge!(usernames: uniq_users.map(&:username), emails: emails)
    end
  end

  def add_owners
    group = Group.find_by(id: params.require(:id))
    raise Discourse::NotFound unless group

    return can_not_modify_automatic if group.automatic
    guardian.ensure_can_edit_group!(group)

    users = users_from_params
    group_action_logger = GroupActionLogger.new(current_user, group)

    users.each do |user|
      if !group.users.include?(user)
        group.add(user)
        group_action_logger.log_add_user_to_group(user)
      end
      group.group_users.where(user_id: user.id).update_all(owner: true)
      group_action_logger.log_make_user_group_owner(user)

      group.notify_added_to_group(user, owner: true) if params[:notify_users].to_s == "true"
    end

    group.restore_user_count!

    render json: success_json.merge!(usernames: users.pluck(:username))
  end

  def join
    ensure_logged_in
    unless current_user.staff?
      RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
    end

    group = Group.find(params[:id])
    raise Discourse::NotFound unless group
    raise Discourse::InvalidAccess unless group.public_admission

    return if group.users.exists?(id: current_user.id)
    add_user_to_group(group, current_user)
  end

  def handle_membership_request
    group = Group.find_by(id: params[:id])
    raise Discourse::InvalidParameters.new(:id) if group.blank?
    guardian.ensure_can_edit!(group)

    user = User.find_by(id: params[:user_id])
    raise Discourse::InvalidParameters.new(:user_id) if user.blank?

    ActiveRecord::Base.transaction do
      if params[:accept]
        group.add(user)
        GroupActionLogger.new(current_user, group).log_add_user_to_group(user)
      end

      GroupRequest.where(group_id: group.id, user_id: user.id).delete_all
    end

    if params[:accept]
      PostCreator.new(
        current_user,
        title: I18n.t("groups.request_accepted_pm.title", group_name: group.name),
        raw: I18n.t("groups.request_accepted_pm.body", group_name: group.name),
        archetype: Archetype.private_message,
        target_usernames: user.username,
        skip_validations: true,
      ).create!
    end

    render json: success_json
  end

  def mentionable
    group = find_group(:group_id, ensure_can_see: false)

    if group
      render json: { mentionable: Group.mentionable(current_user).where(id: group.id).present? }
    else
      raise Discourse::InvalidAccess.new
    end
  end

  def messageable
    group = find_group(:group_id, ensure_can_see: false)

    if group
      render json: { messageable: guardian.can_send_private_message?(group) }
    else
      raise Discourse::InvalidAccess.new
    end
  end

  def check_name
    group_name = params.require(:group_name)
    checker = UsernameCheckerService.new(allow_reserved_username: true)
    render json: checker.check_username(group_name, nil)
  end

  def remove_member
    group = Group.find_by(id: params[:id])
    raise Discourse::NotFound unless group
    guardian.ensure_can_edit!(group)

    # Maintain backwards compatibility
    params[:usernames] = params[:username] if params[:username].present?
    params[:user_emails] = params[:user_email] if params[:user_email].present?

    users = users_from_params
    if users.empty?
      raise Discourse::InvalidParameters.new("user_ids or usernames or user_emails must be present")
    end

    removed_users = []
    skipped_users = []

    users.each do |user|
      if group.remove(user)
        removed_users << user.username
        GroupActionLogger.new(current_user, group).log_remove_user_from_group(user)
      else
        if group.users.exclude? user
          skipped_users << user.username
        else
          raise Discourse::InvalidParameters
        end
      end
    end

    render json: success_json.merge!(usernames: removed_users, skipped_usernames: skipped_users)
  end

  def leave
    ensure_logged_in
    unless current_user.staff?
      RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
    end

    group = Group.find_by(id: params[:id])
    raise Discourse::NotFound unless group
    raise Discourse::InvalidAccess unless group.public_exit

    if group.remove(current_user)
      GroupActionLogger.new(current_user, group).log_remove_user_from_group(current_user)
    end
  end

  MAX_NOTIFIED_OWNERS ||= 20

  def request_membership
    params.require(:reason)

    group = find_group(:id)

    begin
      GroupRequest.create!(group: group, user: current_user, reason: params[:reason])
    rescue ActiveRecord::RecordNotUnique
      return(
        render json: failed_json.merge(error: I18n.t("groups.errors.already_requested_membership")),
               status: 409
      )
    end

    usernames = [current_user.username].concat(
      group
        .users
        .where("group_users.owner")
        .order("users.last_seen_at DESC")
        .limit(MAX_NOTIFIED_OWNERS)
        .pluck("users.username"),
    )

    post =
      PostCreator.new(
        current_user,
        title: I18n.t("groups.request_membership_pm.title", group_name: group.name),
        raw: params[:reason],
        archetype: Archetype.private_message,
        target_usernames: usernames.join(","),
        topic_opts: {
          custom_fields: {
            requested_group_id: group.id,
          },
        },
        skip_validations: true,
      ).create!

    render json: success_json.merge(relative_url: post.topic.relative_url)
  end

  def set_notifications
    group = find_group(:id)
    notification_level = params.require(:notification_level)

    user_id = current_user.id
    user_id = params[:user_id] || user_id if guardian.is_staff?

    GroupUser
      .where(group_id: group.id)
      .where(user_id: user_id)
      .update_all(notification_level: notification_level)

    render json: success_json
  end

  def histories
    group = find_group(:group_id)
    guardian.ensure_can_edit!(group) unless guardian.can_admin_group?(group)

    page_size = 25
    offset = (params[:offset] && params[:offset].to_i) || 0

    group_histories =
      GroupHistory.with_filters(group, params[:filters]).limit(page_size).offset(offset * page_size)

    render_json_dump(
      logs: serialize_data(group_histories, BasicGroupHistorySerializer),
      all_loaded: group_histories.count < page_size,
    )
  end

  def search
    groups =
      Group
        .visible_groups(current_user)
        .where("groups.id <> ?", Group::AUTO_GROUPS[:everyone])
        .includes(:flair_upload)
        .order(:name)

    if (term = params[:term]).present?
      groups =
        groups.where("groups.name ILIKE :term OR groups.full_name ILIKE :term", term: "%#{term}%")
    end

    groups = groups.where(automatic: false) if params[:ignore_automatic].to_s == "true"
    groups = DiscoursePluginRegistry.apply_modifier(:groups_search_query, groups, self)

    if Group.preloaded_custom_field_names.present?
      Group.preload_custom_fields(groups, Group.preloaded_custom_field_names)
    end

    render_serialized(groups, BasicGroupSerializer)
  end

  def permissions
    group = find_group(:id)
    category_groups =
      group.category_groups.select do |category_group|
        guardian.can_see_category?(category_group.category)
      end
    render_serialized(
      category_groups.sort_by { |category_group| category_group.category.name },
      CategoryGroupSerializer,
    )
  end

  def test_email_settings
    params.require(:group_id)
    params.require(:protocol)
    params.require(:port)
    params.require(:host)
    params.require(:username)
    params.require(:password)
    params.require(:ssl)

    group = Group.find(params[:group_id])
    guardian.ensure_can_edit!(group)

    RateLimiter.new(current_user, "group_test_email_settings", 5, 1.minute).performed!

    settings = params.except(:group_id, :protocol)
    enable_tls = settings[:ssl] == "true"
    email_host = params[:host]

    if !%w[smtp imap].include?(params[:protocol])
      raise Discourse::InvalidParameters.new("Valid protocols to test are smtp and imap")
    end

    hijack do
      begin
        case params[:protocol]
        when "smtp"
          enable_starttls_auto = false
          settings.delete(:ssl)

          final_settings =
            settings.merge(
              enable_tls: enable_tls,
              enable_starttls_auto: enable_starttls_auto,
            ).permit(:host, :port, :username, :password, :enable_tls, :enable_starttls_auto, :debug)
          EmailSettingsValidator.validate_as_user(
            current_user,
            "smtp",
            **final_settings.to_h.symbolize_keys,
          )
        when "imap"
          final_settings =
            settings.merge(ssl: enable_tls).permit(:host, :port, :username, :password, :ssl, :debug)
          EmailSettingsValidator.validate_as_user(
            current_user,
            "imap",
            **final_settings.to_h.symbolize_keys,
          )
        end

        render json: success_json
      rescue *EmailSettingsExceptionHandler::EXPECTED_EXCEPTIONS, StandardError => err
        render_json_error(EmailSettingsExceptionHandler.friendly_exception_message(err, email_host))
      end
    end
  end

  protected

  def can_not_modify_automatic
    render_json_error(I18n.t("groups.errors.can_not_modify_automatic"))
  end

  private

  def add_user_to_group(group, user, notify = false)
    group.add(user)
    GroupActionLogger.new(current_user, group).log_add_user_to_group(user)
    group.notify_added_to_group(user) if notify
  rescue ActiveRecord::RecordNotUnique
    # Under concurrency, we might attempt to insert two records quickly and hit a DB
    # constraint. In this case we can safely ignore the error and act as if the user
    # was added to the group.
  end

  def group_params(automatic: false)
    attributes = %i[
      bio_raw
      default_notification_level
      messageable_level
      mentionable_level
      flair_bg_color
      flair_color
      flair_icon
      flair_upload_id
    ]

    if automatic
      attributes.push(:visibility_level)
    else
      attributes.push(
        :title,
        :allow_membership_requests,
        :full_name,
        :public_exit,
        :public_admission,
        :membership_request_template,
      )
    end

    if !automatic && current_user.staff?
      attributes.push(
        :incoming_email,
        :smtp_server,
        :smtp_port,
        :smtp_ssl,
        :smtp_enabled,
        :smtp_updated_by,
        :smtp_updated_at,
        :imap_server,
        :imap_port,
        :imap_ssl,
        :imap_mailbox_name,
        :imap_enabled,
        :imap_updated_by,
        :imap_updated_at,
        :email_username,
        :email_password,
        :email_from_alias,
        :primary_group,
        :visibility_level,
        :members_visibility_level,
        :name,
        :grant_trust_level,
        :automatic_membership_email_domains,
        :publish_read_state,
        :allow_unknown_sender_topic_replies,
      )

      custom_fields = DiscoursePluginRegistry.editable_group_custom_fields
      attributes << { custom_fields: custom_fields } if custom_fields.present?
    end

    if !automatic || current_user.admin
      %i[muted regular tracking watching watching_first_post].each do |level|
        attributes << { "#{level}_category_ids" => [] }
        attributes << { "#{level}_tags" => [] }
      end
    end

    attributes << { associated_group_ids: [] } if guardian.can_associate_groups?
    attributes.concat(DiscoursePluginRegistry.group_params)

    params.require(:group).permit(*attributes)
  end

  def find_group(param_name, ensure_can_see: true)
    name = params.require(param_name)
    group = Group.find_by("LOWER(name) = ?", name.downcase)
    raise Discourse::NotFound if ensure_can_see && !guardian.can_see_group?(group)
    group
  end

  def users_from_params
    if params[:usernames].present?
      users = User.where(username_lower: params[:usernames].split(",").map(&:downcase))
      raise Discourse::InvalidParameters.new(:usernames) if users.blank?
    elsif params[:user_id].present?
      users = User.where(id: params[:user_id].to_i)
      raise Discourse::InvalidParameters.new(:user_id) if users.blank?
    elsif params[:user_ids].present?
      users = User.where(id: params[:user_ids].to_s.split(","))
      raise Discourse::InvalidParameters.new(:user_ids) if users.blank?
    elsif params[:user_emails].present?
      users = User.with_email(params[:user_emails].split(","))
      raise Discourse::InvalidParameters.new(:user_emails) if users.blank?
    else
      users = []
    end
    users
  end

  def reset_group_email_settings_if_disabled!(group, attributes)
    should_clear_imap = group.imap_enabled && attributes[:imap_enabled] == "false"
    should_clear_smtp = group.smtp_enabled && attributes[:smtp_enabled] == "false"

    if should_clear_imap || should_clear_smtp
      attributes[:imap_server] = nil
      attributes[:imap_ssl] = false
      attributes[:imap_port] = nil
      attributes[:imap_mailbox_name] = ""
    end

    if should_clear_smtp
      attributes[:smtp_server] = nil
      attributes[:smtp_ssl] = false
      attributes[:smtp_port] = nil
      attributes[:email_username] = nil
      attributes[:email_password] = nil
    end
  end

  def user_default_notifications(group, params)
    category_notifications =
      group.group_category_notification_defaults.pluck(:category_id, :notification_level).to_h
    tag_notifications =
      group.group_tag_notification_defaults.pluck(:tag_id, :notification_level).to_h
    categories = {}
    tags = {}

    NotificationLevels.all.each do |key, value|
      category_ids = (params["#{key}_category_ids".to_sym] || []) - ["-1"]

      category_ids.each do |category_id|
        category_id = category_id.to_i
        old_value = category_notifications[category_id]

        metadata = { old_value: old_value, new_value: value }

        if old_value.blank?
          metadata[:action] = :create
        elsif old_value == value
          category_notifications.delete(category_id)
          next
        else
          metadata[:action] = :update
        end

        categories[category_id] = metadata
      end

      tag_names = (params["#{key}_tags".to_sym] || []) - ["-1"]
      tag_ids = Tag.where(name: tag_names).pluck(:id)

      tag_ids.each do |tag_id|
        old_value = tag_notifications[tag_id]

        metadata = { old_value: old_value, new_value: value }

        if old_value.blank?
          metadata[:action] = :create
        elsif old_value == value
          tag_notifications.delete(tag_id)
          next
        else
          metadata[:action] = :update
        end

        tags[tag_id] = metadata
      end
    end

    (category_notifications.keys - categories.keys).each do |category_id|
      categories[category_id] = { action: :delete, old_value: category_notifications[category_id] }
    end

    (tag_notifications.keys - tags.keys).each do |tag_id|
      tags[tag_id] = { action: :delete, old_value: tag_notifications[tag_id] }
    end

    notification_level = nil
    default_notification_level = params[:default_notification_level]&.to_i

    if default_notification_level.present? &&
         group.default_notification_level != default_notification_level
      notification_level = {
        old_value: group.default_notification_level,
        new_value: default_notification_level,
      }
    end

    [notification_level, categories, tags]
  end

  %i[count update].each do |action|
    define_method("#{action}_existing_users") do |group_users, notification_level, categories, tags|
      return 0 if notification_level.blank? && categories.blank? && tags.blank?

      ids = []

      if notification_level.present?
        users = group_users.where(notification_level: notification_level[:old_value])

        if action == :update
          users.update_all(notification_level: notification_level[:new_value])
        else
          ids += users.pluck(:user_id)
        end
      end

      categories.each do |category_id, data|
        if data[:action] == :update || data[:action] == :delete
          category_users =
            CategoryUser.where(
              category_id: category_id,
              notification_level: data[:old_value],
              user_id: group_users.select(:user_id),
            )

          if action == :update
            category_users.delete_all
          else
            ids += category_users.pluck(:user_id)
          end

          categories.delete(category_id) if data[:action] == :delete && action == :update
        end
      end

      tags.each do |tag_id, data|
        if data[:action] == :update || data[:action] == :delete
          tag_users =
            TagUser.where(
              tag_id: tag_id,
              notification_level: data[:old_value],
              user_id: group_users.select(:user_id),
            )

          if action == :update
            tag_users.delete_all
          else
            ids += tag_users.pluck(:user_id)
          end

          tags.delete(tag_id) if data[:action] == :delete && action == :update
        end
      end

      if categories.present? || tags.present?
        group_users
          .select(:id, :user_id)
          .find_in_batches do |batch|
            user_ids = batch.pluck(:user_id)

            categories.each do |category_id, data|
              category_users = []
              existing_users =
                CategoryUser.where(category_id: category_id, user_id: user_ids).where(
                  "notification_level IS NOT NULL",
                )
              skip_user_ids = existing_users.pluck(:user_id)

              batch.each do |group_user|
                next if skip_user_ids.include?(group_user.user_id)
                category_users << {
                  category_id: category_id,
                  user_id: group_user.user_id,
                  notification_level: data[:new_value],
                }
              end

              next if category_users.blank?

              if action == :update
                CategoryUser.insert_all!(category_users)
              else
                ids += category_users.pluck(:user_id)
              end
            end

            tags.each do |tag_id, data|
              tag_users = []
              existing_users =
                TagUser.where(tag_id: tag_id, user_id: user_ids).where(
                  "notification_level IS NOT NULL",
                )
              skip_user_ids = existing_users.pluck(:user_id)

              batch.each do |group_user|
                next if skip_user_ids.include?(group_user.user_id)
                tag_users << {
                  tag_id: tag_id,
                  user_id: group_user.user_id,
                  notification_level: data[:new_value],
                  created_at: Time.now,
                  updated_at: Time.now,
                }
              end

              next if tag_users.blank?

              if action == :update
                TagUser.insert_all!(tag_users)
              else
                ids += tag_users.pluck(:user_id)
              end
            end
          end
      end

      ids.uniq.count
    end
  end
end