package modelgen

import (
	_ "embed"
	"fmt"
	"go/types"
	"os"
	"sort"
	"strings"
	"text/template"

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

	"github.com/99designs/gqlgen/codegen/config"
	"github.com/99designs/gqlgen/codegen/templates"
	"github.com/99designs/gqlgen/plugin"
)

//go:embed models.gotpl
var modelTemplate string

type (
	BuildMutateHook = func(b *ModelBuild) *ModelBuild
	FieldMutateHook = func(td *ast.Definition, fd *ast.FieldDefinition, f *Field) (*Field, error)
)

// DefaultFieldMutateHook is the default hook for the Plugin which applies the GoFieldHook and GoTagFieldHook.
func DefaultFieldMutateHook(td *ast.Definition, fd *ast.FieldDefinition, f *Field) (*Field, error) {
	return GoTagFieldHook(td, fd, f)
}

// DefaultBuildMutateHook is the default hook for the Plugin which mutate ModelBuild.
func DefaultBuildMutateHook(b *ModelBuild) *ModelBuild {
	return b
}

type ModelBuild struct {
	PackageName string
	Interfaces  []*Interface
	Models      []*Object
	Enums       []*Enum
	Scalars     []string
}

type Interface struct {
	Description string
	Name        string
	Fields      []*Field
	Implements  []string
	OmitCheck   bool
	Models      []*Object
}

type Object struct {
	Description string
	Name        string
	Fields      []*Field
	Implements  []string
}

type Field struct {
	Description string
	// Name is the field's name as it appears in the schema
	Name string
	// GoName is the field's name as it appears in the generated Go code
	GoName     string
	Type       types.Type
	Tag        string
	IsResolver bool
	Omittable  bool
}

type Enum struct {
	Description string
	Name        string
	Values      []*EnumValue
}

type EnumValue struct {
	Description string
	Name        string
}

func New() plugin.Plugin {
	return &Plugin{
		MutateHook: DefaultBuildMutateHook,
		FieldHook:  DefaultFieldMutateHook,
	}
}

type Plugin struct {
	MutateHook BuildMutateHook
	FieldHook  FieldMutateHook
}

var _ plugin.ConfigMutator = &Plugin{}

func (m *Plugin) Name() string {
	return "modelgen"
}

