# frozen_string_literal: true

module Gitlab
  class SearchResults
    COUNT_LIMIT = 100
    COUNT_LIMIT_MESSAGE = "#{COUNT_LIMIT - 1}+".freeze
    DEFAULT_PAGE = 1
    DEFAULT_PER_PAGE = 20

    attr_reader :current_user, :query, :order_by, :sort, :filters

    # Limit search results by passed projects
    # It allows us to search only for projects user has access to
    attr_reader :limit_projects

    # Whether a custom filter is used to restrict scope of projects.
    # If the default filter (which lists all projects user has access to)
    # is used, we can skip it when filtering merge requests and optimize the
    # query
    attr_reader :default_project_filter

    def initialize(
      current_user,
      query,
      limit_projects = nil,
      order_by: nil,
      sort: nil,
      default_project_filter: false,
      filters: {}
    )
      @current_user = current_user
      @query = query
      @limit_projects = limit_projects || Project.all
      @default_project_filter = default_project_filter
      @order_by = order_by
      @sort = sort
      @filters = filters
    end

    def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
      should_preload = preload_method.present?
      collection = collection_for(scope)
      collection = collection.public_send(preload_method) if should_preload # rubocop:disable GitlabSecurity/PublicSend
      collection = collection.page(page).per(per_page)
      collection.without_count
    end

    def formatted_count(scope)
      case scope
      when 'projects'
        formatted_limited_count(limited_projects_count)
      when 'issues'
        formatted_limited_count(limited_issues_count)
      when 'merge_requests'
        formatted_limited_count(limited_merge_requests_count)
      when 'milestones'
        formatted_limited_count(limited_milestones_count)
      when 'users'
        formatted_limited_count(limited_users_count)
      end
    end

    def formatted_limited_count(count)
      if count >= COUNT_LIMIT
        COUNT_LIMIT_MESSAGE
      else
        count.to_s
      end
    end

    def limited_projects_count
      @limited_projects_count ||= limited_count(projects)
    end

    def limited_issues_count
      return @limited_issues_count if @limited_issues_count

      # By default getting limited count (e.g. 1000+) is fast on issuable
      # collections except for issues, where filtering both not confidential
      # and confidential issues user has access to, is too complex.
      # It's faster to try to fetch all public issues first, then only
      # if necessary try to fetch all issues.
      sum = limited_count(issues(confidential: false))
      @limited_issues_count = sum < count_limit ? limited_count(issues) : sum
    end

    def limited_merge_requests_count
      @limited_merge_requests_count ||= limited_count(merge_requests)
    end

    def limited_milestones_count
      @limited_milestones_count ||= limited_count(milestones)
    end

    def limited_users_count
      @limited_users_count ||= limited_count(users)
    end

    def count_limit
      COUNT_LIMIT
    end

    def users
      return User.none unless Ability.allowed?(current_user, :read_users_list)

      params = { search: query, use_minimum_char_limit: false }

      if current_user && filters[:autocomplete]
        params[:group_member_source_ids] = current_user_authorized_group_ids
        params[:project_member_source_ids] = current_user_authorized_project_ids
      end

      UsersFinder.new(current_user, params).execute
    end

    # highlighting is only performed by Elasticsearch backed results
    def highlight_map(*)
      {}
    end

    # aggregations are only performed by Elasticsearch backed results
    def aggregations(*)
      []
    end

    # direct counts are only performed by Elasticsearch backed results
    def counts(*)
      {}
    end

    def failed?(*)
      false
    end

    def error(*)
      nil
    end

    private

    def collection_for(scope)
      case scope
      when 'projects'
        projects
      when 'issues'
        issues
      when 'merge_requests'
        merge_requests
      when 'milestones'
        milestones
      when 'users'
        users
      end
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def apply_sort(results, scope: nil)
      # Due to different uses of sort param we prefer order_by when
      # present
      sort_by = ::Gitlab::Search::SortOptions.sort_and_direction(order_by, sort)

      # Reset sort to default if the chosen one is not supported by scope
      if Gitlab::Search::SortOptions::SCOPE_ONLY_SORT[sort_by] &&
          Gitlab::Search::SortOptions::SCOPE_ONLY_SORT[sort_by].exclude?(scope)
        sort_by = nil
      end

      case sort_by
      when :created_at_asc
        results.reorder('created_at ASC')
      when :updated_at_asc
        results.reorder('updated_at ASC')
      when :updated_at_desc
        results.reorder('updated_at DESC')
      when :popularity_asc
        results.reorder('upvotes_count ASC')
      when :popularity_desc
        results.reorder('upvotes_count DESC')
      else
        # :created_at_desc is default
        results.reorder('created_at DESC')
      end
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def projects
      scope = limit_projects
      scope = scope.non_archived unless filters[:include_archived]

      scope.search(query, include_namespace: true, use_minimum_char_limit: false)
    end

    def issues(finder_params = {})
      issues = IssuesFinder.new(current_user, issuable_params.merge(finder_params)).execute
                 .preload(:work_item_type) # rubocop: disable CodeReuse/ActiveRecord -- preload for permission checks

      unless default_project_filter
        project_ids = project_ids_relation
        project_ids = project_ids.non_archived unless filters[:include_archived]

        issues = issues.in_projects(project_ids)
                       .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/420046')
      end

      apply_sort(issues, scope: 'issues')
    end

    def milestones
      milestones = Milestone.search(query)
      filter_milestones_by_project(milestones).order_updated_desc
    end

    def merge_requests
      merge_requests = MergeRequestsFinder.new(current_user, issuable_params).execute

      unless default_project_filter
        project_ids = project_ids_relation

        project_ids = project_ids.non_archived unless filters[:include_archived]

        merge_requests = merge_requests.of_projects(project_ids)
      end

      apply_sort(merge_requests, scope: 'merge_requests')
    end

    def default_scope
      'projects'
    end

    # Filter milestones by authorized projects.
    # For performance reasons project_id is being plucked
    # to be used on a smaller query.
    def filter_milestones_by_project(milestones)
      candidate_project_ids = project_ids_relation

      candidate_project_ids = candidate_project_ids.non_archived unless filters[:include_archived]

      project_ids = milestones.of_projects(candidate_project_ids).select(:project_id).distinct.pluck(:project_id) # rubocop: disable CodeReuse/ActiveRecord

      return Milestone.none if project_ids.nil?

      authorized_project_ids_relation = Project.id_in(project_ids).ids_with_issuables_available_for(current_user)

      milestones.of_projects(authorized_project_ids_relation)
    end

    def project_ids_relation
      limit_projects.select(:id).without_order
    end

    def issuable_params
      {}.tap do |params|
        params[:sort] = 'updated_desc'

        if query =~ /#(\d+)\z/
          params[:iids] = Regexp.last_match(1)
        else
          params[:search] = query
        end

        params[:state] = filters[:state] if filters.key?(:state)

        params[:confidential] = filters[:confidential] if [true, false].include?(filters[:confidential])
      end
    end

    def current_user_authorized_group_ids
      GroupsFinder
        .new(current_user, { all_available: false })
        .execute
        .pluck("#{Group.table_name}.#{Group.primary_key}") # rubocop: disable CodeReuse/ActiveRecord -- need to find ids
    end

    def current_user_authorized_project_ids
      ProjectsFinder
        .new(current_user: current_user, params: { non_public: true })
        .execute
        .pluck_primary_key
    end

    def limited_count(relation)
      relation.without_order.limit(count_limit).size
    end
  end
end

Gitlab::SearchResults.prepend_mod_with('Gitlab::SearchResults')
