# frozen_string_literal: true

##
# Allows us to query Bookmark records for lists. Used mainly
# in the user/activity/bookmarks page.

class BookmarkQuery
  def self.on_preload(&blk)
    (@preload ||= Set.new) << blk
  end

  def self.preload(bookmarks, object)
    preload_polymorphic_associations(bookmarks, object.guardian)
    @preload.each { |preload| preload.call(bookmarks, object) } if @preload
  end

  # These polymorphic associations are loaded to make the UserBookmarkListSerializer's
  # life easier, which conditionally chooses the bookmark serializer to use based
  # on the type, and we want the associations all loaded ahead of time to make
  # sure we are not doing N+1s.
  def self.preload_polymorphic_associations(bookmarks, guardian)
    Bookmark.registered_bookmarkables.each do |registered_bookmarkable|
      registered_bookmarkable.perform_preload(bookmarks, guardian)
    end
  end

  attr_reader :guardian, :count

  def initialize(user:, guardian: nil, search_term: nil, page: nil, per_page: nil)
    @user = user
    @search_term = search_term
    @guardian = guardian || Guardian.new(@user)
    @page = page ? page.to_i : 0
    @per_page = per_page ? per_page.to_i : 20
    @count = 0
  end

  def list_all(&blk)
    ts_query = @search_term.present? ? Search.ts_query(term: @search_term) : nil
    search_term_wildcard = @search_term.present? ? "%#{@search_term}%" : nil

    queries =
      Bookmark
        .registered_bookmarkables
        .map do |bookmarkable|
          interim_results = bookmarkable.perform_list_query(@user, @guardian)

          # this could occur if there is some security reason that the user cannot
          # access the bookmarkables that they have bookmarked, e.g. if they had 1 bookmark
          # on a topic and that topic was moved into a private category
          next if interim_results.blank?

          if @search_term.present?
            interim_results =
              bookmarkable.perform_search_query(interim_results, search_term_wildcard, ts_query)
          end

          # this is purely to make the query easy to read and debug, otherwise it's
          # all mashed up into a massive ball in MiniProfiler :)
          "---- #{bookmarkable.model} bookmarkable ---\n\n #{interim_results.to_sql}"
        end
        .compact

    # same for interim results being blank, the user might have been locked out
    # from all their various bookmarks, in which case they will see nothing and
    # no further pagination/ordering/etc is required
    return [] if queries.empty?

    union_sql = queries.join("\n\nUNION\n\n")
    results = Bookmark.select("bookmarks.*").from("(\n\n#{union_sql}\n\n) as bookmarks")
    results =
      results.order(
        "(CASE WHEN bookmarks.pinned THEN 0 ELSE 1 END),
        bookmarks.reminder_at ASC,
        bookmarks.updated_at DESC",
      )

    @count = results.count

    results = results.offset(@page * @per_page) if @page.positive?

    if updated_results = blk&.call(results)
      results = updated_results
    end

    results = results.limit(@per_page).to_a

    BookmarkQuery.preload(results, self)
    results
  end

  def unread_notifications(limit: 20)
    reminder_notifications =
      Notification
        .for_user_menu(@user.id, limit: [limit, 100].min)
        .unread
        .where(notification_type: Notification.types[:bookmark_reminder])

    reminder_bookmark_ids = reminder_notifications.map { |n| n.data_hash[:bookmark_id] }.compact

    # We preload associations like we do above for the list to avoid
    # N1s in the can_see? guardian calls for each bookmark.
    bookmarks = Bookmark.where(user: @user, id: reminder_bookmark_ids)
    BookmarkQuery.preload(bookmarks, self)

    # Any bookmarks that no longer exist, we need to find the associated
    # records using bookmarkable details.
    #
    # First we want to group these by type into a hash to reduce queries:
    #
    # {
    #   "Post": {
    #     1234: <Post>,
    #     566: <Post>,
    #   },
    #   "Topic": {
    #     123: <Topic>,
    #     99: <Topic>,
    #   }
    # }
    #
    # We may not need to do this most of the time. It depends mostly on
    # a user's auto_delete_preference for bookmarks.
    deleted_bookmark_ids = reminder_bookmark_ids - bookmarks.map(&:id)
    deleted_bookmarkables =
      reminder_notifications
        .select do |notif|
          deleted_bookmark_ids.include?(notif.data_hash[:bookmark_id]) &&
            notif.data_hash[:bookmarkable_type].present?
        end
        .inject({}) do |hash, notif|
          hash[notif.data_hash[:bookmarkable_type]] ||= {}
          hash[notif.data_hash[:bookmarkable_type]][notif.data_hash[:bookmarkable_id]] = nil
          hash
        end

    # Then, we can actually find the associated records for each type in the database.
    deleted_bookmarkables.each do |type, bookmarkable|
      records = Bookmark.registered_bookmarkable_from_type(type).model.where(id: bookmarkable.keys)
      records.each { |record| deleted_bookmarkables[type][record.id] = record }
    end

    reminder_notifications.select do |notif|
      bookmark = bookmarks.find { |bm| bm.id == notif.data_hash[:bookmark_id] }

      # This is the happy path, it's easiest to look up using a bookmark
      # that hasn't been deleted.
      if bookmark.present?
        bookmarkable = Bookmark.registered_bookmarkable_from_type(bookmark.bookmarkable_type)
        bookmarkable.can_see?(@guardian, bookmark)
      else
        # Otherwise, we have to use our cached records from the deleted
        # bookmarks' related bookmarkable (e.g. Post, Topic) to determine
        # secure access.
        bookmarkable =
          deleted_bookmarkables.dig(
            notif.data_hash[:bookmarkable_type],
            notif.data_hash[:bookmarkable_id],
          )
        bookmarkable.present? &&
          Bookmark.registered_bookmarkable_from_type(
            notif.data_hash[:bookmarkable_type],
          ).can_see_bookmarkable?(@guardian, bookmarkable)
      end
    end
  end
end