lib/auth/signin/local/index.ex

defmodule Rivet.Auth.Signin.Local do
  @moduledoc """
  Local login scheme
  """
  alias Rivet.Auth
  alias Rivet.Ident
  import Ident.User.Lib, only: [check_user_status: 1]

  @spec signup(String.t(), params :: map()) :: Auth.Domain.result()
  def signup(host, args, type \\ :authed)

  def signup(
        hostname,
        %{"handle" => handle, "password" => password, "email" => eaddr},
        type
      )
      when is_binary(hostname) do
    case Ident.Email.one([address: eaddr], [:user]) do
      {:ok, %Ident.Email{}} ->
        {:error, %Auth.Domain{error: "A signin already exists for `#{eaddr}`"}}

      _ ->
        handle = Ident.Handle.Lib.gen_good_handle(handle)

        Rivet.Auth.Signin.create_user(handle, eaddr, hostname, type, %{
          secret: password
        })
        |> Rivet.Auth.Signin.post_signin()
    end
  end

  def signup(_, _, _) do
    {:error,
     %Auth.Domain{log: "Auth signup failed, arguments don't match", error: "Signup Failed"}}
  end

  ##############################################################################
  @spec check(Auth.Domain.t() | String.t(), params :: map()) :: Auth.Domain.result()
  def check(%Auth.Domain{} = auth, %{"handle" => handle, "password" => password}) do
    {:ok, auth}
    |> load_user(handle)
    |> check_user_status
    |> load_password_factor
    |> valid_user_factor(password)
    |> Rivet.Auth.Signin.post_signin()
  end

  def check(hostname, args = %{"handle" => _, "password" => _}) when is_binary(hostname),
    do: check(%Auth.Domain{hostname: hostname}, args)

  ##############################################################################
  @auth_fail_msg "Unable to sign in. Did you want to sign up instead?"
  @spec load_user(Auth.Domain.result(), handle :: String.t()) :: Auth.Domain.result()
  def load_user({:ok, %Auth.Domain{} = auth}, handle) do
    if String.contains?(handle, "@") do
      case Ident.Email.one([address: handle], [:user]) do
        {:ok, email} ->
          {:ok, %Auth.Domain{auth | user: email.user}}

        _ ->
          {:error,
           %Auth.Domain{
             log: "Cannot find email #{handle}",
             error: @auth_fail_msg
           }}
      end
    else
      case Ident.Handle.one([handle: handle], [:user]) do
        {:ok, handle} ->
          {:ok, %Auth.Domain{auth | handle: handle, user: handle.user}}

        _ ->
          {:error,
           %Auth.Domain{
             log: "Cannot find person ~#{handle}",
             error: @auth_fail_msg
           }}
      end
    end
  end

  ##############################################################################
  @spec load_password_factor(Auth.Domain.result()) :: Auth.Domain.result()
  def load_password_factor({:ok, auth = %Auth.Domain{user: user = %Ident.User{}}}) do
    case Ident.Factor.Lib.preloaded_with(user, :password) do
      %Ident.User{factors: []} = user ->
        Logger.metadata(uid: user.id)
        {:error, %Auth.Domain{auth | log: "No auth factor for user"}}

      %Ident.User{factors: [factor | _]} = user ->
        Logger.metadata(uid: user.id)
        {:ok, %Auth.Domain{auth | factor: factor}}

      _error ->
        Logger.metadata(uid: user.id)
        {:error, %Auth.Domain{auth | log: "Unexpected result from factor preload"}}
    end
  end

  def load_password_factor(pass = {:error, %Auth.Domain{}}), do: pass

  ##############################################################################
  @spec check_password(hash :: String.t() | Ident.User.t(), password :: String.t()) :: boolean()
  def check_password(%Ident.User{} = user, password) do
    case load_password_factor({:ok, %Auth.Domain{user: user}}) do
      {:ok, %Auth.Domain{factor: %Ident.Factor{hash: hashed}}} ->
        check_password(hashed, password)

      _ ->
        false
    end
  end

  def check_password(hash, password) when is_binary(hash) and not is_nil(hash) do
    Bcrypt.verify_pass(password, hash)
  end

  def check_password(_, _), do: false

  @doc """
  """
  @spec valid_user_factor(Auth.Domain.result(), password :: String.t()) :: Auth.Domain.result()
  def valid_user_factor(
        {:ok, auth = %Auth.Domain{user: %Ident.User{}, factor: %Ident.Factor{hash: hashed}}},
        password
      )
      when not is_nil(hashed) and hashed != "N/A" do
    if check_password(hashed, password) do
      {:ok, %Auth.Domain{auth | status: :authed}}
    else
      {:error, %Auth.Domain{auth | log: "Invalid Password"}}
    end
  end

  def valid_user_factor({:ok, auth = %Auth.Domain{}}, _) do
    {:error, %Auth.Domain{auth | log: "No password factor exists for user"}}
  end

  def valid_user_factor(pass = {:error, %Auth.Domain{}}, _), do: pass
  #
  # ##############################################################################
  # @spec post_signin(Auth.Domain.result()) :: Auth.Domain.result()
  # def post_signin({:ok, auth = %Auth.Domain{status: :authed, user: %Ident.User{}}}) do
  #   Logger.info("Signin Success", uid: user.id)
  #   # TODO: events/triggers to table/signin count, etc
  #   {:ok, auth}
  # end
  #
  # def post_signin(pass = {:error, %Auth.Domain{}}), do: pass
end