defmodule WhoNeedHelpWeb.GoogleAuthPending do
  @moduledoc false

  import Plug.Conn

  alias WhoNeedHelp.{Accounts, Repo, Trust}

  @session_key "pending_google_identity"
  @token_salt "pending-google-identity"
  @max_age_seconds 15 * 60
  @supported_locales ~w(en uk ru)

  def put(conn, identity_attrs, locale \\ "en") do
    with {:ok, payload} <- normalize(identity_attrs, locale) do
      token = Phoenix.Token.sign(WhoNeedHelpWeb.Endpoint, @token_salt, payload)
      {:ok, put_session(conn, @session_key, token)}
    end
  end

  def fetch(conn) do
    conn
    |> get_session(@session_key)
    |> verify_token()
  end

  def restore(conn, token) when is_binary(token) do
    case verify_token(token) do
      {:ok, _pending} -> {:ok, put_session(conn, @session_key, token)}
      {:error, reason} -> {:error, conn, reason}
    end
  end

  def restore(conn, _token), do: {:error, conn, :invalid}

  def token(conn) do
    case fetch(conn) do
      {:ok, _pending} -> {:ok, get_session(conn, @session_key)}
      {:error, reason} -> {:error, reason}
    end
  end

  def delete(conn), do: delete_session(conn, @session_key)

  def connect(conn, user) do
    with {:ok, pending} <- fetch(conn),
         true <- normalized_email(user.email) == pending.email,
         {:ok, identity} <- connect_and_audit(user, pending) do
      {:ok, delete(conn), identity}
    else
      false -> {:error, delete(conn), :email_mismatch}
      {:error, reason} -> {:error, delete(conn), reason}
    end
  end

  defp connect_and_audit(user, pending) do
    Repo.transact(fn ->
      with {:ok, identity} <- Accounts.link_google_identity(user, identity_attrs(pending)),
           {:ok, _audit} <-
             Trust.audit(
               user.id,
               "auth_identity.connected",
               "auth_identity",
               identity.id,
               %{"provider" => "google", "method" => "verified_local_sign_in"}
             ) do
        {:ok, identity}
      end
    end)
  end

  defp verify_token(token) when is_binary(token) do
    with {:ok, payload} <-
           Phoenix.Token.verify(WhoNeedHelpWeb.Endpoint, @token_salt, token,
             max_age: @max_age_seconds
           ),
         {:ok, pending} <- normalize(payload, value(payload, :locale)) do
      {:ok, pending}
    else
      _invalid_or_expired -> {:error, :invalid_or_expired}
    end
  end

  defp verify_token(_token), do: {:error, :missing}

  defp normalize(attrs, locale) when is_map(attrs) do
    provider_uid = value(attrs, :provider_uid)
    email = value(attrs, :email)
    email_verified = value(attrs, :email_verified)
    display_name = value(attrs, :display_name)

    cond do
      email_verified != true ->
        {:error, :invalid}

      not is_binary(provider_uid) or provider_uid == "" or byte_size(provider_uid) > 255 ->
        {:error, :invalid}

      not is_binary(email) or email == "" or byte_size(email) > 160 ->
        {:error, :invalid}

      not is_binary(display_name) or String.trim(display_name) == "" ->
        {:error, :invalid}

      true ->
        {:ok,
         %{
           provider_uid: provider_uid,
           email: normalized_email(email),
           email_verified: true,
           display_name: display_name |> String.trim() |> String.slice(0, 80),
           locale: normalize_locale(locale)
         }}
    end
  end

  defp normalize(_attrs, _locale), do: {:error, :invalid}

  defp identity_attrs(pending) do
    Map.take(pending, [:provider_uid, :email, :email_verified, :display_name])
  end

  defp value(attrs, key), do: Map.get(attrs, key) || Map.get(attrs, Atom.to_string(key))

  defp normalized_email(email), do: email |> String.trim() |> String.downcase()

  defp normalize_locale(locale) when locale in @supported_locales, do: locale
  defp normalize_locale(_locale), do: "en"
end
