package federation

import (
	_ "embed"
	"fmt"
	"sort"
	"strings"

	"github.com/vektah/gqlparser/v2/ast"

	"github.com/99designs/gqlgen/codegen"
	"github.com/99designs/gqlgen/codegen/config"
	"github.com/99designs/gqlgen/codegen/templates"
	"github.com/99designs/gqlgen/internal/rewrite"
	"github.com/99designs/gqlgen/plugin"
	"github.com/99designs/gqlgen/plugin/federation/fieldset"
)

//go:embed federation.gotpl
var federationTemplate string

//go:embed requires.gotpl
var explicitRequiresTemplate string

type federation struct {
	Entities       []*Entity
	Version        int
	PackageOptions map[string]bool
}

// New returns a federation plugin that injects
// federated directives and types into the schema
func New(version int) plugin.Plugin {
	if version == 0 {
		version = 1
	}

	return &federation{Version: version}
}

// Name returns the plugin name
func (f *federation) Name() string {
	return "federation"
}

// MutateConfig mutates the configuration
func (f *federation) MutateConfig(cfg *config.Config) error {
	builtins := config.TypeMap{
		"_Service": {
			Model: config.StringList{
				"github.com/99designs/gqlgen/plugin/federation/fedruntime.Service",
			},
		},
		"_Entity": {
			Model: config.StringList{
				"github.com/99designs/gqlgen/plugin/federation/fedruntime.Entity",
			},
		},
		"Entity": {
			Model: config.StringList{
				"github.com/99designs/gqlgen/plugin/federation/fedruntime.Entity",
			},
		},
		"_Any": {
			Model: config.StringList{"github.com/99designs/gqlgen/graphql.Map"},
		},
		"federation__Scope": {
			Model: config.StringList{"github.com/99designs/gqlgen/graphql.String"},
		},
		"federation__Policy": {
			Model: config.StringList{"github.com/99designs/gqlgen/graphql.String"},
		},
	}

	for typeName, entry := range builtins {
		if cfg.Models.Exists(typeName) {
			return fmt.Errorf("%v already exists which must be reserved when Federation is enabled", typeName)
		}
		cfg.Models[typeName] = entry
	}
	cfg.Directives["external"] = config.DirectiveConfig{SkipRuntime: true}
	cfg.Directives["requires"] = config.DirectiveConfig{SkipRuntime: true}
	cfg.Directives["provides"] = config.DirectiveConfig{SkipRuntime: true}
	cfg.Directives["key"] = config.DirectiveConfig{SkipRuntime: true}
	cfg.Directives["extends"] = config.DirectiveConfig{SkipRuntime: true}

	// Federation 2 specific directives
	if f.Version == 2 {
		cfg.Directives["shareable"] = config.DirectiveConfig{SkipRuntime: true}
		cfg.Directives["link"] = config.DirectiveConfig{SkipRuntime: true}
		cfg.Directives["tag"] = config.DirectiveConfig{SkipRuntime: true}
		cfg.Directives["override"] = config.DirectiveConfig{SkipRuntime: true}
		cfg.Directives["inaccessible"] = config.DirectiveConfig{SkipRuntime: true}
		cfg.Directives["authenticated"] = config.DirectiveConfig{SkipRuntime: true}
		cfg.Directives["requiresScopes"] = config.DirectiveConfig{SkipRuntime: true}
		cfg.Directives["policy"] = config.DirectiveConfig{SkipRuntime: true}
		cfg.Directives["interfaceObject"] = config.DirectiveConfig{SkipRuntime: true}
		cfg.Directives["composeDirective"] = config.DirectiveConfig{SkipRuntime: true}
	}

	return nil
}

