package kong

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"reflect"
	"regexp"
	"strings"
)

var (
	callbackReturnSignature = reflect.TypeOf((*error)(nil)).Elem()
)

func failField(parent reflect.Value, field reflect.StructField, format string, args ...any) error {
	name := parent.Type().Name()
	if name == "" {
		name = "<anonymous struct>"
	}
	return fmt.Errorf("%s.%s: %s", name, field.Name, fmt.Sprintf(format, args...))
}

// Must creates a new Parser or panics if there is an error.
func Must(ast any, options ...Option) *Kong {
	k, err := New(ast, options...)
	if err != nil {
		panic(err)
	}
	return k
}

type usageOnError int

const (
	shortUsage usageOnError = iota + 1
	fullUsage
)

// Kong is the main parser type.
type Kong struct {
	// Grammar model.
	Model *Application

	// Termination function (defaults to os.Exit)
	Exit func(int)

	Stdout io.Writer
	Stderr io.Writer

	bindings     bindings
	loader       ConfigurationLoader
	resolvers    []Resolver
	registry     *Registry
	ignoreFields []*regexp.Regexp

	noDefaultHelp   bool
	allowHyphenated bool
	usageOnError    usageOnError
	help            HelpPrinter
	shortHelp       HelpPrinter
	helpFormatter   HelpValueFormatter
	helpOptions     HelpOptions
	helpFlag        *Flag
	groups          []Group
	vars            Vars
	flagNamer       func(string) string

	// Set temporarily by Options. These are applied after build().
	postBuildOptions []Option
	embedded         []embedded
	dynamicCommands  []*dynamicCommand

	hooks map[string][]reflect.Value
}

// New creates a new Kong parser on grammar.
//
// See the README (https://github.com/alecthomas/kong) for usage instructions.
func New(grammar any, options ...Option) (*Kong, error) {
	k := &Kong{
		Exit:          os.Exit,
		Stdout:        os.Stdout,
		Stderr:        os.Stderr,
		registry:      NewRegistry().RegisterDefaults(),
		vars:          Vars{},
		bindings:      bindings{},
		hooks:         make(map[string][]reflect.Value),
		helpFormatter: DefaultHelpValueFormatter,
		ignoreFields:  make([]*regexp.Regexp, 0),
		flagNamer: func(s string) string {
			return strings.ToLower(dashedString(s))
		},
	}

	options = append(options, Bind(k))

	for _, option := range options {
		if err := option.Apply(k); err != nil {
			return nil, err
		}
	}

	if k.help == nil {
		k.help = DefaultHelpPrinter
	}

	if k.shortHelp == nil {
		k.shortHelp = DefaultShortHelpPrinter
	}

	model, err := build(k, grammar)
	if err != nil {
		return k, err
	}
	model.Name = filepath.Base(os.Args[0])
	k.Model = model
	k.Model.HelpFlag = k.helpFlag

	// Embed any embedded structs.
	for _, embed := range k.embedded {
		tag, err := parseTagString(strings.Join(embed.tags, " "))
		if err != nil {
			return nil, err
		}
		tag.Embed = true
		v := reflect.Indirect(reflect.ValueOf(embed.strct))
		node, err := buildNode(k, v, CommandNode, tag, map[string]bool{})
		if err != nil {
			return nil, err
		}
		for _, child := range node.Children {
			child.Parent = k.Model.Node
			k.Model.Children = append(k.Model.Children, child)
		}
		k.Model.Flags = append(k.Model.Flags, node.Flags...)
	}

	// Synthesise command nodes.
	for _, dcmd := range k.dynamicCommands {
		tag, terr := parseTagString(strings.Join(dcmd.tags, " "))
		if terr != nil {
			return nil, terr
		}
		tag.Name = dcmd.name
		tag.Help = dcmd.help
		tag.Group = dcmd.group
		tag.Cmd = true
		v := reflect.Indirect(reflect.ValueOf(dcmd.cmd))
		err = buildChild(k, k.Model.Node, CommandNode, reflect.Value{}, reflect.StructField{
			Name: dcmd.name,
			Type: v.Type(),
		}, v, tag, dcmd.name, map[string]bool{})
		if err != nil {
			return nil, err
		}
	}

	for _, option := range k.postBuildOptions {
		if err = option.Apply(k); err != nil {
			return nil, err
		}
	}
	k.postBuildOptions = nil

	if err = k.interpolate(k.Model.Node); err != nil {
		return nil, err
	}

	k.bindings.add(k.vars)

	if err = checkOverlappingXorAnd(k); err != nil {
		return nil, err
	}

	return k, nil
}

