package sa

import (
	"errors"
	"fmt"
	"io"
	"math"
	"os"
	"time"

	"github.com/spf13/cobra"

	"k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/client/unversioned"
	"k8s.io/kubernetes/pkg/fields"
	"k8s.io/kubernetes/pkg/kubectl"
	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
	"k8s.io/kubernetes/pkg/watch"

	"github.com/openshift/origin/pkg/cmd/templates"
	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
	"github.com/openshift/origin/pkg/cmd/util/term"
	"github.com/openshift/origin/pkg/serviceaccounts"
	osautil "github.com/openshift/origin/pkg/serviceaccounts/util"
)

const (
	NewServiceAccountTokenRecommendedName = "new-token"

	newServiceAccountTokenShort = `Generate a new token for a service account.`

	newServiceAccountTokenUsage = `%s SA-NAME`
)

var (
	newServiceAccountTokenLong = templates.LongDesc(`
    Generate a new token for a service account.

    Service account API tokens are used by service accounts to authenticate to the API.
    This command will generate a new token, which could be used to compartmentalize service
    account actions by executing them with distinct tokens, to rotate out pre-existing token
    on the service account, or for use by an external client. If a label is provided, it will
    be applied to any created token so that tokens created with this command can be idenitifed.`)

	newServiceAccountTokenExamples = templates.Examples(`
    # Generate a new token for service account 'default'
    %[1]s 'default'

    # Generate a new token for service account 'default' and apply
    # labels 'foo' and 'bar' to the new token for identification
    # %[1]s 'default' --labels foo=foo-value,bar=bar-value`)
)

type NewServiceAccountTokenOptions struct {
	SAName        string
	SAClient      unversioned.ServiceAccountsInterface
	SecretsClient unversioned.SecretsInterface

	Labels map[string]string

	Timeout time.Duration

	Out io.Writer
	Err io.Writer
}

func NewCommandNewServiceAccountToken(name, fullname string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
	options := &NewServiceAccountTokenOptions{
		Out:    out,
		Err:    os.Stderr,
		Labels: map[string]string{},
	}

	var requestedLabels string
	newServiceAccountTokenCommand := &cobra.Command{
		Use:     fmt.Sprintf(newServiceAccountTokenUsage, name),
		Short:   newServiceAccountTokenShort,
		Long:    newServiceAccountTokenLong,
		Example: fmt.Sprintf(newServiceAccountTokenExamples, fullname),
		Run: func(cmd *cobra.Command, args []string) {
			cmdutil.CheckErr(options.Complete(args, requestedLabels, f, cmd))
			cmdutil.CheckErr(options.Validate())
			cmdutil.CheckErr(options.Run())
		},
	}

	newServiceAccountTokenCommand.Flags().DurationVar(&options.Timeout, "timeout", 30*time.Second, "the maximum time allowed to generate a token")
	newServiceAccountTokenCommand.Flags().StringVarP(&requestedLabels, "labels", "l", "", "labels to set in all resources for this application, given as a comma-delimited list of key-value pairs")
	return newServiceAccountTokenCommand
}

func (o *NewServiceAccountTokenOptions) Complete(args []string, requestedLabels string, f *clientcmd.Factory, cmd *cobra.Command) error {
	if len(args) != 1 {
		return cmdutil.UsageError(cmd, fmt.Sprintf("expected one service account name as an argument, got %q", args))
	}

	o.SAName = args[0]

	if len(requestedLabels) > 0 {
		labels, err := kubectl.ParseLabels(requestedLabels)
		if err != nil {
			return cmdutil.UsageError(cmd, err.Error())
		}
		o.Labels = labels
	}

	client, err := f.Client()
	if err != nil {
		return err
	}

	namespace, _, err := f.DefaultNamespace()
	if err != nil {
		return fmt.Errorf("could not retrieve default namespace: %v", err)
	}

	o.SAClient = client.ServiceAccounts(namespace)
	o.SecretsClient = client.Secrets(namespace)
	return nil
}

func (o *NewServiceAccountTokenOptions) Validate() error {
	if o.SAName == "" {
		return errors.New("service account name cannot be empty")
	}

	if o.SAClient == nil || o.SecretsClient == nil {
		return errors.New("API clients must not be nil in order to create a new service account token")
	}

	if o.Timeout <= 0 {
		return errors.New("a positive amount of time must be allotted for the timeout")
	}

	if o.Out == nil || o.Err == nil {
		return errors.New("cannot proceed if output or error writers are nil")
	}

	return nil
}

// Run creates a new token secret, waits for the service account token controller to fulfill it, then adds the token to the service account
func (o *NewServiceAccountTokenOptions) Run() error {
	serviceAccount, err := o.SAClient.Get(o.SAName)
	if err != nil {
		return err
	}

	tokenSecret := &api.Secret{
		ObjectMeta: api.ObjectMeta{
			GenerateName: osautil.GetTokenSecretNamePrefix(serviceAccount),
			Namespace:    serviceAccount.Namespace,
			Labels:       o.Labels,
			Annotations: map[string]string{
				api.ServiceAccountNameKey: serviceAccount.Name,
			},
		},
		Type: api.SecretTypeServiceAccountToken,
		Data: map[string][]byte{},
	}

	persistedToken, err := o.SecretsClient.Create(tokenSecret)
	if err != nil {
		return err
	}

	// we need to wait for the service account token controller to make the new token valid
	tokenSecret, err = waitForToken(persistedToken, serviceAccount, o.Timeout, o.SecretsClient)
	if err != nil {
		return err
	}

	token, exists := tokenSecret.Data[api.ServiceAccountTokenKey]
	if !exists {
		return fmt.Errorf("service account token %q did not contain token data", tokenSecret.Name)
	}

	fmt.Fprintf(o.Out, string(token))
	if term.IsTerminalWriter(o.Out) {
		// pretty-print for a TTY
		fmt.Fprintf(o.Out, "\n")
	}
	return nil
}

// waitForToken uses `cmd.Until` to wait for the service account controller to fulfill the token request
func waitForToken(token *api.Secret, serviceAccount *api.ServiceAccount, timeout time.Duration, client unversioned.SecretsInterface) (*api.Secret, error) {
	// there is no provided rounding function, so we use Round(x) === Floor(x + 0.5)
	timeoutSeconds := int64(math.Floor(timeout.Seconds() + 0.5))

	options := api.ListOptions{
		FieldSelector:   fields.SelectorFromSet(fields.Set(map[string]string{"metadata.name": token.Name})),
		Watch:           true,
		ResourceVersion: token.ResourceVersion,
		TimeoutSeconds:  &timeoutSeconds,
	}

	watcher, err := client.Watch(options)
	if err != nil {
		return nil, fmt.Errorf("could not begin watch for token: %v", err)
	}

	event, err := watch.Until(timeout, watcher, func(event watch.Event) (bool, error) {
		if event.Type == watch.Error {
			return false, fmt.Errorf("encountered error while watching for token: %v", event.Object)
		}

		eventToken, ok := event.Object.(*api.Secret)
		if !ok {
			return false, nil
		}

		if eventToken.Name != token.Name {
			return false, nil
		}

		switch event.Type {
		case watch.Modified:
			if serviceaccounts.IsValidServiceAccountToken(serviceAccount, eventToken) {
				return true, nil
			}
		case watch.Deleted:
			return false, errors.New("token was deleted before fulfillment by service account token controller")
		case watch.Added:
			return false, errors.New("unxepected action: token was added after initial creation")
		}
		return false, nil
	})
	if err != nil {
		return nil, err
	}

	return event.Object.(*api.Secret), nil
}