defmodule Absinthe.Logger do
  @default_log true
  @default_filter_variables ~w(token password)
  @default_pipeline false

  @moduledoc """
  Handles logging of Absinthe-specific events.

  ## Variable filtering

  Absinthe can filter out sensitive information like tokens and passwords
  during logging. They are replaced by `"[FILTERED]"`.

  Use the `:filter_variables` configuration setting for this module.
  For example:

      config :absinthe, Absinthe.Logger,
        filter_variables: ["token", "password", "secret"]

  With the configuration above, Absinthe will filter any variable whose name
  includes the terms `token`, `password`, or `secret`. The match is case
  sensitive.

  Note that filtering only applies to GraphQL variables - the query itself is
  logged before any parsing happens.

  The default is `#{inspect(@default_filter_variables)}`.

  ## Pipeline display

  Absinthe can optionally display the list of pipeline phases for each processed
  document when logging. To enable this feature, set the `:pipeline`
  configuration option for this module:

      config :absinthe, Absinthe.Logger,
        pipeline: true

  The default is `#{inspect(@default_pipeline)}`.

  ## Disabling

  To disable Absinthe logging, set the `:log` configuration option to `false`:

      config :absinthe,
        log: false

  The default is `#{inspect(@default_log)}`.

  """
  require Logger

  @doc """
  Log a document being processed.
  """
  @spec log_run(
          level :: Logger.level(),
          {doc :: Absinthe.Pipeline.data_t(), schema :: Absinthe.Schema.t(),
           pipeline :: Absinthe.Pipeline.t(), opts :: Keyword.t()}
        ) :: :ok
  def log_run(level, {doc, schema, pipeline, opts}) do
    if Application.get_env(:absinthe, :log, @default_log) do
      Logger.log(level, fn ->
        [
          "ABSINTHE",
          " schema=",
          inspect(schema),
          " variables=",
          variables_body(opts),
          pipeline_section(pipeline),
          "---",
          ?\n,
          document(doc),
          ?\n,
          "---"
        ]
      end)
    end

    :ok
  end

  @doc false
  @spec document(Absinthe.Pipeline.data_t()) :: binary
  def document(value) when value in ["", nil] do
    "[EMPTY]"
  end

  def document(%Absinthe.Blueprint{name: nil}) do
    "[COMPILED]"
  end

  def document(%Absinthe.Blueprint{name: name}) do
    "[COMPILED#<#{name}>]"
  end

  def document(%Absinthe.Language.Source{body: body}) do
    document(body)
  end

  def document(document) when is_binary(document) do
    String.trim(document)
  end

  def document(other) do
    inspect(other)
  end

  @doc false
  @spec filter_variables(map) :: map
  @spec filter_variables(map, [String.t()]) :: map
  def filter_variables(data, filter_variables \\ variables_to_filter())

  def filter_variables(%{__struct__: mod} = struct, _filter_variables) when is_atom(mod) do
    struct
  end

  def filter_variables(%{} = map, filter_variables) do
    Enum.into(map, %{}, fn {k, v} ->
      if is_binary(k) and String.contains?(k, filter_variables) do
        {k, "[FILTERED]"}
      else
        {k, filter_variables(v, filter_variables)}
      end
    end)
  end

  def filter_variables([_ | _] = list, filter_variables) do
    Enum.map(list, &filter_variables(&1, filter_variables))
  end

  def filter_variables(other, _filter_variables), do: other

  @spec variables_to_filter() :: [String.t()]
  defp variables_to_filter do
    Application.get_env(:absinthe, __MODULE__, [])
    |> Keyword.get(:filter_variables, @default_filter_variables)
  end

  @spec variables_body(Keyword.t()) :: String.t()
  defp variables_body(opts) do
    Keyword.get(opts, :variables, %{})
    |> filter_variables()
    |> inspect()
  end

  @spec pipeline_section(Absinthe.Pipeline.t()) :: iolist
  defp pipeline_section(pipeline) do
    Application.get_env(:absinthe, __MODULE__, [])
    |> Keyword.get(:pipeline, @default_pipeline)
    |> case do
      true ->
        do_pipeline_section(pipeline)

      false ->
        ?\n
    end
  end

  @spec do_pipeline_section(Absinthe.Pipeline.t()) :: iolist
  defp do_pipeline_section(pipeline) do
    [
      " pipeline=",
      pipeline
      |> Enum.map(fn
        {mod, _} -> mod
        mod -> mod
      end)
      |> inspect,
      ?\n
    ]
  end
end