func (f *federation) InjectSourceEarly() *ast.Source {
	input := ``

	// add version-specific changes on key directive, as well as adding the new directives for federation 2
	if f.Version == 1 {
		input += `
	directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
	directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
	directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
	directive @extends on OBJECT | INTERFACE
	directive @external on FIELD_DEFINITION
	scalar _Any
	scalar _FieldSet
`
	} else if f.Version == 2 {
		input += `
	directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
	directive @composeDirective(name: String!) repeatable on SCHEMA
	directive @extends on OBJECT | INTERFACE
	directive @external on OBJECT | FIELD_DEFINITION
	directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
	directive @inaccessible on
	  | ARGUMENT_DEFINITION
	  | ENUM
	  | ENUM_VALUE
	  | FIELD_DEFINITION
	  | INPUT_FIELD_DEFINITION
	  | INPUT_OBJECT
	  | INTERFACE
	  | OBJECT
	  | SCALAR
	  | UNION
	directive @interfaceObject on OBJECT
	directive @link(import: [String!], url: String!) repeatable on SCHEMA
	directive @override(from: String!, label: String) on FIELD_DEFINITION
	directive @policy(policies: [[federation__Policy!]!]!) on 
	  | FIELD_DEFINITION
	  | OBJECT
	  | INTERFACE
	  | SCALAR
	  | ENUM
	directive @provides(fields: FieldSet!) on FIELD_DEFINITION
	directive @requires(fields: FieldSet!) on FIELD_DEFINITION
	directive @requiresScopes(scopes: [[federation__Scope!]!]!) on 
	  | FIELD_DEFINITION
	  | OBJECT
	  | INTERFACE
	  | SCALAR
	  | ENUM
	directive @shareable repeatable on FIELD_DEFINITION | OBJECT
	directive @tag(name: String!) repeatable on
	  | ARGUMENT_DEFINITION
	  | ENUM
	  | ENUM_VALUE
	  | FIELD_DEFINITION
	  | INPUT_FIELD_DEFINITION
	  | INPUT_OBJECT
	  | INTERFACE
	  | OBJECT
	  | SCALAR
	  | UNION
	scalar _Any
	scalar FieldSet
	scalar federation__Policy
	scalar federation__Scope
`
	}
	return &ast.Source{
		Name:    "federation/directives.graphql",
		Input:   input,
		BuiltIn: true,
	}
}

// InjectSourceLate creates a GraphQL Entity type with all
// the fields that had the @key directive
func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source {
	f.setEntities(schema)

	var entities, resolvers, entityResolverInputDefinitions string
	for _, e := range f.Entities {
		if e.Def.Kind != ast.Interface {
			if entities != "" {
				entities += " | "
			}
			entities += e.Name
		} else if len(schema.GetPossibleTypes(e.Def)) == 0 {
			fmt.Println(
				"skipping @key field on interface " + e.Def.Name + " as no types implement it",
			)
		}

		for _, r := range e.Resolvers {
			if e.Multi {
				if entityResolverInputDefinitions != "" {
					entityResolverInputDefinitions += "\n\n"
				}
				entityResolverInputDefinitions += "input " + r.InputTypeName + " {\n"
				for _, keyField := range r.KeyFields {
					entityResolverInputDefinitions += fmt.Sprintf(
						"\t%s: %s\n",
						keyField.Field.ToGo(),
						keyField.Definition.Type.String(),
					)
				}
				entityResolverInputDefinitions += "}"
				resolvers += fmt.Sprintf("\t%s(reps: [%s]!): [%s]\n", r.ResolverName, r.InputTypeName, e.Name)
			} else {
				resolverArgs := ""
				for _, keyField := range r.KeyFields {
					resolverArgs += fmt.Sprintf("%s: %s,", keyField.Field.ToGoPrivate(), keyField.Definition.Type.String())
				}
				resolvers += fmt.Sprintf("\t%s(%s): %s!\n", r.ResolverName, resolverArgs, e.Name)
			}
		}
	}

	var blocks []string
	if entities != "" {
		entities = `# a union of all types that use the @key directive
union _Entity = ` + entities
		blocks = append(blocks, entities)
	}

	// resolvers can be empty if a service defines only "empty
	// extend" types.  This should be rare.
	if resolvers != "" {
		if entityResolverInputDefinitions != "" {
			blocks = append(blocks, entityResolverInputDefinitions)
		}
		resolvers = `# fake type to build resolver interfaces for users to implement
type Entity {
	` + resolvers + `
}`
		blocks = append(blocks, resolvers)
	}

	_serviceTypeDef := `type _Service {
  sdl: String
}`
	blocks = append(blocks, _serviceTypeDef)

	var additionalQueryFields string
	// Quote from the Apollo Federation subgraph specification:
	// If no types are annotated with the key directive, then the
	// _Entity union and _entities field should be removed from the schema
	if len(f.Entities) > 0 {
		additionalQueryFields += `  _entities(representations: [_Any!]!): [_Entity]!
`
	}
	// _service field is required in any case
	additionalQueryFields += `  _service: _Service!`

	extendTypeQueryDef := `extend type ` + schema.Query.Name + ` {
` + additionalQueryFields + `
}`
	blocks = append(blocks, extendTypeQueryDef)

	return &ast.Source{
		Name:    "federation/entity.graphql",
		BuiltIn: true,
		Input:   "\n" + strings.Join(blocks, "\n\n") + "\n",
	}
}

