package resolvable

import (
	"context"
	"fmt"
	"reflect"
	"strings"

	"github.com/graph-gophers/graphql-go/ast"
	"github.com/graph-gophers/graphql-go/decode"
	"github.com/graph-gophers/graphql-go/internal/exec/packer"
)

const (
	Query        = "Query"
	Mutation     = "Mutation"
	Subscription = "Subscription"
)

type Schema struct {
	*Meta
	ast.Schema
	Query                Resolvable
	Mutation             Resolvable
	Subscription         Resolvable
	QueryResolver        reflect.Value
	MutationResolver     reflect.Value
	SubscriptionResolver reflect.Value
}

type Resolvable interface {
	isResolvable()
}

type Object struct {
	Name           string
	Fields         map[string]*Field
	TypeAssertions map[string]*TypeAssertion
	Interfaces     map[string]struct{}
}

type Field struct {
	ast.FieldDefinition
	TypeName        string
	MethodIndex     int
	FieldIndex      []int
	HasContext      bool
	HasError        bool
	IsFieldFunc     bool
	ArgsPacker      *packer.StructPacker
	ValueExec       Resolvable
	OutputType      reflect.Type
	Implementations []*FieldImplementation
	TraceLabel      string
}

type FieldImplementation struct {
	MethodIndex int
	Field       *Field
}

func (f *Field) UseMethodResolver() bool {
	return f.MethodIndex != -1 || f.IsFieldFunc
}

func (f *Field) Resolve(ctx context.Context, resolver reflect.Value, args map[string]any, packedArgs reflect.Value) (output any, err error) {
	if len(f.Implementations) > 0 {
		for _, impl := range f.Implementations {
			out := resolver.Method(impl.MethodIndex).Call(nil)
			if !out[1].Bool() {
				continue
			}
			return impl.Field.resolve(ctx, out[0], args, reflect.Value{})
		}
	}

	return f.resolve(ctx, resolver, args, packedArgs)
}

func (f *Field) resolve(ctx context.Context, resolver reflect.Value, args map[string]any, packedArgs reflect.Value) (output any, err error) {
	if !f.UseMethodResolver() {
		if len(f.FieldIndex) == 0 {
			return nil, fmt.Errorf("missing resolver for field %q", f.Name)
		}
		res := resolver

		// TODO extract out unwrapping ptr logic to a common place
		if res.Kind() == reflect.Pointer {
			res = res.Elem()
		}

		return res.FieldByIndex(f.FieldIndex).Interface(), nil
	}

	var in []reflect.Value
	var callOut []reflect.Value

	if f.HasContext {
		in = append(in, reflect.ValueOf(ctx))
	}

	if f.ArgsPacker != nil {
		if !packedArgs.IsValid() {
			packed, packErr := f.ArgsPacker.Pack(args)
			if packErr != nil {
				return nil, packErr
			}
			packedArgs = packed
		}
		in = append(in, packedArgs)
	}

	if f.IsFieldFunc { // resolver is a struct field of type func
		res := resolver
		if res.Kind() == reflect.Pointer {
			res = resolver.Elem()
		}
		callOut = res.FieldByIndex(f.FieldIndex).Call(in)
	} else {
		callOut = resolver.Method(f.MethodIndex).Call(in)
	}
	result := callOut[0]

	if f.HasError && !callOut[1].IsNil() {
		resolverErr := callOut[1].Interface().(error)
		return result.Interface(), resolverErr
	}

	return result.Interface(), nil
}

type TypeAssertion struct {
	MethodIndex int
	TypeExec    Resolvable
}

type List struct {
	Elem Resolvable
}

type Scalar struct{}

func (*Object) isResolvable() {}
func (*List) isResolvable()   {}
func (*Scalar) isResolvable() {}

