defmodule TarakanWeb.AccountSessionController do
  use TarakanWeb, :controller

  alias Tarakan.Accounts
  alias TarakanWeb.AccountAuth
  alias TarakanWeb.BrowserRateLimit

  def create(conn, %{"_action" => "confirmed"} = params) do
    rate_limited_create(conn, params, "Account confirmed successfully.")
  end

  def create(conn, params) do
    rate_limited_create(conn, params, "Welcome back!")
  end

  defp rate_limited_create(conn, params, info) do
    account_params = Map.get(params, "account", %{})

    identifier =
      account_params["identifier"] || account_params["email"] || account_params["token"] ||
        account_params["session_token"] || "missing"

    remote_ip = remote_ip(conn)
    identifier_key = :crypto.hash(:sha256, identifier |> to_string() |> String.downcase())

    checks = [
      BrowserRateLimit.allowed?(:login_ip, remote_ip),
      BrowserRateLimit.allowed?(:login_pair, {remote_ip, identifier_key})
    ]

    if Enum.all?(checks) do
      create(conn, params, info)
    else
      conn
      |> put_flash(:error, "Invalid credentials or too many attempts. Try again later.")
      |> redirect(to: ~p"/accounts/log-in")
    end
  end

  # instant registration login (one-time session bootstrap token; does not
  # confirm the email address the way a magic-link login does)
  defp create(conn, %{"account" => %{"session_token" => encoded} = account_params}, info) do
    with {:ok, token} <- Base.url_decode64(encoded, padding: false),
         {account, _inserted_at} <- Accounts.get_account_by_session_token(token) do
      # One-time bootstrap: log_in_account mints the real session token.
      Accounts.delete_account_session_token(token)

      conn
      |> put_flash(:info, info)
      |> AccountAuth.log_in_account(account, account_params)
    else
      _other ->
        conn
        |> put_flash(:error, "The link is invalid or it has expired.")
        |> redirect(to: ~p"/accounts/log-in")
    end
  end

  # magic link login
  defp create(conn, %{"account" => %{"token" => token} = account_params}, info) do
    case Accounts.login_account_by_magic_link(token) do
      {:ok, {account, disconnect_ref}} ->
        AccountAuth.disconnect_sessions(disconnect_ref)

        conn
        |> put_flash(:info, info)
        |> AccountAuth.log_in_account(account, account_params)

      _ ->
        conn
        |> put_flash(:error, "The link is invalid or it has expired.")
        |> redirect(to: ~p"/accounts/log-in")
    end
  end

  # handle/email + password login
  defp create(conn, %{"account" => account_params}, info) do
    identifier = Map.get(account_params, "identifier", "")
    password = Map.get(account_params, "password", "")

    if account = Accounts.get_account_by_identifier_and_password(identifier, password) do
      conn
      |> put_flash(:info, info)
      |> AccountAuth.log_in_account(account, account_params)
    else
      # Do not disclose whether the handle or email is registered.
      conn
      |> put_flash(:error, "Invalid handle, email, or password")
      |> put_flash(:identifier, String.slice(identifier, 0, 160))
      |> redirect(to: ~p"/accounts/log-in")
    end
  end

  def update_password(conn, %{"account" => account_params}) do
    account = conn.assigns.current_scope.account

    cond do
      not Accounts.sudo_mode?(account) ->
        # The sudo window can expire between rendering the form and POSTing it;
        # send the account through re-authentication instead of crashing.
        conn
        |> put_flash(
          :error,
          "Confirm it's you with a magic link before changing sensitive settings."
        )
        |> redirect(to: AccountAuth.reauth_path(~p"/accounts/settings"))

      is_nil(account.confirmed_at) ->
        conn
        |> put_flash(
          :error,
          "Confirm your email address with a login link before setting a password."
        )
        |> redirect(to: ~p"/accounts/settings")

      true ->
        {:ok, {account, disconnect_ref}} =
          Accounts.update_account_password(account, account_params)

        # disconnect all existing LiveViews with old sessions
        AccountAuth.disconnect_sessions(disconnect_ref)

        conn
        |> put_session(:account_return_to, ~p"/accounts/settings")
        |> put_flash(:info, "Password updated successfully!")
        |> AccountAuth.log_in_account(account)
    end
  end

  def delete(conn, _params) do
    conn
    |> put_flash(:info, "Logged out successfully.")
    |> AccountAuth.log_out_account()
  end

  # Bucket by /64 for IPv6 so prefix rotation cannot mint fresh rate-limit buckets.
  defp remote_ip(conn), do: TarakanWeb.Plugs.ClientIp.remote_ip_bucket(conn)
end
