# frozen_string_literal: true

require 'carrierwave/orm/activerecord'

class Group < Namespace
  include Gitlab::ConfigHelper
  include AfterCommitQueue
  include AccessRequestable
  include Avatarable
  include SelectForProjectAuthorization
  include LoadedInGroupList
  include GroupDescendant
  include TokenAuthenticatable
  include WithUploads
  include Gitlab::Utils::StrongMemoize
  include GroupAPICompatibility
  include EachBatch
  include BulkMemberAccessLoad
  include BulkUsersByEmailLoad
  include ChronicDurationAttribute
  include RunnerTokenExpirationInterval
  include Importable
  include IdInOrdered
  include Members::Enumerable

  extend ::Gitlab::Utils::Override

  self.allow_legacy_sti_class = true

  README_PROJECT_PATH = 'gitlab-profile'

  def self.sti_name
    'Group'
  end

  def self.supported_keyset_orderings
    { name: [:asc] }
  end

  has_many :all_group_members, -> { non_request }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
  has_many :all_owner_members, -> { non_request.all_owners }, as: :source, class_name: 'GroupMember'
  has_many :group_members, -> { non_request.non_minimal_access }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
  has_many :non_invite_group_members, -> { non_request.non_minimal_access.non_invite }, class_name: 'GroupMember', as: :source
  has_many :non_invite_owner_members, -> { non_request.non_invite.all_owners }, class_name: 'GroupMember', as: :source
  has_many :request_group_members, -> do
    request.non_minimal_access
  end, inverse_of: :group, class_name: 'GroupMember', as: :source

  has_many :namespace_members, -> { non_request.non_minimal_access.unscope(where: %i[source_id source_type]) },
    foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember'
  alias_method :members, :group_members

  has_many :users, through: :group_members
  has_many :owners, through: :all_owner_members, source: :user

  has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
  has_many :namespace_requesters, -> { where.not(requested_at: nil).unscope(where: %i[source_id source_type]) },
    foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember'

  has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
  has_many :namespace_members_and_requesters, -> { unscope(where: %i[source_id source_type]) },
    foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember'

  has_many :milestones
  has_many :integrations

  with_options class_name: 'GroupGroupLink' do
    has_many :shared_group_links, foreign_key: :shared_with_group_id

    with_options foreign_key: :shared_group_id do
      has_many :shared_with_group_links
      has_many :shared_with_group_links_of_ancestors, ->(group) do
        unscope(where: :shared_group_id).where(shared_group: group.ancestors)
      end
      has_many :shared_with_group_links_of_ancestors_and_self, ->(group) do
        unscope(where: :shared_group_id).where(shared_group: group.self_and_ancestors)
      end
    end
  end

  has_many :shared_groups, through: :shared_group_links, source: :shared_group
  with_options source: :shared_with_group do
    has_many :shared_with_groups, through: :shared_with_group_links
    has_many :shared_with_groups_of_ancestors, through: :shared_with_group_links_of_ancestors
    has_many :shared_with_groups_of_ancestors_and_self, through: :shared_with_group_links_of_ancestors_and_self do
      def with_developer_maintainer_owner_access
        merge(GroupGroupLink.with_developer_maintainer_owner_access)
      end
    end
  end

  has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :shared_projects, through: :project_group_links, source: :project

  # Overridden on another method
  # Left here just to be dependent: :destroy
  has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent

  has_many :labels, class_name: 'GroupLabel'
  has_many :variables, class_name: 'Ci::GroupVariable'
  has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult'
  has_many :custom_attributes, class_name: 'GroupCustomAttribute'

  has_many :boards
  has_many :badges, class_name: 'GroupBadge'

  # AR defaults to nullify when trying to delete via has_many associations unless we set dependent: :delete_all
  has_many :crm_organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
  has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
  has_one :crm_settings, class_name: 'Group::CrmSettings', inverse_of: :group
  # Groups for which this is the source of CRM contacts/organizations
  has_many :crm_targets, class_name: 'Group::CrmSettings', inverse_of: :source_group, foreign_key: 'source_group_id'

  has_many :cluster_groups, class_name: 'Clusters::Group'
  has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'

  has_many :container_repositories, through: :projects

  has_many :todos

  has_many :import_export_uploads, dependent: :destroy, inverse_of: :group # rubocop:disable Cop/ActiveRecordDependent -- Previously was has_one association, dependent: :destroy to be removed in a separate issue and cascade FK will be added

  has_many :import_failures, inverse_of: :group

  has_one :import_state, class_name: 'GroupImportState', inverse_of: :group

  has_many :bulk_import_exports, class_name: 'BulkImports::Export', inverse_of: :group
  has_many :bulk_import_entities, class_name: 'BulkImports::Entity', foreign_key: :namespace_id, inverse_of: :group

  has_many :group_deploy_tokens
  has_many :deploy_tokens, through: :group_deploy_tokens
  has_many :oauth_applications, class_name: 'Authn::OauthApplication', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent

  has_one :dependency_proxy_setting, class_name: 'DependencyProxy::GroupSetting'
  has_one :dependency_proxy_image_ttl_policy, class_name: 'DependencyProxy::ImageTtlGroupPolicy'
  has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob'
  has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest'

  has_one :observability_group_o11y_setting, class_name: 'Observability::GroupO11ySetting', inverse_of: :group

  has_one :deletion_schedule, class_name: 'GroupDeletionSchedule'
  delegate :deleting_user, :marked_for_deletion_on, to: :deletion_schedule, allow_nil: true

  scope :aimed_for_deletion, -> { where.associated(:deletion_schedule) }
  scope :self_or_ancestors_aimed_for_deletion, -> { where(self_or_ancestors_deletion_schedule_subquery.exists) }

  scope :not_aimed_for_deletion, -> { where.missing(:deletion_schedule) }
  scope :self_and_ancestors_not_aimed_for_deletion, -> { where.not(self_or_ancestors_deletion_schedule_subquery.exists) }

  scope :with_deletion_schedule, -> { preload(deletion_schedule: :deleting_user) }
  scope :with_deletion_schedule_only, -> { preload(:deletion_schedule) }

  scope :marked_for_deletion_before, ->(date) do
    joins(:deletion_schedule)
      .where('group_deletion_schedules.marked_for_deletion_on <= ?', date)
  end

  scope :marked_for_deletion_on, ->(date) do
    joins(:deletion_schedule)
      .where(group_deletion_schedules: { marked_for_deletion_on: date })
  end

  scope :with_integrations, -> { joins(:integrations) }

  has_one :harbor_integration, class_name: 'Integrations::Harbor'
  has_one :jira_integration, class_name: 'Integrations::Jira'

  # debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
  has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent

  has_many :group_callouts, class_name: 'Users::GroupCallout', foreign_key: :group_id

  has_many :protected_branches, inverse_of: :group, foreign_key: :namespace_id

  has_one :group_feature, inverse_of: :group, class_name: 'Groups::FeatureSetting'

  delegate(
    :default_branch_name,
    :jobs_to_be_done,
    :new_user_signups_cap,
    :prevent_sharing_groups_outside_hierarchy,
    :require_dpop_for_manage_api_endpoints,
    :require_dpop_for_manage_api_endpoints?,
    :require_dpop_for_manage_api_endpoints=,
    :seat_control,
    :setup_for_company,
    :step_up_auth_required_oauth_provider,
    :step_up_auth_required_oauth_provider=,
    to: :namespace_settings
  )

  delegate(
    :force_pages_access_control,
    :force_pages_access_control=,
    :model_prompt_cache_enabled,
    :model_prompt_cache_enabled=,
    :project_runner_token_expiration_interval_human_readable,
    :project_runner_token_expiration_interval_human_readable=,
    :project_runner_token_expiration_interval,
    :project_runner_token_expiration_interval=,
    :runner_token_expiration_interval_human_readable,
    :runner_token_expiration_interval_human_readable=,
    :runner_token_expiration_interval,
    :runner_token_expiration_interval=,
    :subgroup_runner_token_expiration_interval_human_readable,
    :subgroup_runner_token_expiration_interval_human_readable=,
    :subgroup_runner_token_expiration_interval,
    :subgroup_runner_token_expiration_interval=,
    to: :namespace_settings, allow_nil: true
  )

  accepts_nested_attributes_for :variables, allow_destroy: true
  accepts_nested_attributes_for :group_feature, update_only: true

  validate :visibility_level_allowed_by_projects
  validate :visibility_level_allowed_by_sub_groups
  validate :visibility_level_allowed_by_organization, if: :should_validate_visibility_level?
  validate :visibility_level_allowed_by_parent
  validate :two_factor_authentication_allowed
  validates :variables, nested_attributes_duplicates: { scope: :environment_scope }

  validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }

  validates :name,
    html_safety: true,
    format: {
      with: Gitlab::Regex.group_name_regex,
      message: Gitlab::Regex.group_name_regex_message
    },
    if: :name_changed?

  validates :group_feature, presence: true

  validate :top_level_group_name_not_assigned_to_pages_unique_domain, if: :path_changed?

  add_authentication_token_field :runners_token,
    encrypted: :required,
    format_with_prefix: :runners_token_prefix,
    require_prefix_for_validation: true

  after_create :post_create_hook
  after_create -> { create_or_load_association(:group_feature) }
  after_update :path_changed_hook, if: :saved_change_to_path?
  after_destroy :post_destroy_hook
  after_commit :update_two_factor_requirement

  scope :with_users, -> { includes(:users) }

  scope :active, -> { non_archived.not_aimed_for_deletion }
  scope :self_and_ancestors_active, -> { self_and_ancestors_non_archived.self_and_ancestors_not_aimed_for_deletion }

  scope :inactive, -> do
    joins(:namespace_settings)
      .left_joins(:deletion_schedule)
      .where(<<~SQL)
        #{reflections['namespace_settings'].table_name}.archived = TRUE
        OR #{reflections['deletion_schedule'].table_name}.#{reflections['deletion_schedule'].foreign_key} IS NOT NULL
      SQL
  end
  scope :self_or_ancestors_inactive, -> { self_or_ancestors_archived.or(self_or_ancestors_aimed_for_deletion) }

  scope :with_non_archived_projects, -> { includes(:non_archived_projects) }

  scope :with_non_invite_group_members, -> { includes(:non_invite_group_members) }
  scope :with_request_group_members, -> { includes(:request_group_members) }

  scope :in_accessible_sub_namespaces, -> do
    where('id IN (SELECT id FROM accessible_sub_namespace_ids)')
  end

  scope :by_id, ->(groups) { where(id: groups) }

  scope :by_ids_or_paths, ->(ids, paths) do
    return by_id(ids) unless paths.present?

    ids_by_full_path = Route
      .for_routable_type(Namespace.name)
      .where('LOWER(routes.path) IN (?)', paths.map(&:downcase))
      .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
      .select(:namespace_id)

    Group.from_union([by_id(ids), by_id(ids_by_full_path), where('LOWER(path) IN (?)', paths.map(&:downcase))])
  end

  scope :excluding_groups, ->(groups) { where.not(id: groups) }

  scope :by_visibility_level, ->(visibility) do
    where(visibility_level: Gitlab::VisibilityLevel.level_value(visibility)) if visibility.present?
  end

  scope :for_authorized_group_members, ->(user_ids) do
    joins(:group_members)
      .where(members: { user_id: user_ids })
      .where("access_level >= ?", Gitlab::Access::GUEST)
  end

  scope :for_authorized_project_members, ->(user_ids) do
    joins(projects: :project_authorizations)
      .where(project_authorizations: { user_id: user_ids })
  end

  scope :with_project_creation_levels, ->(project_creation_levels) do
    where(project_creation_level: project_creation_levels)
  end

  scope :excluding_restricted_visibility_levels_for_user, ->(user) do
    return all if user.can_admin_all_resources?

    levels = Array.wrap(Gitlab::CurrentSettings.restricted_visibility_levels).sort

    case levels
    when [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::PUBLIC],
         [Gitlab::VisibilityLevel::PRIVATE]
      where.not(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
    when [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL]
      where.not(visibility_level: [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL])
    when Gitlab::VisibilityLevel.values
      none
    else
      all
    end
  end

  scope :project_creation_allowed, ->(user) do
    project_creation_levels_for_user = project_creation_levels_for_user(user)

    with_project_creation_levels(project_creation_levels_for_user)
      .excluding_restricted_visibility_levels_for_user(user)
  end

  scope :shared_into_ancestors, ->(group) do
    joins(:shared_group_links)
      .where(group_group_links: { shared_group_id: group.self_and_ancestors })
  end

  # Returns all groups that are shared with the given group (see :shared_with_group)
  # and all descendents of the given group
  # returns none if the given group is nil
  scope :descendants_with_shared_with_groups, ->(group) do
    return none if group.nil?

    descendants_query = group.descendants.select(:id)
    # since we're only interested in ids, we query GroupGroupLink directly instead of using :shared_with_group
    # to avoid an extra JOIN in the resulting query
    shared_groups_query = GroupGroupLink
      .where(shared_group_id: group.id)
      .select('shared_with_group_id AS id')

    combined_query = Group
      .from_union(descendants_query, shared_groups_query, alias_as: :combined)
      .unscope(where: :type)
      .select(:id)

    id_in(combined_query)
  end

  # WARNING: This method should never be used on its own
  # please do make sure the number of rows you are filtering is small
  # enough for this query
  #
  # It's a replacement for `public_or_visible_to_user` that correctly
  # supports subgroup permissions
  scope :accessible_to_user, ->(user) do
    if user
      Preloaders::GroupPolicyPreloader.new(self, user).execute

      select { |group| user.can?(:read_group, group) }
    else
      public_to_user
    end
  end

  # .sorted_by_similarity_desc can generate poorly performing queries.
  # Only apply this scope in combination with other filters, ideally on sets of
  # less than 100,000 records.
  scope :sorted_by_similarity_desc, ->(search) do
    order_expression = Gitlab::Database::SimilarityScore.build_expression(
      search: search,
      rules: [
        { column: arel_table["path"], multiplier: 1 },
        { column: arel_table["name"], multiplier: 0.7 }
      ])

    order = Gitlab::Pagination::Keyset::Order.build(
      [
        Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
          attribute_name: 'similarity',
          column_expression: order_expression,
          order_expression: order_expression.desc,
          order_direction: :desc,
          add_to_projections: true
        ),
        # Tie-breaker for if two results have the same similarity score.
        Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
          attribute_name: 'id',
          order_expression: Namespace.arel_table['id'].asc
        )
      ])

    order.apply_cursor_conditions(reorder(order))
  end

  scope :order_storage_size_keyset, ->(direction) do
    order = Gitlab::Pagination::Keyset::Order.build([
      Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
        attribute_name: 'storage_size',
        column_expression: Arel.sql('storage_size'),
        order_expression: Arel.sql("storage_size #{direction == :desc ? 'DESC' : 'ASC'}"),
        reversed_order_expression: Arel.sql("storage_size #{direction == :desc ? 'ASC' : 'DESC'}"),
        nullable: direction == :desc ? :nulls_last : :nulls_first,
        order_direction: direction,
        add_to_projections: true
      ),
      Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
        attribute_name: 'id',
        order_expression: direction == :desc ? Namespace.arel_table['id'].desc : Namespace.arel_table['id'].asc
      )
    ])

    with_statistics.order(order)
  end

  scope :order_path_asc, -> { reorder(self.arel_table['path'].asc) }
  scope :order_path_desc, -> { reorder(self.arel_table['path'].desc) }
  scope :in_organization, ->(organization) { where(organization: organization) }
  scope :by_min_access_level, ->(user, access_level) { joins(:group_members).where(members: { user: user }).where('members.access_level >= ?', access_level) }

  class << self
    def sort_by_attribute(method)
      case method.to_s
      when 'storage_size_keyset_asc'
        order_storage_size_keyset(:asc)
      when 'storage_size_keyset_desc'
        order_storage_size_keyset(:desc)
      when 'storage_size_desc'
        # storage_size is a virtual column so we need to
        # pass a string to avoid AR adding the table name
        reorder('storage_size DESC, namespaces.id DESC')
      when 'path_asc'
        order_path_asc
      when 'path_desc'
        order_path_desc
      else
        order_by(method)
      end
    end

    # WARNING: This method should never be used on its own
    # please do make sure the number of rows you are filtering is small
    # enough for this query
    def public_or_visible_to_user(user)
      return public_to_user unless user

      public_for_user = public_to_user_arel(user)
      visible_for_user = visible_to_user_arel(user)
      public_or_visible = public_for_user.or(visible_for_user)

      where(public_or_visible)
    end

    def select_for_project_authorization
      if current_scope.joins_values.include?(:shared_projects)
        joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
          .where(project_namespace: { share_with_group_lock: false })
          .select("projects.id AS project_id", "LEAST(project_group_links.group_access, members.access_level) AS access_level")
      else
        super
      end
    end

    def without_integration(integration)
      integrations = Integration
        .select('1')
        .where("#{Integration.table_name}.group_id = namespaces.id")
        .where(type: integration.type)

      where('NOT EXISTS (?)', integrations)
    end

    def groups_user_can(groups, user, action, same_root: false)
      DeclarativePolicy.user_scope do
        groups.select { |group| Ability.allowed?(user, action, group) }
      end
    end

    # This method can be used only if all groups have the same top-level
    # group
    def preset_root_ancestor_for(groups)
      return groups if groups.size < 2

      root = groups.first.root_ancestor
      groups.drop(1).each { |group| group.root_ancestor = root }
    end

    # Returns the ids of the passed group models where the `emails_enabled`
    # column is set to false anywhere in the ancestor hierarchy.
    def ids_with_disabled_email(groups)
      inner_groups = Group.where('id = namespaces_with_emails_disabled.id')
      inner_query = inner_groups
        .self_and_ancestors
        .joins(:namespace_settings)
        .where(namespace_settings: { emails_enabled: false })
        .select('1')
        .limit(1)

      group_ids = Namespace
        .from('(SELECT * FROM namespaces) as namespaces_with_emails_disabled')
        .where(namespaces_with_emails_disabled: { id: groups })
        .where('EXISTS (?)', inner_query)
        .pluck(:id)

      Set.new(group_ids)
    end

    def get_ids_by_ids_or_paths(ids, paths)
      by_ids_or_paths(ids, paths).pluck(:id)
    end

    def descendant_groups_counts
      left_joins(:children).group(:id).count(:children_namespaces)
    end

    def projects_counts
      left_joins(:non_archived_projects).group(:id).count(:projects)
    end

    def group_members_counts
      left_joins(:group_members).group(:id).count(:members)
    end

    def with_api_scopes
      preload(
        :namespace_settings,
        :namespace_settings_with_ancestors_inherited_settings,
        :namespace_details,
        :group_feature,
        :parent,
        :deletion_schedule
      )
    end

    # Handle project creation permissions based on application setting and group setting. The `default_project_creation`
    # application setting is the default value and can be overridden by the `project_creation_level` group setting.
    # `nil` value of namespaces.project_creation_level` means that allowed creation level has not been explicitly set by
    # the group owner and is a placeholder value for inheriting the value from the ApplicationSetting.
    def project_creation_levels_for_user(user)
      project_creation_allowed_on_levels = [
        ::Gitlab::Access::DEVELOPER_PROJECT_ACCESS,
        ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS,
        ::Gitlab::Access::OWNER_PROJECT_ACCESS,
        nil
      ]

      if user.can_admin_all_resources?
        project_creation_allowed_on_levels << ::Gitlab::Access::ADMINISTRATOR_PROJECT_ACCESS
      end

      default_project_creation = ::Gitlab::CurrentSettings.default_project_creation
      prevent_project_creation_by_default = prevent_project_creation?(user, default_project_creation)

      # Remove nil (i.e. inherited `default_project_creation`) when the application setting is:
      # 1. NO_ONE_PROJECT_ACCESS
      # 2. ADMINISTRATOR_PROJECT_ACCESS and the user is not an admin
      #
      # To prevent showing groups in the namespaces dropdown on the project creation page that have no explicit group
      # setting for `project_creation_level`.
      project_creation_allowed_on_levels.delete(nil) if prevent_project_creation_by_default

      project_creation_allowed_on_levels
    end

    def prevent_project_creation?(user, project_creation_setting)
      return true if project_creation_setting == ::Gitlab::Access::NO_ONE_PROJECT_ACCESS
      return false if Ability.allowed?(user, :admin_all_resources)

      project_creation_setting == ::Gitlab::Access::ADMINISTRATOR_PROJECT_ACCESS
    end

    def self_or_ancestors_deletion_schedule_subquery
      deletion_schedule_reflection = reflect_on_association(:deletion_schedule)
      deletion_schedule_table = Arel::Table.new(deletion_schedule_reflection.table_name)
      traversal_ids_ref = "#{arel_table.name}.#{arel_table[:traversal_ids].name}"

      deletion_schedule_table
        .project(1)
        .where(
          deletion_schedule_table[deletion_schedule_reflection.foreign_key]
            .eq(Arel.sql("ANY (#{traversal_ids_ref})"))
        )
    end

    private

    def public_to_user_arel(user)
      self.arel_table[:visibility_level]
        .in(Gitlab::VisibilityLevel.levels_for_user(user))
    end

    def visible_to_user_arel(user)
      groups_table = self.arel_table
      authorized_groups = user.authorized_groups.arel.as('authorized')

      groups_table.project(1)
        .from(authorized_groups)
        .where(authorized_groups[:id].eq(groups_table[:id]))
        .exists
    end
  end

  # Overrides notification_settings has_many association
  # This allows to apply notification settings from parent groups
  # to child groups and projects.
  def notification_settings(hierarchy_order: nil)
    source_type = self.class.base_class.name
    settings = NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids)

    return settings unless hierarchy_order && self_and_ancestors_ids.length > 1

    settings
      .joins("LEFT JOIN (#{self_and_ancestors(hierarchy_order: hierarchy_order).to_sql}) AS ordered_groups ON notification_settings.source_id = ordered_groups.id")
      .select('notification_settings.*, ordered_groups.depth AS depth')
      .order("ordered_groups.depth #{hierarchy_order}")
  end

  def notification_settings_for(user, hierarchy_order: nil)
    notification_settings(hierarchy_order: hierarchy_order).where(user: user)
  end

  def packages_feature_enabled?
    ::Gitlab.config.packages.enabled
  end

  def dependency_proxy_feature_available?
    ::Gitlab.config.dependency_proxy.enabled
  end

  def notification_email_for(user)
    # Finds the closest notification_setting with a `notification_email`
    notification_settings = notification_settings_for(user, hierarchy_order: :asc)
    notification_settings.find { |n| n.notification_email.present? }&.notification_email
  end

  def dependency_proxy_image_prefix
    # The namespace path can include uppercase letters, which
    # Docker doesn't allow. The proxy expects it to be downcased.
    url = "#{Gitlab::Routing.url_helpers.group_url(self).downcase}#{DependencyProxy::URL_SUFFIX}"

    # Docker images do not include the protocol
    url.partition('//').last
  end

  def human_name
    full_name
  end

  def to_human_reference(from = nil)
    return unless cross_namespace_reference?(from)

    human_name
  end

  def visibility_level_allowed_by_organization?(level = self.visibility_level)
    return true unless organization

    level <= organization.visibility_level
  end

  def visibility_level_allowed_by_parent?(level = self.visibility_level)
    return true unless parent_id && parent_id.nonzero?

    level <= parent.visibility_level
  end

  def visibility_level_allowed_by_projects?(level = self.visibility_level)
    !projects.not_aimed_for_deletion.with_visibility_level_greater_than(level).exists?
  end

  def visibility_level_allowed_by_sub_groups?(level = self.visibility_level)
    !children.with_visibility_level_greater_than(level).exists?
  end

  def visibility_level_allowed?(level = self.visibility_level)
    visibility_level_allowed_by_organization?(level) &&
      visibility_level_allowed_by_parent?(level) &&
      visibility_level_allowed_by_projects?(level) &&
      visibility_level_allowed_by_sub_groups?(level)
  end

  def lfs_enabled?
    return false unless Gitlab.config.lfs.enabled
    return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?

    self[:lfs_enabled]
  end

  def owned_by?(user)
    return false unless user

    non_invite_owner_members.exists?(user: user)
  end

  def add_members(users, access_level, current_user: nil, expires_at: nil)
    Members::Groups::CreatorService.add_members( # rubocop:disable CodeReuse/ServiceClass
      self,
      users,
      access_level,
      current_user: current_user,
      expires_at: expires_at
    )
  end

  def add_member(user, access_level, ...)
    Members::Groups::CreatorService.add_member(self, user, access_level, ...) # rubocop:disable CodeReuse/ServiceClass
  end

  def add_guest(user, current_user = nil)
    add_member(user, :guest, current_user: current_user)
  end

  def add_planner(user, current_user = nil)
    add_member(user, :planner, current_user: current_user)
  end

  def add_reporter(user, current_user = nil)
    add_member(user, :reporter, current_user: current_user)
  end

  def add_developer(user, current_user = nil)
    add_member(user, :developer, current_user: current_user)
  end

  def add_maintainer(user, current_user = nil)
    add_member(user, :maintainer, current_user: current_user)
  end

  def add_owner(user, current_user = nil)
    add_member(user, :owner, current_user: current_user)
  end

  def member?(user, min_access_level = Gitlab::Access::GUEST)
    return false unless user

    max_member_access_for_user(user) >= min_access_level
  end

  def has_owner?(user)
    return false unless user

    members_with_parents.all_owners.exists?(user_id: user)
  end

  def blocked_owners
    members.blocked.where(access_level: Gitlab::Access::OWNER)
  end

  def has_maintainer?(user)
    return false unless user

    members_with_parents.maintainers.exists?(user_id: user)
  end

  def has_container_repository_including_subgroups?
    ::ContainerRepository.for_group_and_its_subgroups(self).exists?
  end

  # Check if user is a last owner of the group.
  # Excludes non-direct owners for top-level group
  # Excludes project_bots
  def last_owner?(user)
    return false unless user

    all_owners = member_owners_excluding_project_bots_and_service_accounts
    last_owner_in_list?(user, all_owners)
  end

  # This is used in BillableMember Entity to
  # avoid multiple "member_owners_excluding_project_bots_and_service_accounts" calls
  # for each billable members
  def last_owner_in_list?(user, all_owners)
    return false unless user

    all_owners.size == 1 && all_owners.first.user_id == user.id
  end

  # Excludes non-direct owners for top-level group
  # Excludes project_bots
  # Excludes service accounts
  def member_owners_excluding_project_bots_and_service_accounts
    members_from_hierarchy = if root?
                               members.non_minimal_access.without_invites_and_requests
                             else
                               members_with_parents(only_active_users: false)
                             end

    owners = []
    members_from_hierarchy.all_owners.non_invite.each_batch do |relation|
      owners += relation.preload(:user, :source).load.reject do |member|
        member.user.nil? || member.user.project_bot? || member.user.service_account?
      end
    end

    owners
  end

  def ldap_synced?
    false
  end

  def post_create_hook
    Gitlab::AppLogger.info("Group \"#{name}\" was created")

    system_hook_service.execute_hooks_for(self, :create)
  end

  def post_destroy_hook
    Gitlab::AppLogger.info("Group \"#{name}\" was removed")

    system_hook_service.execute_hooks_for(self, :destroy)
  end

  # rubocop: disable CodeReuse/ServiceClass
  def system_hook_service
    SystemHooksService.new
  end
  # rubocop: enable CodeReuse/ServiceClass

  # rubocop: disable CodeReuse/ServiceClass
  def refresh_members_authorized_projects(
    priority: UserProjectAccessChangedService::HIGH_PRIORITY,
    direct_members_only: false
  )

    user_ids = if direct_members_only
                 users_ids_of_direct_members
               else
                 user_ids_for_project_authorizations
               end

    UserProjectAccessChangedService
      .new(user_ids)
      .execute(priority: priority)
  end
  # rubocop: enable CodeReuse/ServiceClass

  def users_ids_of_direct_members
    direct_members.pluck_user_ids
  end

  def user_ids_for_project_authorizations
    members_with_parents.pluck(Arel.sql('DISTINCT members.user_id'))
  end

  def self_and_hierarchy_intersecting_with_user_groups(user)
    user_groups = GroupsFinder.new(user).execute.unscope(:order)
    self_and_hierarchy.unscope(:order).where(id: user_groups)
  end

  def self_and_ancestors_ids
    strong_memoize(:self_and_ancestors_ids) do
      self_and_ancestors.pluck(:id)
    end
  end

  def self_and_descendants_ids
    strong_memoize(:self_and_descendants_ids) do
      self_and_descendants.pluck(:id)
    end
  end

  def self_and_ancestors_asc
    self_and_ancestors(hierarchy_order: :asc)
  end
  strong_memoize_attr :self_and_ancestors_asc

  # Only for direct and not requested members with higher access level than MIMIMAL_ACCESS
  # It returns true for non-active users
  def has_user?(user)
    return false unless user

    group_members.non_invite.exists?(user: user)
  end

  def direct_members
    GroupMember.active_without_invites_and_requests
               .non_minimal_access
               .where(source_id: id)
  end

  def authorizable_members_with_parents
    Members::MembersWithParents.new(self).all_members.authorizable
  end

  def members_with_parents(only_active_users: true)
    Members::MembersWithParents
      .new(self)
      .members(active_users: only_active_users)
  end

  def members_from_self_and_ancestors_with_effective_access_level
    members_with_parents.select([:user_id, 'MAX(access_level) AS access_level'])
                        .group(:user_id)
  end

  def members_with_descendants
    GroupMember
      .active_without_invites_and_requests
      .where(source_id: self_and_descendants.without_order.select(:id))
  end

  # Returns all members that are part of the group, it's subgroups, and ancestor groups
  def hierarchy_members
    GroupMember
      .active_without_invites_and_requests
      .where(source_id: self_and_hierarchy.without_order.select(:id))
  end

  def hierarchy_members_with_inactive
    GroupMember
      .non_request
      .non_invite
      .where(source_id: self_and_hierarchy.without_order.select(:id))
  end

  def descendant_project_members_with_inactive
    ProjectMember
      .with_source_id(all_projects)
      .non_request
      .non_invite
  end

  def users_with_descendants
    User
      .id_in(members_with_descendants.select(:user_id))
      .without_order
  end

  def users_count
    members.count
  end

  # Return the highest access level for a user
  #
  # A special case is handled here when the user is a GitLab admin
  # which implies it has "OWNER" access everywhere, but should not
  # officially appear as a member of a group unless specifically added to it
  #
  # @param user [User]
  # @param only_concrete_membership [Bool] whether require admin concrete membership status
  def max_member_access_for_user(user, only_concrete_membership: false)
    return GroupMember::NO_ACCESS unless user

    unless only_concrete_membership
      return GroupMember::OWNER if user.can_admin_all_resources?
      return GroupMember::OWNER if user.can_admin_organization?(organization)
    end

    max_member_access(user)
  end

  def mattermost_team_params
    max_length = 59

    {
      name: path[0..max_length],
      display_name: name[0..max_length],
      type: public? ? 'O' : 'I' # Open vs Invite-only
    }
  end

  def member(user)
    if group_members.loaded?
      group_members.find { |gm| gm.user_id == user.id }
    else
      group_members.find_by(user_id: user)
    end
  end

  def highest_group_member(user)
    GroupMember
      .where(source_id: self_and_ancestors_ids, user_id: user.id)
      .non_request
      .order(:access_level)
      .last
  end

  def bots
    users.project_bot
  end

  def related_group_ids
    [id,
      *ancestors.pluck(:id),
      *shared_with_group_links.pluck(:shared_with_group_id)]
  end

  def hashed_storage?(_feature)
    false
  end

  def refresh_project_authorizations
    refresh_members_authorized_projects
  end

  # each existing group needs to have a `runners_token`.
  # we do this on read since migrating all existing groups is not a feasible
  # solution.
  def runners_token
    return unless allow_runner_registration_token?

    ensure_runners_token!
  end

  def project_creation_level
    super || ::Gitlab::CurrentSettings.default_project_creation
  end

  def subgroup_creation_level
    super || ::Gitlab::Access::OWNER_SUBGROUP_ACCESS
  end

  def access_request_approvers_to_be_notified
    members.owners.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
  end

  def membership_locked?
    false # to support project and group calling this as 'source'
  end

  def supports_events?
    false
  end

  def import_export_upload_by_user(user)
    import_export_uploads.find_by(user_id: user.id)
  end

  def export_file_exists?(user)
    import_export_upload_by_user(user)&.export_file_exists?
  end

  def export_file(user)
    import_export_upload_by_user(user)&.export_file
  end

  def export_archive_exists?(user)
    import_export_upload_by_user(user)&.export_archive_exists?
  end

  def execute_hooks(data, hooks_scope)
    # NOOP
    # TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904
  end

  def find_or_initialize_integration(integration)
    Integration.find_or_initialize_non_project_specific_integration(integration, group_id: id)
  end

  def execute_integrations(data, hooks_scope)
    integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
      integration.async_execute(data)
    end
  end

  def preload_shared_group_links
    ActiveRecord::Associations::Preloader.new(
      records: [self],
      associations: { shared_with_group_links: [shared_with_group: :route] }
    ).call
  end

  def first_owner
    first_owner_member = all_group_members.all_owners.order(:user_id).first

    first_owner_member&.user || parent&.first_owner || owner
  end

  def default_branch_name
    namespace_settings&.default_branch_name
  end

  def access_level_roles
    GroupMember.access_level_roles
  end

  def access_level_values
    access_level_roles.values
  end

  def parent_allows_two_factor_authentication?
    return true unless has_parent?

    ancestor_settings = ancestors.find_top_level.namespace_settings
    ancestor_settings.allow_mfa_for_subgroups
  end

  def has_project_with_service_desk_enabled?
    ::ServiceDesk.supported? && all_projects.service_desk_enabled.exists?
  end
  strong_memoize_attr :has_project_with_service_desk_enabled?

  # rubocop: disable CodeReuse/ServiceClass
  def open_issues_count(current_user = nil)
    Groups::OpenIssuesCountService.new(self, current_user).count
  end
  # rubocop: enable CodeReuse/ServiceClass

  # rubocop: disable CodeReuse/ServiceClass
  def open_merge_requests_count(current_user = nil)
    Groups::MergeRequestsCountService.new(self, current_user).count
  end
  # rubocop: enable CodeReuse/ServiceClass

  def timelogs
    Timelog.in_group(self)
  end

  def dependency_proxy_image_ttl_policy
    super || build_dependency_proxy_image_ttl_policy
  end

  def dependency_proxy_setting
    super || build_dependency_proxy_setting
  end

  def group_feature
    super || build_group_feature
  end

  def crm_enabled?
    crm_settings.nil? || crm_settings.enabled?
  end

  def shared_with_group_links_visible_to_user(user)
    shared_with_group_links.preload_shared_with_groups.filter { |link| Ability.allowed?(user, :read_group, link.shared_with_group) }
  end

  def enforced_runner_token_expiration_interval
    all_parent_groups = Gitlab::ObjectHierarchy.new(Group.where(id: id)).ancestors
    all_group_settings = NamespaceSetting.where(namespace_id: all_parent_groups)
    group_interval = all_group_settings.where.not(subgroup_runner_token_expiration_interval: nil).minimum(:subgroup_runner_token_expiration_interval)&.seconds

    [
      Gitlab::CurrentSettings.group_runner_token_expiration_interval&.seconds,
      group_interval
    ].compact.min
  end

  def work_items_feature_flag_enabled?
    feature_flag_enabled_for_self_or_ancestor?(:work_items)
  end

  def work_items_beta_feature_flag_enabled?
    feature_flag_enabled_for_self_or_ancestor?(:work_items_beta, type: :beta)
  end

  def work_items_alpha_feature_flag_enabled?
    feature_flag_enabled_for_self_or_ancestor?(:work_items_alpha)
  end

  def work_items_group_issues_list_feature_flag_enabled?
    feature_flag_enabled_for_self_or_ancestor?(:work_items_group_issues_list, type: :beta)
  end

  def work_item_status_mvc2_feature_flag_enabled?
    feature_flag_enabled_for_self_or_ancestor?(:work_item_status_mvc2, type: :beta)
  end

  def markdown_placeholders_feature_flag_enabled?
    feature_flag_enabled_for_self_or_ancestor?(:markdown_placeholders, type: :gitlab_com_derisk)
  end

  def glql_load_on_click_feature_flag_enabled?
    feature_flag_enabled_for_self_or_ancestor?(:glql_load_on_click, type: :ops)
  end

  def allow_iframes_in_markdown_feature_flag_enabled?
    feature_flag_enabled_for_self_or_ancestor?(:allow_iframes_in_markdown, type: :wip)
  end

  # overriden in EE
  def supports_group_work_items?
    false
  end

  # overriden in EE
  def has_active_hooks?(hooks_scope = :push_hooks)
    false
  end

  # overriden in EE
  def enterprise_user_settings_available?(user = nil)
    false
  end

  def create_group_level_work_items_feature_flag_enabled?
    ::Feature.enabled?(:create_group_level_work_items, self, type: :wip) && supports_group_work_items?
  end

  def supports_lock_on_merge?
    feature_flag_enabled_for_self_or_ancestor?(:enforce_locked_labels_on_merge, type: :ops)
  end

  def usage_quotas_enabled?
    root?
  end

  def supports_saved_replies?
    false
  end

  # Check for enabled features, similar to `Project#feature_available?`
  # NOTE: We still want to keep this after removing `Namespace#feature_available?`.
  override :feature_available?
  def feature_available?(feature, user = nil)
    # when we check the :issues feature at group level we need to check the `epics` license feature instead
    feature = feature == :issues ? :epics : feature

    if ::Groups::FeatureSetting.available_features.include?(feature)
      group_feature.feature_available?(feature, user) # rubocop:disable Gitlab/FeatureAvailableUsage
    else
      super
    end
  end

  def gitlab_deploy_token
    strong_memoize(:gitlab_deploy_token) do
      deploy_tokens.gitlab_deploy_token
    end
  end

  def packages_policy_subject
    ::Packages::Policies::Group.new(self)
  end

  def dependency_proxy_for_containers_policy_subject
    ::Packages::Policies::DependencyProxy::Group.new(self)
  end

  def update_two_factor_requirement_for_members
    hierarchy_members.find_each(&:update_two_factor_requirement)
  end

  def readme_project
    projects.find_by(path: README_PROJECT_PATH)
  end
  strong_memoize_attr :readme_project

  def notification_group
    self
  end

  def group_readme
    readme_project&.repository&.readme
  end
  strong_memoize_attr :group_readme

  def hook_attrs
    {
      group_name: name,
      group_path: path,
      group_id: id,
      full_path: full_path
    }
  end

  def crm_group
    Group.id_in_ordered(traversal_ids.reverse)
      .joins(:crm_settings)
      .where.not(crm_settings: { source_group_id: nil })
      .first&.crm_settings&.source_group || root_ancestor
  end
  strong_memoize_attr :crm_group

  def crm_group?
    return true if root? && crm_settings&.source_group_id.nil?

    crm_targets.present?
  end
  strong_memoize_attr :crm_group?

  def has_issues_with_contacts?
    CustomerRelations::IssueContact.joins(:issue).where(issue: { project_id: Project.where(namespace_id: self_and_descendant_ids) }).exists?
  end

  def delete_contacts
    CustomerRelations::Contact.where(group_id: id).delete_all
  end

  def delete_organizations
    CustomerRelations::Organization.where(group_id: id).delete_all
  end

  def cluster_agents
    ::Clusters::Agent.for_projects(all_projects)
  end

  def active?
    self_and_ancestors.inactive.none?
  end

  def pending_delete?
    return false unless deletion_schedule

    deletion_schedule.marked_for_deletion_on.future?
  end

  def assigning_role_too_high?(current_user, access_level)
    return false if current_user.can_admin_all_resources?
    return false unless access_level

    max_access_level = max_member_access(current_user)

    !Authz::Role.access_level_encompasses?(current_access_level: max_access_level, level_to_assign: access_level)
  end

  private

  def feature_flag_enabled_for_self_or_ancestor?(feature_flag, type: :development)
    actors = [root_ancestor]
    actors << self if root_ancestor != self

    actors.any? do |actor|
      ::Feature.enabled?(feature_flag, actor, type: type)
    end
  end

  def max_member_access(user)
    Gitlab::SafeRequestLoader.execute(
      resource_key: max_member_access_for_resource_key(User),
      resource_ids: [user.id],
      default_value: Gitlab::Access::NO_ACCESS
    ) do |_|
      next {} unless user.active?

      members_with_parents(only_active_users: false).where(user_id: user.id).group(:user_id).maximum(:access_level)
    end.fetch(user.id)
  end

  def update_two_factor_requirement
    return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period?

    Groups::UpdateTwoFactorRequirementForMembersWorker.perform_async(self.id)
  end

  def path_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

  def should_validate_visibility_level?
    new_record? || changes.has_key?(:visibility_level)
  end

  def visibility_level_allowed_by_organization
    return if visibility_level_allowed_by_organization?

    errors.add(:visibility_level, "#{visibility} is not allowed since the organization has a #{organization.visibility} visibility.")
  end

  def visibility_level_allowed_by_parent
    return if visibility_level_allowed_by_parent?

    errors.add(:visibility_level, "#{visibility} is not allowed since the parent group has a #{parent.visibility} visibility.")
  end

  def visibility_level_allowed_by_projects
    return if visibility_level_allowed_by_projects?

    errors.add(:visibility_level, "#{visibility} is not allowed since this group contains projects with higher visibility.")
  end

  def visibility_level_allowed_by_sub_groups
    return if visibility_level_allowed_by_sub_groups?

    errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
  end

  def two_factor_authentication_allowed
    return unless has_parent?
    return unless require_two_factor_authentication

    return if parent_allows_two_factor_authentication?

    errors.add(:require_two_factor_authentication, _('is forbidden by a top-level group'))
  end

  def runners_token_prefix
    RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
  end

  def top_level_group_name_not_assigned_to_pages_unique_domain
    return unless parent_id.nil?

    return unless ProjectSetting.unique_domain_exists?(path)

    # We cannot disclose the Pages unique domain, hence returning generic error message
    errors.add(:path, _('has already been taken'))
  end

  # Overriding of Namespaces::AdjournedDeletable method
  override :ancestors_scheduled_for_deletion
  def ancestors_scheduled_for_deletion
    ancestors(hierarchy_order: :asc).joins(:deletion_schedule)
  end
end

Group.prepend_mod_with('Group')