func ApplyResolver(s *ast.Schema, resolver any, useFieldResolvers bool) (*Schema, error) {
	if resolver == nil {
		return &Schema{Meta: newMeta(s), Schema: *s}, nil
	}

	b := newBuilder(s, useFieldResolvers)

	var query, mutation, subscription Resolvable

	resolvers := map[string]any{}

	rv := reflect.ValueOf(resolver)
	// use separate resolvers in case Query, Mutation and/or Subscription methods are defined
	for _, op := range [...]string{Query, Mutation, Subscription} {
		m := rv.MethodByName(op)
		if m.IsValid() { // if the root resolver has a method for the current operation
			mt := m.Type()
			if mt.NumIn() != 0 {
				return nil, fmt.Errorf("method %q of %v must not accept any arguments, got %d", op, rv.Type(), mt.NumIn())
			}
			if mt.NumOut() != 1 {
				return nil, fmt.Errorf("method %q of %v must have 1 return value, got %d", op, rv.Type(), mt.NumOut())
			}
			ot := mt.Out(0)
			if ot.Kind() != reflect.Pointer && ot.Kind() != reflect.Interface {
				return nil, fmt.Errorf("method %q of %v must return an interface or a pointer, got %+v", op, rv.Type(), ot)
			}
			out := m.Call(nil)
			res := out[0]
			if res.IsNil() {
				return nil, fmt.Errorf("method %q of %v must return a non-nil result, got %v", op, rv.Type(), res)
			}
			switch res.Kind() {
			case reflect.Pointer:
				resolvers[op] = res.Elem().Addr().Interface()
			case reflect.Interface:
				resolvers[op] = res.Elem().Interface()
			default:
				panic("ureachable")
			}
		}
		// If a method for the current operation is not defined in the root resolver,
		// then use the root resolver for the operation.
		if resolvers[op] == nil {
			resolvers[op] = resolver
		}
	}

	if t, ok := s.RootOperationTypes["query"]; ok {
		if err := b.assignExec(&query, t, reflect.TypeOf(resolvers[Query])); err != nil {
			return nil, err
		}
	}

	if t, ok := s.RootOperationTypes["mutation"]; ok {
		if err := b.assignExec(&mutation, t, reflect.TypeOf(resolvers[Mutation])); err != nil {
			return nil, err
		}
	}

	if t, ok := s.RootOperationTypes["subscription"]; ok {
		if err := b.assignExec(&subscription, t, reflect.TypeOf(resolvers[Subscription])); err != nil {
			return nil, err
		}
	}

	if err := b.finish(); err != nil {
		return nil, err
	}

	return &Schema{
		Meta:                 newMeta(s),
		Schema:               *s,
		QueryResolver:        reflect.ValueOf(resolvers[Query]),
		MutationResolver:     reflect.ValueOf(resolvers[Mutation]),
		SubscriptionResolver: reflect.ValueOf(resolvers[Subscription]),
		Query:                query,
		Mutation:             mutation,
		Subscription:         subscription,
	}, nil
}

type execBuilder struct {
	schema            *ast.Schema
	resMap            map[typePair]*resMapEntry
	packerBuilder     *packer.Builder
	useFieldResolvers bool
}

type typePair struct {
	graphQLType  ast.Type
	resolverType reflect.Type
}

type resMapEntry struct {
	exec    Resolvable
	targets []*Resolvable
}

func newBuilder(s *ast.Schema, useFieldResolvers bool) *execBuilder {
	return &execBuilder{
		schema:            s,
		resMap:            make(map[typePair]*resMapEntry),
		packerBuilder:     packer.NewBuilder(),
		useFieldResolvers: useFieldResolvers,
	}
}

func (b *execBuilder) finish() error {
	for _, entry := range b.resMap {
		for _, target := range entry.targets {
			*target = entry.exec
		}
	}

	return b.packerBuilder.Finish()
}

func (b *execBuilder) assignExec(target *Resolvable, t ast.Type, resolverType reflect.Type) error {
	exec, err := b.lookupOrBuildExec(t, resolverType)
	if err != nil {
		return err
	}
	// assign exec to all targets waiting for this type
	k := typePair{t, resolverType}
	ref := b.resMap[k]
	ref.exec = exec
	ref.targets = append(ref.targets, target)
	return nil
}