func (m *Plugin) MutateConfig(cfg *config.Config) error {
	b := &ModelBuild{
		PackageName: cfg.Model.Package,
	}

	for _, schemaType := range cfg.Schema.Types {
		if cfg.Models.UserDefined(schemaType.Name) {
			continue
		}
		switch schemaType.Kind {
		case ast.Interface, ast.Union:
			var fields []*Field
			var err error
			if !cfg.OmitGetters {
				fields, err = m.generateFields(cfg, schemaType)
				if err != nil {
					return err
				}
			}

			it := &Interface{
				Description: schemaType.Description,
				Name:        schemaType.Name,
				Implements:  schemaType.Interfaces,
				Fields:      fields,
				OmitCheck:   cfg.OmitInterfaceChecks,
			}

			// if the interface has a key directive as an entity interface, allow it to implement _Entity
			if schemaType.Directives.ForName("key") != nil {
				it.Implements = append(it.Implements, "_Entity")
			}

			b.Interfaces = append(b.Interfaces, it)
		case ast.Object, ast.InputObject:
			if cfg.IsRoot(schemaType) {
				if !cfg.OmitRootModels {
					b.Models = append(b.Models, &Object{
						Description: schemaType.Description,
						Name:        schemaType.Name,
					})
				}
				continue
			}

			fields, err := m.generateFields(cfg, schemaType)
			if err != nil {
				return err
			}

			it := &Object{
				Description: schemaType.Description,
				Name:        schemaType.Name,
				Fields:      fields,
			}

			// If Interface A implements interface B, and Interface C also implements interface B
			// then both A and C have methods of B.
			// The reason for checking unique is to prevent the same method B from being generated twice.
			uniqueMap := map[string]bool{}
			for _, implementor := range cfg.Schema.GetImplements(schemaType) {
				if !uniqueMap[implementor.Name] {
					it.Implements = append(it.Implements, implementor.Name)
					uniqueMap[implementor.Name] = true
				}
				// for interface implements
				for _, iface := range implementor.Interfaces {
					if !uniqueMap[iface] {
						it.Implements = append(it.Implements, iface)
						uniqueMap[iface] = true
					}
				}
			}

			b.Models = append(b.Models, it)
		case ast.Enum:
			it := &Enum{
				Name:        schemaType.Name,
				Description: schemaType.Description,
			}

			for _, v := range schemaType.EnumValues {
				it.Values = append(it.Values, &EnumValue{
					Name:        v.Name,
					Description: v.Description,
				})
			}

			b.Enums = append(b.Enums, it)
		case ast.Scalar:
			b.Scalars = append(b.Scalars, schemaType.Name)
		}
	}
	sort.Slice(b.Enums, func(i, j int) bool { return b.Enums[i].Name < b.Enums[j].Name })
	sort.Slice(b.Models, func(i, j int) bool { return b.Models[i].Name < b.Models[j].Name })
	sort.Slice(b.Interfaces, func(i, j int) bool { return b.Interfaces[i].Name < b.Interfaces[j].Name })

	// if we are not just turning all struct-type fields in generated structs into pointers, we need to at least
	// check for cyclical relationships and recursive structs
	if !cfg.StructFieldsAlwaysPointers {
		findAndHandleCyclicalRelationships(b)
	}

	for _, it := range b.Enums {
		cfg.Models.Add(it.Name, cfg.Model.ImportPath()+"."+templates.ToGoModelName(it.Name))
	}
	for _, it := range b.Models {
		cfg.Models.Add(it.Name, cfg.Model.ImportPath()+"."+templates.ToGoModelName(it.Name))
	}
	for _, it := range b.Interfaces {
		// On a given interface we want to keep a reference to all the models that implement it
		for _, model := range b.Models {
			for _, impl := range model.Implements {
				if impl == it.Name {
					// check if this isn't an implementation of an entity interface
					if impl != "_Entity" {
						// If this model has an implementation, add it to the Interface's Models
						it.Models = append(it.Models, model)
					}
				}
			}
		}
		cfg.Models.Add(it.Name, cfg.Model.ImportPath()+"."+templates.ToGoModelName(it.Name))
	}
	for _, it := range b.Scalars {
		cfg.Models.Add(it, "github.com/99designs/gqlgen/graphql.String")
	}

	if len(b.Models) == 0 && len(b.Enums) == 0 && len(b.Interfaces) == 0 && len(b.Scalars) == 0 {
		return nil
	}

	if m.MutateHook != nil {
		b = m.MutateHook(b)
	}

	getInterfaceByName := func(name string) *Interface {
		// Allow looking up interfaces, so template can generate getters for each field
		for _, i := range b.Interfaces {
			if i.Name == name {
				return i
			}
		}

		return nil
	}
	gettersGenerated := make(map[string]map[string]struct{})
	generateGetter := func(model *Object, field *Field) string {
		if model == nil || field == nil {
			return ""
		}

		// Let templates check if a given getter has been generated already
		typeGetters, exists := gettersGenerated[model.Name]
		if !exists {
			typeGetters = make(map[string]struct{})
			gettersGenerated[model.Name] = typeGetters
		}

		_, exists = typeGetters[field.GoName]
		typeGetters[field.GoName] = struct{}{}
		if exists {
			return ""
		}

		_, interfaceFieldTypeIsPointer := field.Type.(*types.Pointer)
		var structFieldTypeIsPointer bool
		for _, f := range model.Fields {
			if f.GoName == field.GoName {
				_, structFieldTypeIsPointer = f.Type.(*types.Pointer)
				break
			}
		}
		goType := templates.CurrentImports.LookupType(field.Type)
		if strings.HasPrefix(goType, "[]") {
			getter := fmt.Sprintf("func (this %s) Get%s() %s {\n", templates.ToGo(model.Name), field.GoName, goType)
			getter += fmt.Sprintf("\tif this.%s == nil { return nil }\n", field.GoName)
			getter += fmt.Sprintf("\tinterfaceSlice := make(%s, 0, len(this.%s))\n", goType, field.GoName)
			getter += fmt.Sprintf("\tfor _, concrete := range this.%s { interfaceSlice = append(interfaceSlice, ", field.GoName)
			if interfaceFieldTypeIsPointer && !structFieldTypeIsPointer {
				getter += "&"
			} else if !interfaceFieldTypeIsPointer && structFieldTypeIsPointer {
				getter += "*"
			}
			getter += "concrete) }\n"
			getter += "\treturn interfaceSlice\n"
			getter += "}"
			return getter
		}
		getter := fmt.Sprintf("func (this %s) Get%s() %s { return ", templates.ToGo(model.Name), field.GoName, goType)

		if interfaceFieldTypeIsPointer && !structFieldTypeIsPointer {
			getter += "&"
		} else if !interfaceFieldTypeIsPointer && structFieldTypeIsPointer {
			getter += "*"
		}

		getter += fmt.Sprintf("this.%s }", field.GoName)
		return getter
	}
	funcMap := template.FuncMap{
		"getInterfaceByName": getInterfaceByName,
		"generateGetter":     generateGetter,
	}
	newModelTemplate := modelTemplate
	if cfg.Model.ModelTemplate != "" {
		newModelTemplate = readModelTemplate(cfg.Model.ModelTemplate)
	}

	err := templates.Render(templates.Options{
		PackageName:     cfg.Model.Package,
		Filename:        cfg.Model.Filename,
		Data:            b,
		GeneratedHeader: true,
		Packages:        cfg.Packages,
		Template:        newModelTemplate,
		Funcs:           funcMap,
	})
	if err != nil {
		return err
	}

	// We may have generated code in a package we already loaded, so we reload all packages
	// to allow packages to be compared correctly
	cfg.ReloadAllPackages()

	return nil
}

