# typed: false
class Domain::Twitter::Job::UserTimelineTweetsJob < Domain::Twitter::Job::TwitterJobBase
  GDLClient = Scraper::GalleryDlClient
  queue_as :twitter_timeline_tweets

  def perform(args)
    @name = args[:name]
    @user = find_or_create_user_from_args(args)
    logger.prefix = proc { "[user: #{(@user.name || args[:name])&.bold}]" }

    @proxy_name = Rails.application.config.x.proxy_name.to_s
    @caused_by_entry = args[:caused_by_entry]
    @force_scan = !!args[:force_scan]
    @first_twitter_caused_by = nil
    @updated_user_from_tweet = false
    @num_scanned_tweets = 0
    @num_created_tweets = 0
    @num_created_medias = 0

    logger.info("start tweet timeline scan (force: #{@force_scan.to_s.bold})")

    if @user.state == "error" &&
         @user.tweets_protected_error_proxies.include?(@proxy_name)
      fatal_error(
        "abort scan, this proxy (#{@proxy_name}) is in error proxies: #{@user.tweets_protected_error_proxies}",
      )
    end

    if !@force_scan && !@user.due_for_timeline_tweets_scan?
      logger.warn(
        "scanned #{time_ago_in_words(@user.scanned_timeline_at)}, skipping",
      )
      return
    end

    gallery_dl_client.start_twitter_user(
      @name || @user.name,
      caused_by_entry: @caused_by_entry,
    )

    while true
      event =
        gallery_dl_client.next_message(
          caused_by_entry: @first_twitter_caused_by || @caused_by_entry,
        )
      fatal_error("nil event from gallery_dl_client") if event.nil?

      case event
      when GDLClient::StartEvent
        logger.info("start tweets (#{event.extractor.to_s.bold})")
      when GDLClient::FinishEvent
        logger.info("finish - #{@num_created_tweets.to_s.bold} saw total")
        break
      when GDLClient::HttpRequestEvent
        @first_twitter_caused_by ||= event.log_entry
        maybe_extract_user_info(event)
      when GDLClient::TweetEvent
        update_user_from_tweet(event)
        break if handle_tweet_event(event) == :break
      when GDLClient::TweetMediaEvent
        handle_media_event(event)
      else
        fatal_error("unhandled event: `#{event.inspect}`")
      end
    end

    logger.info(
      [
        "created #{@num_created_tweets.to_s.bold} tweets",
        "scanned #{@num_scanned_tweets.to_s.bold} tweets",
        "created #{@num_created_medias.to_s.bold} medias",
      ].join(", "),
    )

    @user.scanned_timeline_at = Time.now
    @user.state = "ok"
    @user.save!
  rescue => e
    if e.message =~ /Tweets are protected/
      @user.state = "error"
      @user.state_detail[:error] = e.message
      @user.tweets_protected_error_proxies << @proxy_name
      @user.tweets_protected_error_proxies.uniq!
      @user.tweets_protected_error_proxies.sort!
      @user.save!
      raise "tweets are protected, tried on proxies: #{@user.tweets_protected_error_proxies}"
    elsif e.message =~ /Requested user could not be found/
      logger.error("user could not be found: #{@user.name}")
      @user.state = "error"
      @user.state_detail[:error] = e.message
      @user.save!
      # for users which can't be found, no point in attempting to re-scan
      return
    end
    raise e
  end

  private

  def maybe_extract_user_info(http_event)
    return unless http_event.response_code == 200
    unless http_event.response_headers[:"content-type"].starts_with?(
             "application/json",
           )
      return
    end
    json = JSON.parse(http_event.body)
    return unless json
    typename = json.dig("data", "user", "result", "__typename")
    return unless typename == "User"
    tw_id = json.dig("data", "user", "result", "rest_id")
    return unless tw_id
    tw_id = tw_id.to_i

    # tw_id is already up to date, do nothing
    return if @user.tw_id == tw_id

    existing = Domain::Twitter::User.find_by(tw_id: tw_id)
    if existing
      logger.info "found existing user with tw_id #{tw_id.to_s.bold}"
      @user = existing
      @updated_user_from_tweet = false
    else
      if @user.tw_id
        raise "tw_id mismatch" unless @user.tw_id == tw_id
      else
        logger.info "extracted tw_id #{tw_id.to_s.bold}"
        @user.tw_id = tw_id
        @user.save!
      end
    end
  end

  def handle_tweet_event(tweet_event)
    tweet_hash = tweet_event.tweet
    tweet = Domain::Twitter::Tweet.find_by(id: tweet_hash[:id])
    @num_scanned_tweets += 1

    if tweet
      logger.info("found existing tweet: #{tweet.id}")
      return nil if @force_scan
      # if we've done a full timeline scan, and now encountered a tweet we've
      # already seen, stop scanning
      return :break if @user.scanned_timeline_at
      return nil
    end

    logger.info(
      "+ tweet (#{@num_created_tweets.to_s.bold}) #{tweet_hash[:id].to_s.bold}",
    )

    Domain::Twitter::Tweet.new(
      {
        id: tweet_hash[:id],
        author: @user,
        content: tweet_hash[:content],
        reply_to_tweet_id: tweet_hash[:reply_to],
        tweeted_at: Time.at(tweet_hash[:date]),
      },
    ).save!

    @num_created_tweets += 1
  end

  def handle_media_event(media_event)
    media = Domain::Twitter::Media.find_by(id: media_event.filename)
    if media
      logger.info("skip existing media #{media.id}")
      enqueue_media_file(media) unless media.file
      return
    end

    logger.info(
      "+ media (#{@num_created_medias.to_s.bold}) #{media_event.filename.bold}",
    )

    media =
      Domain::Twitter::Media.new(
        {
          id: media_event.filename,
          tweet_id: media_event.tweet_id,
          url_str: media_event.file_url,
        },
      )
    media.save!
    @num_created_medias += 1
    enqueue_media_file(media)
  end

  def enqueue_media_file(media)
    defer_job(
      Domain::Twitter::Job::MediaJob,
      { media: media || raise, caused_by_entry: @first_twitter_caused_by },
      { priority: self.priority },
    )
  end

  def update_user_from_tweet(tweet_event)
    return if @updated_user_from_tweet

    author = tweet_event.author
    raise("no tw_id on user") unless @user.tw_id
    unless @user.tw_id == author[:id]
      raise("tw_id mismatch: #{@user.tw_id} != #{author[:id]}")
    end

    @user.name = author[:name]
    @user.nick = author[:nick]
    @user.description = author[:description]
    @user.num_fav_count = author[:favourites_count]
    @user.num_followers = author[:followers_count]
    @user.num_following = author[:friends_count]
    @user.registered_at = Time.at(author[:date])
    @user.save!
    @updated_user_from_tweet = true
  end
end