func (b *execBuilder) lookupOrBuildExec(t ast.Type, resolverType reflect.Type) (Resolvable, error) {
	k := typePair{t, resolverType}
	ref, ok := b.resMap[k]
	if !ok {
		ref = &resMapEntry{}
		b.resMap[k] = ref
		if isObjectLikeType(t) {
			ref.exec = &Object{}
		}
		var err error
		ref.exec, err = b.makeExec(t, resolverType)
		if err != nil {
			ref.exec = nil
			delete(b.resMap, k)
			return nil, err
		}
	}
	return ref.exec, nil
}

func (b *execBuilder) makeExec(t ast.Type, resolverType reflect.Type) (Resolvable, error) {
	rawType := t
	var nonNull bool
	t, nonNull = unwrapNonNull(t)

	switch t := t.(type) {
	case *ast.ObjectTypeDefinition:
		return b.makeObjectExec(rawType, t.Name, t.Fields, nil, t.Interfaces, nonNull, resolverType)

	case *ast.InterfaceTypeDefinition:
		return b.makeObjectExec(rawType, t.Name, t.Fields, t.PossibleTypes, nil, nonNull, resolverType)

	case *ast.Union:
		return b.makeObjectExec(rawType, t.Name, nil, t.UnionMemberTypes, nil, nonNull, resolverType)
	}

	if !nonNull {
		if resolverType.Kind() != reflect.Pointer {
			return nil, fmt.Errorf("%s is not a pointer", resolverType)
		}
		resolverType = resolverType.Elem()
	}

	switch t := t.(type) {
	case *ast.ScalarTypeDefinition:
		return makeScalarExec(t, resolverType)

	case *ast.EnumTypeDefinition:
		return &Scalar{}, nil

	case *ast.List:
		if resolverType.Kind() != reflect.Slice {
			return nil, fmt.Errorf("%s is not a slice", resolverType)
		}
		e := &List{}
		if err := b.assignExec(&e.Elem, t.OfType, resolverType.Elem()); err != nil {
			return nil, err
		}
		return e, nil

	default:
		panic("invalid type: " + t.String())
	}
}

func makeScalarExec(t *ast.ScalarTypeDefinition, resolverType reflect.Type) (Resolvable, error) {
	implementsType := false
	switch r := reflect.New(resolverType).Interface().(type) {
	case *int32:
		implementsType = t.Name == "Int"
	case *float64:
		implementsType = t.Name == "Float"
	case *string:
		implementsType = t.Name == "String"
	case *bool:
		implementsType = t.Name == "Boolean"
	case decode.Unmarshaler:
		implementsType = r.ImplementsGraphQLType(t.Name)
	}

	if !implementsType {
		return nil, fmt.Errorf("can not use %s as %s", resolverType, t.Name)
	}
	return &Scalar{}, nil
}

func (b *execBuilder) makeObjectExec(rawType ast.Type, typeName string, fields ast.FieldsDefinition, possibleTypes []*ast.ObjectTypeDefinition, interfaces []*ast.InterfaceTypeDefinition, nonNull bool, resolverType reflect.Type) (*Object, error) {
	obj := b.objectShell(rawType, resolverType)
	if obj == nil {
		obj = &Object{}
	}
	return b.populateObjectExec(obj, typeName, fields, possibleTypes, interfaces, nonNull, resolverType)
}

