package kong

import (
	"fmt"
	"reflect"
	"strings"
)

// Plugins are dynamically embedded command-line structures.
//
// Each element in the Plugins list *must* be a pointer to a structure.
type Plugins []any

func build(k *Kong, ast any) (app *Application, err error) {
	v := reflect.ValueOf(ast)
	iv := reflect.Indirect(v)
	if v.Kind() != reflect.Ptr || iv.Kind() != reflect.Struct {
		return nil, fmt.Errorf("expected a pointer to a struct but got %T", ast)
	}

	app = &Application{}
	extraFlags := k.extraFlags()
	seenFlags := map[string]bool{}
	for _, flag := range extraFlags {
		seenFlags[flag.Name] = true
	}

	node, err := buildNode(k, iv, ApplicationNode, newEmptyTag(), seenFlags)
	if err != nil {
		return nil, err
	}
	if len(node.Positional) > 0 && len(node.Children) > 0 {
		return nil, fmt.Errorf("can't mix positional arguments and branching arguments on %T", ast)
	}
	app.Node = node
	app.Node.Flags = append(extraFlags, app.Node.Flags...)
	app.Tag = newEmptyTag()
	app.Tag.Vars = k.vars
	return app, nil
}

func dashedString(s string) string {
	return strings.Join(camelCase(s), "-")
}

type flattenedField struct {
	field reflect.StructField
	value reflect.Value
	tag   *Tag
}

func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err error) {
	v = reflect.Indirect(v)
	if v.Kind() != reflect.Struct {
		return out, nil
	}
	ignored := map[string]bool{}
	for i := 0; i < v.NumField(); i++ {
		ft := v.Type().Field(i)
		fv := v.Field(i)
		tag, err := parseTag(v, ft)
		if err != nil {
			return nil, err
		}
		if tag.Ignored || ignored[ft.Name] {
			ignored[ft.Name] = true
			continue
		}
		// Assign group if it's not already set.
		if tag.Group == "" {
			tag.Group = ptag.Group
		}
		// Accumulate prefixes.
		tag.Prefix = ptag.Prefix + tag.Prefix
		tag.EnvPrefix = ptag.EnvPrefix + tag.EnvPrefix
		tag.XorPrefix = ptag.XorPrefix + tag.XorPrefix
		// Combine parent vars.
		tag.Vars = ptag.Vars.CloneWith(tag.Vars)
		// Command and embedded structs can be pointers, so we hydrate them now.
		if (tag.Cmd || tag.Embed) && ft.Type.Kind() == reflect.Ptr {
			fv = reflect.New(ft.Type.Elem()).Elem()
			v.FieldByIndex(ft.Index).Set(fv.Addr())
		}
		if !ft.Anonymous && !tag.Embed {
			if fv.CanSet() {
				field := flattenedField{field: ft, value: fv, tag: tag}
				out = append(out, field)
			}
			continue
		}

		// Embedded type.
		if fv.Kind() == reflect.Interface {
			fv = fv.Elem()
		} else if fv.Type() == reflect.TypeOf(Plugins{}) {
			for i := 0; i < fv.Len(); i++ {
				fields, ferr := flattenedFields(fv.Index(i).Elem(), tag)
				if ferr != nil {
					return nil, ferr
				}
				out = append(out, fields...)
			}
			continue
		}
		sub, err := flattenedFields(fv, tag)
		if err != nil {
			return nil, err
		}
		out = append(out, sub...)
	}
	out = removeIgnored(out, ignored)
	return out, nil
}

func removeIgnored(fields []flattenedField, ignored map[string]bool) []flattenedField {
	j := 0
	for i := 0; i < len(fields); i++ {
		if ignored[fields[i].field.Name] {
			continue
		}
		if i != j {
			fields[j] = fields[i]
		}
		j++
	}
	if j != len(fields) {
		fields = fields[:j]
	}
	return fields
}

