// Copyright (c) Contributors to the Apptainer project, established as
//   Apptainer a Series of LF Projects LLC.
//   For website terms of use, trademark policy, privacy policy and other
//   project policies see https://lfprojects.org/policies
// Copyright (c) 2019, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package cmdline

import (
	"fmt"
	"os"
	"strings"

	"github.com/apptainer/apptainer/pkg/sylog"

	"github.com/spf13/cobra"
)

// CommandManager holds root command or first parent
// can stores group of command. A command group can be
// composed of one or many commands
type CommandManager struct {
	rootCmd   *cobra.Command
	groupCmds map[string][]*cobra.Command
	errPool   []error
	fm        *flagManager
}

// object for turning deprecation messages into warnings
type warnDeprecated struct{}

// implement io.Writer function for redirecting deprecated messages to
// sylog.Warningf()
func (w *warnDeprecated) Write(b []byte) (n int, err error) {
	s := string(b[:])
	if strings.Contains(s, "has been deprecated") {
		sylog.Warningf("%v", s)
		return len(s), nil
	}
	// This is only supposed to be for the "Out" stream, but some
	// error messages come through here too that by default go
	// to stderr.
	if strings.HasPrefix(s, "Error") {
		return os.Stderr.Write(b)
	}
	return os.Stdout.Write(b)
}

// FlagError represents a flag error type
type FlagError string

func (f FlagError) Error() string {
	return string(f)
}

// CommandError represents a command error type
type CommandError string

func (c CommandError) Error() string {
	return string(c)
}

func onError(_ *cobra.Command, err error) error {
	return FlagError(err.Error())
}

// NewCommandManager instantiates a CommandManager.
func NewCommandManager(rootCmd *cobra.Command) *CommandManager {
	if rootCmd == nil {
		panic("nil root command passed")
	}
	cm := &CommandManager{
		rootCmd:   rootCmd,
		groupCmds: make(map[string][]*cobra.Command),
		fm:        newFlagManager(),
	}
	rootCmd.SetFlagErrorFunc(onError)
	rootCmd.SetOut(&warnDeprecated{})
	return cm
}

func (m *CommandManager) isRegistered(cmd *cobra.Command) bool {
	c := cmd.Parent()
	for c != nil {
		if c == m.rootCmd {
			break
		}
		c = c.Parent()
	}
	return c != nil
}

func (m *CommandManager) pushError(err error) {
	m.errPool = append(m.errPool, err)
}

// GetError returns the error pool.
func (m *CommandManager) GetError() []error {
	return m.errPool
}

// RegisterCmd registers a child command of the root command.
// The registered command is automatically affected to a unique group
// containing this command only, the group name is based on the command name
func (m *CommandManager) RegisterCmd(cmd *cobra.Command) {
	// panic here because it's a misuse of API and generally from
	// global context or init() functions
	if cmd == nil {
		panic("nil command passed")
	}
	cmd.SetFlagErrorFunc(onError)
	m.rootCmd.AddCommand(cmd)
	cmd.Flags().SetInterspersed(false)
	m.SetCmdGroup(m.GetCmdName(cmd), cmd)
}

// RegisterSubCmd registers a child command for parent command given as argument.
// The registered command is automatically affected to a unique group containing
// this command only, the group name is based on the command name appended to
// parents command name (see GetCmdName for details)
func (m *CommandManager) RegisterSubCmd(parentCmd, childCmd *cobra.Command) {
	// panic here because it's a misuse of API and generally from
	// global context or init() functions
	if parentCmd == nil {
		panic("nil parent command passed")
	} else if childCmd == nil {
		panic("nil child command passed")
	} else if !m.isRegistered(parentCmd) {
		panic("parent command not registered")
	}
	parentCmd.AddCommand(childCmd)
	childCmd.Flags().SetInterspersed(false)
	m.SetCmdGroup(m.GetCmdName(childCmd), childCmd)
}

// SetCmdGroup creates a unique group of commands identified by name.
// If group already exists or empty command is passed, this function
// will panic
func (m *CommandManager) SetCmdGroup(name string, cmds ...*cobra.Command) {
	if m.groupCmds[name] != nil {
		panic(fmt.Sprintf("group %s already exists", name))
	}
	tmp := make([]*cobra.Command, 0, len(cmds))
	for _, c := range cmds {
		if c != nil {
			tmp = append(tmp, c)
		}
	}
	// cmds could contain only nil commands, we check length of
	// the temporary allocated array containing only non nil
	// commands
	if len(tmp) == 0 {
		panic(fmt.Sprintf("creation of an empty group %q", name))
	}
	m.groupCmds[name] = tmp
}

// GetRootCmd returns the root command
func (m *CommandManager) GetRootCmd() *cobra.Command {
	return m.rootCmd
}

// GetCmdGroup returns all commands associated with the group name
func (m *CommandManager) GetCmdGroup(name string) []*cobra.Command {
	return m.groupCmds[name]
}

// GetCmd returns a single command based on its unique group name.
// If the command group has more than one command this function
// return a nil command instead.
func (m *CommandManager) GetCmd(name string) *cobra.Command {
	cmds := m.groupCmds[name]
	if cmds == nil || len(cmds) > 1 {
		return nil
	}
	return cmds[0]
}

// GetCmdName returns name associated with the provided command.
// If command is named "child" and has two parents named "parent1"
// and "parent2", this function will return "parent1_parent2_child".
// Passing the root command to this function returns an empty string.
func (m *CommandManager) GetCmdName(cmd *cobra.Command) string {
	var names []string

	for c := cmd; c != nil; c = c.Parent() {
		if c == m.rootCmd {
			break
		}
		names = append(names, c.Name())
	}
	// reverse slice
	for i, j := 0, len(names)-1; i < j; i, j = i+1, j-1 {
		names[i], names[j] = names[j], names[i]
	}
	return strings.Join(names, "_")
}

// RegisterFlagForCmd registers a flag for one or many commands
func (m *CommandManager) RegisterFlagForCmd(flag *Flag, cmds ...*cobra.Command) {
	if err := m.fm.registerFlagForCmd(flag, cmds...); err != nil {
		m.pushError(err)
	}
}

// UpdateCmdFlagFromEnv updates flag's values based on environment variables
// associated with all flags belonging to command provided as argument
func (m *CommandManager) UpdateCmdFlagFromEnv(cmd *cobra.Command, precedence int, foundKeys map[string]string) error {
	return m.fm.updateCmdFlagFromEnv(cmd, precedence, foundKeys)
}
