defmodule Mv.Accounts.User.Validations.OidcEmailCollision do
  @moduledoc """
  Validation that checks for email collisions during OIDC registration.

  This validation prevents unauthorized account takeovers and enforces proper
  account linking flows based on user state.

  ## Scenarios:

  1. **User exists with matching oidc_id**: 
     - Allow (upsert will update the existing user)

  2. **User exists with different oidc_id**:
     - Hard error: Cannot link multiple OIDC providers to same account
     - No linking possible - user must use original OIDC provider

  3. **User exists without oidc_id** (password-protected OR passwordless):
     - Raise PasswordVerificationRequired error
     - User is redirected to LinkOidcAccountLive which will:
       - Show password form if user has password
       - Auto-link immediately if user is passwordless

  4. **No user exists with this email**:
     - Allow (new user will be created)
  """
  use Ash.Resource.Validation
  require Logger

  alias Mv.Accounts.User.Errors.PasswordVerificationRequired

  @impl true
  def init(opts), do: {:ok, opts}

  @impl true
  def validate(changeset, _opts, _context) do
    # Get the email and oidc_id from the changeset
    email = Ash.Changeset.get_attribute(changeset, :email)
    oidc_id = Ash.Changeset.get_attribute(changeset, :oidc_id)
    user_info = Ash.Changeset.get_argument(changeset, :user_info)

    # Only validate if we have both email and oidc_id (from OIDC registration)
    if email && oidc_id && user_info do
      # Check if a user with this oidc_id already exists
      # If yes, this will be an upsert (email update), not a new registration
      # Use SystemActor for authorization during OIDC registration (no logged-in actor)
      system_actor = Mv.Helpers.SystemActor.get_system_actor()

      existing_oidc_user =
        case Mv.Accounts.User
             |> Ash.Query.filter(oidc_id == ^to_string(oidc_id))
             |> Ash.read_one(actor: system_actor) do
          {:ok, user} -> user
          _ -> nil
        end

      check_email_collision(email, oidc_id, user_info, existing_oidc_user, system_actor)
    else
      :ok
    end
  end

  defp check_email_collision(email, new_oidc_id, user_info, existing_oidc_user, system_actor) do
    # Find existing user with this email
    # Use SystemActor for authorization during OIDC registration (no logged-in actor)
    case Mv.Accounts.User
         |> Ash.Query.filter(email == ^to_string(email))
         |> Ash.read_one(actor: system_actor) do
      {:ok, nil} ->
        # No user exists with this email - OK to create new user
        :ok

      {:ok, user_with_email} ->
        # User exists with this email - check if it's an upsert or registration
        is_upsert = not is_nil(existing_oidc_user)

        if is_upsert do
          handle_upsert_scenario(user_with_email, user_info, existing_oidc_user)
        else
          handle_create_scenario(user_with_email, new_oidc_id, user_info)
        end

      {:error, error} ->
        # Database error - log for debugging but don't expose internals to user
        Logger.error("Email uniqueness check failed during OIDC registration: #{inspect(error)}")
        {:error, field: :email, message: "Could not verify email uniqueness. Please try again."}
    end
  end

  # Handle email update for existing OIDC user
  defp handle_upsert_scenario(user_with_email, user_info, existing_oidc_user) do
    cond do
      # Same user updating their own record
      not is_nil(existing_oidc_user) and user_with_email.id == existing_oidc_user.id ->
        :ok

      # Different user exists with target email
      not is_nil(existing_oidc_user) and user_with_email.id != existing_oidc_user.id ->
        handle_email_conflict(user_with_email, user_info)

      # Should not reach here
      true ->
        {:error, field: :email, message: "Unexpected error during email update"}
    end
  end

  # Handle email conflict during upsert
  defp handle_email_conflict(user_with_email, user_info) do
    email = Map.get(user_info, "preferred_username", "unknown")
    email_user_oidc_id = user_with_email.oidc_id

    # Check if target email belongs to another OIDC user
    if not is_nil(email_user_oidc_id) and email_user_oidc_id != "" do
      different_oidc_error(email)
    else
      email_taken_error(email)
    end
  end

  # Handle new OIDC user registration scenarios
  defp handle_create_scenario(user_with_email, new_oidc_id, user_info) do
    email_user_oidc_id = user_with_email.oidc_id

    cond do
      # Same oidc_id (should not happen in practice, but allow for safety)
      email_user_oidc_id == new_oidc_id ->
        :ok

      # Different oidc_id exists (hard error)
      not is_nil(email_user_oidc_id) and email_user_oidc_id != "" and
          email_user_oidc_id != new_oidc_id ->
        email = Map.get(user_info, "preferred_username", "unknown")
        different_oidc_error(email)

      # No oidc_id (require account linking)
      is_nil(email_user_oidc_id) or email_user_oidc_id == "" ->
        {:error,
         PasswordVerificationRequired.exception(
           user_id: user_with_email.id,
           oidc_user_info: user_info
         )}

      # Should not reach here
      true ->
        {:error, field: :email, message: "Unexpected error during OIDC registration"}
    end
  end

  # Generate error for different OIDC account conflict
  defp different_oidc_error(email) do
    {:error,
     field: :email,
     message:
       "Email '#{email}' is already linked to a different OIDC account. " <>
         "Cannot link multiple OIDC providers to the same account."}
  end

  # Generate error for email already taken
  defp email_taken_error(email) do
    {:error,
     field: :email,
     message:
       "Cannot update email to '#{email}': This email is already registered to another account. " <>
         "Please change your email in the identity provider."}
  end

  @impl true
  def atomic?(), do: false

  @impl true
  def describe(_opts) do
    [
      message: "OIDC email collision detected",
      vars: []
    ]
  end
end