func checkOverlappingXorAnd(k *Kong) error {
	xorGroups := map[string][]string{}
	andGroups := map[string][]string{}
	for _, flag := range k.Model.Node.Flags {
		for _, xor := range flag.Xor {
			xorGroups[xor] = append(xorGroups[xor], flag.Name)
		}
		for _, and := range flag.And {
			andGroups[and] = append(andGroups[and], flag.Name)
		}
	}
	for xor, xorSet := range xorGroups {
		for and, andSet := range andGroups {
			overlappingEntries := []string{}
			for _, xorTag := range xorSet {
				for _, andTag := range andSet {
					if xorTag == andTag {
						overlappingEntries = append(overlappingEntries, xorTag)
					}
				}
			}
			if len(overlappingEntries) > 1 {
				return fmt.Errorf("invalid xor and combination, %s and %s overlap with more than one: %s", xor, and, overlappingEntries)
			}
		}
	}
	return nil
}

type varStack []Vars

func (v *varStack) head() Vars { return (*v)[len(*v)-1] }
func (v *varStack) pop()       { *v = (*v)[:len(*v)-1] }
func (v *varStack) push(vars Vars) Vars {
	if len(*v) != 0 {
		vars = (*v)[len(*v)-1].CloneWith(vars)
	}
	*v = append(*v, vars)
	return vars
}

// Interpolate variables into model.
func (k *Kong) interpolate(node *Node) (err error) {
	stack := varStack{}
	return Visit(node, func(node Visitable, next Next) error {
		switch node := node.(type) {
		case *Node:
			vars := stack.push(node.Vars())
			node.Help, err = interpolate(node.Help, vars, nil)
			if err != nil {
				return fmt.Errorf("help for %s: %s", node.Path(), err)
			}
			err = next(nil)
			stack.pop()
			return err

		case *Value:
			return next(k.interpolateValue(node, stack.head()))
		}
		return next(nil)
	})
}

func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) {
	if len(value.Tag.Vars) > 0 {
		vars = vars.CloneWith(value.Tag.Vars)
	}
	if varsContributor, ok := value.Mapper.(VarsContributor); ok {
		vars = vars.CloneWith(varsContributor.Vars(value))
	}

	initialVars := vars.CloneWith(nil)
	for n, v := range initialVars {
		if vars[n], err = interpolate(v, initialVars, nil); err != nil {
			return fmt.Errorf("variable %s for %s: %s", n, value.Summary(), err)
		}
	}

	if value.Default, err = interpolate(value.Default, vars, nil); err != nil {
		return fmt.Errorf("default value for %s: %s", value.Summary(), err)
	}
	if value.Enum, err = interpolate(value.Enum, vars, nil); err != nil {
		return fmt.Errorf("enum value for %s: %s", value.Summary(), err)
	}
	updatedVars := map[string]string{
		"default": value.Default,
		"enum":    value.Enum,
	}
	for i, env := range value.Tag.Envs {
		if value.Tag.Envs[i], err = interpolate(env, vars, updatedVars); err != nil {
			return fmt.Errorf("env value for %s: %s", value.Summary(), err)
		}
	}
	updatedVars["env"] = ""
	if len(value.Tag.Envs) != 0 {
		updatedVars["env"] = value.Tag.Envs[0]
	}
	if value.Flag != nil {
		value.Flag.Envs = value.Tag.Envs
		value.Flag.PlaceHolder, err = interpolate(value.Flag.PlaceHolder, vars, updatedVars)
		if err != nil {
			return fmt.Errorf("placeholder value for %s: %s", value.Summary(), err)
		}
	}
	value.Help, err = interpolate(value.Help, vars, updatedVars)
	if err != nil {
		return fmt.Errorf("help for %s: %s", value.Summary(), err)
	}
	return nil
}

