defmodule Absinthe.Federation.Schema do
  @moduledoc """
  Module for injecting custom `Absinthe.Phase`s for adding federated types and directives.

  ## Example

      defmodule MyApp.MySchema do
        use Absinthe.Schema
      + use Absinthe.Federation.Schema

        query do
          ...
        end
      end
  """

  alias Absinthe.Federation.Schema.Prototype, as: FederationPrototype
  alias Absinthe.Phase.Schema.TypeImports
  alias Absinthe.Pipeline

  defmacro __using__(opts) do
    do_using(opts)
  end

  defp do_using(opts) do
    has_custom_prototype? = Keyword.has_key?(opts, :prototype_schema)
    prototype_schema = if has_custom_prototype?, do: opts[:prototype_schema], else: FederationPrototype

    quote do
      @pipeline_modifier unquote(__MODULE__)
      use Absinthe.Federation.Notation

      if Keyword.has_key?(unquote(opts), :skip_prototype) do
        raise ArgumentError,
              ":skip_prototype option is no longer supported. \n" <>
                "Please provide your custom prototype module with the :prototype_schema option instead. \n" <>
                "Example: `use Absinthe.Federation.Schema, prototype_schema: MySchema.Prototype`"
      end

      if unquote(has_custom_prototype?) do
        @prototype_schema unquote(prototype_schema)

        import_types unquote(prototype_schema),
          except: unquote(Map.keys(FederationPrototype.__absinthe_types__()))

        import_directives unquote(prototype_schema),
          except: unquote(Map.keys(FederationPrototype.__absinthe_directives__()))
      else
        @prototype_schema FederationPrototype
      end

      import_types Absinthe.Federation.Types
    end
  end

  @doc """
  Injects custom compile-time `Absinthe.Phase`
  """
  def pipeline(pipeline) do
    Pipeline.insert_after(pipeline, TypeImports, [
      __MODULE__.Phase.AddFederatedTypes,
      __MODULE__.Phase.AddFederatedDirectives
      # __MODULE__.Phase.Validation.KeyFieldsMustExist,
      # __MODULE__.Phase.Validation.KeyFieldsMustBeValidWhenExtends
    ])
  end

  @spec remove_federated_types_pipeline(schema :: Absinthe.Schema.t()) :: Absinthe.Pipeline.t()
  def remove_federated_types_pipeline(schema) do
    schema
    |> Absinthe.Pipeline.for_schema(prototype_schema: schema.__absinthe_prototype_schema__())
    |> Absinthe.Pipeline.upto({Absinthe.Phase.Schema.Validation.Result, pass: :final})
    |> Absinthe.Schema.apply_modifiers(schema)

    # TODO: Due to an issue found with rendering the SDL we had to revert this functionality
    # https://github.com/DivvyPayHQ/absinthe_federation/issues/28
    # |> Absinthe.Pipeline.without(__MODULE__.Phase.AddFederatedTypes)
    |> Absinthe.Pipeline.insert_before(
      Absinthe.Phase.Schema.ApplyDeclaration,
      __MODULE__.Phase.RemoveResolveReferenceFields
    )
  end

  @spec to_federated_sdl(schema :: Absinthe.Schema.t()) :: String.t()
  def to_federated_sdl(schema) do
    pipeline = remove_federated_types_pipeline(schema)

    # we can be assertive here, since this same pipeline was already used to
    # successfully compile the schema.
    {:ok, bp, _} = Absinthe.Pipeline.run(schema.__absinthe_blueprint__(), pipeline)

    Absinthe.Schema.Notation.SDL.Render.inspect(bp, %{pretty: true})
  end
end
