# frozen_string_literal: true

# Snippets Finder
#
# Used to filter Snippets collections by a set of params
#
# Arguments.
#
# current_user - The current user, nil also can be used.
# params:
#   visibility (integer) - Individual snippet visibility: Public(20), internal(10) or private(0).
#   project (Project) - Project related.
#   author (User) - Author related.
#
# params are optional
class SnippetsFinder < UnionFinder
  include Gitlab::Allowable
  include FinderMethods

  attr_accessor :current_user, :project, :params

  def initialize(current_user, params = {})
    @current_user = current_user
    @params = params
    @project = params[:project]
  end

  def execute
    items = init_collection
    items = by_author(items)
    items = by_visibility(items)

    items.fresh
  end

  private

  def init_collection
    if project.present?
      authorized_snippets_from_project
    else
      authorized_snippets
    end
  end

  # rubocop: disable CodeReuse/ActiveRecord
  def authorized_snippets_from_project
    if can?(current_user, :read_project_snippet, project)
      if project.team.member?(current_user)
        project.snippets
      else
        project.snippets.public_to_user(current_user)
      end
    else
      Snippet.none
    end
  end
  # rubocop: enable CodeReuse/ActiveRecord

  # rubocop: disable CodeReuse/ActiveRecord
  def authorized_snippets
    # This query was intentionally converted to a raw one to get it work in Rails 5.0.
    # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
    # Please convert it back when on rails 5.2 as it works again as expected since 5.2.
    Snippet.where("#{feature_available_projects} OR #{not_project_related}")
      .public_or_visible_to_user(current_user)
  end
  # rubocop: enable CodeReuse/ActiveRecord

  # Returns a collection of projects that is either public or visible to the
  # logged in user.
  #
  # A caller must pass in a block to modify individual parts of
  # the query, e.g. to apply .with_feature_available_for_user on top of it.
  # This is useful for performance as we can stick those additional filters
  # at the bottom of e.g. the UNION.
  # rubocop: disable CodeReuse/ActiveRecord
  def projects_for_user
    return yield(Project.public_to_user) unless current_user

    # If the current_user is allowed to see all projects,
    # we can shortcut and just return.
    return yield(Project.all) if current_user.full_private_access?

    authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects))

    levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
    visible_projects = yield(Project.where(visibility_level: levels))

    # We use a UNION here instead of OR clauses since this results in better
    # performance.
    Project.from_union([authorized_projects, visible_projects])
  end
  # rubocop: enable CodeReuse/ActiveRecord

  def feature_available_projects
    # Don't return any project related snippets if the user cannot read cross project
    return table[:id].eq(nil).to_sql unless Ability.allowed?(current_user, :read_cross_project)

    projects = projects_for_user do |part|
      part.with_feature_available_for_user(:snippets, current_user)
    end.select(:id)

    # This query was intentionally converted to a raw one to get it work in Rails 5.0.
    # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531
    # Please convert it back when on rails 5.2 as it works again as expected since 5.2.
    "snippets.project_id IN (#{projects.to_sql})"
  end

  def not_project_related
    table[:project_id].eq(nil).to_sql
  end

  def table
    Snippet.arel_table
  end

  # rubocop: disable CodeReuse/ActiveRecord
  def by_visibility(items)
    visibility = params[:visibility] || visibility_from_scope

    return items unless visibility

    items.where(visibility_level: visibility)
  end
  # rubocop: enable CodeReuse/ActiveRecord

  # rubocop: disable CodeReuse/ActiveRecord
  def by_author(items)
    return items unless params[:author]

    items.where(author_id: params[:author].id)
  end
  # rubocop: enable CodeReuse/ActiveRecord

  def visibility_from_scope
    case params[:scope].to_s
    when 'are_private'
      Snippet::PRIVATE
    when 'are_internal'
      Snippet::INTERNAL
    when 'are_public'
      Snippet::PUBLIC
    else
      nil
    end
  end
end
