package token

import (
	"context"
	"fmt"
	"io"
	"strings"
	"time"

	"github.com/spf13/cobra"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/duration"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	kubeclient "k8s.io/client-go/kubernetes"
	bootstrapapi "k8s.io/cluster-bootstrap/token/api"
	bootstraputil "k8s.io/cluster-bootstrap/token/util"
	"k8s.io/klog/v2"
	"k8s.io/kubectl/pkg/cmd/get"
	"k8s.io/kubectl/pkg/util/templates"

	"github.com/karmada-io/karmada/pkg/karmadactl/options"
	"github.com/karmada-io/karmada/pkg/karmadactl/util"
	tokenutil "github.com/karmada-io/karmada/pkg/karmadactl/util/bootstraptoken"
)

var (
	tokenLong = templates.LongDesc(`
	This command manages bootstrap tokens. It is optional and needed only for advanced use cases.

	In short, bootstrap tokens are used for establishing bidirectional trust between a client and a server.
	A bootstrap token can be used when a client (for example a member cluster that is about to join control plane) needs
	to trust the server it is talking to. Then a bootstrap token with the "signing" usage can be used.
	bootstrap tokens can also function as a way to allow short-lived authentication to the API Server
	(the token serves as a way for the API Server to trust the client), for example for doing the TLS Bootstrap.

	What is a bootstrap token more exactly?
	 - It is a Secret in the kube-system namespace of type "bootstrap.kubernetes.io/token".
	 - A bootstrap token must be of the form "[a-z0-9]{6}.[a-z0-9]{16}". The former part is the public token ID,
	   while the latter is the Token Secret and it must be kept private at all circumstances!
	 - The name of the Secret must be named "bootstrap-token-(token-id)".

	This command is same as 'kubeadm token', but it will create tokens that are used by member clusters.`)

	tokenExamples = templates.Examples(`
	# Create a token and print the full '%[1]s register' flag needed to join the cluster using the token.
	%[1]s token create --print-register-command
	`)
)

// NewCmdToken returns cobra.Command for token management
func NewCmdToken(f util.Factory, parentCommand string, streams genericclioptions.IOStreams) *cobra.Command {
	opts := &CommandTokenOptions{
		parentCommand: parentCommand,
		TTL: &metav1.Duration{
			Duration: 0,
		},
	}

	cmd := &cobra.Command{
		Use:     "token",
		Short:   "Manage bootstrap tokens",
		Long:    tokenLong,
		Example: fmt.Sprintf(tokenExamples, parentCommand),
		Annotations: map[string]string{
			util.TagCommandGroup: util.GroupClusterRegistration,
		},
	}

	cmd.AddCommand(NewCmdTokenCreate(f, streams.Out, opts))
	cmd.AddCommand(NewCmdTokenList(f, streams.Out, streams.ErrOut, opts))
	cmd.AddCommand(NewCmdTokenDelete(f, streams.Out, opts))

	return cmd
}

// CommandTokenOptions holds all command options for token
type CommandTokenOptions struct {
	TTL         *metav1.Duration
	Description string
	Groups      []string
	Usages      []string

	PrintRegisterCommand bool

	parentCommand string
}

// NewCmdTokenCreate returns cobra.Command to create token
func NewCmdTokenCreate(f util.Factory, out io.Writer, tokenOpts *CommandTokenOptions) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "create",
		Short: "Create bootstrap tokens on the server",
		Long: templates.LongDesc(`
			This command will create a bootstrap token for you.
			You can specify the usages for this token, the "time to live" and an optional human friendly description.

			This should be a securely generated random token of the form "[a-z0-9]{6}.[a-z0-9]{16}".
		`),
		SilenceUsage:          true,
		DisableFlagsInUseLine: true,
		RunE: func(Cmd *cobra.Command, args []string) error {
			// Get control plane kube-apiserver client
			client, err := f.KubernetesClientSet()
			if err != nil {
				return err
			}

			return tokenOpts.runCreateToken(out, client)
		},
		Args: cobra.NoArgs,
	}

	options.AddKubeConfigFlags(cmd.Flags())
	cmd.Flags().BoolVar(&tokenOpts.PrintRegisterCommand, "print-register-command", false, fmt.Sprintf("Instead of printing only the token, print the full '%s register' flag needed to register the member cluster using the token.", tokenOpts.parentCommand))
	cmd.Flags().DurationVar(&tokenOpts.TTL.Duration, "ttl", tokenutil.DefaultTokenDuration, "The duration before the token is automatically deleted (e.g. 1s, 2m, 3h). If set to '0', the token will never expire")
	cmd.Flags().StringSliceVar(&tokenOpts.Usages, "usages", tokenutil.DefaultUsages, fmt.Sprintf("Describes the ways in which this token can be used. You can pass --usages multiple times or provide a comma separated list of options. Valid options: [%s]", strings.Join(bootstrapapi.KnownTokenUsages, ",")))
	cmd.Flags().StringSliceVar(&tokenOpts.Groups, "groups", tokenutil.DefaultGroups, fmt.Sprintf("Extra groups that this token will authenticate as when used for authentication. Must match %q", bootstrapapi.BootstrapGroupPattern))
	cmd.Flags().StringVar(&tokenOpts.Description, "description", tokenOpts.Description, "A human friendly description of how this token is used.")

	return cmd
}

