defmodule Absinthe.Type.BuiltIns.Introspection do
  @moduledoc false

  use Absinthe.Schema.Notation

  object :__schema do
    description "Represents a schema"

    field :description, :string do
      resolve(fn _, %{schema: schema} ->
        {:ok, Absinthe.Schema.schema_declaration(schema).description}
      end)
    end

    field :types, non_null(list_of(non_null(:__type))) do
      resolve fn _, %{schema: schema} ->
        types =
          Absinthe.Schema.types(schema)
          |> Enum.sort_by(& &1.identifier)

        {:ok, types}
      end
    end

    field :query_type,
      type: :__type,
      resolve: fn _, %{schema: schema} ->
        {:ok, Absinthe.Schema.lookup_type(schema, :query)}
      end

    field :mutation_type,
      type: :__type,
      resolve: fn _, %{schema: schema} ->
        {:ok, Absinthe.Schema.lookup_type(schema, :mutation)}
      end

    field :subscription_type,
      type: :__type,
      resolve: fn _, %{schema: schema} ->
        {:ok, Absinthe.Schema.lookup_type(schema, :subscription)}
      end

    field :directives,
      type: non_null(list_of(non_null(:__directive))),
      resolve: fn _, %{schema: schema} ->
        directives =
          Absinthe.Schema.directives(schema)
          |> Enum.sort_by(& &1.identifier)

        {:ok, directives}
      end
  end

  object :__directive do
    description "Represents a directive"

    field :name,
      type: non_null(:string),
      resolve: fn _, %{adapter: adapter, source: source} ->
        {:ok, adapter.to_external_name(source.name, :field)}
      end

    field :description, :string

    field :is_repeatable, non_null(:boolean),
      resolve: fn _, %{source: source} ->
        {:ok, source.repeatable}
      end

    field :args,
      type: non_null(list_of(non_null(:__inputvalue))),
      args: [
        include_deprecated: [
          type: :boolean,
          default_value: default_include_deprecated()
        ]
      ],
      resolve: fn %{include_deprecated: show_deprecated}, %{source: source} ->
        args =
          source.args
          |> Map.values()
          |> filter_deprecated(show_deprecated)
          |> Enum.sort_by(& &1.identifier)

        {:ok, args}
      end

    field :locations, non_null(list_of(non_null(:__directive_location)))
  end

  enum :__type_kind,
    values: Absinthe.Introspection.TypeKind.values()

  enum :__directive_location,
    values: Absinthe.Introspection.DirectiveLocation.values()

  object :__type do
    description "Represents scalars, interfaces, object types, unions, enums in the system"

    field :kind,
      type: non_null(:__type_kind),
      resolve: fn _, %{source: %{__struct__: type}} ->
        {:ok, type.kind()}
      end

    field :name, :string

    field :description, :string

    field :fields, list_of(non_null(:__field)) do
      arg :include_deprecated, :boolean, default_value: false

      resolve fn
        %{include_deprecated: show_deprecated}, %{source: %{__struct__: str, fields: fields}}
        when str in [Absinthe.Type.Object, Absinthe.Type.Interface] ->
          result =
            fields
            |> Map.values()
            |> filter_deprecated(show_deprecated)
            |> Enum.filter(&(!Absinthe.Type.introspection?(&1)))
            |> Enum.sort_by(& &1.identifier)

          {:ok, result}

        _, _ ->
          {:ok, nil}
      end
    end

    field :interfaces,
      type: list_of(non_null(:__type)),
      resolve: fn
        _, %{schema: schema, source: %{interfaces: interfaces}} ->
          interfaces =
            interfaces
            |> Enum.map(&Absinthe.Schema.lookup_type(schema, &1))
            |> Enum.sort_by(& &1.identifier)

          {:ok, interfaces}

        _, _ ->
          {:ok, nil}
      end

    field :possible_types,
      type: list_of(non_null(:__type)),
      resolve: fn
        _, %{schema: schema, source: %{types: types}} ->
          possible_types =
            types
            |> Enum.map(&Absinthe.Schema.lookup_type(schema, &1))
            |> Enum.sort_by(& &1.identifier)

          {:ok, possible_types}

        _, %{schema: schema, source: %Absinthe.Type.Interface{identifier: ident}} ->
          {:ok, Absinthe.Schema.implementors(schema, ident)}

        _, _ ->
          {:ok, nil}
      end

    field :enum_values,
      type: list_of(non_null(:__enumvalue)),
      args: [
        include_deprecated: [
          type: :boolean,
          default_value: default_include_deprecated()
        ]
      ],
      resolve: fn
        %{include_deprecated: show_deprecated}, %{source: %Absinthe.Type.Enum{values: values}} ->
          result =
            values
            |> Map.values()
            |> filter_deprecated(show_deprecated)
            |> Enum.sort_by(& &1.value)

          {:ok, result}

        _, _ ->
          {:ok, nil}
      end

    field :input_fields,
      type: list_of(non_null(:__inputvalue)),
      args: [
        include_deprecated: [
          type: :boolean,
          default_value: default_include_deprecated()
        ]
      ],
      resolve: fn
        %{include_deprecated: show_deprecated},
        %{source: %Absinthe.Type.InputObject{fields: fields}} ->
          input_fields =
            fields
            |> Map.values()
            |> filter_deprecated(show_deprecated)
            |> Enum.sort_by(& &1.identifier)

          {:ok, input_fields}

        _, %{source: _} ->
          {:ok, nil}
      end

    field :of_type,
      type: :__type,
      resolve: fn
        _, %{schema: schema, source: %{of_type: type}} ->
          {:ok, Absinthe.Schema.lookup_type(schema, type, unwrap: false)}

        _, _ ->
          {:ok, nil}
      end
  end

  object :__field do
    field :name,
      type: non_null(:string),
      resolve: fn _, %{adapter: adapter, source: source} ->
        {:ok, adapter.to_external_name(source.name, :field)}
      end

    field :description, :string

    field :args,
      type: non_null(list_of(non_null(:__inputvalue))),
      args: [
        include_deprecated: [
          type: :boolean,
          default_value: default_include_deprecated()
        ]
      ],
      resolve: fn %{include_deprecated: show_deprecated}, %{source: %{args: args}} ->
        args =
          args
          |> Map.values()
          |> filter_deprecated(show_deprecated)
          |> Enum.sort_by(& &1.identifier)

        {:ok, args}
      end

    field :type,
      type: non_null(:__type),
      resolve: fn _, %{schema: schema, source: source} ->
        result =
          case source.type do
            type when is_atom(type) ->
              Absinthe.Schema.lookup_type(schema, source.type)

            type ->
              type
          end

        {:ok, result}
      end

    field :is_deprecated,
      type: non_null(:boolean),
      resolve: fn
        _, %{source: %{deprecation: nil}} ->
          {:ok, false}

        _, _ ->
          {:ok, true}
      end

    field :deprecation_reason,
      type: :string,
      resolve: fn
        _, %{source: %{deprecation: nil}} ->
          {:ok, nil}

        _, %{source: %{deprecation: dep}} ->
          {:ok, dep.reason}
      end
  end

  object :__inputvalue, name: "__InputValue" do
    field :name,
      type: non_null(:string),
      resolve: fn _, %{adapter: adapter, source: source} ->
        {:ok, adapter.to_external_name(source.name, :field)}
      end

    field :description, :string

    field :type,
      type: non_null(:__type),
      resolve: fn _, %{schema: schema, source: %{type: ident}} ->
        type = Absinthe.Schema.lookup_type(schema, ident, unwrap: false)
        {:ok, type}
      end

    field :default_value,
      type: :string,
      resolve: fn
        _, %{source: %{default_value: nil}} ->
          {:ok, nil}

        _, %{schema: schema, source: %{default_value: value, type: type}, adapter: adapter} ->
          {:ok, render_default_value(schema, adapter, type, value)}

        _, %{source: _} ->
          {:ok, nil}
      end

    field :is_deprecated,
      type: non_null(:boolean),
      resolve: fn
        _, %{source: %{deprecation: nil}} ->
          {:ok, false}

        _, _ ->
          {:ok, true}
      end

    field :deprecation_reason,
      type: :string,
      resolve: fn
        _, %{source: %{deprecation: nil}} ->
          {:ok, nil}

        _, %{source: %{deprecation: dep}} ->
          {:ok, dep.reason}
      end
  end

  object :__enumvalue, name: "__EnumValue" do
    field :name, non_null(:string)

    field :description, :string

    field :is_deprecated,
      type: non_null(:boolean),
      resolve: fn
        _, %{source: %{deprecation: nil}} ->
          {:ok, false}

        _, _ ->
          {:ok, true}
      end

    field :deprecation_reason,
      type: :string,
      resolve: fn
        _, %{source: %{deprecation: nil}} ->
          {:ok, nil}

        _, %{source: %{deprecation: dep}} ->
          {:ok, dep.reason}
      end
  end

  def render_default_value(schema, adapter, type, value) do
    case Absinthe.Schema.lookup_type(schema, type, unwrap: false) do
      %Absinthe.Type.InputObject{fields: fields} ->
        object_values =
          fields
          |> Map.take(Map.keys(value))
          |> Map.values()
          |> Enum.sort_by(& &1.identifier)
          |> Enum.map(&render_default_value(schema, adapter, &1, value))
          |> Enum.join(", ")

        "{#{object_values}}"

      %Absinthe.Type.List{of_type: type} ->
        list_values =
          value
          |> List.wrap()
          |> Enum.map(&render_default_value(schema, adapter, type, &1))
          |> Enum.join(", ")

        "[#{list_values}]"

      %Absinthe.Type.Field{type: type, name: name, identifier: identifier} ->
        key = adapter.to_external_name(name, :field)
        val = render_default_value(schema, adapter, type, value[identifier])
        "#{key}: #{val}"

      %Absinthe.Type.Enum{values_by_internal_value: values} ->
        values[value].name

      %Absinthe.Type.NonNull{of_type: type} ->
        render_default_value(schema, adapter, type, value)

      %Absinthe.Type.Scalar{} = sc ->
        inspect(Absinthe.Type.Scalar.serialize(sc, value))
    end
  end

  defp filter_deprecated(values, show_deprecated) do
    Enum.filter(values, fn %{deprecation: is_deprecated} ->
      !is_deprecated || show_deprecated
    end)
  end

  defp default_include_deprecated do
    Application.get_env(:absinthe, :include_deprecated, false)
  end
end
