package controller

import (
	"strings"

	"github.com/golang/glog"

	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"

	deployapi "github.com/openshift/origin/pkg/deploy/api"
	imageapi "github.com/openshift/origin/pkg/image/api"
)

// ImageChangeController watches for changes to ImageRepositories and regenerates
// DeploymentConfigs when a new version of a tag referenced by a DeploymentConfig
// is available.
type ImageChangeController struct {
	DeploymentConfigInterface icDeploymentConfigInterface
	NextImageRepository       func() *imageapi.ImageRepository
	DeploymentConfigStore     cache.Store
	// Stop is an optional channel that controls when the controller exits
	Stop <-chan struct{}
}

type icDeploymentConfigInterface interface {
	UpdateDeploymentConfig(namespace string, config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error)
	GenerateDeploymentConfig(namespace, name string) (*deployapi.DeploymentConfig, error)
}

// Run processes ImageRepository events one by one.
func (c *ImageChangeController) Run() {
	go util.Until(c.HandleImageRepo, 0, c.Stop)
}

// HandleImageRepo processes the next ImageRepository event.
func (c *ImageChangeController) HandleImageRepo() {
	imageRepo := c.NextImageRepository()
	configNames := []string{}
	firedTriggersForConfig := make(map[string][]deployapi.DeploymentTriggerImageChangeParams)

	for _, c := range c.DeploymentConfigStore.List() {
		config := c.(*deployapi.DeploymentConfig)
		glog.V(4).Infof("Detecting changed images for deploymentConfig %s", config.Name)

		// Extract relevant triggers for this imageRepo for this config
		triggersForConfig := []deployapi.DeploymentTriggerImageChangeParams{}
		for _, trigger := range config.Triggers {
			if trigger.Type == deployapi.DeploymentTriggerOnImageChange &&
				trigger.ImageChangeParams.Automatic &&
				trigger.ImageChangeParams.RepositoryName == imageRepo.DockerImageRepository {
				triggersForConfig = append(triggersForConfig, *trigger.ImageChangeParams)
			}
		}

		for _, params := range triggersForConfig {
			glog.V(4).Infof("Processing image triggers for deploymentConfig %s", config.Name)
			containerNames := util.NewStringSet(params.ContainerNames...)
			for _, container := range config.Template.ControllerTemplate.Template.Spec.Containers {
				if !containerNames.Has(container.Name) {
					continue
				}

				// The container image's tag name is by convention the same as the image ID it references
				_, containerImageID := parseImage(container.Image)
				if repoImageID, repoHasTag := imageRepo.Tags[params.Tag]; repoHasTag && repoImageID != containerImageID {
					configNames = append(configNames, config.Name)
					firedTriggersForConfig[config.Name] = append(firedTriggersForConfig[config.Name], params)
				}
			}
		}
	}

	for _, configName := range configNames {
		glog.V(4).Infof("Regenerating deploymentConfig %s", configName)
		err := c.regenerate(imageRepo.Namespace, configName, firedTriggersForConfig[configName])
		if err != nil {
			glog.V(2).Infof("Error regenerating deploymentConfig %v: %v", configName, err)
		}
	}
}

func (c *ImageChangeController) regenerate(namespace, configName string, triggers []deployapi.DeploymentTriggerImageChangeParams) error {
	newConfig, err := c.DeploymentConfigInterface.GenerateDeploymentConfig(namespace, configName)
	if err != nil {
		glog.V(2).Infof("Error generating new version of deploymentConfig %v", configName)
		return err
	}

	// update the deployment config with the trigger that resulted in the new config being generated
	newConfig.Details = generateTriggerDetails(triggers)

	_, err = c.DeploymentConfigInterface.UpdateDeploymentConfig(newConfig.Namespace, newConfig)
	if err != nil {
		glog.V(2).Infof("Error updating deploymentConfig %v", configName)
		return err
	}

	return nil
}

func parseImage(name string) (string, string) {
	index := strings.LastIndex(name, ":")
	if index == -1 {
		return "", ""
	}

	return name[:index], name[index+1:]
}

func generateTriggerDetails(triggers []deployapi.DeploymentTriggerImageChangeParams) *deployapi.DeploymentDetails {
	// Generate the DeploymentCause objects from each DeploymentTriggerImageChangeParams object
	// Using separate structs to ensure flexibility in the future if these structs need to diverge
	causes := []*deployapi.DeploymentCause{}
	for _, trigger := range triggers {
		causes = append(causes,
			&deployapi.DeploymentCause{
				Type: deployapi.DeploymentTriggerOnImageChange,
				ImageTrigger: &deployapi.DeploymentCauseImageTrigger{
					RepositoryName: trigger.RepositoryName,
					Tag:            trigger.Tag,
				},
			})
	}
	return &deployapi.DeploymentDetails{
		Causes: causes,
	}
}