func (m *Plugin) generateFields(cfg *config.Config, schemaType *ast.Definition) ([]*Field, error) {
	binder := cfg.NewBinder()
	fields := make([]*Field, 0)

	for _, field := range schemaType.Fields {
		f, err := m.generateField(cfg, binder, schemaType, field)
		if err != nil {
			return nil, err
		}

		if f == nil {
			continue
		}

		fields = append(fields, f)
	}

	fields = append(fields, getExtraFields(cfg, schemaType.Name)...)

	return fields, nil
}

func (m *Plugin) generateField(
	cfg *config.Config,
	binder *config.Binder,
	schemaType *ast.Definition,
	field *ast.FieldDefinition,
) (*Field, error) {
	var typ types.Type
	fieldDef := cfg.Schema.Types[field.Type.Name()]

	if cfg.Models.UserDefined(field.Type.Name()) {
		var err error
		typ, err = binder.FindTypeFromName(cfg.Models[field.Type.Name()].Model[0])
		if err != nil {
			return nil, err
		}
	} else {
		switch fieldDef.Kind {
		case ast.Scalar:
			// no user defined model, referencing a default scalar
			typ = types.NewNamed(
				types.NewTypeName(0, cfg.Model.Pkg(), "string", nil),
				nil,
				nil,
			)

		case ast.Interface, ast.Union:
			// no user defined model, referencing a generated interface type
			typ = types.NewNamed(
				types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
				types.NewInterfaceType([]*types.Func{}, []types.Type{}),
				nil,
			)

		case ast.Enum:
			// no user defined model, must reference a generated enum
			typ = types.NewNamed(
				types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
				nil,
				nil,
			)

		case ast.Object, ast.InputObject:
			// no user defined model, must reference a generated struct
			typ = types.NewNamed(
				types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGoModelName(field.Type.Name()), nil),
				types.NewStruct(nil, nil),
				nil,
			)

		default:
			panic(fmt.Errorf("unknown ast type %s", fieldDef.Kind))
		}
	}

	name := templates.ToGo(field.Name)
	if nameOverride := cfg.Models[schemaType.Name].Fields[field.Name].FieldName; nameOverride != "" {
		name = nameOverride
	}

	typ = binder.CopyModifiersFromAst(field.Type, typ)

	if cfg.StructFieldsAlwaysPointers {
		if isStruct(typ) && (fieldDef.Kind == ast.Object || fieldDef.Kind == ast.InputObject) {
			typ = types.NewPointer(typ)
		}
	}

	// Replace to user-defined field type if provided.
	if userDefinedType := cfg.Models[schemaType.Name].Fields[field.Name].Type; userDefinedType != "" {
		typ = buildType(userDefinedType)
	}

	f := &Field{
		Name:        field.Name,
		GoName:      name,
		Type:        typ,
		Description: field.Description,
		Tag:         getStructTagFromField(cfg, field),
		Omittable:   cfg.NullableInputOmittable && schemaType.Kind == ast.InputObject && !field.Type.NonNull,
		IsResolver:  cfg.Models[schemaType.Name].Fields[field.Name].Resolver,
	}

	if omittable := cfg.Models[schemaType.Name].Fields[field.Name].Omittable; omittable != nil {
		f.Omittable = *omittable
	}

	if m.FieldHook != nil {
		mf, err := m.FieldHook(schemaType, field, f)
		if err != nil {
			return nil, fmt.Errorf("generror: field %v.%v: %w", schemaType.Name, field.Name, err)
		}

		if mf == nil {
			// the field hook wants to omit the field
			return nil, nil
		}

		f = mf
	}

	if f.IsResolver && cfg.OmitResolverFields {
		return nil, nil
	}

	if f.Omittable {
		if schemaType.Kind != ast.InputObject || field.Type.NonNull {
			return nil, fmt.Errorf("generror: field %v.%v: omittable is only applicable to nullable input fields", schemaType.Name, field.Name)
		}

		omittableType, err := binder.FindTypeFromName("github.com/99designs/gqlgen/graphql.Omittable")
		if err != nil {
			return nil, err
		}

		f.Type, err = binder.InstantiateType(omittableType, []types.Type{f.Type})
		if err != nil {
			return nil, fmt.Errorf("generror: field %v.%v: %w", schemaType.Name, field.Name, err)
		}
	}

	return f, nil
}

