package token

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"text/tabwriter"
	"time"

	"github.com/k3s-io/k3s/pkg/cli/cmds"
	"github.com/k3s-io/k3s/pkg/clientaccess"
	"github.com/k3s-io/k3s/pkg/kubeadm"
	"github.com/k3s-io/k3s/pkg/proctitle"
	"github.com/k3s-io/k3s/pkg/server"
	"github.com/k3s-io/k3s/pkg/util"
	"github.com/k3s-io/k3s/pkg/version"
	"github.com/pkg/errors"
	"github.com/urfave/cli"
	"gopkg.in/yaml.v2"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/util/duration"
	"k8s.io/client-go/tools/clientcmd"
	bootstrapapi "k8s.io/cluster-bootstrap/token/api"
	bootstraputil "k8s.io/cluster-bootstrap/token/util"
	"k8s.io/utils/ptr"
)

func Create(app *cli.Context) error {
	if err := cmds.InitLogging(); err != nil {
		return err
	}
	return create(app, &cmds.TokenConfig)
}

func create(app *cli.Context, cfg *cmds.Token) error {
	if err := kubeadm.SetDefaults(app, cfg); err != nil {
		return err
	}

	cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig)
	client, err := util.GetClientSet(cfg.Kubeconfig)
	if err != nil {
		return err
	}

	restConfig, err := clientcmd.BuildConfigFromFlags("", cfg.Kubeconfig)
	if err != nil {
		return err
	}

	if len(restConfig.TLSClientConfig.CAData) == 0 && restConfig.TLSClientConfig.CAFile != "" {
		restConfig.TLSClientConfig.CAData, err = os.ReadFile(restConfig.TLSClientConfig.CAFile)
		if err != nil {
			return err
		}
	}

	bts, err := kubeadm.NewBootstrapTokenString(cfg.Token)
	if err != nil {
		return err
	}

	bt := kubeadm.BootstrapToken{
		Token:       bts,
		Description: cfg.Description,
		TTL:         &metav1.Duration{Duration: cfg.TTL},
		Usages:      cfg.Usages,
		Groups:      cfg.Groups,
	}

	secretName := bootstraputil.BootstrapTokenSecretName(bt.Token.ID)
	if secret, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Get(context.TODO(), secretName, metav1.GetOptions{}); secret != nil && err == nil {
		return fmt.Errorf("a token with id %q already exists", bt.Token.ID)
	}

	secret := kubeadm.BootstrapTokenToSecret(&bt)
	if _, err := client.CoreV1().Secrets(metav1.NamespaceSystem).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil {
		return err
	}

	token, err := clientaccess.FormatTokenBytes(bt.Token.String(), restConfig.TLSClientConfig.CAData)
	if err != nil {
		return err
	}

	fmt.Println(token)
	return nil
}

func Delete(app *cli.Context) error {
	if err := cmds.InitLogging(); err != nil {
		return err
	}
	return delete(app, &cmds.TokenConfig)
}

func delete(app *cli.Context, cfg *cmds.Token) error {
	args := app.Args()
	if len(args) < 1 {
		return errors.New("missing argument; 'token delete' is missing token")
	}

	cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig)
	client, err := util.GetClientSet(cfg.Kubeconfig)
	if err != nil {
		return err
	}

	for _, token := range args {
		if !bootstraputil.IsValidBootstrapTokenID(token) {
			bts, err := kubeadm.NewBootstrapTokenString(cfg.Token)
			if err != nil {
				return fmt.Errorf("given token didn't match pattern %q or %q", bootstrapapi.BootstrapTokenIDPattern, bootstrapapi.BootstrapTokenIDPattern)
			}
			token = bts.ID
		}
		secretName := bootstraputil.BootstrapTokenSecretName(token)
		if err := client.CoreV1().Secrets(metav1.NamespaceSystem).Delete(context.TODO(), secretName, metav1.DeleteOptions{}); err != nil {
			return errors.Wrapf(err, "failed to delete bootstrap token %q", err)
		}

		fmt.Printf("bootstrap token %q deleted\n", token)
	}
	return nil
}

func Generate(app *cli.Context) error {
	if err := cmds.InitLogging(); err != nil {
		return err
	}
	return generate(app, &cmds.TokenConfig)
}