func (b *execBuilder) populateObjectExec(obj *Object, typeName string, fields ast.FieldsDefinition, possibleTypes []*ast.ObjectTypeDefinition, interfaces []*ast.InterfaceTypeDefinition, nonNull bool, resolverType reflect.Type) (*Object, error) {
	if !nonNull {
		if resolverType.Kind() != reflect.Pointer && resolverType.Kind() != reflect.Interface {
			return nil, fmt.Errorf("%s is not a pointer or interface", resolverType)
		}
	}

	methodHasReceiver := resolverType.Kind() != reflect.Interface

	Fields := make(map[string]*Field)
	fieldBuildErrs := make(map[string]error)
	rt := unwrapPtr(resolverType)
	fieldsCount, fieldTagsCount := fieldCount(rt, map[string]int{}, map[string]int{})
	for _, f := range fields {
		var fieldIndex []int
		methodIndex := findMethod(resolverType, f.Name)
		if b.useFieldResolvers && methodIndex == -1 {
			// If a resolver field is ambiguous thrown an error unless there is exactly one field with the given graphql
			// reflect tag. In that case use the field with the reflect tag.
			if fieldTagsCount[f.Name] > 1 {
				return nil, fmt.Errorf("%s does not resolve %q: multiple fields have a graphql reflect tag %q", resolverType, typeName, f.Name)
			} else if fieldsCount[strings.ToLower(stripUnderscore(f.Name))] > 1 && fieldTagsCount[f.Name] != 1 {
				return nil, fmt.Errorf("%s does not resolve %q: ambiguous field %q", resolverType, typeName, f.Name)
			}
			fieldIndex = findField(rt, f.Name, []int{}, fieldTagsCount)
		}
		if methodIndex == -1 && len(fieldIndex) == 0 {
			var hint string
			if findMethod(reflect.PointerTo(resolverType), f.Name) != -1 {
				hint = " (hint: the method exists on the pointer type)"
			}
			if len(possibleTypes) != 0 {
				Fields[f.Name] = &Field{
					FieldDefinition: *f,
					TypeName:        typeName,
					MethodIndex:     -1,
					TraceLabel:      fmt.Sprintf("GraphQL field: %s.%s", typeName, f.Name),
				}
				fieldBuildErrs[f.Name] = fmt.Errorf("%s does not resolve %q: missing method for field %q%s", resolverType, typeName, f.Name, hint)
				continue
			}
			return nil, fmt.Errorf("%s does not resolve %q: missing method for field %q%s", resolverType, typeName, f.Name, hint)
		}

		var m reflect.Method
		var sf reflect.StructField
		if methodIndex != -1 {
			m = resolverType.Method(methodIndex)
		} else {
			sf = rt.FieldByIndex(fieldIndex)
		}
		fe, err := b.makeFieldExec(typeName, f, m, sf, methodIndex, fieldIndex, methodHasReceiver)
		if err != nil {
			if len(possibleTypes) != 0 {
				Fields[f.Name] = &Field{
					FieldDefinition: *f,
					TypeName:        typeName,
					MethodIndex:     -1,
					OutputType:      nil,
					TraceLabel:      fmt.Sprintf("GraphQL field: %s.%s", typeName, f.Name),
				}
				fieldBuildErrs[f.Name] = err
				continue
			}
			var resolverName string
			if methodIndex != -1 {
				resolverName = m.Name
			} else {
				resolverName = sf.Name
			}
			return nil, fmt.Errorf("%s\n\tused by (%s).%s", err, resolverType, resolverName)
		}
		Fields[f.Name] = fe
	}

	// Check type assertions when
	//	1) using method resolvers
	//	2) Or resolver is not an interface type
	typeAssertions := make(map[string]*TypeAssertion)
	if !b.useFieldResolvers || resolverType.Kind() != reflect.Interface {
		for _, impl := range possibleTypes {
			methodIndex := findMethod(resolverType, "To"+impl.Name)
			if methodIndex == -1 {
				return nil, fmt.Errorf("%s does not resolve %q: missing method %q to convert to %q", resolverType, typeName, "To"+impl.Name, impl.Name)
			}
			m := resolverType.Method(methodIndex)
			expectedIn := 0
			if methodHasReceiver {
				expectedIn = 1
			}
			if m.Type.NumIn() != expectedIn {
				return nil, fmt.Errorf("%s does not resolve %q: method %q shouldn't have any arguments", resolverType, typeName, "To"+impl.Name)
			}
			if m.Type.NumOut() != 2 {
				return nil, fmt.Errorf("%s does not resolve %q: method %q should return a value and a bool indicating success", resolverType, typeName, "To"+impl.Name)
			}
			a := &TypeAssertion{
				MethodIndex: methodIndex,
			}
			if err := b.assignExec(&a.TypeExec, impl, resolverType.Method(methodIndex).Type.Out(0)); err != nil {
				return nil, err
			}
			if len(Fields) == 0 {
				typeAssertions[impl.Name] = a
				continue
			}
			implOutType := resolverType.Method(methodIndex).Type.Out(0)
			implExec, err := b.lookupOrBuildExec(impl, implOutType)
			if err != nil {
				return nil, err
			}
			objExec, ok := implExec.(*Object)
			if !ok {
				return nil, fmt.Errorf("%s does not resolve %q: type assertion %q did not resolve to an object", resolverType, typeName, impl.Name)
			}
			for fieldName, field := range Fields {
				implField := objExec.Fields[fieldName]
				if implField == nil {
					continue
				}
				_, needsFallback := fieldBuildErrs[fieldName]
				if needsFallback || fieldHasImplementationSpecificArgs(field.FieldDefinition, implField.FieldDefinition) {
					field.Implementations = append(field.Implementations, &FieldImplementation{
						MethodIndex: methodIndex,
						Field:       implField,
					})
				}
				if needsFallback && field.OutputType == nil {
					field.OutputType = implField.OutputType
					if err := b.assignExec(&field.ValueExec, field.Type, implField.OutputType); err != nil {
						return nil, err
					}
				}
			}
			typeAssertions[impl.Name] = a
		}
	}

	for fieldName, err := range fieldBuildErrs {
		if len(Fields[fieldName].Implementations) == 0 {
			return nil, fmt.Errorf("%s\n\tused by (%s).%s", err, resolverType, fieldName)
		}
	}

	ifaces := make(map[string]struct{})
	for _, iface := range interfaces {
		ifaces[iface.Name] = struct{}{}
	}

	obj.Name = typeName
	obj.Fields = Fields
	obj.TypeAssertions = typeAssertions
	obj.Interfaces = ifaces
	return obj, nil
}