func getExtraFields(cfg *config.Config, modelName string) []*Field {
	modelcfg := cfg.Models[modelName]

	extraFieldsCount := len(modelcfg.ExtraFields) + len(modelcfg.EmbedExtraFields)
	if extraFieldsCount == 0 {
		return nil
	}

	extraFields := make([]*Field, 0, extraFieldsCount)

	makeExtraField := func(fname string, fspec config.ModelExtraField) *Field {
		ftype := buildType(fspec.Type)

		tag := `json:"-"`
		if fspec.OverrideTags != "" {
			tag = fspec.OverrideTags
		}

		return &Field{
			Name:        fname,
			GoName:      fname,
			Type:        ftype,
			Description: fspec.Description,
			Tag:         tag,
		}
	}

	if len(modelcfg.ExtraFields) > 0 {
		for fname, fspec := range modelcfg.ExtraFields {
			extraFields = append(extraFields, makeExtraField(fname, fspec))
		}
	}

	if len(modelcfg.EmbedExtraFields) > 0 {
		for _, fspec := range modelcfg.EmbedExtraFields {
			extraFields = append(extraFields, makeExtraField("", fspec))
		}
	}

	sort.Slice(extraFields, func(i, j int) bool {
		if extraFields[i].Name == "" && extraFields[j].Name == "" {
			return extraFields[i].Type.String() < extraFields[j].Type.String()
		}

		if extraFields[i].Name == "" {
			return false
		}

		if extraFields[j].Name == "" {
			return true
		}

		return extraFields[i].Name < extraFields[j].Name
	})

	return extraFields
}

func getStructTagFromField(cfg *config.Config, field *ast.FieldDefinition) string {
	var tags []string

	if !field.Type.NonNull && (cfg.EnableModelJsonOmitemptyTag == nil || *cfg.EnableModelJsonOmitemptyTag) {
		tags = append(tags, "omitempty")
	}

	if !field.Type.NonNull && (cfg.EnableModelJsonOmitzeroTag == nil || *cfg.EnableModelJsonOmitzeroTag) {
		tags = append(tags, "omitzero")
	}

	if len(tags) > 0 {
		return `json:"` + field.Name + `,` + strings.Join(tags, ",") + `"`
	}
	return `json:"` + field.Name + `"`
}

// GoTagFieldHook prepends the goTag directive to the generated Field f.
// When applying the Tag to the field, the field
// name is used if no value argument is present.
func GoTagFieldHook(td *ast.Definition, fd *ast.FieldDefinition, f *Field) (*Field, error) {
	args := make([]string, 0)
	for _, goTag := range fd.Directives.ForNames("goTag") {
		key := ""
		value := fd.Name

		if arg := goTag.Arguments.ForName("key"); arg != nil {
			if k, err := arg.Value.Value(nil); err == nil {
				key = k.(string)
			}
		}

		if arg := goTag.Arguments.ForName("value"); arg != nil {
			if v, err := arg.Value.Value(nil); err == nil {
				value = v.(string)
			}
		}

		args = append(args, key+":\""+value+"\"")
	}

	if len(args) > 0 {
		f.Tag = removeDuplicateTags(f.Tag + " " + strings.Join(args, " "))
	}

	return f, nil
}

// splitTagsBySpace split tags by space, except when space is inside quotes
func splitTagsBySpace(tagsString string) []string {
	var tags []string
	var currentTag string
	inQuotes := false

	for _, c := range tagsString {
		if c == '"' {
			inQuotes = !inQuotes
		}
		if c == ' ' && !inQuotes {
			tags = append(tags, currentTag)
			currentTag = ""
		} else {
			currentTag += string(c)
		}
	}
	tags = append(tags, currentTag)

	return tags
}

