# typed: strict
# Gather and record all the users that this user follows
# This will be used to create an index of follower -> followed
# of a specific user, for recommender training
class Domain::Fa::Job::UserFollowsJob < Domain::Fa::Job::Base
  include HasMeasureDuration
  include HasBulkEnqueueJobs

  queue_as :fa_user_follows
  queue_with_priority do
    T.bind(self, Domain::Fa::Job::UserFollowsJob)
    user_from_args!.scanned_follows_at.nil? ? -12 : -2
  end

  USERS_PER_FULL_PAGE = T.let(Rails.env.test? ? 9 : 190, Integer)

  sig { params(args: T.untyped).void }
  def initialize(*args)
    super(*T.unsafe(args))
    @page_number = T.let(1, Integer)
    @total_follows_seen = T.let(0, Integer)
    @last_in_user_list = T.let(nil, T.nilable(String))
    @scanned_followed_ids = T.let(Set.new, T::Set[Integer])
  end

  sig { override.params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
  def perform(args)
    user = user_from_args!
    return if buggy_user?(user)

    if !user.follows_scan.due? && !force_scan?
      logger.warn("scanned #{user.follows_scan.ago_in_words}, skipping")
      return
    end

    # buggy (sentinal) user
    return if user.url_name == "click here"

    # ensure user has an ID before we start scanning
    user.save! if user.new_record?

    while true
      break if scan_follows_page(user) == :break
      # bail out at 100,000 users
      break if @page_number > 500
      @page_number += 1
    end

    existing_followed_ids = Set.new(user.user_user_follows_from.pluck(:to_id))
    to_add = @scanned_followed_ids - existing_followed_ids
    logger.info "[calc changed follows] [add #{to_add.size.to_s.bold}]"

    ReduxApplicationRecord.transaction do
      to_add.each_slice(1000) do |slice|
        Domain::UserUserFollow.insert_all!(
          slice.map { |id| { from_id: user.id, to_id: id } },
        )
      end
      user.scanned_follows_at = Time.current
      user.save!
    end

    logger.info "[updated following users list] [users: #{user.followed_users.count.to_s.bold}]"
  end

  private

  sig { params(user: Domain::User::FaUser).returns(T.nilable(Symbol)) }
  def scan_follows_page(user)
    ret = nil

    url =
      if @page_number > 1
        "https://www.furaffinity.net/watchlist/by/#{user.url_name}/#{@page_number}/?"
      else
        "https://www.furaffinity.net/watchlist/by/#{user.url_name}/"
      end
    response = http_client.get(url)
    if response.status_code != 200
      fatal_error(
        "http #{response.status_code.to_s.red.bold}, " +
          "log entry #{response.log_entry.id.to_s.bold}",
      )
    end

    page =
      Domain::Fa::Parser::Page.from_log_entry(
        response.log_entry,
        require_logged_in: false,
      )
    user_list = page.user_list.uniq

    if user_list.empty?
      logger.info("page #{@page_number.to_s.bold} has no users, break")
      return :break
    end

    if user_list.last&.url_name == @last_in_user_list
      logger.info(
        "page #{@page_number.to_s.bold} saw same user as last page, break",
      )
      return :break
    end

    # the last page will have < 200 users, we know we're at the end
    ret = :break if user_list.length < USERS_PER_FULL_PAGE

    @last_in_user_list = user_list.last&.url_name
    @total_follows_seen += user_list.length

    existing_url_name_to_id =
      T.cast(
        Domain::User::FaUser
          .where(url_name: user_list.map(&:url_name))
          .pluck(:id, :url_name)
          .map { |id, url_name| [url_name, id] }
          .to_h,
        T::Hash[String, Integer],
      )

    users_to_create =
      user_list
        .reject { |user| existing_url_name_to_id[user.url_name] }
        .map do |user|
          Domain::User::FaUser.new(url_name: user.url_name, name: user.name)
        end

    created_user_ids =
      users_to_create.map do |user|
        user.save!
        T.cast(user.id, Integer)
      end

    logger.info [
                  "[page #{@page_number.to_s.bold}]",
                  "[users: #{user_list.length.to_s.bold}]",
                  "[created: #{users_to_create.size.to_s.bold}]",
                ].join(" ")

    enqueue_new_user_pagescan_jobs(users_to_create)
    followed_user_ids = created_user_ids + existing_url_name_to_id.values
    followed_user_ids.each { |user_id| @scanned_followed_ids.add(user_id) }

    ret
  end

  sig { params(users: T::Array[Domain::User::FaUser]).void }
  def enqueue_new_user_pagescan_jobs(users)
    bulk_enqueue_jobs do
      users.each { |user| defer_job(Domain::Fa::Job::UserPageJob, { user: }) }
    end
  end
end
