defmodule Ash.Policy.Check.RelatesToActorVia do
  @moduledoc "This check passes if the data relates to the actor via the specified relationship or path of relationships."
  use Ash.Policy.FilterCheck

  @impl true
  def describe(opts) do
    path = Enum.join(opts[:relationship_path], ".")
    "record.#{path} == actor"
  end

  @impl true
  def filter(_actor, context, opts) do
    opts = Keyword.update!(opts, :relationship_path, &List.wrap/1)
    actor_field = Keyword.get(opts, :field)
    {last_relationship, to_many?} = relationship_info(context.resource, opts[:relationship_path])

    pkey =
      last_relationship.destination
      |> Ash.Resource.Info.primary_key()

    if to_many? || opts[:ash_field_policy?] do
      expr(
        exists(
          ^opts[:relationship_path],
          ^Enum.map(pkey, &{&1, {:_actor, actor_path(&1, actor_field)}})
        )
      )
    else
      Enum.reduce(pkey, nil, fn pkey_field, expr ->
        actor_path = actor_path(pkey_field, actor_field)

        if expr do
          expr(^expr and ^ref(opts[:relationship_path], pkey_field) == ^actor(actor_path))
        else
          expr(^ref(opts[:relationship_path], pkey_field) == ^actor(actor_path))
        end
      end)
    end
  end

  defp actor_path(pkey_field, actor_field) when is_nil(actor_field), do: pkey_field
  defp actor_path(pkey_field, actor_field), do: [actor_field, pkey_field]

  @impl true
  def reject(actor, context, opts) do
    opts = Keyword.update!(opts, :relationship_path, &List.wrap/1)
    {last_relationship, to_many?} = relationship_info(context.resource, opts[:relationship_path])

    pkey =
      last_relationship.destination
      |> Ash.Resource.Info.primary_key()

    if to_many? do
      Ash.Expr.expr(not (^filter(actor, context, opts)))
    else
      expr =
        Enum.reduce(pkey, nil, fn field, expr ->
          if expr do
            Ash.Expr.expr(^expr and is_nil(^ref(opts[:relationship_path], field)))
          else
            Ash.Expr.expr(is_nil(^ref(opts[:relationship_path], field)))
          end
        end)

      Ash.Expr.expr(not (^filter(actor, context, opts)) or ^expr)
    end
  end

  defp relationship_info(resource, path, to_many? \\ false)

  defp relationship_info(resource, [rel_key], to_many?) do
    rel = Ash.Resource.Info.relationship(resource, rel_key)

    raise_if_nil(rel, rel_key, resource)

    {rel, to_many? || rel.cardinality == :many}
  end

  defp relationship_info(resource, [rel_key | rest], to_many?) do
    rel = Ash.Resource.Info.relationship(resource, rel_key)

    raise_if_nil(rel, rel_key, resource)

    relationship_info(rel.destination, rest, to_many? || rel.cardinality == :many)
  end

  defp raise_if_nil(nil, rel_key, resource) do
    raise "No such relationship ':#{rel_key}' for #{resource}, required in `relates_to_actor` check"
  end

  defp raise_if_nil(_, _, _) do
    :ok
  end
end