// Provide additional builtin flags, if any.
func (k *Kong) extraFlags() []*Flag {
	if k.noDefaultHelp {
		return nil
	}
	var helpTarget helpFlag
	value := reflect.ValueOf(&helpTarget).Elem()
	helpFlag := &Flag{
		Short: 'h',
		Value: &Value{
			Name:         "help",
			Help:         "Show context-sensitive help.",
			OrigHelp:     "Show context-sensitive help.",
			Target:       value,
			Tag:          &Tag{},
			Mapper:       k.registry.ForValue(value),
			DefaultValue: reflect.ValueOf(false),
		},
	}
	helpFlag.Flag = helpFlag
	k.helpFlag = helpFlag
	return []*Flag{helpFlag}
}

// Parse arguments into target.
//
// The return Context can be used to further inspect the parsed command-line, to format help, to find the
// selected command, to run command Run() methods, and so on. See Context and README for more information.
//
// Will return a ParseError if a *semantically* invalid command-line is encountered (as opposed to a syntactically
// invalid one, which will report a normal error).
func (k *Kong) Parse(args []string) (ctx *Context, err error) {
	ctx, err = Trace(k, args)
	if err != nil { // Trace is not expected to return an err
		return nil, &ParseError{error: err, Context: ctx, exitCode: exitUsageError}
	}
	if ctx.Error != nil {
		return nil, &ParseError{error: ctx.Error, Context: ctx, exitCode: exitUsageError}
	}
	if err = k.applyHook(ctx, "BeforeReset"); err != nil {
		return nil, &ParseError{error: err, Context: ctx}
	}
	if err = ctx.Reset(); err != nil {
		return nil, &ParseError{error: err, Context: ctx}
	}
	if err = k.applyHook(ctx, "BeforeResolve"); err != nil {
		return nil, &ParseError{error: err, Context: ctx}
	}
	if err = ctx.Resolve(); err != nil {
		return nil, &ParseError{error: err, Context: ctx}
	}
	if err = k.applyHook(ctx, "BeforeApply"); err != nil {
		return nil, &ParseError{error: err, Context: ctx}
	}
	if _, err = ctx.Apply(); err != nil { // Apply is not expected to return an err
		return nil, &ParseError{error: err, Context: ctx}
	}
	if err = ctx.Validate(); err != nil {
		return nil, &ParseError{error: err, Context: ctx, exitCode: exitUsageError}
	}
	if err = k.applyHook(ctx, "AfterApply"); err != nil {
		return nil, &ParseError{error: err, Context: ctx}
	}
	return ctx, nil
}

func (k *Kong) applyHook(ctx *Context, name string) error {
	for _, trace := range ctx.Path {
		var value reflect.Value
		switch {
		case trace.App != nil:
			value = trace.App.Target
		case trace.Argument != nil:
			value = trace.Argument.Target
		case trace.Command != nil:
			value = trace.Command.Target
		case trace.Positional != nil:
			value = trace.Positional.Target
		case trace.Flag != nil:
			value = trace.Flag.Value.Target
		default:
			panic("unsupported Path")
		}
		for _, method := range k.getMethods(value, name) {
			binds := k.bindings.clone()
			binds.add(ctx, trace)
			binds.add(trace.Node().Vars().CloneWith(k.vars))
			binds.merge(ctx.bindings)
			if err := callFunction(method, binds); err != nil {
				return err
			}
		}
	}
	// Path[0] will always be the app root.
	return k.applyHookToDefaultFlags(ctx, ctx.Path[0].Node(), name)
}

