package cli

import (
	"context"
	"fmt"
	"io"
	"slices"
	"strings"
)

const (
	// ignoreFlagPrefix is to ignore test flags when adding flags from other packages
	ignoreFlagPrefix = "test."

	commandContextKey = contextKey("cli.context")
)

type contextKey string

// Command contains everything needed to run an application that
// accepts a string slice of arguments such as os.Args. A given
// Command may contain Flags and sub-commands in Commands.
type Command struct {
	// The name of the command
	Name string `json:"name"`
	// A list of aliases for the command
	Aliases []string `json:"aliases"`
	// A short description of the usage of this command
	Usage string `json:"usage"`
	// Text to override the USAGE section of help
	UsageText string `json:"usageText"`
	// A short description of the arguments of this command
	ArgsUsage string `json:"argsUsage"`
	// Version of the command
	Version string `json:"version"`
	// Longer explanation of how the command works
	Description string `json:"description"`
	// DefaultCommand is the (optional) name of a command
	// to run if no command names are passed as CLI arguments.
	DefaultCommand string `json:"defaultCommand"`
	// The category the command is part of
	Category string `json:"category"`
	// List of child commands
	Commands []*Command `json:"commands"`
	// List of flags to parse
	Flags []Flag `json:"flags"`
	// Boolean to hide built-in help command and help flag
	HideHelp bool `json:"hideHelp"`
	// Ignored if HideHelp is true.
	HideHelpCommand bool `json:"hideHelpCommand"`
	// Boolean to hide built-in version flag and the VERSION section of help
	HideVersion bool `json:"hideVersion"`
	// Boolean to enable shell completion commands
	EnableShellCompletion bool `json:"-"`
	// Shell Completion generation command name
	ShellCompletionCommandName string `json:"-"`
	// The function to call when checking for shell command completions
	ShellComplete ShellCompleteFunc `json:"-"`
	// The function to configure a shell completion command
	ConfigureShellCompletionCommand ConfigureShellCompletionCommand `json:"-"`
	// An action to execute before any subcommands are run, but after the context is ready
	// If a non-nil error is returned, no subcommands are run
	Before BeforeFunc `json:"-"`
	// An action to execute after any subcommands are run, but after the subcommand has finished
	// It is run even if Action() panics
	After AfterFunc `json:"-"`
	// The function to call when this command is invoked
	Action ActionFunc `json:"-"`
	// Execute this function if the proper command cannot be found
	CommandNotFound CommandNotFoundFunc `json:"-"`
	// Execute this function if a usage error occurs.
	OnUsageError OnUsageErrorFunc `json:"-"`
	// Execute this function when an invalid flag is accessed from the context
	InvalidFlagAccessHandler InvalidFlagAccessFunc `json:"-"`
	// Boolean to hide this command from help or completion
	Hidden bool `json:"hidden"`
	// List of all authors who contributed (string or fmt.Stringer)
	// TODO: ~string | fmt.Stringer when interface unions are available
	Authors []any `json:"authors"`
	// Copyright of the binary if any
	Copyright string `json:"copyright"`
	// Reader reader to write input to (useful for tests)
	Reader io.Reader `json:"-"`
	// Writer writer to write output to
	Writer io.Writer `json:"-"`
	// ErrWriter writes error output
	ErrWriter io.Writer `json:"-"`
	// ExitErrHandler processes any error encountered while running a Command before it is
	// returned to the caller. If no function is provided, HandleExitCoder is used as the
	// default behavior.
	ExitErrHandler ExitErrHandlerFunc `json:"-"`
	// Other custom info
	Metadata map[string]any `json:"metadata"`
	// Carries a function which returns app specific info.
	ExtraInfo func() map[string]string `json:"-"`
	// CustomRootCommandHelpTemplate the text template for app help topic.
	// cli.go uses text/template to render templates. You can
	// render custom help text by setting this variable.
	CustomRootCommandHelpTemplate string `json:"-"`
	// SliceFlagSeparator is used to customize the separator for SliceFlag, the default is ","
	SliceFlagSeparator string `json:"sliceFlagSeparator"`
	// DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false
	DisableSliceFlagSeparator bool `json:"disableSliceFlagSeparator"`
	// MapFlagKeyValueSeparator is used to customize the separator for MapFlag, the default is "="
	MapFlagKeyValueSeparator string `json:"mapFlagKeyValueSeparator"`
	// Boolean to enable short-option handling so user can combine several
	// single-character bool arguments into one
	// i.e. foobar -o -v -> foobar -ov
	UseShortOptionHandling bool `json:"useShortOptionHandling"`
	// Enable suggestions for commands and flags
	Suggest bool `json:"suggest"`
	// Allows global flags set by libraries which use flag.XXXVar(...) directly
	// to be parsed through this library
	AllowExtFlags bool `json:"allowExtFlags"`
	// Treat all flags as normal arguments if true
	SkipFlagParsing bool `json:"skipFlagParsing"`
	// CustomHelpTemplate the text template for the command help topic.
	// cli.go uses text/template to render templates. You can
	// render custom help text by setting this variable.
	CustomHelpTemplate string `json:"-"`
	// Use longest prefix match for commands
	PrefixMatchCommands bool `json:"prefixMatchCommands"`
	// Custom suggest command for matching
	SuggestCommandFunc SuggestCommandFunc `json:"-"`
	// Flag exclusion group
	MutuallyExclusiveFlags []MutuallyExclusiveFlags `json:"mutuallyExclusiveFlags"`
	// Arguments to parse for this command
	Arguments []Argument `json:"arguments"`
	// Whether to read arguments from stdin
	// applicable to root command only
	ReadArgsFromStdin bool `json:"readArgsFromStdin"`
	// StopOnNthArg provides v2-like behavior for specific commands by stopping
	// flag parsing after N positional arguments are encountered. When set to N,
	// all remaining arguments after the Nth positional argument will be treated
	// as arguments, not flags.
	//
	// A value of 0 means all arguments are treated as positional (no flag parsing).
	// A nil value means normal v3 flag parsing behavior (flags can appear anywhere).
	StopOnNthArg *int `json:"stopOnNthArg"`

	// categories contains the categorized commands and is populated on app startup
	categories CommandCategories
	// flagCategories contains the categorized flags and is populated on app startup
	flagCategories FlagCategories
	// flags that have been applied in current parse
	appliedFlags []Flag
	// flags that have been set
	setFlags map[Flag]struct{}
	// The parent of this command. This value will be nil for the
	// command at the root of the graph.
	parent *Command
	// parsed args
	parsedArgs Args
	// track state of error handling
	isInError bool
	// track state of defaults
	didSetupDefaults bool
	// whether in shell completion mode
	shellCompletion bool
	// whether global help flag was added
	globaHelpFlagAdded bool
	// whether global version flag was added
	globaVersionFlagAdded bool
	// generated root version flag
	versionFlag Flag
	// whether this is a completion command
	isCompletionCommand bool
	// whether this is the built-in help command
	builtInHelp bool
}