// NewCmdTokenList returns cobra.Command to list tokens
func NewCmdTokenList(f util.Factory, out io.Writer, errW io.Writer, tokenOpts *CommandTokenOptions) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "list",
		Short: "List bootstrap tokens on the server",
		Long:  "This command will list all bootstrap tokens for you.",
		RunE: func(tokenCmd *cobra.Command, args []string) error {
			// Get control plane kube-apiserver client
			client, err := f.KubernetesClientSet()
			if err != nil {
				return err
			}

			return tokenOpts.runListTokens(client, out, errW)
		},
		Args: cobra.NoArgs,
	}

	options.AddKubeConfigFlags(cmd.Flags())

	return cmd
}

// NewCmdTokenDelete returns cobra.Command to delete tokens
func NewCmdTokenDelete(f util.Factory, out io.Writer, tokenOpts *CommandTokenOptions) *cobra.Command {
	cmd := &cobra.Command{
		Use:                   "delete [token-value] ...",
		DisableFlagsInUseLine: true,
		Short:                 "Delete bootstrap tokens on the server",
		Long: templates.LongDesc(`
			This command will delete a list of bootstrap tokens for you.

			The [token-value] is the full Token of the form "[a-z0-9]{6}.[a-z0-9]{16}" or the
			Token ID of the form "[a-z0-9]{6}" to delete.
		`),
		RunE: func(tokenCmd *cobra.Command, args []string) error {
			if len(args) < 1 {
				return fmt.Errorf("missing subcommand; 'token delete' is missing token of form %q", bootstrapapi.BootstrapTokenIDPattern)
			}

			// Get control plane kube-apiserver client
			client, err := f.KubernetesClientSet()
			if err != nil {
				return err
			}

			return tokenOpts.runDeleteTokens(out, client, args)
		},
	}

	options.AddKubeConfigFlags(cmd.Flags())

	return cmd
}

// runCreateToken generates a new bootstrap token and stores it as a secret on the server.
func (o *CommandTokenOptions) runCreateToken(out io.Writer, client kubeclient.Interface) error {
	klog.V(1).Infoln("[token] creating token")
	bootstrapToken, err := tokenutil.GenerateRandomBootstrapToken(o.TTL, o.Description, o.Groups, o.Usages)
	if err != nil {
		return err
	}

	if err := tokenutil.CreateNewToken(client, bootstrapToken); err != nil {
		return err
	}

	tokenStr := bootstrapToken.Token.ID + "." + bootstrapToken.Token.Secret

	// if --print-register-command was specified, print a machine-readable full `karmadactl register` command
	// otherwise, just print the token
	if o.PrintRegisterCommand {
		joinCommand, err := tokenutil.GenerateRegisterCommand(*options.DefaultConfigFlags.KubeConfig, o.parentCommand, tokenStr, *options.DefaultConfigFlags.Context)
		if err != nil {
			return fmt.Errorf("failed to get register command, err: %w", err)
		}
		fmt.Fprintln(out, joinCommand)
	} else {
		fmt.Fprintln(out, tokenStr)
	}

	return nil
}