func (b *execBuilder) objectShell(t ast.Type, resolverType reflect.Type) *Object {
	ref, ok := b.resMap[typePair{t, resolverType}]
	if !ok {
		return nil
	}
	obj, _ := ref.exec.(*Object)
	return obj
}

var (
	contextType = reflect.TypeFor[context.Context]()
	errorType   = reflect.TypeFor[error]()
)

func (b *execBuilder) makeFieldExec(typeName string, f *ast.FieldDefinition, m reflect.Method, sf reflect.StructField, methodIndex int, fieldIndex []int, methodHasReceiver bool) (*Field, error) {
	var argsPacker *packer.StructPacker
	var hasError bool
	var hasContext bool
	var isFieldFunc bool

	if methodIndex == -1 && len(fieldIndex) > 0 {
		if sf.Type.Kind() == reflect.Func {
			m.Type = sf.Type
			methodHasReceiver = false
			isFieldFunc = true
		}
	}
	// Validate resolver method only when there is one
	if methodIndex != -1 || isFieldFunc {
		in := make([]reflect.Type, m.Type.NumIn())
		for i := range in {
			in[i] = m.Type.In(i)
		}
		if methodHasReceiver {
			in = in[1:] // first parameter is receiver
		}

		hasContext = len(in) > 0 && in[0] == contextType
		if hasContext {
			in = in[1:]
		}

		if len(f.Arguments) > 0 {
			if len(in) == 0 {
				return nil, fmt.Errorf("must have `args struct { ... }` argument for field arguments")
			}
			var err error
			argsPacker, err = b.packerBuilder.MakeStructPacker(f.Arguments, in[0])
			if err != nil {
				return nil, err
			}
			in = in[1:]
		}

		if len(in) > 0 {
			return nil, fmt.Errorf("too many arguments")
		}

		maxNumOfReturns := 2
		if m.Type.NumOut() < maxNumOfReturns-1 {
			return nil, fmt.Errorf("too few return values")
		}

		if m.Type.NumOut() > maxNumOfReturns {
			return nil, fmt.Errorf("too many return values")
		}

		hasError = m.Type.NumOut() == maxNumOfReturns
		if hasError {
			if m.Type.Out(maxNumOfReturns-1) != errorType {
				return nil, fmt.Errorf(`must have "error" as its last return value`)
			}
		}
	}

	fe := &Field{
		FieldDefinition: *f,
		TypeName:        typeName,
		MethodIndex:     methodIndex,
		FieldIndex:      fieldIndex,
		IsFieldFunc:     isFieldFunc,
		HasContext:      hasContext,
		ArgsPacker:      argsPacker,
		HasError:        hasError,
		TraceLabel:      fmt.Sprintf("GraphQL field: %s.%s", typeName, f.Name),
	}
	var out reflect.Type
	if methodIndex != -1 || isFieldFunc {
		out = m.Type.Out(0)
		sub, ok := b.schema.RootOperationTypes["subscription"]
		if ok && typeName == sub.TypeName() && out.Kind() == reflect.Chan {
			out = m.Type.Out(0).Elem()
		}
	} else {
		out = sf.Type
	}

	fe.OutputType = out
	if err := b.assignExec(&fe.ValueExec, f.Type, out); err != nil {
		return nil, err
	}

	return fe, nil
}