func (f *federation) GenerateCode(data *codegen.Data) error {
	// requires imports
	requiresImports := make(map[string]bool, 0)
	requiresImports["context"] = true
	requiresImports["fmt"] = true

	requiresEntities := make(map[string]*Entity, 0)

	// Save package options on f for template use
	f.PackageOptions = data.Config.Federation.Options

	if len(f.Entities) > 0 {
		if data.Objects.ByName("Entity") != nil {
			data.Objects.ByName("Entity").Root = true
		}
		for _, e := range f.Entities {
			obj := data.Objects.ByName(e.Def.Name)

			if e.Def.Kind == ast.Interface {
				if len(data.Interfaces[e.Def.Name].Implementors) == 0 {
					fmt.Println(
						"skipping @key field on interface " + e.Def.Name + " as no types implement it",
					)
					continue
				}
				obj = data.Objects.ByName(data.Interfaces[e.Def.Name].Implementors[0].Name)
			}

			for _, r := range e.Resolvers {
				// fill in types for key fields
				//
				for _, keyField := range r.KeyFields {
					if len(keyField.Field) == 0 {
						fmt.Println(
							"skipping @key field " + keyField.Definition.Name + " in " + r.ResolverName + " in " + e.Def.Name,
						)
						continue
					}
					cgField := keyField.Field.TypeReference(obj, data.Objects)
					keyField.Type = cgField.TypeReference
				}
			}

			// fill in types for requires fields
			//
			for _, reqField := range e.Requires {
				if len(reqField.Field) == 0 {
					fmt.Println("skipping @requires field " + reqField.Name + " in " + e.Def.Name)
					continue
				}
				// keep track of which entities have requires
				requiresEntities[e.Def.Name] = e
				// make a proper import path
				typeString := strings.Split(obj.Type.String(), ".")
				requiresImports[strings.Join(typeString[:len(typeString)-1], ".")] = true

				cgField := reqField.Field.TypeReference(obj, data.Objects)
				reqField.Type = cgField.TypeReference
			}

			// add type info to entity
			e.Type = obj.Type
		}
	}

	// fill in types for resolver inputs
	//
	for _, entity := range f.Entities {
		if !entity.Multi {
			continue
		}

		for _, resolver := range entity.Resolvers {
			obj := data.Inputs.ByName(resolver.InputTypeName)
			if obj == nil {
				return fmt.Errorf("input object %s not found", resolver.InputTypeName)
			}

			resolver.InputType = obj.Type
		}
	}

	if data.Config.Federation.Options["explicit_requires"] && len(requiresEntities) > 0 {
		// check for existing requires functions
		type Populator struct {
			FuncName       string
			Exists         bool
			Comment        string
			Implementation string
			Entity         *Entity
		}
		populators := make([]Populator, 0)

		rewriter, err := rewrite.New(data.Config.Federation.Dir())
		if err != nil {
			return err
		}

		for name, entity := range requiresEntities {
			populator := Populator{
				FuncName: fmt.Sprintf("Populate%sRequires", name),
				Entity:   entity,
			}

			populator.Comment = strings.TrimSpace(strings.TrimLeft(rewriter.GetMethodComment("executionContext", populator.FuncName), `\`))
			populator.Implementation = strings.TrimSpace(rewriter.GetMethodBody("executionContext", populator.FuncName))

			if populator.Implementation == "" {
				populator.Exists = false
				populator.Implementation = fmt.Sprintf("panic(fmt.Errorf(\"not implemented: %v\"))", populator.FuncName)
			}
			populators = append(populators, populator)
		}

		sort.Slice(populators, func(i, j int) bool {
			return populators[i].FuncName < populators[j].FuncName
		})

		requiresFile := data.Config.Federation.Dir() + "/federation.requires.go"
		existingImports := rewriter.ExistingImports(requiresFile)
		for _, imp := range existingImports {
			if imp.Alias == "" {
				// import exists in both places, remove
				delete(requiresImports, imp.ImportPath)
			}
		}

		for k := range requiresImports {
			existingImports = append(existingImports, rewrite.Import{ImportPath: k})
		}

		// render requires populators
		err = templates.Render(templates.Options{
			PackageName: data.Config.Federation.Package,
			Filename:    requiresFile,
			Data: struct {
				federation
				ExistingImports []rewrite.Import
				Populators      []Populator
				OriginalSource  string
			}{*f, existingImports, populators, ""},
			GeneratedHeader: false,
			Packages:        data.Config.Packages,
			Template:        explicitRequiresTemplate,
		})
		if err != nil {
			return err
		}
	}

	return templates.Render(templates.Options{
		PackageName: data.Config.Federation.Package,
		Filename:    data.Config.Federation.Filename,
		Data: struct {
			federation
			UsePointers bool
		}{*f, data.Config.ResolversAlwaysReturnPointers},
		GeneratedHeader: true,
		Packages:        data.Config.Packages,
		Template:        federationTemplate,
	})
}

func (f *federation) setEntities(schema *ast.Schema) {
	for _, schemaType := range schema.Types {
		keys, ok := isFederatedEntity(schemaType)
		if !ok {
			continue
		}

		if (schemaType.Kind == ast.Interface) && (len(schema.GetPossibleTypes(schemaType)) == 0) {
			fmt.Printf("@key directive found on unused \"interface %s\". Will be ignored.\n", schemaType.Name)
			continue
		}

		e := &Entity{
			Name:      schemaType.Name,
			Def:       schemaType,
			Resolvers: nil,
			Requires:  nil,
		}

		// Let's process custom entity resolver settings.
		dir := schemaType.Directives.ForName("entityResolver")
		if dir != nil {
			if dirArg := dir.Arguments.ForName("multi"); dirArg != nil {
				if dirVal, err := dirArg.Value.Value(nil); err == nil {
					e.Multi = dirVal.(bool)
				}
			}
		}

		// If our schema has a field with a type defined in
		// another service, then we need to define an "empty
		// extend" of that type in this service, so this service
		// knows what the type is like.  But the graphql-server
		// will never ask us to actually resolve this "empty
		// extend", so we don't require a resolver function for
		// it.  (Well, it will never ask in practice; it's
		// unclear whether the spec guarantees this.  See
		// https://github.com/apollographql/apollo-server/issues/3852
		// ).  Example:
		//    type MyType {
		//       myvar: TypeDefinedInOtherService
		//    }
		//    // Federation needs this type, but
		//    // it doesn't need a resolver for it!
		//    extend TypeDefinedInOtherService @key(fields: "id") {
		//       id: ID @external
		//    }
		if !e.allFieldsAreExternal(f.Version) {
			for _, dir := range keys {
				if len(dir.Arguments) > 2 {
					panic("More than two arguments provided for @key declaration.")
				}
				var arg *ast.Argument

				// since keys are able to now have multiple arguments, we need to check both possible for a possible @key(fields="" fields="")
				for _, a := range dir.Arguments {
					if a.Name == "fields" {
						if arg != nil {
							panic("More than one `fields` provided for @key declaration.")
						}
						arg = a
					}
				}

				keyFieldSet := fieldset.New(arg.Value.Raw, nil)

				keyFields := make([]*KeyField, len(keyFieldSet))
				resolverFields := []string{}
				for i, field := range keyFieldSet {
					def := field.FieldDefinition(schemaType, schema)

					if def == nil {
						panic(fmt.Sprintf("no field for %v", field))
					}

					keyFields[i] = &KeyField{Definition: def, Field: field}
					resolverFields = append(resolverFields, keyFields[i].Field.ToGo())
				}

				resolverFieldsToGo := schemaType.Name + "By" + strings.Join(resolverFields, "And")
				var resolverName string
				if e.Multi {
					resolverFieldsToGo += "s" // Pluralize for better API readability
					resolverName = fmt.Sprintf("findMany%s", resolverFieldsToGo)
				} else {
					resolverName = fmt.Sprintf("find%s", resolverFieldsToGo)
				}

				e.Resolvers = append(e.Resolvers, &EntityResolver{
					ResolverName:  resolverName,
					KeyFields:     keyFields,
					InputTypeName: resolverFieldsToGo + "Input",
				})
			}

			e.Requires = []*Requires{}
			for _, f := range schemaType.Fields {
				dir := f.Directives.ForName("requires")
				if dir == nil {
					continue
				}
				if len(dir.Arguments) != 1 || dir.Arguments[0].Name != "fields" {
					panic("Exactly one `fields` argument needed for @requires declaration.")
				}
				requiresFieldSet := fieldset.New(dir.Arguments[0].Value.Raw, nil)
				for _, field := range requiresFieldSet {
					e.Requires = append(e.Requires, &Requires{
						Name:  field.ToGoPrivate(),
						Field: field,
					})
				}
			}
		}
		f.Entities = append(f.Entities, e)
	}

	// make sure order remains stable across multiple builds
	sort.Slice(f.Entities, func(i, j int) bool {
		return f.Entities[i].Name < f.Entities[j].Name
	})
}

func isFederatedEntity(schemaType *ast.Definition) ([]*ast.Directive, bool) {
	switch schemaType.Kind {
	case ast.Object:
		keys := schemaType.Directives.ForNames("key")
		if len(keys) > 0 {
			return keys, true
		}
	case ast.Interface:
		keys := schemaType.Directives.ForNames("key")
		if len(keys) > 0 {
			return keys, true
		}

		// TODO: support @extends for interfaces
		if dir := schemaType.Directives.ForName("extends"); dir != nil {
			panic(
				fmt.Sprintf(
					"@extends directive is not currently supported for interfaces, use \"extend interface %s\" instead.",
					schemaType.Name,
				))
		}
	default:
		// ignore
	}
	return nil, false
}
