# typed: strict
# Processes a page of submissions from the API search endpoint.
#
# This is a separate class so it can be used by the UserGalleryJob, which
# needs to process multiple pages of submissions, and also by the
# LatestPostsJob, which needs to process a single page of submissions.
class Domain::Inkbunny::Job::ApiSearchPageProcessor
  extend T::Sig
  include HasColorLogger

  SUBMISSIONS_PER_PAGE = 100
  MAX_PAGE_COUNT = T.let(Rails.env.development? ? 2 : 50, Integer)

  sig { returns(T::Array[Domain::Post::InkbunnyPost]) }
  attr_reader :changed_posts

  sig { void }
  def initialize
    @shallow_posts_by_ib_post_id =
      T.let({}, T::Hash[Integer, Domain::Post::InkbunnyPost])
    @users_by_ib_user_id =
      T.let({}, T::Hash[Integer, Domain::User::InkbunnyUser])
    @changed_posts = T.let([], T::Array[Domain::Post::InkbunnyPost])
    @total_new_posts = T.let(0, Integer)
  end

  sig { returns(T::Array[Domain::Post::InkbunnyPost]) }
  def all_posts
    @shallow_posts_by_ib_post_id.values
  end

  sig { returns(T::Array[Domain::User::InkbunnyUser]) }
  def all_users
    @users_by_ib_user_id.values
  end

  # Process a page of submissions from the API search endpoint.
  #
  # Returns a hash with the following keys:
  # - num_total_posts: the total number of posts processed for all submission_jsons so far
  # - num_total_users: the total number of users processed for all submission_jsons so far
  # - num_total_changed_posts: the number of posts that were changed for all submission_jsons so far
  # - num_total_changed_users: the number of users that were changed for all submission_jsons so far
  # - num_total_new_posts: the total number of new posts processed for all submission_jsons so far
  # - num_new_posts: the number of new posts in the page for the submission_json that was just processed (indicates to the caller that the next page should be fetched)
  # - num_pages: the total number of pages in the submission_json that was just processed
  # - rid: the RID for the submission_json that was just processed
  sig do
    params(
      submissions_json: T::Hash[String, T.untyped],
      caused_by_entry: T.nilable(HttpLogEntry),
    ).returns(T::Hash[Symbol, T.any(Integer, String)])
  end
  def process!(submissions_json, caused_by_entry: nil)
    num_new_posts = 0
    submissions_json["submissions"].each do |submission_json|
      if upsert_post_from_submission_json!(
           submission_json,
           caused_by_entry: caused_by_entry,
         )
        num_new_posts += 1
        @total_new_posts += 1
      end
    end

    {
      num_total_posts: @shallow_posts_by_ib_post_id.size,
      num_total_users: @users_by_ib_user_id.size,
      num_total_changed_posts: @changed_posts.size,
      num_total_new_posts: @total_new_posts,
      num_new_posts: num_new_posts,
      num_pages: submissions_json["pages_count"]&.to_i,
      rid: submissions_json["rid"],
    }
  end

  # Builds the URL for the API search endpoint.
  #
  # If rid is provided, then the page is for a specific RID set, constructed from a previous API search.
  # If ib_user_id is provided, then the page is for the user's gallery.
  # If pool_id is provided, then the page is for a specific pool.
  # If neither is provided, then the page is for the latest posts.
  sig do
    params(
      ib_user_id: T.nilable(Integer),
      username: T.nilable(String),
      rid: T.nilable(String),
      pool_id: T.nilable(Integer),
      page: Integer,
    ).returns(String)
  end
  def self.build_api_search_url(
    ib_user_id: nil,
    username: nil,
    rid: nil,
    pool_id: nil,
    page: 1
  )
    base_url = URI.parse("https://inkbunny.net/api_search.php")
    query_params = {
      "submissions_per_page" => SUBMISSIONS_PER_PAGE,
      "page" => page,
    }

    if rid.present?
      query_params["rid"] = rid
    elsif pool_id.present?
      query_params["get_rid"] = "yes"
      query_params["orderby"] = "create_datetime"
      query_params["pool_id"] = pool_id
      query_params["keywords"] = "no"
    elsif ib_user_id.present?
      query_params["get_rid"] = "yes"
      query_params["orderby"] = "create_datetime"
      query_params["user_id"] = ib_user_id
      query_params["keywords"] = "no"
    else
      query_params["get_rid"] = "yes"
      query_params["orderby"] = "create_datetime"
      query_params["keywords"] = "no"
    end

    base_url.query = URI.encode_www_form(query_params)
    base_url.to_s
  end

  # Update a post from a submission JSON. Submission JSONs are returned from
  # the API search endpoint, which contains a shallow version of post data.
  #
  # Returns true if the post was newly created.
  #
  # Expected information from the endpoint (see fixture api_search.json):
  # - submission_id
  # - title
  # - create_datetime
  # - last_file_update_datetime
  # - pagecount
  # - rating_id
  # - submission_type_id
  sig do
    params(
      submission_json: T::Hash[String, T.untyped],
      caused_by_entry: T.nilable(HttpLogEntry),
    ).returns(T::Boolean)
  end
  def upsert_post_from_submission_json!(submission_json, caused_by_entry: nil)
    ib_post_id = submission_json["submission_id"]&.to_i
    raise "ib_post_id is blank" if ib_post_id.blank?
    return false if post = @shallow_posts_by_ib_post_id[ib_post_id]

    post =
      Domain::Post::InkbunnyPost.includes(:creator).find_or_initialize_by(
        ib_id: ib_post_id,
      )
    @shallow_posts_by_ib_post_id[ib_post_id] = post
    is_new_post = post.new_record?

    creator =
      upsert_user_from_submission_json!(
        submission_json,
        caused_by_entry: caused_by_entry,
      )

    if post.creator && (post.creator&.ib_id != creator.ib_id)
      raise "post.creator.ib_id (#{post.creator&.ib_id}) != creator.ib_id (#{creator.ib_id})"
    end

    post.creator = creator
    post.shallow_updated_at = Time.now
    post.title = submission_json["title"]
    post.posted_at =
      Time.parse(submission_json["create_datetime"]).in_time_zone("UTC")

    # rarely, a post will have a null last_file_update_datetime.
    # This is a bug with Inkbunny, so handle it gracefully.
    last_updated_str = submission_json["last_file_update_datetime"]
    if last_updated_str.present?
      post.last_file_updated_at =
        Time.parse(last_updated_str).in_time_zone("UTC")
    end

    post.num_files = submission_json["pagecount"]&.to_i
    if rating_id = submission_json["rating_id"]&.to_i
      post.rating = Domain::Post::InkbunnyPost::RATING_TYPE_MAP[rating_id]
    end
    if submission_type_id = submission_json["submission_type_id"]&.to_i
      post.submission_type =
        Domain::Post::InkbunnyPost::SUBMISSION_TYPE_MAP[submission_type_id]
    end
    post.ib_detail_raw["submission_json"] = submission_json

    post_changed =
      post.changed? || post.files.count != post.num_files ||
        creator.avatar.blank?

    if post_changed
      post.shallow_update_log_entry = caused_by_entry
      post.save!
      @changed_posts << post
    end

    is_new_post
  end

  private

  # Update a user from a submission JSON. Submission JSONs are returned from
  # the API search endpoint, which contains a shallow version of user data.
  #
  # Expected information from the endpoint (see fixture api_search.json):
  # - username
  # - user_id
  sig do
    params(
      submission_json: T::Hash[String, T.untyped],
      caused_by_entry: T.nilable(HttpLogEntry),
    ).returns(Domain::User::InkbunnyUser)
  end
  def upsert_user_from_submission_json!(submission_json, caused_by_entry: nil)
    ib_user_id = submission_json["user_id"]&.to_i
    if user = @users_by_ib_user_id[ib_user_id]
      return user
    end
    raise "ib_user_id is blank" if ib_user_id.blank?
    user = Domain::User::InkbunnyUser.find_or_initialize_by(ib_id: ib_user_id)
    @users_by_ib_user_id[ib_user_id] = user
    user.name = submission_json["username"]
    if user.changed?
      logger.info(
        "[ApiSearchPageProcessor][user changed][ib_id: #{user.ib_id_change&.join(" -> ")}][name: #{user.name_change&.join(" -> ")}]",
      )
      user.shallow_update_log_entry = caused_by_entry
      user.save!
    end
    user
  end
end