func findMethod(t reflect.Type, name string) int {
	for i := 0; i < t.NumMethod(); i++ {
		if strings.EqualFold(stripUnderscore(name), stripUnderscore(t.Method(i).Name)) {
			return i
		}
	}
	return -1
}

func fieldHasImplementationSpecificArgs(ifaceField ast.FieldDefinition, implField ast.FieldDefinition) bool {
	for _, implArg := range implField.Arguments {
		if ifaceField.Arguments.Get(implArg.Name.Name) == nil {
			return true
		}
	}
	return false
}

func findField(t reflect.Type, name string, index []int, matchingTagsCount map[string]int) []int {
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)

		if field.Type.Kind() == reflect.Struct && field.Anonymous {
			newIndex := findField(field.Type, name, []int{i}, matchingTagsCount)
			if len(newIndex) > 1 {
				return append(index, newIndex...)
			}
		}

		if gt, ok := field.Tag.Lookup("graphql"); ok {
			if name == gt {
				return append(index, i)
			}
		}

		// The current field's tag didn't match, however, if the tag of another field matches,
		// then skip the name matching until we find the desired field with the correct tag.
		if matchingTagsCount[name] > 0 {
			continue
		}

		if strings.EqualFold(stripUnderscore(name), stripUnderscore(field.Name)) {
			return append(index, i)
		}
	}

	return index
}

// fieldCount helps resolve ambiguity when more than one embedded struct contains fields with the same name.
// or when a field has a `graphql` reflect tag with the same name as some other field causing name collision.
func fieldCount(t reflect.Type, count, tagsCount map[string]int) (map[string]int, map[string]int) {
	if t.Kind() != reflect.Struct {
		return nil, nil
	}

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		var fieldName, gt string
		var hasTag bool
		if gt, hasTag = field.Tag.Lookup("graphql"); hasTag && gt != "" {
			fieldName = gt
		} else {
			fieldName = strings.ToLower(stripUnderscore(field.Name))
		}

		if field.Type.Kind() == reflect.Struct && field.Anonymous {
			count, tagsCount = fieldCount(field.Type, count, tagsCount)
		} else {
			if _, ok := count[fieldName]; !ok {
				count[fieldName] = 0
			}
			count[fieldName]++
			if !hasTag {
				continue
			}
			if _, ok := count[gt]; !ok {
				tagsCount[gt] = 0
			}
			tagsCount[gt]++
		}
	}

	return count, tagsCount
}

func unwrapNonNull(t ast.Type) (ast.Type, bool) {
	if nn, ok := t.(*ast.NonNull); ok {
		return nn.OfType, true
	}
	return t, false
}

func stripUnderscore(s string) string {
	return strings.ReplaceAll(s, "_", "")
}

func unwrapPtr(t reflect.Type) reflect.Type {
	if t.Kind() == reflect.Pointer {
		return t.Elem()
	}
	return t
}

func isObjectLikeType(t ast.Type) bool {
	unwrapType, _ := unwrapNonNull(t)
	switch unwrapType.(type) {
	case *ast.ObjectTypeDefinition, *ast.InterfaceTypeDefinition, *ast.Union:
		return true
	default:
		return false
	}
}