func (cmd *Command) Command(name string) *Command {
	for _, subCmd := range cmd.Commands {
		if subCmd.HasName(name) {
			return subCmd
		}
	}

	return nil
}

func (cmd *Command) checkHelp() bool {
	tracef("checking if help is wanted (cmd=%[1]q)", cmd.Name)

	return HelpFlag != nil && slices.ContainsFunc(HelpFlag.Names(), cmd.Bool)
}

func (cmd *Command) allFlags() []Flag {
	var flags []Flag
	flags = append(flags, cmd.Flags...)
	for _, grpf := range cmd.MutuallyExclusiveFlags {
		for _, f1 := range grpf.Flags {
			flags = append(flags, f1...)
		}
	}
	return flags
}

// useShortOptionHandling traverses Lineage() for *any* ancestors
// with UseShortOptionHandling
func (cmd *Command) useShortOptionHandling() bool {
	for _, pCmd := range cmd.Lineage() {
		if pCmd.UseShortOptionHandling {
			return true
		}
	}

	return false
}

func (cmd *Command) suggestFlagFromError(err error, commandName string) (string, error) {
	fl, parseErr := flagFromError(err)
	if parseErr != nil {
		return "", err
	}

	flags := cmd.Flags
	hideHelp := cmd.hideHelp()

	if commandName != "" {
		subCmd := cmd.Command(commandName)
		if subCmd == nil {
			return "", err
		}
		flags = subCmd.Flags
		hideHelp = hideHelp || subCmd.HideHelp
	}

	suggestion := SuggestFlag(flags, fl, hideHelp)
	if len(suggestion) == 0 {
		return "", err
	}

	return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion) + "\n\n", nil
}

