# typed: strict
class Domain::Fa::Job::FavsJob < Domain::Fa::Job::Base
  include HasBulkEnqueueJobs
  queue_as :fa_user_favs
  queue_with_priority do
    T.bind(self, Domain::Fa::Job::FavsJob)
    user_from_args!.scanned_favs_at.nil? ? -11 : -1
  end

  FAVED_POSTS_PER_PAGE_THRESHOLD = T.let(Rails.env.test? ? 2 : 36, Integer)
  MAX_PAGE_NUMBER = 2000

  sig { params(args: T.untyped).void }
  def initialize(*args)
    super(*T.unsafe(args))
    @page_id = T.let(nil, T.nilable(String))
    @request_number = T.let(0, Integer)
  end

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

    unless user_due_for_favs_scan?(user)
      logger.warn(format_tags("user not due for favs scan, skipping"))
      return
    end

    if user.state_account_disabled?
      logger.warn(format_tags("user is disabled, skipping"))
      return
    end

    faved_post_ids = T.let(Set.new, T::Set[Integer])
    existing_faved_post_ids =
      T.let(Set.new(user.user_post_favs.pluck(:post_id)), T::Set[Integer])

    logger.info(
      format_tags(
        make_tag("server num favorites", user.num_favorites),
        make_tag("existing favorites", existing_faved_post_ids.size),
        make_tag("max page number", MAX_PAGE_NUMBER),
      ),
    )

    while true
      ret = scan_next_page(user: user)
      break if ret.is_a?(ScanPageResult::Stop)

      faved_post_ids += ret.faved_post_ids_on_page
      new_faved_post_ids_on_page =
        ret.faved_post_ids_on_page - existing_faved_post_ids

      logger.info(
        format_tags(
          make_tag("request number", @request_number),
          make_tag("new favs", new_faved_post_ids_on_page.size),
          make_tag("created post models", ret.posts_created_ids.size),
        ),
      )

      if !full_scan &&
           (new_faved_post_ids_on_page.size < FAVED_POSTS_PER_PAGE_THRESHOLD)
        logger.info(format_tags("incremenetal scan, stopping"))
        break
      end

      unless ret.keep_scanning
        logger.info(format_tags("no next favs page, stopping"))
        break
      end

      if @request_number > MAX_PAGE_NUMBER
        logger.warn(
          format_tags(
            "request number(#{@request_number}) > max page number(#{MAX_PAGE_NUMBER})",
          ),
        )
        break
      end

      @request_number += 1
    end

    faved_post_ids_to_add = faved_post_ids - existing_faved_post_ids
    user.upsert_new_favs(
      faved_post_ids_to_add.to_a,
      log_entry: causing_log_entry!,
    )
  ensure
    user.save! if user
  end

  private

  module ScanPageResult
    extend T::Sig

    class Stop < T::Struct
    end

    class Ok < T::Struct
      include T::Struct::ActsAsComparable
      const :faved_post_ids_on_page, T::Set[Integer]
      const :posts_created_ids, T::Set[Integer]
      const :keep_scanning, T::Boolean
    end

    Result = T.type_alias { T.any(Stop, Ok) }
  end

  sig { params(user: Domain::User::FaUser).returns(ScanPageResult::Result) }
  def scan_next_page(user:)
    url =
      if @page_id
        "https://www.furaffinity.net/favorites/#{user.url_name}/#{@page_id}/next"
      else
        "https://www.furaffinity.net/favorites/#{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

    disabled_or_not_found = user_disabled_or_not_found?(user, response)
    return ScanPageResult::Stop.new if disabled_or_not_found

    page_parser = Domain::Fa::Parser::Page.from_log_entry(response.log_entry)
    return ScanPageResult::Stop.new unless page_parser.probably_listings_page?

    listing_page_stats =
      update_and_enqueue_posts_from_listings_page(
        ListingPageType::FavsPage.new(page_number: @page_id, user:),
        page_parser:,
      )

    ReduxApplicationRecord.transaction do
      self.class.update_favs_and_dates(user:, page_parser:)
    end

    @page_id = page_parser.favorites_next_button_id
    ScanPageResult::Ok.new(
      faved_post_ids_on_page:
        listing_page_stats.all_posts.map(&:id).compact.to_set,
      posts_created_ids: listing_page_stats.new_posts.map(&:id).compact.to_set,
      keep_scanning: @page_id.present?,
    )
  end

  class FavsAndDatesStats < T::ImmutableStruct
    extend T::Sig
    include T::Struct::ActsAsComparable
    NH = NumberHelper

    const :num_updated_with_fav_fa_id, Integer, default: 0
    const :num_updated_with_date, Integer, default: 0
    const :num_updated_total, Integer, default: 0

    sig { params(other: FavsAndDatesStats).returns(FavsAndDatesStats) }
    def +(other)
      FavsAndDatesStats.new(
        num_updated_with_fav_fa_id:
          num_updated_with_fav_fa_id + other.num_updated_with_fav_fa_id,
        num_updated_with_date:
          num_updated_with_date + other.num_updated_with_date,
        num_updated_total: num_updated_total + other.num_updated_total,
      )
    end

    sig { returns(FavsAndDatesStats) }
    def self.zero
      FavsAndDatesStats.new()
    end

    sig { returns(String) }
    def to_s
      [
        num_updated_with_fav_fa_id,
        num_updated_with_date,
        num_updated_total,
      ].map { |n| (NH.number_with_delimiter(n) || "(none)") }.join(" / ")
    end
  end

  class FavUpsertData < T::ImmutableStruct
    extend T::Sig
    include T::Struct::ActsAsComparable
    const :post_id, Integer
    const :fav_id, Integer
  end

  # Creates or updates Domain::UserPostFav::FaUserPostFav records for the user's favs
  # and dates. The first faved post is special-cased to be the most recent
  # faved post, so we can use the date from the page parser to set its date.
  # The rest of the faved posts are updated with the fav_fa_id from the
  # page parser.
  sig do
    params(
      user: Domain::User::FaUser,
      page_parser: Domain::Fa::Parser::Page,
    ).returns(FavsAndDatesStats)
  end
  def self.update_favs_and_dates(user:, page_parser:)
    num_updated_with_fav_fa_id = 0
    num_updated_with_date = 0
    num_updated_total = 0

    fa_id_to_post_id =
      T.let(
        Domain::Post::FaPost
          .where(fa_id: page_parser.submissions_parsed.map(&:id))
          .pluck(:fa_id, :id)
          .to_h,
        T::Hash[Integer, Integer],
      )

    page_parser
      .submissions_parsed
      .filter { |sub| fa_id_to_post_id[T.must(sub.id)].nil? }
      .each do |sub|
        fa_id = T.must(sub.id)
        model = Domain::Post::FaPost.find_or_create_by!(fa_id:)
        fa_id_to_post_id[fa_id] = T.must(model.id)
      end

    first_faved = page_parser.submissions_parsed[0]
    if (fa_id = first_faved&.id) && (post_id = fa_id_to_post_id[fa_id]) &&
         (fav_id = first_faved.fav_id) &&
         (explicit_time = page_parser.most_recent_faved_at_time)
      num_updated_with_date += 1
      num_updated_total += 1
      user.update_fav_model(post_id:, fav_id:, explicit_time:)
    end

    (page_parser.submissions_parsed[1..] || [])
      .filter_map do |sub_data|
        post_id = (id = sub_data.id) && fa_id_to_post_id[id]
        next if post_id.nil?
        fav_id = sub_data.fav_id
        next if fav_id.nil?

        FavUpsertData
          .new(post_id:, fav_id:)
          .tap do
            num_updated_with_fav_fa_id += 1
            num_updated_total += 1
          end
      end
      .group_by(&:post_id)
      .values
      .filter_map { |data_arr| data_arr.max_by(&:fav_id) }
      .each do |data|
        user.update_fav_model(post_id: data.post_id, fav_id: data.fav_id)
      end

    FavsAndDatesStats.new(
      num_updated_with_fav_fa_id:,
      num_updated_with_date:,
      num_updated_total:,
    )
  end
end