func generate(app *cli.Context, cfg *cmds.Token) error {
	token, err := bootstraputil.GenerateBootstrapToken()
	if err != nil {
		return err
	}
	fmt.Println(token)
	return nil
}

func Rotate(app *cli.Context) error {
	if err := cmds.InitLogging(); err != nil {
		return err
	}
	fmt.Println("\033[33mWARNING\033[0m: Recommended to keep a record of the old token. If restoring from a snapshot, you must use the token associated with that snapshot.")
	info, err := serverAccess(&cmds.TokenConfig)
	if err != nil {
		return err
	}
	b, err := json.Marshal(server.TokenRotateRequest{
		NewToken: ptr.To(cmds.TokenConfig.NewToken),
	})
	if err != nil {
		return err
	}
	if err = info.Put("/v1-"+version.Program+"/token", b); err != nil {
		return err
	}
	// wait for etcd db propagation delay
	time.Sleep(1 * time.Second)
	fmt.Println("Token rotated, restart", version.Program, "nodes with new token")
	return nil
}

func serverAccess(cfg *cmds.Token) (*clientaccess.Info, error) {
	// hide process arguments from ps output, since they likely contain tokens.
	proctitle.SetProcTitle(os.Args[0] + " token")

	dataDir, err := server.ResolveDataDir("")
	if err != nil {
		return nil, err
	}

	if cfg.Token == "" {
		fp := filepath.Join(dataDir, "token")
		tokenByte, err := os.ReadFile(fp)
		if err != nil {
			return nil, err
		}
		cfg.Token = string(bytes.TrimRight(tokenByte, "\n"))
	}
	return clientaccess.ParseAndValidateToken(cfg.ServerURL, cfg.Token, clientaccess.WithUser("server"))
}

func List(app *cli.Context) error {
	if err := cmds.InitLogging(); err != nil {
		return err
	}
	return list(app, &cmds.TokenConfig)
}

func list(app *cli.Context, cfg *cmds.Token) error {
	if err := kubeadm.SetDefaults(app, cfg); err != nil {
		return err
	}

	cfg.Kubeconfig = util.GetKubeConfigPath(cfg.Kubeconfig)
	client, err := util.GetClientSet(cfg.Kubeconfig)
	if err != nil {
		return err
	}

	tokenSelector := fields.SelectorFromSet(
		map[string]string{
			"type": string(bootstrapapi.SecretTypeBootstrapToken),
		},
	)
	listOptions := metav1.ListOptions{
		FieldSelector: tokenSelector.String(),
	}

	secrets, err := client.CoreV1().Secrets(metav1.NamespaceSystem).List(context.TODO(), listOptions)
	if err != nil {
		return errors.Wrapf(err, "failed to list bootstrap tokens")
	}

	tokens := make([]*kubeadm.BootstrapToken, len(secrets.Items))
	for i, secret := range secrets.Items {
		token, err := kubeadm.BootstrapTokenFromSecret(&secret)
		if err != nil {
			fmt.Printf("%v", err)
			continue
		}
		tokens[i] = token
	}

	switch cfg.Output {
	case "json":
		if err := json.NewEncoder(os.Stdout).Encode(tokens); err != nil {
			return err
		}
		return nil
	case "yaml":
		if err := yaml.NewEncoder(os.Stdout).Encode(tokens); err != nil {
			return err
		}
		return nil
	default:
		format := "%s\t%s\t%s\t%s\t%s\t%s\n"
		w := tabwriter.NewWriter(os.Stdout, 10, 4, 3, ' ', 0)
		defer w.Flush()

		fmt.Fprintf(w, format, "TOKEN", "TTL", "EXPIRES", "USAGES", "DESCRIPTION", "EXTRA GROUPS")
		for _, token := range tokens {
			ttl := "<forever>"
			expires := "<never>"
			if token.Expires != nil {
				ttl = duration.ShortHumanDuration(token.Expires.Sub(time.Now()))
				expires = token.Expires.Format(time.RFC3339)
			}

			fmt.Fprintf(w, format, token.Token.ID, ttl, expires, joinOrNone(token.Usages...), joinOrNone(token.Description), joinOrNone(token.Groups...))
		}
	}

	return nil
}

// joinOrNone joins strings with a comma. If the resulting output is an empty string,
// it instead returns the replacement string "<none>"
func joinOrNone(s ...string) string {
	j := strings.Join(s, ",")
	if j == "" {
		return "<none>"
	}
	return j
}
