defmodule Absinthe.Phase do
  @moduledoc """
  Behaviour for Absinthe Phases.

  A phase takes an `Absinthe.Blueprint` document and returns another blueprint document.
  All validation, resolution, and result building happens via phases. See
  `Absinthe.Pipeline` for information on how to run phases. See the code under
  this namespace for information on individual phases.
  """

  @type t :: module
  @type result_t ::
          {:ok, any}
          | {:jump, any, t}
          | {:insert, any, t | [t]}
          | {:replace, any, t | [t]}
          | {:error, any}
          | {:record_phases, any, (any, any -> any)}

  alias __MODULE__
  alias Absinthe.Blueprint

  defmacro __using__(_) do
    quote do
      @behaviour Phase
      import(unquote(__MODULE__))

      @spec flag_invalid(node :: Blueprint.node_t()) :: Blueprint.node_t()
      def flag_invalid(%{flags: _} = node) do
        Absinthe.Blueprint.put_flag(node, :invalid, __MODULE__)
      end

      @spec flag_invalid(node :: Blueprint.node_t(), flag :: atom) :: Blueprint.node_t()
      def flag_invalid(%{flags: _} = node, flag) do
        flagging = %{:invalid => __MODULE__, flag => __MODULE__}
        update_in(node.flags, &Map.merge(&1, flagging))
      end

      @spec flag_invalid(node :: Blueprint.node_t(), flag :: atom, reason :: String.t()) ::
              Blueprint.node_t()
      def flag_invalid(%{flags: _} = node, flag, reason) do
        flagging = %{:invalid => {__MODULE__, reason}, flag => __MODULE__}
        update_in(node.flags, &Map.merge(&1, flagging))
      end

      def put_flag(%{flags: _} = node, flag) do
        Absinthe.Blueprint.put_flag(node, flag, __MODULE__)
      end

      def inherit_invalid(%{flags: _} = node, children, add_flag) do
        case any_invalid?(children) do
          true ->
            flag_invalid(node, add_flag)

          false ->
            node
        end
      end
    end
  end

  @spec put_error(Blueprint.node_t(), Phase.Error.t()) :: Blueprint.node_t()
  def put_error(%{errors: _} = node, error) do
    update_in(node.errors, &[error | &1])
  end

  def any_invalid?(nodes) do
    Enum.any?(nodes, &match?(%{flags: %{invalid: _}}, &1))
  end

  @callback run(any, any) :: result_t
end
