package introspection

import (
	_ "embed"
)

// Query is the query generated by graphiql to determine type information
//
//go:embed introspection.graphql
var Query string

// Response is the introspection query response
type Response struct {
	Schema *Schema `json:"__schema"`
}

type Schema struct {
	QueryType struct {
		Name string `json:"name,omitempty"`
	} `json:"queryType,omitempty"`
	MutationType *struct {
		Name string `json:"name,omitempty"`
	} `json:"mutationType,omitempty"`
	SubscriptionType *struct {
		Name string `json:"name,omitempty"`
	} `json:"subscriptionType,omitempty"`

	Types Types `json:"types"`
}

func (s *Schema) Query() *Type {
	return s.Types.Get(s.QueryType.Name)
}

func (s *Schema) Mutation() *Type {
	if s.MutationType == nil {
		return nil
	}
	return s.Types.Get(s.MutationType.Name)
}

func (s *Schema) Subscription() *Type {
	if s.SubscriptionType == nil {
		return nil
	}
	return s.Types.Get(s.SubscriptionType.Name)
}

func (s *Schema) Visit() []*Type {
	v := Visitor{schema: s}
	return v.Run()
}

// Remove all occurrences of a type from the schema, including
// any fields, input fields, and enum values that reference it.
func (s *Schema) ScrubType(typeName string) {
	filteredTypes := make(Types, 0, len(s.Types))
	for _, t := range s.Types {
		if t.ScrubType(typeName) {
			continue
		}
		filteredTypes = append(filteredTypes, t)
	}
	s.Types = filteredTypes
}

type TypeKind string

const (
	TypeKindScalar      = TypeKind("SCALAR")
	TypeKindObject      = TypeKind("OBJECT")
	TypeKindInterface   = TypeKind("INTERFACE")
	TypeKindUnion       = TypeKind("UNION")
	TypeKindEnum        = TypeKind("ENUM")
	TypeKindInputObject = TypeKind("INPUT_OBJECT")
	TypeKindList        = TypeKind("LIST")
	TypeKindNonNull     = TypeKind("NON_NULL")
)

type Scalar string

const (
	ScalarInt     = Scalar("Int")
	ScalarFloat   = Scalar("Float")
	ScalarString  = Scalar("String")
	ScalarBoolean = Scalar("Boolean")
)

type Type struct {
	Kind        TypeKind     `json:"kind"`
	Name        string       `json:"name"`
	Description string       `json:"description,omitempty"`
	Fields      []*Field     `json:"fields,omitempty"`
	InputFields []InputValue `json:"inputFields,omitempty"`
	EnumValues  []EnumValue  `json:"enumValues,omitempty"`
	Interfaces  []*Type      `json:"interfaces"`
}

// Remove all occurrences of a type from the schema, including
// any fields, input fields, and enum values that reference it.
// Returns true if this type should be removed, whether because
// it is the type being scrubbed, or because it is now empty after
// scrubbing its references.
func (t *Type) ScrubType(typeName string) bool {
	if t.Kind == TypeKindScalar {
		return t.Name == typeName
	}

	filteredFields := make([]*Field, 0, len(t.Fields))
	for _, f := range t.Fields {
		if f.TypeRef.ReferencesType(typeName) {
			continue
		}
		filteredFields = append(filteredFields, f)
	}
	t.Fields = filteredFields

	filteredInputFields := make([]InputValue, 0, len(t.InputFields))
	for _, f := range t.InputFields {
		if f.Name == typeName {
			continue
		}
		if f.TypeRef.ReferencesType(typeName) {
			continue
		}
		filteredInputFields = append(filteredInputFields, f)
	}
	t.InputFields = filteredInputFields

	filteredEnumValues := make([]EnumValue, 0, len(t.EnumValues))
	for _, e := range t.EnumValues {
		if e.Name == typeName {
			continue
		}
		filteredEnumValues = append(filteredEnumValues, e)
	}
	t.EnumValues = filteredEnumValues

	// check if we removed everything from it, in which case it should
	// be removed itself
	isEmpty := len(t.Fields) == 0 && len(t.InputFields) == 0 && len(t.EnumValues) == 0
	return t.Name == typeName || isEmpty
}

type Types []*Type

func (t Types) Get(name string) *Type {
	for _, i := range t {
		if i.Name == name {
			return i
		}
	}
	return nil
}

type Field struct {
	Name              string      `json:"name"`
	Description       string      `json:"description"`
	TypeRef           *TypeRef    `json:"type"`
	Args              InputValues `json:"args"`
	IsDeprecated      bool        `json:"isDeprecated"`
	DeprecationReason string      `json:"deprecationReason"`

	ParentObject *Type `json:"-"`
}

func (f *Field) ReferencesType(typeName string) bool {
	// check return
	if f.TypeRef.ReferencesType(typeName) {
		return true
	}
	// check args
	for _, arg := range f.Args {
		if arg.TypeRef.ReferencesType(typeName) {
			return true
		}
	}
	return false
}

type TypeRef struct {
	Kind   TypeKind `json:"kind"`
	Name   string   `json:"name,omitempty"`
	OfType *TypeRef `json:"ofType,omitempty"`
}

func (r TypeRef) IsOptional() bool {
	return r.Kind != TypeKindNonNull
}

func (r TypeRef) IsScalar() bool {
	ref := r
	if r.Kind == TypeKindNonNull {
		ref = *ref.OfType
	}
	if ref.Kind == TypeKindScalar {
		return true
	}
	if ref.Kind == TypeKindEnum {
		return true
	}
	return false
}

func (r TypeRef) IsObject() bool {
	ref := r
	if r.Kind == TypeKindNonNull {
		ref = *ref.OfType
	}
	if ref.Kind == TypeKindObject {
		return true
	}
	return false
}

func (r TypeRef) IsList() bool {
	ref := r
	if r.Kind == TypeKindNonNull {
		ref = *ref.OfType
	}
	if ref.Kind == TypeKindList {
		return true
	}
	return false
}

func (r TypeRef) ReferencesType(typeName string) bool {
	if r.OfType != nil {
		return r.OfType.ReferencesType(typeName)
	}
	return r.Name == typeName
}

type InputValues []InputValue

func (i InputValues) HasOptionals() bool {
	for _, v := range i {
		if v.TypeRef.IsOptional() {
			return true
		}
	}
	return false
}

type InputValue struct {
	Name         string   `json:"name"`
	Description  string   `json:"description"`
	DefaultValue *string  `json:"defaultValue"`
	TypeRef      *TypeRef `json:"type"`
}

type EnumValue struct {
	Name              string `json:"name"`
	Description       string `json:"description"`
	IsDeprecated      bool   `json:"isDeprecated"`
	DeprecationReason string `json:"deprecationReason"`
}