// Names returns the names including short names and aliases.
func (cmd *Command) Names() []string {
	return append([]string{cmd.Name}, cmd.Aliases...)
}

// HasName returns true if Command.Name matches given name
func (cmd *Command) HasName(name string) bool {
	return slices.Contains(cmd.Names(), name)
}

// VisibleCategories returns a slice of categories and commands that are
// Hidden=false
func (cmd *Command) VisibleCategories() []CommandCategory {
	ret := []CommandCategory{}
	for _, category := range cmd.categories.Categories() {
		if visible := func() CommandCategory {
			if len(category.VisibleCommands()) > 0 {
				return category
			}
			return nil
		}(); visible != nil {
			ret = append(ret, visible)
		}
	}
	return ret
}

// VisibleCommands returns a slice of the Commands with Hidden=false
func (cmd *Command) VisibleCommands() []*Command {
	var ret []*Command
	for _, command := range cmd.Commands {
		if command.Hidden || command.Name == helpName {
			continue
		}
		ret = append(ret, command)
	}
	return ret
}

// VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain
func (cmd *Command) VisibleFlagCategories() []VisibleFlagCategory {
	if cmd.flagCategories == nil {
		cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags())
	}
	return cmd.flagCategories.VisibleCategories()
}

// VisibleFlags returns a slice of the Flags with Hidden=false
func (cmd *Command) VisibleFlags() []Flag {
	return visibleFlags(cmd.allFlags())
}

func (cmd *Command) appendFlag(fl Flag) {
	if !hasFlag(cmd.Flags, fl) {
		cmd.Flags = append(cmd.Flags, fl)
	}
}

// VisiblePersistentFlags returns a slice of [LocalFlag] with Persistent=true and Hidden=false.
func (cmd *Command) VisiblePersistentFlags() []Flag {
	if cmd.isCompletionCommand {
		return nil
	}
	var flags []Flag
	for _, fl := range cmd.Root().Flags {
		pfl, ok := fl.(LocalFlag)
		if !ok || pfl.IsLocal() {
			continue
		}
		flags = append(flags, fl)
	}
	return visibleFlags(flags)
}

func (cmd *Command) appendCommand(aCmd *Command) {
	if !slices.Contains(cmd.Commands, aCmd) {
		aCmd.parent = cmd
		cmd.Commands = append(cmd.Commands, aCmd)
	}
}

func (cmd *Command) handleExitCoder(ctx context.Context, err error) error {
	if cmd.parent != nil {
		return cmd.parent.handleExitCoder(ctx, err)
	}

	if cmd.ExitErrHandler != nil {
		cmd.ExitErrHandler(ctx, cmd, err)
		return err
	}

	HandleExitCoder(err)
	return err
}

func (cmd *Command) argsWithDefaultCommand(oldArgs Args) Args {
	rawArgs := append([]string{cmd.DefaultCommand}, oldArgs.Slice()...)
	newArgs := &stringSliceArgs{v: rawArgs}

	return newArgs
}

// Root returns the Command at the root of the graph
func (cmd *Command) Root() *Command {
	if cmd.parent == nil {
		return cmd
	}

	return cmd.parent.Root()
}

func (cmd *Command) set(fName string, f Flag, val string) error {
	cmd.setFlags[f] = struct{}{}
	cmd.setMultiValueParsingConfig(f)
	if err := f.Set(fName, val); err != nil {
		return fmt.Errorf("invalid value %q for flag -%s: %v", val, fName, err)
	}
	return nil
}

