defmodule Pleroma.LoadTesting.Users do
  @moduledoc """
  Module for generating users with friends.
  """
  import Ecto.Query
  import Pleroma.LoadTesting.Helper, only: [to_sec: 1]

  alias Pleroma.Repo
  alias Pleroma.User
  alias Pleroma.User.Query

  @defaults [
    users: 20_000,
    friends: 100
  ]

  @max_concurrency 10

  @spec generate(keyword()) :: User.t()
  def generate(opts \\ []) do
    opts = Keyword.merge(@defaults, opts)

    generate_users(opts[:users])

    main_user =
      Repo.one(from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1))

    make_friends(main_user, opts[:friends])

    User.get_by_id(main_user.id)
  end

  def generate_users(max) do
    IO.puts("Starting generating #{max} users...")

    {time, users} =
      :timer.tc(fn ->
        Task.async_stream(
          1..max,
          &generate_user(&1),
          max_concurrency: @max_concurrency,
          timeout: 30_000
        )
        |> Enum.to_list()
      end)

    IO.puts("Generating users took #{to_sec(time)} sec.\n")
    users
  end

  defp generate_user(i) do
    remote = Enum.random([true, false])

    %User{
      name: "Test ใƒ†ใ‚นใƒˆ User #{i}",
      email: "user#{i}@example.com",
      nickname: "nick#{i}",
      password_hash: Pbkdf2.hash_pwd_salt("test"),
      bio: "Tester Number #{i}",
      local: !remote
    }
    |> user_urls()
    |> Repo.insert!()
  end

  defp user_urls(%{local: true} = user) do
    urls = %{
      ap_id: User.ap_id(user),
      follower_address: User.ap_followers(user),
      following_address: User.ap_following(user)
    }

    Map.merge(user, urls)
  end

  defp user_urls(%{local: false} = user) do
    base_domain = Enum.random(["domain1.com", "domain2.com", "domain3.com"])

    ap_id = "https://#{base_domain}/users/#{user.nickname}"

    urls = %{
      ap_id: ap_id,
      follower_address: ap_id <> "/followers",
      following_address: ap_id <> "/following"
    }

    Map.merge(user, urls)
  end

  def make_friends(main_user, max) when is_integer(max) do
    IO.puts("Starting making friends for #{max} users...")

    {time, _} =
      :timer.tc(fn ->
        number_of_users =
          (max / 2)
          |> Kernel.trunc()

        main_user
        |> get_users(%{limit: number_of_users, local: :local})
        |> run_stream(main_user)

        main_user
        |> get_users(%{limit: number_of_users, local: :external})
        |> run_stream(main_user)
      end)

    IO.puts("Making friends took #{to_sec(time)} sec.\n")
  end

  def make_friends(%User{} = main_user, %User{} = user) do
    {:ok, _} = User.follow(main_user, user)
    {:ok, _} = User.follow(user, main_user)
  end

  @spec get_users(User.t(), keyword()) :: [User.t()]
  def get_users(user, opts) do
    criteria = %{limit: opts[:limit]}

    criteria =
      if opts[:local] do
        Map.put(criteria, opts[:local], true)
      else
        criteria
      end

    criteria =
      if opts[:friends?] do
        Map.put(criteria, :friends, user)
      else
        criteria
      end

    query =
      criteria
      |> Query.build()
      |> random_without_user(user)

    query =
      if opts[:friends?] == false do
        friends_ids =
          %{friends: user}
          |> Query.build()
          |> Repo.all()
          |> Enum.map(& &1.id)

        from(u in query, where: u.id not in ^friends_ids)
      else
        query
      end

    Repo.all(query)
  end

  defp random_without_user(query, user) do
    from(u in query,
      where: u.id != ^user.id,
      order_by: fragment("RANDOM()")
    )
  end

  defp run_stream(users, main_user) do
    Task.async_stream(users, &make_friends(main_user, &1),
      max_concurrency: @max_concurrency,
      timeout: 30_000
    )
    |> Stream.run()
  end

  @spec prepare_users(User.t(), keyword()) :: map()
  def prepare_users(user, opts) do
    friends_limit = opts[:friends_used]
    non_friends_limit = opts[:non_friends_used]

    %{
      user: user,
      friends_local: fetch_users(user, friends_limit, :local, true),
      friends_remote: fetch_users(user, friends_limit, :external, true),
      non_friends_local: fetch_users(user, non_friends_limit, :local, false),
      non_friends_remote: fetch_users(user, non_friends_limit, :external, false)
    }
  end

  defp fetch_users(user, limit, local, friends?) do
    user
    |> get_users(limit: limit, local: local, friends?: friends?)
    |> Enum.shuffle()
  end
end