package controllers

import (
	"fmt"
	"time"

	"github.com/golang/glog"

	"k8s.io/kubernetes/pkg/api"
	kapierrors "k8s.io/kubernetes/pkg/api/errors"
	"k8s.io/kubernetes/pkg/client/cache"
	kclientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
	"k8s.io/kubernetes/pkg/controller/framework"
	"k8s.io/kubernetes/pkg/fields"
	"k8s.io/kubernetes/pkg/runtime"
	utilruntime "k8s.io/kubernetes/pkg/util/runtime"
	"k8s.io/kubernetes/pkg/util/wait"
	"k8s.io/kubernetes/pkg/watch"
)

// NumServiceAccountUpdateRetries controls the number of times we will retry on conflict errors.
// This happens when multiple service account controllers update at the same time.
const NumServiceAccountUpdateRetries = 10

// DockercfgDeletedControllerOptions contains options for the DockercfgDeletedController
type DockercfgDeletedControllerOptions struct {
	// Resync is the time.Duration at which to fully re-list secrets.
	// If zero, re-list will be delayed as long as possible
	Resync time.Duration
}

// NewDockercfgDeletedController returns a new *DockercfgDeletedController.
func NewDockercfgDeletedController(cl kclientset.Interface, options DockercfgDeletedControllerOptions) *DockercfgDeletedController {
	e := &DockercfgDeletedController{
		client: cl,
	}

	dockercfgSelector := fields.OneTermEqualSelector(api.SecretTypeField, string(api.SecretTypeDockercfg))
	_, e.secretController = framework.NewInformer(
		&cache.ListWatch{
			ListFunc: func(options api.ListOptions) (runtime.Object, error) {
				opts := api.ListOptions{FieldSelector: dockercfgSelector}
				return e.client.Core().Secrets(api.NamespaceAll).List(opts)
			},
			WatchFunc: func(options api.ListOptions) (watch.Interface, error) {
				opts := api.ListOptions{FieldSelector: dockercfgSelector, ResourceVersion: options.ResourceVersion}
				return e.client.Core().Secrets(api.NamespaceAll).Watch(opts)
			},
		},
		&api.Secret{},
		options.Resync,
		framework.ResourceEventHandlerFuncs{
			DeleteFunc: e.secretDeleted,
		},
	)

	return e
}

// The DockercfgDeletedController watches for service account dockercfg secrets to be deleted
// It removes the corresponding token secret and service account references.
type DockercfgDeletedController struct {
	stopChan chan struct{}

	client kclientset.Interface

	secretController *framework.Controller
}

// Runs controller loops and returns immediately
func (e *DockercfgDeletedController) Run() {
	if e.stopChan == nil {
		e.stopChan = make(chan struct{})
		go e.secretController.Run(e.stopChan)
	}
}

// Stop gracefully shuts down this controller
func (e *DockercfgDeletedController) Stop() {
	if e.stopChan != nil {
		close(e.stopChan)
		e.stopChan = nil
	}
}

// secretDeleted reacts to a Secret being deleted by looking to see if it's a dockercfg secret for a service account, in which case it
// it removes the references from the service account and removes the token created to back the dockercfgSecret
func (e *DockercfgDeletedController) secretDeleted(obj interface{}) {
	dockercfgSecret, ok := obj.(*api.Secret)
	if !ok {
		return
	}
	if _, exists := dockercfgSecret.Annotations[ServiceAccountTokenSecretNameKey]; !exists {
		return
	}

	for i := 1; i <= NumServiceAccountUpdateRetries; i++ {
		if err := e.removeDockercfgSecretReference(dockercfgSecret); err != nil {
			if kapierrors.IsConflict(err) && i < NumServiceAccountUpdateRetries {
				time.Sleep(wait.Jitter(100*time.Millisecond, 0.0))
				continue
			}

			glog.Error(err)
			break
		}

		break
	}

	// remove the reference token secret
	if err := e.client.Core().Secrets(dockercfgSecret.Namespace).Delete(dockercfgSecret.Annotations[ServiceAccountTokenSecretNameKey], nil); (err != nil) && !kapierrors.IsNotFound(err) {
		utilruntime.HandleError(err)
	}
}

// removeDockercfgSecretReference updates the given ServiceAccount to remove ImagePullSecret and Secret references
func (e *DockercfgDeletedController) removeDockercfgSecretReference(dockercfgSecret *api.Secret) error {
	serviceAccount, err := e.getServiceAccount(dockercfgSecret)
	if kapierrors.IsNotFound(err) {
		// if the service account is gone, no work to do
		return nil
	}
	if err != nil {
		return err
	}

	changed := false

	secrets := []api.ObjectReference{}
	for _, s := range serviceAccount.Secrets {
		if s.Name == dockercfgSecret.Name {
			changed = true
			continue
		}

		secrets = append(secrets, s)
	}
	serviceAccount.Secrets = secrets

	imagePullSecrets := []api.LocalObjectReference{}
	for _, s := range serviceAccount.ImagePullSecrets {
		if s.Name == dockercfgSecret.Name {
			changed = true
			continue
		}

		imagePullSecrets = append(imagePullSecrets, s)
	}
	serviceAccount.ImagePullSecrets = imagePullSecrets

	if changed {
		_, err = e.client.Core().ServiceAccounts(dockercfgSecret.Namespace).Update(serviceAccount)
		if err != nil {
			return err
		}
	}

	return nil
}

// getServiceAccount returns the ServiceAccount referenced by the given secret.  return nil, but no error if the secret doesn't reference a service account
func (e *DockercfgDeletedController) getServiceAccount(secret *api.Secret) (*api.ServiceAccount, error) {
	saName, saUID := secret.Annotations[api.ServiceAccountNameKey], secret.Annotations[api.ServiceAccountUIDKey]
	if len(saName) == 0 || len(saUID) == 0 {
		return nil, nil
	}

	serviceAccount, err := e.client.Core().ServiceAccounts(secret.Namespace).Get(saName)
	if err != nil {
		return nil, err
	}

	if saUID != string(serviceAccount.UID) {
		return nil, fmt.Errorf("secret (%v) service account UID (%v) does not match service account (%v) UID (%v)", secret.Name, saUID, serviceAccount.Name, serviceAccount.UID)
	}
	return serviceAccount, nil
}