defmodule Mobilizon.Web.Email.User do
  @moduledoc """
  Handles emails sent to users.
  """

  use Phoenix.Swoosh, view: Mobilizon.Web.EmailView

  import Mobilizon.Web.Gettext, only: [gettext: 2]

  alias Mobilizon.{Config, Crypto, Users}
  alias Mobilizon.Storage.Repo
  alias Mobilizon.Users.User

  alias Mobilizon.Web.Email

  require Logger

  @spec confirmation_email(User.t(), String.t()) :: Swoosh.Email.t()
  def confirmation_email(
        %User{email: email, confirmation_token: confirmation_token},
        locale \\ "en"
      ) do
    Gettext.put_locale(locale)

    subject =
      gettext(
        "Instructions to confirm your Mobilizon account on %{instance}",
        instance: Config.instance_name()
      )

    [to: email, subject: subject]
    |> Email.base_email()
    |> render_body(:registration_confirmation, %{
      locale: locale,
      token: confirmation_token,
      subject: subject,
      offer_unsupscription: false
    })
  end

  @spec reset_password_email(User.t(), String.t()) :: Swoosh.Email.t()
  def reset_password_email(
        %User{email: email, reset_password_token: reset_password_token},
        locale \\ "en"
      ) do
    Gettext.put_locale(locale)

    subject =
      gettext(
        "Instructions to reset your password on %{instance}",
        instance: Config.instance_name()
      )

    [to: email, subject: subject]
    |> Email.base_email()
    |> render_body(:password_reset, %{
      locale: locale,
      token: reset_password_token,
      subject: subject,
      offer_unsupscription: false
    })
  end

  @spec check_confirmation_token(String.t()) ::
          {:ok, User.t()} | {:error, :invalid_token | Ecto.Changeset.t()}
  def check_confirmation_token(token) when is_binary(token) do
    case Users.get_user_by_activation_token(token) do
      %User{} = user ->
        case Users.update_user(user, %{
               confirmed_at: DateTime.utc_now() |> DateTime.truncate(:second),
               confirmation_sent_at: nil,
               confirmation_token: nil,
               email: user.unconfirmed_email || user.email
             }) do
          {:ok, %User{} = user} ->
            Logger.info("User #{user.email} has been confirmed")
            {:ok, user}

          {:error, %Ecto.Changeset{} = err} ->
            {:error, err}
        end

      nil ->
        {:error, :invalid_token}
    end
  end

  def resend_confirmation_email(%User{} = user, locale \\ "en") do
    with :ok <- we_can_send_email(user, :confirmation_sent_at),
         {:ok, user} <-
           Users.update_user(user, %{
             "confirmation_sent_at" => DateTime.utc_now() |> DateTime.truncate(:second)
           }) do
      case send_confirmation_email(user, locale) do
        {:ok, _} ->
          Logger.info("Sent confirmation email again to #{user.email}")
          {:ok, user.email}

        {:error, err} ->
          Logger.error("Failed sending email to #{user.email}. #{inspect(err)}")
          {:error, :failed_sending_mail}
      end
    end
  end

  @spec send_confirmation_email(User.t(), String.t()) :: {:ok, term} | {:error, term}
  def send_confirmation_email(%User{} = user, locale \\ "en") do
    user
    |> Email.User.confirmation_email(locale)
    |> Email.Mailer.send_email()
  end

  @doc """
  Check that the provided token is correct and update provided password
  """
  @spec check_reset_password_token(String.t(), String.t()) ::
          {:ok, User.t()} | {:error, :user_not_found | Ecto.Changeset.t()}
  def check_reset_password_token(password, token) do
    case Users.get_user_by_reset_password_token(token) do
      %User{} = user ->
        user
        |> User.password_reset_changeset(%{
          "password" => password,
          "reset_password_sent_at" => nil,
          "reset_password_token" => nil
        })
        |> Repo.update()

      nil ->
        {:error, :user_not_found}
    end
  end

  @doc """
  Send the email reset password, if it's not too soon since the last send
  """
  @spec send_password_reset_email(User.t(), String.t()) :: tuple
  def send_password_reset_email(%User{} = user, locale \\ "en") do
    with :ok <- we_can_send_email(user, :reset_password_sent_at),
         {:ok, %User{} = user_updated} <-
           Repo.update(
             User.send_password_reset_changeset(user, %{
               "reset_password_token" => Crypto.random_string(30),
               "reset_password_sent_at" => DateTime.utc_now() |> DateTime.truncate(:second)
             })
           ) do
      user_updated
      |> Email.User.reset_password_email(locale)
      |> Email.Mailer.send_email()

      {:ok, user.email}
    else
      {:error, reason} -> {:error, reason}
    end
  end

  def send_email_reset_old_email(%User{
        locale: user_locale,
        email: email,
        unconfirmed_email: unconfirmed_email
      }) do
    Gettext.put_locale(user_locale)

    subject =
      gettext(
        "Mobilizon on %{instance}: email changed",
        instance: Config.instance_name()
      )

    [to: email, subject: subject]
    |> Email.base_email()
    |> render_body(:email_changed_old, %{
      locale: user_locale,
      new_email: unconfirmed_email,
      subject: subject,
      offer_unsupscription: false
    })
  end

  def send_email_reset_new_email(%User{
        locale: user_locale,
        unconfirmed_email: unconfirmed_email,
        confirmation_token: confirmation_token
      }) do
    Gettext.put_locale(user_locale)

    subject =
      gettext(
        "Mobilizon on %{instance}: confirm your email address",
        instance: Config.instance_name()
      )

    [to: unconfirmed_email, subject: subject]
    |> Email.base_email()
    |> render_body(:email_changed_new, %{
      locale: user_locale,
      token: confirmation_token,
      subject: subject,
      offer_unsupscription: false
    })
  end

  @spec we_can_send_email(User.t(), atom) :: :ok | {:error, :email_too_soon}
  defp we_can_send_email(%User{} = user, key) do
    case Map.get(user, key) do
      nil ->
        :ok

      _ ->
        case DateTime.compare(
               DateTime.add(Map.get(user, key), 100),
               DateTime.utc_now() |> DateTime.truncate(:second)
             ) do
          :lt ->
            :ok

          _ ->
            {:error, :email_too_soon}
        end
    end
  end
end