defmodule Mix.Tasks.Mobilizon.Users.New do
  @moduledoc """
  Task to create a new user
  """
  use Mix.Task
  import Mix.Tasks.Mobilizon.Common
  import Mix.Tasks.Mobilizon.Actors.Utils
  alias Mobilizon.Actors.Actor
  alias Mobilizon.Service.Auth.Authenticator
  alias Mobilizon.Users
  alias Mobilizon.Users.User

  @shortdoc "Manages Mobilizon users"

  @impl Mix.Task
  def run([email | rest]) do
    {options, [], []} =
      OptionParser.parse(
        rest,
        strict: [
          password: :string,
          moderator: :boolean,
          admin: :boolean,
          profile_username: :string,
          profile_display_name: :string,
          provider: :string
        ],
        aliases: [
          p: :password
        ]
      )

    start_mobilizon()

    password =
      Keyword.get(
        options,
        :password,
        :crypto.strong_rand_bytes(16) |> Base.encode64() |> binary_part(0, 16)
      )

    provider = Keyword.get(options, :provider)

    case create_user(email, provider, password, options) do
      {:ok, %User{} = user} ->
        profile = maybe_create_profile(user, options)

        shell_info("""
        An user has been created with the following information:
          - email: #{user.email}
          - password: #{if is_nil(provider), do: password, else: "Your #{provider} password"}
          - Role: #{user.role}
        """)

        if is_nil(profile) do
          shell_info("""
          The user will be prompted to create a new profile after login for the first time.
          """)
        else
          shell_info("""
          A profile was added with the following information:
          - username: #{profile.preferred_username}
          - display name: #{profile.name}
          """)
        end

      {:error, %Ecto.Changeset{errors: errors}} ->
        shell_error(inspect(errors))
        shell_error("User has not been created because of the above reason.")
    end
  end

  def run(_) do
    shell_error("mobilizon.users.new requires an email as argument")
  end

  @spec create_user(String.t(), String.t() | nil, String.t(), Keyword.t()) ::
          {:ok, User.t()} | {:error, Ecto.Changeset.t()}
  defp create_user(email, provider, password, options) do
    role = get_role(options)

    if is_nil(provider) do
      create_database_user(email, password, role)
    else
      check_password_and_provider_options(options)

      if provider != nil && provider != Authenticator.provider_name() do
        shell_info("""
        Warning: The #{provider} provider isn't currently configured as default authenticator.
        """)
      end

      create_user_from_provider(email, provider, role)
    end
  end

  @spec create_database_user(String.t(), String.t(), role()) ::
          {:ok, User.t()} | {:error, Ecto.Changeset.t()}
  defp create_database_user(email, password, role) do
    Users.register(%{
      email: email,
      password: password,
      role: role,
      confirmed_at: DateTime.utc_now(),
      confirmation_sent_at: nil,
      confirmation_token: nil
    })
  end

  @spec create_user_from_provider(String.t(), String.t(), role()) ::
          {:ok, User.t()} | {:error, Ecto.Changeset.t()}
  defp create_user_from_provider(email, provider, role) do
    Users.create_external(email, provider, %{role: role})
  end

  @spec maybe_create_profile(User.t(), Keyword.t()) :: Actor.t() | nil
  defp maybe_create_profile(%User{} = user, options) do
    profile_username = Keyword.get(options, :profile_username)
    profile_name = Keyword.get(options, :profile_display_name)

    if profile_name != nil || profile_username != nil do
      create_profile(user, profile_username, profile_name)
    else
      nil
    end
  end

  @type role :: :administrator | :moderator | :user

  @spec get_role(Keyword.t()) :: role()
  defp get_role(options) do
    moderator? = Keyword.get(options, :moderator, false)
    admin? = Keyword.get(options, :admin, false)

    cond do
      admin? -> :administrator
      moderator? -> :moderator
      true -> :user
    end
  end

  @spec check_password_and_provider_options(Keyword.t()) :: nil | no_return()
  defp check_password_and_provider_options(options) do
    if Keyword.get(options, :password) != nil && Keyword.get(options, :provider) != nil do
      shell_error("""
      The --password and --provider options can't be specified at the same time.
      """)
    end
  end
end