// Build a Node in the Kong data model.
//
// "v" is the value to create the node from, "typ" is the output Node type.
func buildNode(k *Kong, v reflect.Value, typ NodeType, tag *Tag, seenFlags map[string]bool) (*Node, error) { //nolint:gocyclo
	node := &Node{
		Type:   typ,
		Target: v,
		Tag:    tag,
	}
	fields, err := flattenedFields(v, tag)
	if err != nil {
		return nil, err
	}

MAIN:
	for _, field := range fields {
		for _, r := range k.ignoreFields {
			if r.MatchString(v.Type().Name() + "." + field.field.Name) {
				continue MAIN
			}
		}

		ft := field.field
		fv := field.value

		tag := field.tag
		name := tag.Name
		if name == "" {
			name = tag.Prefix + k.flagNamer(ft.Name)
		} else {
			name = tag.Prefix + name
		}

		if len(tag.Envs) != 0 {
			for i := range tag.Envs {
				tag.Envs[i] = tag.EnvPrefix + tag.Envs[i]
			}
		}

		if len(tag.Xor) != 0 {
			for i := range tag.Xor {
				tag.Xor[i] = tag.XorPrefix + tag.Xor[i]
			}
		}

		if len(tag.And) != 0 {
			for i := range tag.And {
				tag.And[i] = tag.XorPrefix + tag.And[i]
			}
		}

		// Nested structs are either commands or args, unless they implement the Mapper interface.
		if field.value.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil {
			typ := CommandNode
			if tag.Arg {
				typ = ArgumentNode
			}
			err = buildChild(k, node, typ, v, ft, fv, tag, name, seenFlags)
		} else {
			err = buildField(k, node, v, ft, fv, tag, name, seenFlags)
		}
		if err != nil {
			return nil, err
		}
	}

	// Validate if there are no duplicate names
	if err := checkDuplicateNames(node, v); err != nil {
		return nil, err
	}

	// "Unsee" flags.
	for _, flag := range node.Flags {
		delete(seenFlags, "--"+flag.Name)
		if flag.Short != 0 {
			delete(seenFlags, "-"+string(flag.Short))
		}
		if negFlag := negatableFlagName(flag.Name, flag.Tag.Negatable); negFlag != "" {
			delete(seenFlags, negFlag)
		}
		for _, aflag := range flag.Aliases {
			delete(seenFlags, "--"+aflag)
		}
	}

	if err := validatePositionalArguments(node); err != nil {
		return nil, err
	}

	return node, nil
}

func validatePositionalArguments(node *Node) error {
	var last *Value
	for i, curr := range node.Positional {
		if last != nil {
			// Scan through argument positionals to ensure optional is never before a required.
			if !last.Required && curr.Required {
				return fmt.Errorf("%s: required %q cannot come after optional %q", node.FullPath(), curr.Name, last.Name)
			}

			// Cumulative argument needs to be last.
			if last.IsCumulative() {
				return fmt.Errorf("%s: argument %q cannot come after cumulative %q", node.FullPath(), curr.Name, last.Name)
			}
		}

		last = curr
		curr.Position = i
	}

	return nil
}

func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error {
	child, err := buildNode(k, fv, typ, newEmptyTag(), seenFlags)
	if err != nil {
		return err
	}
	child.Name = name
	child.Tag = tag
	child.Parent = node
	child.Help = tag.Help
	child.Hidden = tag.Hidden
	child.Group = buildGroupForKey(k, tag.Group)
	child.Aliases = tag.Aliases

	if provider, ok := fv.Addr().Interface().(HelpProvider); ok {
		child.Detail = provider.Help()
	}

	// A branching argument. This is a bit hairy, as we let buildNode() do the parsing, then check that
	// a positional argument is provided to the child, and move it to the branching argument field.
	if tag.Arg {
		if len(child.Positional) == 0 {
			return failField(v, ft, "positional branch must have at least one child positional argument named %q", name)
		}
		if child.Positional[0].Name != name {
			return failField(v, ft, "first field in positional branch must have the same name as the parent field (%s).", child.Name)
		}

		child.Argument = child.Positional[0]
		child.Positional = child.Positional[1:]
		if child.Help == "" {
			child.Help = child.Argument.Help
		}
	} else {
		if tag.HasDefault {
			if node.DefaultCmd != nil {
				return failField(v, ft, "can't have more than one default command under %s", node.Summary())
			}
			if tag.Default != "withargs" && (len(child.Children) > 0 || len(child.Positional) > 0) {
				return failField(v, ft, "default command %s must not have subcommands or arguments", child.Summary())
			}
			node.DefaultCmd = child
		}
		if tag.Passthrough {
			if len(child.Children) > 0 || len(child.Flags) > 0 {
				return failField(v, ft, "passthrough command %s must not have subcommands or flags", child.Summary())
			}
			if len(child.Positional) != 1 {
				return failField(v, ft, "passthrough command %s must contain exactly one positional argument", child.Summary())
			}
			if !checkPassthroughArg(child.Positional[0].Target) {
				return failField(v, ft, "passthrough command %s must contain exactly one positional argument of []string type", child.Summary())
			}
			child.Passthrough = true
		}
	}
	node.Children = append(node.Children, child)

	if len(child.Positional) > 0 && len(child.Children) > 0 {
		return failField(v, ft, "can't mix positional arguments and branching arguments")
	}

	return nil
}

