defmodule Neuron.Fragment do
  alias Neuron.Store

  @moduledoc """
  This module can be used to register fragments to be used in your GraphQL queries
  """

  @doc ~S"""
  registers a fragment that will automatically be added to future mutations and queries that require it

  ## Example

      iex> Neuron.Fragment.register("
      ...>  NameParts on Person {
      ...>    firstName
      ...>    lastName
      ...>  }
      ...> ")
      :ok
  """

  @spec register(query_string :: String.t()) :: :ok
  def register(query_string), do: register(:global, query_string)

  @spec register(context :: :global | :process, query_string :: String.t()) :: :ok
  def register(context, query_string) do
    fragment = query_string |> process_fragment_string
    stored_fragments = Store.get(context, :fragments, [])
    Store.set(context, :fragments, [fragment | stored_fragments])
  end

  defp process_fragment_string(query_string) do
    fragment_name = Regex.run(~r/^(?:\W*)(\w+)/, query_string) |> List.last() |> String.to_atom()

    fragment = query_string |> construct_fragment_string
    dependencies = fragment |> find_in_query()

    {fragment_name, {fragment, dependencies}}
  end

  @doc false
  def insert_into_query(query_string) do
    stored_fragments = Store.get(:global, :fragments, []) ++ Store.get(:process, :fragments, [])

    fragments_to_add =
      query_string
      |> find_in_query
      |> Enum.map(&List.keyfind(stored_fragments, &1, 0, &1))

    fragments_to_add =
      fragments_to_add
      |> Enum.reject(&is_atom/1)
      |> Enum.reduce(fragments_to_add, &load_missing_fragments(&1, stored_fragments, &2))

    missing_fragments = fragments_to_add |> Enum.filter(&is_atom/1) |> Enum.map(&Atom.to_string/1)

    if Enum.any?(missing_fragments),
      do: raise(Neuron.MissingFragmentsError, missing_fragments: missing_fragments)

    fragments_to_add
    |> Enum.map(&elem(&1, 1))
    |> Enum.map(&elem(&1, 0))
    |> Enum.reduce(query_string, &"#{&1} \n #{&2}")
  end

  defp find_in_query(query_string) do
    Regex.scan(~r/(?<=\.\.\.)\w+(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/, query_string)
    |> List.flatten()
    |> Enum.map(&String.to_atom/1)
    |> Enum.uniq()
  end

  defp construct_fragment_string(fragment_string) do
    "fragment #{fragment_string}"
  end

  defp load_missing_fragments({_, {_, []}}, _available, loaded), do: loaded

  defp load_missing_fragments({_, {_, [name]}}, available, loaded),
    do: load_fragment(name, available, loaded)

  defp load_missing_fragments({_, {_, extra}}, available, loaded) do
    extra |> Enum.reduce(loaded, &load_fragment(&1, available, &2))
  end

  defp load_fragment(name, available, loaded) do
    loaded
    |> Keyword.get(name)
    |> case do
      nil ->
        List.keyfind(available, name, 0, name)
        |> case do
          {^name, _} = fragment ->
            load_missing_fragments(fragment, available, [fragment | loaded])

          ^name ->
            [name | loaded]
        end

      _ ->
        loaded
    end
  end
end
