# typed: strict
class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
  extend T::Sig

  VERSION_0 = Domain::Fa::Parser::Page::VERSION_0
  VERSION_1 = Domain::Fa::Parser::Page::VERSION_1
  VERSION_2 = Domain::Fa::Parser::Page::VERSION_2

  sig do
    params(
      elem: Nokogiri::HTML::Document,
      page_version: Symbol,
      page: Domain::Fa::Parser::Page,
    ).void
  end
  def initialize(elem, page_version, page)
    @name = T.let(nil, T.nilable(String))
    @account_status = T.let(nil, T.nilable(Symbol))
    @full_name = T.let(nil, T.nilable(String))
    @artist_type = T.let(nil, T.nilable(String))
    @profile_thumb_url = T.let(nil, T.nilable(String))
    @registered_since = T.let(nil, T.nilable(Time))
    @mood = T.let(nil, T.nilable(String))
    @profile_html = T.let(nil, T.nilable(String))
    @num_pageviews = T.let(nil, T.nilable(Integer))
    @num_submissions = T.let(nil, T.nilable(Integer))
    @num_comments_recieved = T.let(nil, T.nilable(Integer))
    @num_comments_given = T.let(nil, T.nilable(Integer))
    @num_journals = T.let(nil, T.nilable(Integer))
    @num_favorites = T.let(nil, T.nilable(Integer))
    @recent_fav_fa_ids = T.let(nil, T.nilable(T::Array[Integer]))
    @recent_gallery_fa_ids = T.let(nil, T.nilable(T::Array[Integer]))
    @recent_watchers = T.let(nil, T.nilable(T::Array[RecentUser]))
    @recent_watching = T.let(nil, T.nilable(T::Array[RecentUser]))
    @statistics = T.let(nil, T.nilable(Nokogiri::XML::Element))
    @main_about = T.let(nil, T.nilable(Nokogiri::XML::Element))
    @num_watched_by = T.let(nil, T.nilable(Integer))
    @num_watching = T.let(nil, T.nilable(Integer))
    @submissions_json_data = T.let(nil, T.nilable(T::Array[SubmissionData]))
    @march_2025_update =
      T.let(elem.css(".js-displayName").first.present?, T::Boolean)

    @elem = elem
    @page_version = page_version
    @page = page
  end

  # https://webcache.googleusercontent.com/search?q=cache:L85loB7VJXwJ:https://forums.furaffinity.net/threads/question-about-the-username-prefix.1655691/&cd=10&hl=en&ct=clnk&gl=us
  PREFIXES = [
    "~", # regular user
    "!", # suspended
    "-", # banned (permanent)
    "@", # admin
    "∞", # deceased
  ]

  sig { returns(T.nilable(String)) }
  def name
    @name ||=
      begin
        if @march_2025_update
          return @elem.css(".js-displayName")&.first&.text&.strip
        end

        elem =
          case @page_version
          when VERSION_0, VERSION_1
            elem = @elem.css(".addpad.lead b")
          when VERSION_2
            @elem.css("userpage-nav-user-details username")
          else
            unimplemented_version!
          end

        name = elem&.first&.text&.strip

        if name
          if @elem.css("userpage-nav-header img.userIcon.type-admin").first
            # in newer versions of FA user pages, admins have no '@' prefix,
            # but rather an indicator image icon
            if PREFIXES.include?(name[0])
              raise("invalid prefix for admin user name: #{name}")
            end
            name
          elsif PREFIXES.include?(name[0])
            name[1..]
          else
            if @elem.css("img.userIcon.faplus-icon")
              # FA+ users have no prefix
              name
            else
              raise("invalid prefix for name: #{name}")
            end
          end
        end
      end
  end

  sig { returns(T.nilable(Symbol)) }
  def account_status
    @account_status ||=
      begin
        if @elem.css("userpage-nav-header img.userIcon.type-admin").first
          :admin
        else
          if @march_2025_update
            elem = @elem.css(".c-usernameBlock__symbol")
            symbol = elem&.first&.text&.strip || ""
          else
            elem =
              case @page_version
              when VERSION_2
                @elem.css("userpage-nav-user-details username")
              else
                unimplemented_version!
              end
            name = elem&.first&.text&.strip || ""
            symbol = name[0]
          end

          case symbol
          when "~"
            :active
          when "!"
            :suspended
          when "@"
            :admin
          when "-"
            :banned
          when "∞"
            :deceased
          else
            nil
          end
        end
      end
  end

  sig { returns(T.nilable(String)) }
  def full_name
    @full_name ||=
      Domain::Fa::Parser::Page
        .elem_after_text_match(main_about.children, /Full/)
        &.text
        &.strip
  end

  sig { returns(T.nilable(String)) }
  def artist_type
    @artist_type ||=
      Domain::Fa::Parser::Page
        .elem_after_text_match(main_about.children, /Type/)
        &.text
        &.strip
  end

  sig { returns(T.nilable(String)) }
  def profile_thumb_url
    @profile_thumb_url ||=
      case @page_version
      when VERSION_0
        @elem.css(".addpad.alt1 a img.avatar").first.try(:[], "src")
      when VERSION_2
        @elem.css("userpage-nav-avatar a.current img").first.try(:[], "src")
      else
        unimplemented_version!
      end
  end

  sig { returns(T.nilable(Time)) }
  def registered_since
    @registered_since ||=
      begin
        time_str =
          case @page_version
          when VERSION_0, VERSION_1
            Domain::Fa::Parser::Page
              .elem_after_text_match(main_about.children, /Registered/)
              &.text
              &.strip
          when VERSION_2
            if @march_2025_update
              @elem
                .css("userpage-nav-user-details .user-title .hideonmobile")
                .find { |elem| elem&.text&.strip == "Registered:" }
                &.next_sibling
                &.text
                &.strip
            else
              @elem
                .css("username span")
                .find { |elem| elem&.text&.strip == "Registered:" }
                &.next_sibling
                &.text
                &.strip
            end
          else
            unimplemented_version!
          end

        @page.logged_in_user_tz.parse(time_str) if time_str
      end
  end

  sig { returns(T.nilable(String)) }
  def mood
    @mood ||=
      Domain::Fa::Parser::Page
        .elem_after_text_match(main_about.children, /mood/)
        &.text
        &.strip
  end

  sig { returns(String) }
  def profile_html
    @profile_html ||= main_about.inner_html.force_encoding("utf-8")
  end

  sig { returns(Integer) }
  def num_pageviews
    @num_pageviews ||= stat_value(:pvs, 0)
  end

  sig { returns(Integer) }
  def num_submissions
    @num_submissions ||= stat_value(:subs, 1)
  end

  sig { returns(Integer) }
  def num_comments_recieved
    @num_comments_recieved ||= stat_value(:crec, 3)
  end

  sig { returns(Integer) }
  def num_comments_given
    @num_comments_given ||= stat_value(:cgiv, 4)
  end

  sig { returns(Integer) }
  def num_journals
    @num_journals ||= stat_value(:njr, 5)
  end

  sig { returns(Integer) }
  def num_favorites
    @num_favorites ||= stat_value(:nfav, 2)
  end

  sig { returns(T::Array[Integer]) }
  def recent_fav_fa_ids
    @recent_fav_fa_ids ||=
      case @page_version
      when VERSION_0
        @elem
          .css(".userpage-first-favorite s a, .userpage-favorites s a")
          .filter_map do |elem|
            href = elem["href"]
            %r{/view/(\d+)}.match(href)&.[](1)&.to_i || next
          end
      when VERSION_2
        @elem
          .css("#gallery-latest-favorites")
          .first
          &.css("figure a")
          &.map do |elem|
            href = elem["href"]
            %r{/view/(\d+)}.match(href)&.[](1)&.to_i ||
              raise("invalid url: #{href}")
          end || []
      else
        unimplemented_version!
      end
  end

  class SubmissionData < T::ImmutableStruct
    include T::Struct::ActsAsComparable

    const :fa_id, Integer
    const :title, String
    const :user_full_name, String
    const :url_name, String
    const :date_full, String
    const :icon_rating, String
    const :faved_at, Time
  end

  sig { returns(T::Array[SubmissionData]) }
  def submissions_json_data
    @submissions_json_data ||=
      case @page_version
      when VERSION_0, VERSION_2
        json =
          T.cast(
            if (e = @elem.css("#js-submissionData").first)
              e.inner_html&.to_s
            else
              @elem.inner_html.match(/submission_data = (.*);$/)&.[](1)&.to_s
            end,
            T.nilable(String),
          )
        raise("no submission data json found") unless json
        JSON
          .parse(json)
          .map do |fa_id, data|
            date_full =
              data["date_full"] ||
                begin
                  html_date = data["html_date"]
                  parsed = html_date && Nokogiri::XML.parse(html_date)
                  parsed&.css("span")&.attr("title")&.to_s ||
                    raise("invalid html date: #{data}")
                end
            faved_at = @page.logged_in_user_tz.parse(date_full)
            SubmissionData.new(
              fa_id: fa_id.to_i,
              title: data["title"],
              user_full_name: data["username"],
              url_name: data["lower"],
              date_full:,
              icon_rating: data["icon_rating"],
              faved_at:,
            )
          end
      else
        unimplemented_version!
      end
  end

  sig { returns(T::Array[Integer]) }
  def recent_gallery_fa_ids
    @recent_gallery_fa_ids ||=
      case @page_version
      when VERSION_2
        @elem
          .css("#gallery-latest-submissions")
          .first
          &.css("figure a")
          &.map do |elem|
            href = elem["href"]
            %r{/view/(\d+)}.match(href)&.[](1)&.to_i ||
              raise("invalid url: #{href}")
          end || []
      else
        unimplemented_version!
      end
  end

  class JSONSubmissionData < T::ImmutableStruct
    include T::Struct::ActsAsComparable

    const :fa_id, Integer
    const :title, String
    const :creator, Domain::User::FaUser
  end

  class RecentUser < T::Struct
    include T::Struct::ActsAsComparable
    extend T::Sig

    const :name, String
    const :url_name, String

    sig { returns(T::Array[String]) }
    def to_a
      [name, url_name]
    end
  end

  sig { returns(T::Array[RecentUser]) }
  def recent_watchers
    @recent_watchers ||= recent_users_for_section("Recent Watchers")
  end

  sig { returns(T::Array[RecentUser]) }
  def recent_watching
    @recent_watching ||= recent_users_for_section("Recently Watched")
  end

  sig { returns(Integer) }
  def num_watched_by
    @num_watched_by ||=
      begin
        watchers_text =
          recent_users_section("Recent Watchers")
            .parent
            .css(".floatright")
            .first
            .text
        watchers_text.match(/Watched by (\d+)/)[1].to_i
      end
  end

  sig { returns(Integer) }
  def num_watching
    @num_watching ||=
      begin
        watchers_text =
          recent_users_section("Recently Watched")
            .parent
            .css(".floatright")
            .first
            .text
        watchers_text.match(/Watching (\d+)/)[1].to_i
      end
  end

  private

  sig { params(section_name: String).returns(T::Array[RecentUser]) }
  def recent_users_for_section(section_name)
    case @page_version
    when VERSION_2
      section_elem =
        @elem
          .css(".userpage-section-left")
          .find do |elem|
            elem.css(".section-header h2")&.first&.text&.strip == section_name
          end

      section_elem = section_elem.css(".section-body").first

      section_elem
        .css("a")
        .map do |link_elem|
          href = link_elem["href"]
          url_name =
            %r{/user/(.+)/}.match(href)&.[](1) || raise("invalid url: #{href}")

          if @march_2025_update
            name =
              link_elem
                .css(".c-usernameBlockSimple__displayName")
                .first
                .text
                .strip
          else
            name = link_elem.css(".artist_name").first.text.strip
          end
          RecentUser.new(name:, url_name:)
        end
    else
      unimplemented_version!
    end
  end

  sig { params(section_name: String).returns(Nokogiri::XML::Element) }
  def recent_users_section(section_name)
    case @page_version
    when VERSION_2
      @elem
        .css(".userpage-section-left")
        .find do |elem|
          elem.css(".section-header h2")&.first&.text&.strip == section_name
        end
    else
      unimplemented_version!
    end
  end

  sig { params(legacy_name: Symbol, redux_idx: Integer).returns(Integer) }
  def stat_value(legacy_name, redux_idx)
    legacy_map = # if false # old mode?
      # { pvs: 2, subs: 5, crec: 8, cgiv: 11, njr: 14, nfav: 17 }
      # else
      { pvs: 2, subs: 6, crec: 10, cgiv: 14, njr: 18, nfav: 22 }
    # end

    value =
      case @page_version
      when VERSION_0, VERSION_1
        statistics.children[legacy_map[legacy_name] || raise].text.strip.to_i
      when VERSION_2
        statistics.css(".highlight")[redux_idx]&.next_sibling&.text&.strip&.to_i
      else
        unimplemented_version!
      end

    # FA databases can be a little weird
    if value >= (2**32 - 1)
      0
    else
      value
    end
  end

  sig { returns(Nokogiri::XML::Element) }
  def statistics
    @statistics ||=
      case @page_version
      when VERSION_0, VERSION_1
        @elem.css(".ldot table tr:last-child td").first
      when VERSION_2
        @elem
          .css(".userpage-layout-right-col-content .userpage-section-right")
          .find do |child|
            child.css(".section-header h2")&.first&.text&.strip == "Stats"
          end
      else
        unimplemented_version!
      end
  end

  sig { returns(Nokogiri::XML::Element) }
  def main_about
    @main_about ||=
      case @page_version
      when VERSION_0, VERSION_1
        @elem.css(".alt1.addpad .ldot").first
      when VERSION_2
        @elem.css(".section-body.userpage-profile").first
      else
        unimplemented_version!
      end
  end
end