// runListTokens lists details on all existing bootstrap tokens on the server.
func (o *CommandTokenOptions) runListTokens(client kubeclient.Interface, out io.Writer, errW io.Writer) error {
	// First, build our selector for bootstrap tokens only
	klog.V(1).Infoln("[token] preparing selector for bootstrap token")
	tokenSelector := fields.SelectorFromSet(
		map[string]string{
			"type": string(bootstrapapi.SecretTypeBootstrapToken),
		},
	)
	listOptions := metav1.ListOptions{
		FieldSelector: tokenSelector.String(),
	}

	klog.V(1).Info("[token] retrieving list of bootstrap tokens")
	secrets, err := client.CoreV1().Secrets(metav1.NamespaceSystem).List(context.TODO(), listOptions)
	if err != nil {
		return fmt.Errorf("failed to list bootstrap tokens, err: %w", err)
	}

	printFlags := get.NewGetPrintFlags()

	printFlags.SetKind(schema.GroupKind{Group: "output.karmada.io", Kind: "BootstrapToken"})

	printer, err := printFlags.ToPrinter()
	if err != nil {
		return err
	}

	// only print token with table format
	printer = &get.TablePrinter{Delegate: printer}

	tokenTable := &metav1.Table{}
	setColumnDefinition(tokenTable)

	for idx := range secrets.Items {
		// Get the BootstrapToken struct representation from the Secret object
		token, err := tokenutil.GetBootstrapTokenFromSecret(&secrets.Items[idx])
		if err != nil {
			fmt.Fprintf(errW, "%v", err)
			continue
		}

		outputToken := tokenutil.BootstrapToken{
			Token:       &tokenutil.Token{ID: token.Token.ID, Secret: token.Token.Secret},
			Description: token.Description,
			TTL:         token.TTL,
			Expires:     token.Expires,
			Usages:      token.Usages,
			Groups:      token.Groups,
		}

		tokenTable.Rows = append(tokenTable.Rows, constructTokenTableRow(outputToken))
	}

	if err := printer.PrintObj(tokenTable, out); err != nil {
		return fmt.Errorf("unable to print tokens, err: %w", err)
	}

	return nil
}

// runDeleteTokens removes a bootstrap tokens from the server.
func (o *CommandTokenOptions) runDeleteTokens(out io.Writer, client kubeclient.Interface, tokenIDsOrTokens []string) error {
	for _, tokenIDOrToken := range tokenIDsOrTokens {
		// Assume this is a token id and try to parse it
		tokenID := tokenIDOrToken
		klog.V(1).Info("[token] parsing token")
		if !bootstraputil.IsValidBootstrapTokenID(tokenIDOrToken) {
			// Okay, the full token with both id and secret was probably passed. Parse it and extract the ID only
			bts, err := tokenutil.NewToken(tokenIDOrToken)
			if err != nil {
				return fmt.Errorf("given token didn't match pattern %q or %q",
					bootstrapapi.BootstrapTokenIDPattern, bootstrapapi.BootstrapTokenIDPattern)
			}
			tokenID = bts.ID
		}

		tokenSecretName := bootstraputil.BootstrapTokenSecretName(tokenID)
		klog.V(1).Infof("[token] deleting token %q", tokenID)
		if err := client.CoreV1().Secrets(metav1.NamespaceSystem).Delete(context.TODO(), tokenSecretName, metav1.DeleteOptions{}); err != nil {
			return fmt.Errorf("failed to delete bootstrap token %q, err: %w", tokenID, err)
		}
		fmt.Fprintf(out, "bootstrap token %q deleted\n", tokenID)
	}
	return nil
}

// constructTokenTableRow construct token table row
func constructTokenTableRow(token tokenutil.BootstrapToken) metav1.TableRow {
	var row metav1.TableRow

	tokenString := token.Token.ID + "." + token.Token.Secret

	ttl := "<forever>"
	expires := "<never>"
	if token.Expires != nil {
		ttl = duration.ShortHumanDuration(time.Until(token.Expires.Time))
		expires = token.Expires.Format(time.RFC3339)
	}

	usages := strings.Join(token.Usages, ",")
	if len(usages) == 0 {
		usages = "<none>"
	}

	description := token.Description
	if len(description) == 0 {
		description = "<none>"
	}

	groups := strings.Join(token.Groups, ",")
	if len(groups) == 0 {
		groups = "<none>"
	}

	row.Cells = append(row.Cells, tokenString, ttl, expires, usages, description, groups)

	return row
}

// setColumnDefinition set print ColumnDefinition
func setColumnDefinition(table *metav1.Table) {
	tokenColumns := []metav1.TableColumnDefinition{
		{Name: "TOKEN", Type: "string", Format: "", Priority: 0},
		{Name: "TTL", Type: "string", Format: "", Priority: 0},
		{Name: "EXPIRES", Type: "string", Format: "", Priority: 0},
		{Name: "USAGES", Type: "string", Format: "", Priority: 0},
		{Name: "DESCRIPTION", Type: "string", Format: "", Priority: 0},
		{Name: "EXTRA GROUPS", Type: "string", Format: "", Priority: 0},
	}
	table.ColumnDefinitions = tokenColumns
}