func (k *Kong) getMethods(value reflect.Value, name string) []reflect.Value {
	return append(
		// Identify callbacks by reflecting on value
		getMethods(value, name),

		// Identify callbacks that were registered with a kong.Option
		k.hooks[name]...,
	)
}

// Call hook on any unset flags with default values or values supplied via env.
func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error {
	if node == nil {
		return nil
	}
	return Visit(node, func(n Visitable, next Next) error {
		node, ok := n.(*Node)
		if !ok {
			return next(nil)
		}
		binds := k.bindings.clone().add(ctx).add(node.Vars().CloneWith(k.vars))
		for _, flag := range node.Flags {
			// Flags handled here are the ones that won't show up in ctx.Path:
			// they got their value from the default tag or an env var, both of
			// which Reset() applies straight to the target without touching the
			// parse path. Anything actually parsed off argv or set by a resolver
			// shows up in ctx.values and is covered by the main hook loop.
			if ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() {
				continue
			}
			if !flag.HasDefault && !atLeastOneEnvSet(flag.Tag.Envs) {
				continue
			}
			for _, method := range getMethods(flag.Target, name) {
				path := &Path{Flag: flag}
				if err := callFunction(method, binds.clone().add(path)); err != nil {
					return next(err)
				}
			}
		}
		return next(nil)
	})
}

func formatMultilineMessage(w io.Writer, leaders []string, format string, args ...any) {
	lines := strings.Split(strings.TrimRight(fmt.Sprintf(format, args...), "\n"), "\n")
	leader := ""
	for _, l := range leaders {
		if l == "" {
			continue
		}
		leader += l + ": "
	}
	fmt.Fprintf(w, "%s%s\n", leader, lines[0])
	for _, line := range lines[1:] {
		fmt.Fprintf(w, "%*s%s\n", len(leader), " ", line)
	}
}

// Printf writes a message to Kong.Stdout with the application name prefixed.
func (k *Kong) Printf(format string, args ...any) *Kong {
	formatMultilineMessage(k.Stdout, []string{k.Model.Name}, format, args...)
	return k
}

// Errorf writes a message to Kong.Stderr with the application name prefixed.
func (k *Kong) Errorf(format string, args ...any) *Kong {
	formatMultilineMessage(k.Stderr, []string{k.Model.Name, "error"}, format, args...)
	return k
}

// Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with status 1.
func (k *Kong) Fatalf(format string, args ...any) {
	k.Errorf(format, args...)
	k.Exit(1)
}

// FatalIfErrorf terminates with an error message if err != nil.
// If the error implements the ExitCoder interface, the ExitCode() method is called and
// the application exits with that status. Otherwise, the application exits with status 1.
func (k *Kong) FatalIfErrorf(err error, args ...any) {
	if err == nil {
		return
	}
	msg := err.Error()
	if len(args) > 0 {
		msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error() //nolint
	}
	// Maybe display usage information.
	var parseErr *ParseError
	if errors.As(err, &parseErr) {
		switch k.usageOnError {
		case fullUsage:
			_ = parseErr.Context.printHelp(k.helpOptions)
			fmt.Fprintln(k.Stdout)
		case shortUsage:
			_ = k.shortHelp(k.helpOptions, parseErr.Context)
			fmt.Fprintln(k.Stdout)
		}
	}
	k.Errorf("%s", msg)
	k.Exit(exitCodeFromError(err))
}

// LoadConfig from path using the loader configured via Configuration(loader).
//
// "path" will have ~ and any variables expanded.
func (k *Kong) LoadConfig(path string) (Resolver, error) {
	var err error
	path = ExpandPath(path)
	path, err = interpolate(path, k.vars, nil)
	if err != nil {
		return nil, err
	}
	r, err := os.Open(path) //nolint: gas
	if err != nil {
		return nil, err
	}
	defer r.Close()

	return k.loader(r)
}
