defmodule Mv.Membership.Member.Validations.EmailChangePermission do
  @moduledoc """
  Validates that only admins or the linked user may change a linked member's email.

  This validation runs on member update when the email attribute is changing.
  It allows the change only if:
  - The member is not linked to a user, or
  - The actor has the admin permission set (via `Mv.Authorization.Actor.admin?/1`), or
  - The actor is the user linked to this member (actor.member_id == member.id).

  This prevents non-admins from changing another user's linked member email,
  which would sync to that user's account and break email synchronization.

  Missing actor is not allowed; the system actor counts as admin (via `Actor.admin?/1`).
  """
  use Ash.Resource.Validation
  use Gettext, backend: MvWeb.Gettext, otp_app: :mv

  alias Mv.Authorization.Actor
  alias Mv.EmailSync.Loader

  @doc """
  Validates that the actor may change the member's email when the member is linked.

  Only runs when the email attribute is changing (checked inside). Skips when
  member is not linked. Allows when actor is admin or owns the linked member.
  """
  @impl true
  def validate(changeset, _opts, context) do
    if Ash.Changeset.changing_attribute?(changeset, :email) do
      validate_linked_member_email_change(changeset, context)
    else
      :ok
    end
  end

  defp validate_linked_member_email_change(changeset, context) do
    linked_user = Loader.get_linked_user(changeset.data)

    if is_nil(linked_user) do
      :ok
    else
      actor = resolve_actor(changeset, context)
      member_id = changeset.data.id

      if Actor.admin?(actor) or actor_owns_member?(actor, member_id) do
        :ok
      else
        msg =
          dgettext(
            "default",
            "Only administrators or the linked user can change the email for members linked to users"
          )

        {:error, field: :email, message: msg}
      end
    end
  end

  # Ash stores actor in changeset.context.private.actor; validation context has .actor; some callsites use context.actor
  defp resolve_actor(changeset, context) do
    ctx = changeset.context || %{}

    get_in(ctx, [:private, :actor]) ||
      Map.get(ctx, :actor) ||
      (context && Map.get(context, :actor))
  end

  defp actor_owns_member?(nil, _member_id), do: false

  defp actor_owns_member?(actor, member_id) do
    actor_member_id = Map.get(actor, :member_id) || Map.get(actor, "member_id")
    actor_member_id == member_id
  end
end
