defmodule Mv.Authorization.Checks.ForbidMemberUserLinkUnlessAdmin do
  @moduledoc """
  Policy check: forbids setting or changing the member–user link unless the actor is admin.

  Used on Member create_member and update_member actions. When the `:user` argument
  **is present** (key in arguments, regardless of value), only admins may perform the action.
  This covers:
  - **Linking:** `user: %{id: user_id}` → only admin
  - **Unlinking:** explicit `user: nil` or `user: %{}` on update_member → only admin
  Non-admin users can create and update members only when they do **not** pass the
  `:user` argument; omitting `:user` leaves the relationship unchanged.

  ## Unlink semantics (update_member)

  The Member resource uses `on_missing: :ignore` for the `:user` relationship on update.
  So **omitting** `:user` from params does **not** change the link (no "unlink by omission").
  Unlink is only possible by **explicitly** passing `:user` (e.g. `user: nil`), which this
  check forbids for non-admins. Admins may link or unlink via the `:user` argument.

  ## Usage

  In Member resource policies, restrict to create/update only:

      policy action_type([:create, :update]) do
        forbid_if Mv.Authorization.Checks.ForbidMemberUserLinkUnlessAdmin
        authorize_if Mv.Authorization.Checks.HasPermission
      end

  ## Behaviour

  - If the `:user` argument **key is not present** → does not forbid.
  - If `:user` is present (any value, including nil or %{}) and actor is not admin → forbids.
  - If actor is nil → treated as non-admin (forbid when :user present). `Actor.admin?(nil)` is defined and returns false.
  - If actor is admin (or system actor) → does not forbid.
  """
  use Ash.Policy.Check

  alias Mv.Authorization.Actor

  @impl true
  def describe(_opts), do: "forbid setting member–user link unless actor is admin"

  @impl true
  def strict_check(actor, authorizer, _opts) do
    # Nil actor: treat as non-admin (Actor.admin?(nil) returns false; no crash)
    actor = if is_nil(actor), do: nil, else: Actor.ensure_loaded(actor)

    if user_argument_present?(authorizer) and not Actor.admin?(actor) do
      {:ok, true}
    else
      {:ok, false}
    end
  end

  # Forbid when :user was passed at all (link, unlink via nil/empty, or invalid value).
  # Check argument key presence (atom or string) for defense-in-depth.
  defp user_argument_present?(authorizer) do
    args = get_arguments(authorizer) || %{}
    Map.has_key?(args, :user) or Map.has_key?(args, "user")
  end

  defp get_arguments(authorizer) do
    subject = authorizer.changeset || authorizer.subject

    cond do
      is_struct(subject, Ash.Changeset) -> subject.arguments
      is_struct(subject, Ash.ActionInput) -> subject.arguments
      true -> %{}
    end
  end
end
