package controller

import (
	"fmt"
	"strings"

	"github.com/golang/glog"
	kerrors "k8s.io/kubernetes/pkg/api/errors"

	kapi "k8s.io/kubernetes/pkg/api"
	utilruntime "k8s.io/kubernetes/pkg/util/runtime"

	buildapi "github.com/openshift/origin/pkg/build/api"
	buildclient "github.com/openshift/origin/pkg/build/client"
	buildutil "github.com/openshift/origin/pkg/build/util"
	oscache "github.com/openshift/origin/pkg/client/cache"
	imageapi "github.com/openshift/origin/pkg/image/api"
)

// ImageChangeController watches for changes to ImageRepositories and triggers
// builds when a new version of a tag referenced by a BuildConfig
// is available.
type ImageChangeController struct {
	BuildConfigIndex        oscache.StoreToBuildConfigLister
	BuildConfigInstantiator buildclient.BuildConfigInstantiator
}

// getImageStreamNameFromReference strips off the :tag or @id suffix
// from an ImageStream[Tag,Image,''].Name
func getImageStreamNameFromReference(ref *kapi.ObjectReference) string {
	name := strings.Split(ref.Name, ":")[0]
	return strings.Split(name, "@")[0]
}

// HandleImageStream processes the next ImageStream event.
func (c *ImageChangeController) HandleImageStream(stream *imageapi.ImageStream) error {
	glog.V(4).Infof("Build image change controller detected ImageStream change %s", stream.Status.DockerImageRepository)

	// Loop through all build configurations and record if there was an error
	// instead of breaking the loop. The error will be returned in the end, so the
	// retry controller can retry. Any BuildConfigs that were processed successfully
	// should have had their LastTriggeredImageID updated, so the retry should result
	// in a no-op for them.
	hasError := false

	bcs, err := c.BuildConfigIndex.GetConfigsForImageStreamTrigger(stream.Namespace, stream.Name)
	if err != nil {
		return err
	}
	for _, config := range bcs {
		var (
			from            *kapi.ObjectReference
			shouldBuild     = false
			triggeredImage  = ""
			latestReference string
		)
		// For every ImageChange trigger find the latest tagged image from the image stream and
		// invoke a build using that image id. A new build is triggered only if the latest tagged image id or pull spec
		// differs from the last triggered build recorded on the build config for that trigger
		for _, trigger := range config.Spec.Triggers {
			if trigger.Type != buildapi.ImageChangeBuildTriggerType {
				continue
			}
			if trigger.ImageChange.From != nil {
				from = trigger.ImageChange.From
			} else {
				from = buildutil.GetInputReference(config.Spec.Strategy)
			}

			if from == nil || from.Kind != "ImageStreamTag" {
				continue
			}
			fromStreamName, tag, ok := imageapi.SplitImageStreamTag(from.Name)
			if !ok {
				glog.Errorf("Invalid image stream tag: %s in build config %s/%s", from.Name, config.Name, config.Namespace)
				continue
			}

			fromNamespace := from.Namespace
			if len(fromNamespace) == 0 {
				fromNamespace = config.Namespace
			}

			// only trigger a build if this image stream matches the name and namespace of the stream ref in the build trigger
			// also do not trigger if the imagerepo does not have a valid DockerImageRepository value for us to pull
			// the image from
			if len(stream.Status.DockerImageRepository) == 0 || fromStreamName != stream.Name || fromNamespace != stream.Namespace {
				continue
			}

			// This split is safe because ImageStreamTag names always have the form
			// name:tag.
			latestReference, ok = imageapi.ResolveLatestTaggedImage(stream, tag)
			if !ok {
				glog.V(4).Infof("unable to find tagged image: no image recorded for %s/%s:%s", stream.Namespace, stream.Name, tag)
				continue
			}
			glog.V(4).Infof("Found ImageStream %s/%s with tag %s", stream.Namespace, stream.Name, tag)

			// (must be different) to trigger a build
			last := trigger.ImageChange.LastTriggeredImageID
			next := latestReference

			if len(last) == 0 || (len(next) > 0 && next != last) {
				triggeredImage = next
				shouldBuild = true
				// it doesn't really make sense to have multiple image change triggers any more,
				// so just exit the loop now
				break
			}
		}

		if shouldBuild {
			glog.V(4).Infof("Running build for BuildConfig %s/%s", config.Namespace, config.Name)
			buildCauses := []buildapi.BuildTriggerCause{}
			// instantiate new build
			request := &buildapi.BuildRequest{
				ObjectMeta: kapi.ObjectMeta{
					Name:      config.Name,
					Namespace: config.Namespace,
				},
				TriggeredBy: append(buildCauses,
					buildapi.BuildTriggerCause{
						Message: buildapi.BuildTriggerCauseImageMsg,
						ImageChangeBuild: &buildapi.ImageChangeCause{
							ImageID: latestReference,
							FromRef: from,
						},
					},
				),
				TriggeredByImage: &kapi.ObjectReference{
					Kind: "DockerImage",
					Name: triggeredImage,
				},
				From: from,
			}
			if _, err := c.BuildConfigInstantiator.Instantiate(config.Namespace, request); err != nil {
				if kerrors.IsConflict(err) {
					utilruntime.HandleError(fmt.Errorf("unable to instantiate Build for BuildConfig %s/%s due to a conflicting update: %v", config.Namespace, config.Name, err))
				} else {
					utilruntime.HandleError(fmt.Errorf("error instantiating Build from BuildConfig %s/%s: %v", config.Namespace, config.Name, err))
				}
				hasError = true
				continue
			}
		}
	}
	if hasError {
		return fmt.Errorf("an error occurred processing 1 or more build configurations; the image change trigger for image stream %s will be retried", stream.Status.DockerImageRepository)
	}
	return nil
}