package systemtokens

import (
	"errors"
	"fmt"
	"strings"

	"github.com/rancher/rancher/pkg/auth/tokens"
	v3 "github.com/rancher/rancher/pkg/generated/norman/management.cattle.io/v3"
	"github.com/rancher/rancher/pkg/types/config"
	"github.com/rancher/rancher/pkg/types/config/systemtokens"
	"github.com/rancher/wrangler/pkg/randomtoken"
	"github.com/sirupsen/logrus"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func NewSystemTokensFromScale(mgmt *config.ScaledContext) systemtokens.Interface {
	return &systemTokens{
		tokenLister: mgmt.Management.Tokens("").Controller().Lister(),
		tokenClient: mgmt.Management.Tokens(""),
	}
}

type systemTokens struct {
	tokenClient v3.TokenInterface
	tokenLister v3.TokenLister
}

// EnsureSystemToken creates tokens or updates their values if they already exist and returns their value.
// TTL defaults to 1 hour, after that this method will auto-refresh. If your token will be in use for more
// than one hour without calling this method again you must pass in an overrideTTL.
// However, the overrideTTL must not be 0, otherwise the token will never be cleaned up.
func (t *systemTokens) EnsureSystemToken(tokenName, description, kind, username string, overrideTTL *int64, randomize bool) (string, error) {
	var err error
	if !randomize {
		_, err = t.tokenLister.Get("", tokenName)
		if err != nil && !apierrors.IsNotFound(err) {
			return "", err
		}
	}

	// needs fresh token because its missing or expired
	val, err := t.createOrUpdateSystemToken(tokenName, description, kind, username, overrideTTL, randomize)
	if err != nil {
		return "", err
	}

	return val, nil
}

func (t *systemTokens) DeleteToken(tokenName string) error {
	return t.tokenClient.Delete(tokenName, &v1.DeleteOptions{})
}

// Creates token obj with hashed token, returns token. Overwrites if pre-existing.
func (t *systemTokens) createOrUpdateSystemToken(tokenName, description, kind, userName string, overrideTTL *int64, randomize bool) (string, error) {
	if strings.HasPrefix(tokenName, "token-") {
		return "", errors.New("token names can't start with token-")
	}
	key, err := randomtoken.Generate()
	if err != nil {
		return "", errors.New("failed to generate token key")
	}

	token, err := t.tokenLister.Get("", tokenName)
	if err != nil {
		if !apierrors.IsNotFound(err) {
			return "", err
		}
		// not found error, make new token
		token = &v3.Token{
			ObjectMeta: v1.ObjectMeta{
				Name: tokenName,
				Labels: map[string]string{
					tokens.UserIDLabel:    userName,
					tokens.TokenKindLabel: kind,
				},
			},
			TTLMillis:    86400000, // 24 hours, token purge daemon will cleanup
			Description:  description,
			UserID:       userName,
			AuthProvider: "local",
			IsDerived:    true,
			Token:        key,
		}
		if overrideTTL != nil {
			token.TTLMillis = *overrideTTL
		}
		if randomize {
			token.ObjectMeta.Name = ""
			token.ObjectMeta.GenerateName = tokenName
		}

		err = tokens.ConvertTokenKeyToHash(token)
		if err != nil {
			return "", err
		}
		logrus.Infof("Creating system token for %v, token: %v", userName, tokenName)
		token, err = t.tokenClient.Create(token)
		if err != nil {
			return "", err
		}
	} else {
		token.Token = key
		err = tokens.ConvertTokenKeyToHash(token)
		if err != nil {
			return "", err
		}
		logrus.Infof("Updating system token for %v, token: %v", userName, tokenName)
		token, err = t.tokenClient.Update(token)
		if err != nil {
			return "", err
		}
	}

	fullVal := fmt.Sprintf("%s:%s", token.Name, key)
	return fullVal, nil
}