// containsInvalidSpace checks if the tagsString contains invalid space
func containsInvalidSpace(valuesString string) bool {
	// get rid of quotes
	valuesString = strings.ReplaceAll(valuesString, "\"", "")
	if strings.Contains(valuesString, ",") {
		// split by comma,
		values := strings.Split(valuesString, ",")
		for _, value := range values {
			if strings.TrimSpace(value) != value {
				return true
			}
		}
		return false
	}
	if strings.Contains(valuesString, ";") {
		// split by semicolon, which is common in gorm
		values := strings.Split(valuesString, ";")
		for _, value := range values {
			if strings.TrimSpace(value) != value {
				return true
			}
		}
		return false
	}
	// single value
	if strings.TrimSpace(valuesString) != valuesString {
		return true
	}
	return false
}

func removeDuplicateTags(t string) string {
	processed := make(map[string]bool)
	tt := splitTagsBySpace(t)
	returnTags := ""

	// iterate backwards through tags so appended goTag directives are prioritized
	for i := len(tt) - 1; i >= 0; i-- {
		ti := tt[i]
		// check if ti contains ":", and not contains any empty space. if not, tag is in wrong format
		// correct example: json:"name"
		if !strings.Contains(ti, ":") {
			panic(fmt.Errorf("wrong format of tags: %s. goTag directive should be in format: @goTag(key: \"something\", value:\"value\"), ", t))
		}

		kv := strings.Split(ti, ":")
		if len(kv) == 0 || processed[kv[0]] {
			continue
		}

		key := kv[0]
		value := strings.Join(kv[1:], ":")
		processed[key] = true
		if returnTags != "" {
			returnTags = " " + returnTags
		}

		isContained := containsInvalidSpace(value)
		if isContained {
			panic(fmt.Errorf("tag value should not contain any leading or trailing spaces: %s", value))
		}

		returnTags = key + ":" + value + returnTags
	}

	return returnTags
}

// GoFieldHook is a noop
// TODO: This will be removed in the next breaking release
func GoFieldHook(td *ast.Definition, fd *ast.FieldDefinition, f *Field) (*Field, error) {
	return f, nil
}

func isStruct(t types.Type) bool {
	_, is := t.Underlying().(*types.Struct)
	return is
}

// findAndHandleCyclicalRelationships checks for cyclical relationships between generated structs and replaces them
// with pointers. These relationships will produce compilation errors if they are not pointers.
// Also handles recursive structs.
func findAndHandleCyclicalRelationships(b *ModelBuild) {
	for ii, structA := range b.Models {
		for _, fieldA := range structA.Fields {
			if strings.Contains(fieldA.Type.String(), "NotCyclicalA") {
				fmt.Print()
			}
			if !isStruct(fieldA.Type) {
				continue
			}

			// the field Type string will be in the form "github.com/99designs/gqlgen/codegen/testserver/followschema.LoopA"
			// we only want the part after the last dot: "LoopA"
			// this could lead to false positives, as we are only checking the name of the struct type, but these
			// should be extremely rare, if it is even possible at all.
			fieldAStructNameParts := strings.Split(fieldA.Type.String(), ".")
			fieldAStructName := fieldAStructNameParts[len(fieldAStructNameParts)-1]

			// find this struct type amongst the generated structs
			for jj, structB := range b.Models {
				if structB.Name != fieldAStructName {
					continue
				}

				// check if structB contains a cyclical reference back to structA
				var cyclicalReferenceFound bool
				for _, fieldB := range structB.Fields {
					if !isStruct(fieldB.Type) {
						continue
					}

					fieldBStructNameParts := strings.Split(fieldB.Type.String(), ".")
					fieldBStructName := fieldBStructNameParts[len(fieldBStructNameParts)-1]
					if fieldBStructName == structA.Name {
						cyclicalReferenceFound = true
						fieldB.Type = types.NewPointer(fieldB.Type)
						// keep looping in case this struct has additional fields of this type
					}
				}

				// if this is a recursive struct (i.e. structA == structB), ensure that we only change this field to a pointer once
				if cyclicalReferenceFound && ii != jj {
					fieldA.Type = types.NewPointer(fieldA.Type)
					break
				}
			}
		}
	}
}

func readModelTemplate(customModelTemplate string) string {
	contentBytes, err := os.ReadFile(customModelTemplate)
	if err != nil {
		panic(err)
	}
	return string(contentBytes)
}