func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error {
	mapper := k.registry.ForNamedValue(tag.Type, fv)
	if mapper == nil {
		return failField(v, ft, "unsupported field type %s, perhaps missing a cmd:\"\" tag?", ft.Type)
	}

	value := &Value{
		Name:            name,
		Help:            tag.Help,
		OrigHelp:        tag.Help,
		HasDefault:      tag.HasDefault,
		Default:         tag.Default,
		DefaultValue:    reflect.New(fv.Type()).Elem(),
		Mapper:          mapper,
		Tag:             tag,
		Target:          fv,
		Enum:            tag.Enum,
		Passthrough:     tag.Passthrough,
		PassthroughMode: tag.PassthroughMode,

		// Flags are optional by default, and args are required by default.
		Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional),
		Format:   tag.Format,
	}

	if tag.Arg {
		node.Positional = append(node.Positional, value)
	} else {
		if seenFlags["--"+value.Name] {
			return failField(v, ft, "duplicate flag --%s", value.Name)
		}
		seenFlags["--"+value.Name] = true
		for _, alias := range tag.Aliases {
			aliasFlag := "--" + alias
			if seenFlags[aliasFlag] {
				return failField(v, ft, "duplicate flag %s", aliasFlag)
			}
			seenFlags[aliasFlag] = true
		}
		if tag.Short != 0 {
			if seenFlags["-"+string(tag.Short)] {
				return failField(v, ft, "duplicate short flag -%c", tag.Short)
			}
			seenFlags["-"+string(tag.Short)] = true
		}
		if tag.Negatable != "" {
			negFlag := negatableFlagName(value.Name, tag.Negatable)
			if seenFlags[negFlag] {
				return failField(v, ft, "duplicate negation flag %s", negFlag)
			}
			seenFlags[negFlag] = true
		}
		flag := &Flag{
			Value:       value,
			Aliases:     tag.Aliases,
			Short:       tag.Short,
			PlaceHolder: tag.PlaceHolder,
			Envs:        tag.Envs,
			Group:       buildGroupForKey(k, tag.Group),
			Xor:         tag.Xor,
			And:         tag.And,
			Hidden:      tag.Hidden,
		}
		value.Flag = flag
		node.Flags = append(node.Flags, flag)
	}
	return nil
}

func buildGroupForKey(k *Kong, key string) *Group {
	if key == "" {
		return nil
	}
	for _, group := range k.groups {
		if group.Key == key {
			return &group
		}
	}

	// No group provided with kong.ExplicitGroups. We create one ad-hoc for this key.
	return &Group{
		Key:   key,
		Title: key,
	}
}

func checkDuplicateNames(node *Node, v reflect.Value) error {
	seenNames := make(map[string]struct{})
	for _, node := range node.Children {
		if _, ok := seenNames[node.Name]; ok {
			name := v.Type().Name()
			if name == "" {
				name = "<anonymous struct>"
			}
			return fmt.Errorf("duplicate command name %q in command %q", node.Name, name)
		}

		seenNames[node.Name] = struct{}{}
	}

	return nil
}