func (cmd *Command) lFlag(name string) Flag {
	for _, f := range cmd.allFlags() {
		if slices.Contains(f.Names(), name) {
			tracef("flag found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
			return f
		}
	}
	return nil
}

func (cmd *Command) hasPersistentFlagOnAncestor(fl Flag) bool {
	for pCmd := cmd.parent; pCmd != nil; pCmd = pCmd.parent {
		for _, pFl := range pCmd.allFlags() {
			if pFl != fl {
				continue
			}

			pfl, ok := pFl.(LocalFlag)
			if ok && !pfl.IsLocal() {
				return true
			}
		}
	}
	return false
}

func (cmd *Command) lookupFlag(name string) Flag {
	for _, pCmd := range cmd.Lineage() {
		if f := pCmd.lFlag(name); f != nil {
			return f
		}
	}

	tracef("flag NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
	cmd.onInvalidFlag(context.TODO(), name)
	return nil
}

// this looks up only allowed flags, i.e. local flags for current command
// or persistent flags from ancestors
func (cmd *Command) lookupAppliedFlag(name string) Flag {
	for _, f := range cmd.appliedFlags {
		if slices.Contains(f.Names(), name) {
			tracef("appliedFlag found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
			return f
		}
	}

	tracef("lookupAppliedflag NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
	cmd.onInvalidFlag(context.TODO(), name)
	return nil
}

func (cmd *Command) checkRequiredFlag(f Flag) (bool, string) {
	if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() {
		flagName := f.Names()[0]
		if !f.IsSet() {
			return false, flagName
		}
	}
	return true, ""
}

func (cmd *Command) checkAllRequiredFlags() requiredFlagsErr {
	// The help and completion commands are allowed to run without
	// enforcement of required flags, since they do not invoke user
	// actions that depend on those flag values.
	if cmd.builtInHelp || cmd.isCompletionCommand {
		return nil
	}
	for pCmd := cmd; pCmd != nil; pCmd = pCmd.parent {
		if err := pCmd.checkRequiredFlags(); err != nil {
			return err
		}
	}
	return nil
}

func (cmd *Command) checkRequiredFlags() requiredFlagsErr {
	tracef("checking for required flags (cmd=%[1]q)", cmd.Name)

	missingFlags := []string{}

	for _, f := range cmd.appliedFlags {
		if ok, name := cmd.checkRequiredFlag(f); !ok {
			missingFlags = append(missingFlags, name)
		}
	}

	if len(missingFlags) != 0 {
		tracef("found missing required flags %[1]q (cmd=%[2]q)", missingFlags, cmd.Name)

		return &errRequiredFlags{missingFlags: missingFlags}
	}

	tracef("all required flags set (cmd=%[1]q)", cmd.Name)

	return nil
}

func (cmd *Command) onInvalidFlag(ctx context.Context, name string) {
	for cmd != nil {
		if cmd.InvalidFlagAccessHandler != nil {
			cmd.InvalidFlagAccessHandler(ctx, cmd, name)
			break
		}
		cmd = cmd.parent
	}
}

// NumFlags returns the number of flags set
func (cmd *Command) NumFlags() int {
	tracef("numFlags numAppliedFlags %d", len(cmd.appliedFlags))
	count := 0
	for _, f := range cmd.appliedFlags {
		if f.IsSet() {
			count++
		}
	}
	return count // cmd.flagSet.NFlag()
}

func (cmd *Command) setMultiValueParsingConfig(f Flag) {
	tracef("setMultiValueParsingConfig %T, %+v", f, f)
	if cf, ok := f.(multiValueParsingConfigSetter); ok {
		cf.setMultiValueParsingConfig(multiValueParsingConfig{
			SliceFlagSeparator:        cmd.SliceFlagSeparator,
			DisableSliceFlagSeparator: cmd.DisableSliceFlagSeparator,
			MapFlagKeyValueSeparator:  cmd.MapFlagKeyValueSeparator,
		})
	}
}

// Set sets a context flag to a value.
func (cmd *Command) Set(name, value string) error {
	if f := cmd.lookupFlag(name); f != nil {
		cmd.setMultiValueParsingConfig(f)
		return f.Set(name, value)
	}

	return fmt.Errorf("no such flag -%s", name)
}

// IsSet determines if the flag was actually set
func (cmd *Command) IsSet(name string) bool {
	fl := cmd.lookupFlag(name)
	if fl == nil {
		tracef("flag with name %[1]q NOT found; assuming not set (cmd=%[2]q)", name, cmd.Name)
		return false
	}

	isSet := fl.IsSet()
	if isSet {
		tracef("flag with name %[1]q is set (cmd=%[2]q)", name, cmd.Name)
	} else {
		tracef("flag with name %[1]q is no set (cmd=%[2]q)", name, cmd.Name)
	}

	return isSet
}

// LocalFlagNames returns a slice of flag names used in this
// command.
func (cmd *Command) LocalFlagNames() []string {
	names := []string{}

	// Check the flags which have been set via env or file
	for _, f := range cmd.allFlags() {
		if f.IsSet() {
			names = append(names, f.Names()...)
		}
	}

	// Sort out the duplicates since flag could be set via multiple
	// paths
	m := map[string]struct{}{}
	uniqNames := []string{}

	for _, name := range names {
		if _, ok := m[name]; !ok {
			m[name] = struct{}{}
			uniqNames = append(uniqNames, name)
		}
	}

	return uniqNames
}

// FlagNames returns a slice of flag names used by the this command
// and all of its parent commands.
func (cmd *Command) FlagNames() []string {
	names := cmd.LocalFlagNames()

	if cmd.parent != nil {
		names = append(cmd.parent.FlagNames(), names...)
	}

	return names
}

// Lineage returns *this* command and all of its ancestor commands
// in order from child to parent
func (cmd *Command) Lineage() []*Command {
	lineage := []*Command{cmd}

	if cmd.parent != nil {
		lineage = append(lineage, cmd.parent.Lineage()...)
	}

	return lineage
}

// FullName returns the full name of the command.
// Includes parent commands separated by space.
func (cmd *Command) FullName() string {
	return strings.Join(cmd.Path(), " ")
}

// Path returns the path of command names from the root to cmd, inclusive.
// Each element is a Command.Name. Path traverses upward via parent pointers
// similar to Lineage. FullName() is equivalent to strings.Join(cmd.Path(), " ").
func (cmd *Command) Path() []string {
	if cmd.parent != nil {
		return append(cmd.parent.Path(), cmd.Name)
	}
	return []string{cmd.Name}
}

// Walk visits cmd and every descendant. If fn returns a non-nil error, the
// walk terminates and the error is returned to the caller.
func (cmd *Command) Walk(fn func(*Command) error) error {
	if fn == nil {
		return nil
	}
	if err := fn(cmd); err != nil {
		return err
	}
	for _, sub := range cmd.Commands {
		if err := sub.Walk(fn); err != nil {
			return err
		}
	}
	return nil
}

// Count returns the num of occurrences of this flag
func (cmd *Command) Count(name string) int {
	if cf, ok := cmd.lookupFlag(name).(Countable); ok {
		return cf.Count()
	}
	return 0
}

// Value returns the value of the flag corresponding to `name`
func (cmd *Command) Value(name string) any {
	if fs := cmd.lookupFlag(name); fs != nil {
		tracef("value found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
		return fs.Get()
	}

	tracef("value NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name)
	return nil
}

// Args returns the command line arguments associated with the
// command.
func (cmd *Command) Args() Args {
	return cmd.parsedArgs
}

// NArg returns the number of the command line arguments.
func (cmd *Command) NArg() int {
	return cmd.Args().Len()
}

func (cmd *Command) runFlagActions(ctx context.Context) error {
	tracef("runFlagActions")
	// run the flag actions in the same order that they are defined
	// to maintain consistency.
	for _, fl := range cmd.appliedFlags {
		if _, inSet := cmd.setFlags[fl]; inSet {
			if af, ok := fl.(ActionableFlag); ok {
				if err := af.RunAction(ctx, cmd); err != nil {
					return err
				}
			}
		}
	}

	return nil
}
