# typed: true
class Domain::UsersController < DomainController
  extend T::Sig
  extend T::Helpers

  before_action :set_user!, only: %i[show followed_by following]
  before_action :set_post!, only: %i[users_faving_post]
  skip_before_action :authenticate_user!,
                     only: %i[
                       show
                       search_by_name
                       users_faving_post
                       similar_users
                     ]

  # GET /users
  sig(:final) { void }
  def index
    authorize Domain::User
    @users = policy_scope(Domain::User).order(created_at: :desc)
  end

  sig(:final) { void }
  def followed_by
    @user = T.must(@user)
    authorize @user
    @users =
      @user
        .followed_by_users
        .includes(avatar: :log_entry)
        .page(params[:page])
        .per(50)
    @index_type = :followed_by
    render :index
  end

  sig(:final) { void }
  def following
    @user = T.must(@user)
    authorize @user
    @users =
      @user
        .followed_users
        .includes(avatar: :log_entry)
        .page(params[:page])
        .per(50)
    @index_type = :following
    render :index
  end

  sig(:final) { void }
  def users_faving_post
    @post = T.must(@post)
    authorize @post
    @users =
      T
        .unsafe(@post)
        .faving_users
        .includes(avatar: :log_entry)
        .page(params[:page])
        .per(50)

    @index_type = :users_faving_post
    render :index
  end

  # GET /users/:id
  sig(:final) { void }
  def show
    authorize @user
  end

  sig(:final) { void }
  def search_by_name
    authorize Domain::User
    name = params[:name]&.downcase
    name = ReduxApplicationRecord.sanitize_sql_like(name)
    @user_search_names =
      Domain::UserSearchName
        .select("domain_user_search_names.*, domain_users.*")
        .select("levenshtein(name, '#{name}') as distance")
        .select(
          "(SELECT COUNT(*) FROM domain_user_post_creations dupc WHERE dupc.user_id = domain_users.id) as num_posts",
        )
        .joins(:user)
        .where(
          "(name ilike ?) OR (similarity(dmetaphone(name), dmetaphone(?)) > 0.8)",
          "%#{name}%",
          name,
        )
        .where(
          "NOT EXISTS (
            SELECT 1
            FROM domain_user_search_names dns2
            WHERE dns2.user_id = domain_user_search_names.user_id
            AND levenshtein(dns2.name, ?) < levenshtein(domain_user_search_names.name, ?)
          )",
          name,
          name,
        )
        .order("distance ASC")
        .limit(10)
  end

  sig { void }
  def similar_users
    url_name = params[:url_name]
    exclude_url_name = params[:exclude_url_name]

    user = Domain::User::FaUser.find_by(url_name: url_name)
    if user.nil?
      render status: 404,
             json: {
               error: "user '#{url_name}' not found",
               error_type: "user_not_found",
             }
      return
    end

    all_similar_users =
      users_similar_to_by_followers(user, limit: 10).map do |u|
        user_to_similarity_entry(u)
      end

    if all_similar_users.nil?
      render status: 500,
             json: {
               error:
                 "user '#{url_name}' has not had recommendations computed yet",
               error_type: "recs_not_computed",
             }
      return
    end

    not_followed_similar_users = nil
    if exclude_url_name
      exclude_followed_by =
        Domain::User::FaUser.find_by(url_name: exclude_url_name)
      if exclude_followed_by.nil?
        render status: 500,
               json: {
                 error: "user '#{exclude_url_name}' not found",
                 error_type: "exclude_user_not_found",
               }
        return
      elsif exclude_followed_by.scanned_follows_at.nil?
        render status: 500,
               json: {
                 error:
                   "user '#{exclude_url_name}' followers list hasn't been scanned",
                 error_type: "exclude_user_not_scanned",
               }
        return
      else
        not_followed_similar_users =
          users_similar_to_by_followers(
            user,
            limit: 10,
            exclude_followed_by: exclude_followed_by,
          ).map { |u| user_to_similarity_entry(u) }
      end
    end

    render json: {
             all: all_similar_users,
             not_followed: not_followed_similar_users,
           }
  end

  private

  sig { override.returns(DomainController::DomainParamConfig) }
  def self.param_config
    DomainController::DomainParamConfig.new(
      user_id_param: :id,
      post_id_param: :domain_post_id,
      post_group_id_param: :domain_post_group_id,
    )
  end

  # TODO - make a typed ImmutableStruct for the return type
  sig { params(user: Domain::User::FaUser).returns(T::Hash[Symbol, T.untyped]) }
  def user_to_similarity_entry(user)
    profile_thumb_url = user.avatar&.log_entry&.uri_str
    profile_thumb_url ||=
      begin
        pp_log_entry = get_best_user_page_http_log_entry_for(user)
        if pp_log_entry
          parser =
            Domain::Fa::Parser::Page.from_log_entry(
              pp_log_entry,
              require_logged_in: false,
            )
          parser.user_page.profile_thumb_url
        end
      rescue StandardError
        logger.error("error getting profile_thumb_url: #{$!.message}")
      end || "https://a.furaffinity.net/0/#{user.url_name}.gif"

    {
      name: user.name,
      url_name: user.url_name,
      profile_thumb_url: profile_thumb_url,
      external_url: "https://www.furaffinity.net/user/#{user.url_name}/",
      refurrer_url: request.base_url + helpers.domain_user_path(user),
    }
  end

  sig { params(user: Domain::User::FaUser).returns(T.nilable(HttpLogEntry)) }
  def get_best_user_page_http_log_entry_for(user)
    for_path =
      proc do |uri_path|
        HttpLogEntry
          .where(
            uri_scheme: "https",
            uri_host: "www.furaffinity.net",
            uri_path: uri_path,
          )
          .order(created_at: :desc)
          .first
      end

    # older versions don't end in a trailing slash
    user.last_user_page_log_entry || for_path.call("/user/#{user.url_name}/") ||
      for_path.call("/user/#{user.url_name}")
  end

  sig do
    params(
      user: Domain::User::FaUser,
      limit: Integer,
      exclude_followed_by: T.nilable(Domain::User::FaUser),
    ).returns(T::Array[Domain::User::FaUser])
  end
  def users_similar_to_by_followers(user, limit: 10, exclude_followed_by: nil)
    factors = Domain::Factors::UserUserFollowToFactors.find_by(user: user)
    return [] if factors.nil?

    relation =
      Domain::NeighborFinder
        .find_neighbors(factors)
        .limit(limit)
        .includes(:user)

    if exclude_followed_by
      relation =
        relation.where.not(
          user_id: exclude_followed_by.followed_users.select(:to_id),
        )
    end

    relation.map(&:user)
  end
end
