package imagestreamtag

import (
	"fmt"

	kapi "k8s.io/kubernetes/pkg/api"
	kapierrors "k8s.io/kubernetes/pkg/api/errors"
	"k8s.io/kubernetes/pkg/api/rest"
	"k8s.io/kubernetes/pkg/api/unversioned"
	"k8s.io/kubernetes/pkg/runtime"

	oapi "github.com/openshift/origin/pkg/api"
	"github.com/openshift/origin/pkg/image/api"
	"github.com/openshift/origin/pkg/image/registry/image"
	"github.com/openshift/origin/pkg/image/registry/imagestream"
)

// REST implements the RESTStorage interface for ImageStreamTag
// It only supports the Get method and is used to simplify retrieving an Image by tag from an ImageStream
type REST struct {
	imageRegistry       image.Registry
	imageStreamRegistry imagestream.Registry
}

// NewREST returns a new REST.
func NewREST(imageRegistry image.Registry, imageStreamRegistry imagestream.Registry) *REST {
	return &REST{imageRegistry: imageRegistry, imageStreamRegistry: imageStreamRegistry}
}

// New is only implemented to make REST implement RESTStorage
func (r *REST) New() runtime.Object {
	return &api.ImageStreamTag{}
}

// NewList returns a new list object
func (r *REST) NewList() runtime.Object {
	return &api.ImageStreamTagList{}
}

// nameAndTag splits a string into its name component and tag component, and returns an error
// if the string is not in the right form.
func nameAndTag(id string) (name string, tag string, err error) {
	name, tag, err = api.ParseImageStreamTagName(id)
	if err != nil {
		err = kapierrors.NewBadRequest("ImageStreamTags must be retrieved with <name>:<tag>")
	}
	return
}

func (r *REST) List(ctx kapi.Context, options *kapi.ListOptions) (runtime.Object, error) {
	imageStreams, err := r.imageStreamRegistry.ListImageStreams(ctx, options)
	if err != nil {
		return nil, err
	}

	matcher := MatchImageStreamTag(oapi.ListOptionsToSelectors(options))

	list := &api.ImageStreamTagList{}
	for _, currIS := range imageStreams.Items {
		for currTag := range currIS.Status.Tags {
			istag, err := newISTag(currTag, &currIS, nil, false)
			if err != nil {
				if kapierrors.IsNotFound(err) {
					continue
				}
				return nil, err
			}
			matches, err := matcher.Matches(istag)
			if err != nil {
				return nil, err
			}

			if matches {
				list.Items = append(list.Items, *istag)
			}
		}
	}

	return list, nil
}

// Get retrieves an image that has been tagged by stream and tag. `id` is of the format <stream name>:<tag>.
func (r *REST) Get(ctx kapi.Context, id string) (runtime.Object, error) {
	name, tag, err := nameAndTag(id)
	if err != nil {
		return nil, err
	}

	imageStream, err := r.imageStreamRegistry.GetImageStream(ctx, name)
	if err != nil {
		return nil, err
	}

	image, err := r.imageFor(ctx, tag, imageStream)
	if err != nil {
		return nil, err
	}

	return newISTag(tag, imageStream, image, false)
}

func (r *REST) Update(ctx kapi.Context, tagName string, objInfo rest.UpdatedObjectInfo) (runtime.Object, bool, error) {
	name, tag, err := nameAndTag(tagName)
	if err != nil {
		return nil, false, err
	}

	create := false
	imageStream, err := r.imageStreamRegistry.GetImageStream(ctx, name)
	if err != nil {
		if !kapierrors.IsNotFound(err) {
			return nil, false, err
		}
		namespace, ok := kapi.NamespaceFrom(ctx)
		if !ok {
			return nil, false, kapierrors.NewBadRequest("namespace is required on ImageStreamTags")
		}
		imageStream = &api.ImageStream{
			ObjectMeta: kapi.ObjectMeta{
				Namespace: namespace,
				Name:      name,
			},
		}
		kapi.FillObjectMetaSystemFields(ctx, &imageStream.ObjectMeta)
		create = true
	}

	// create the synthetic old istag
	old, err := newISTag(tag, imageStream, nil, true)
	if err != nil {
		return nil, false, err
	}

	obj, err := objInfo.UpdatedObject(ctx, old)
	if err != nil {
		return nil, false, err
	}

	istag, ok := obj.(*api.ImageStreamTag)
	if !ok {
		return nil, false, kapierrors.NewBadRequest(fmt.Sprintf("obj is not an ImageStreamTag: %#v", obj))
	}

	// check for conflict
	switch {
	case len(istag.ResourceVersion) == 0:
		// should disallow blind PUT, but this was previously supported
		istag.ResourceVersion = imageStream.ResourceVersion
	case len(imageStream.ResourceVersion) == 0:
		// image stream did not exist, cannot update
		return nil, false, kapierrors.NewNotFound(api.Resource("imagestreamtags"), tagName)
	case imageStream.ResourceVersion != istag.ResourceVersion:
		// conflicting input and output
		return nil, false, kapierrors.NewConflict(api.Resource("imagestreamtags"), istag.Name, fmt.Errorf("another caller has updated the resource version to %s", imageStream.ResourceVersion))
	}

	if create {
		if err := rest.BeforeCreate(Strategy, ctx, obj); err != nil {
			return nil, false, err
		}
	} else {
		if err := rest.BeforeUpdate(Strategy, ctx, obj, old); err != nil {
			return nil, false, err
		}
	}

	// update the spec tag
	if imageStream.Spec.Tags == nil {
		imageStream.Spec.Tags = map[string]api.TagReference{}
	}
	tagRef, exists := imageStream.Spec.Tags[tag]
	// if the caller set tag, override the spec tag
	if istag.Tag != nil {
		tagRef = *istag.Tag
		tagRef.Name = tag
	}
	tagRef.Annotations = istag.Annotations
	imageStream.Spec.Tags[tag] = tagRef

	// mutate the image stream
	var newImageStream *api.ImageStream
	if create {
		newImageStream, err = r.imageStreamRegistry.CreateImageStream(ctx, imageStream)
	} else {
		newImageStream, err = r.imageStreamRegistry.UpdateImageStream(ctx, imageStream)
	}
	if err != nil {
		return nil, false, err
	}

	image, err := r.imageFor(ctx, tag, newImageStream)
	if err != nil {
		if !kapierrors.IsNotFound(err) {
			return nil, false, err
		}
	}

	newISTag, err := newISTag(tag, newImageStream, image, true)
	return newISTag, !exists, err
}

// Delete removes a tag from a stream. `id` is of the format <stream name>:<tag>.
// The associated image that the tag points to is *not* deleted.
// The tag history remains intact and is not deleted.
func (r *REST) Delete(ctx kapi.Context, id string) (runtime.Object, error) {
	name, tag, err := nameAndTag(id)
	if err != nil {
		return nil, err
	}

	stream, err := r.imageStreamRegistry.GetImageStream(ctx, name)
	if err != nil {
		return nil, err
	}

	notFound := true

	// Try to delete the status tag
	if _, ok := stream.Status.Tags[tag]; ok {
		delete(stream.Status.Tags, tag)
		notFound = false
	}

	// Try to delete the spec tag
	if _, ok := stream.Spec.Tags[tag]; ok {
		delete(stream.Spec.Tags, tag)
		notFound = false
	}

	if notFound {
		return nil, kapierrors.NewNotFound(api.Resource("imagestreamtags"), tag)
	}

	if _, err = r.imageStreamRegistry.UpdateImageStream(ctx, stream); err != nil {
		return nil, fmt.Errorf("cannot remove tag from image stream: %v", err)
	}

	return &unversioned.Status{Status: unversioned.StatusSuccess}, nil
}

// imageFor retrieves the most recent image for a tag in a given imageStreem.
func (r *REST) imageFor(ctx kapi.Context, tag string, imageStream *api.ImageStream) (*api.Image, error) {
	event := api.LatestTaggedImage(imageStream, tag)
	if event == nil || len(event.Image) == 0 {
		return nil, kapierrors.NewNotFound(api.Resource("imagestreamtags"), api.JoinImageStreamTag(imageStream.Name, tag))
	}

	return r.imageRegistry.GetImage(ctx, event.Image)
}

// newISTag initializes an image stream tag from an image stream and image. The allowEmptyEvent will create a tag even
// in the event that the status tag does does not exist yet (no image has successfully been tagged) or the image is nil.
func newISTag(tag string, imageStream *api.ImageStream, image *api.Image, allowEmptyEvent bool) (*api.ImageStreamTag, error) {
	istagName := api.JoinImageStreamTag(imageStream.Name, tag)

	event := api.LatestTaggedImage(imageStream, tag)
	if event == nil || len(event.Image) == 0 {
		if !allowEmptyEvent {
			return nil, kapierrors.NewNotFound(api.Resource("imagestreamtags"), istagName)
		}
		event = &api.TagEvent{
			Created: imageStream.CreationTimestamp,
		}
	}

	ist := &api.ImageStreamTag{
		ObjectMeta: kapi.ObjectMeta{
			Namespace:         imageStream.Namespace,
			Name:              istagName,
			CreationTimestamp: event.Created,
			Annotations:       map[string]string{},
			ResourceVersion:   imageStream.ResourceVersion,
			UID:               imageStream.UID,
		},
		Generation: event.Generation,
		Conditions: imageStream.Status.Tags[tag].Conditions,
	}

	if imageStream.Spec.Tags != nil {
		if tagRef, ok := imageStream.Spec.Tags[tag]; ok {
			// copy the spec tag
			ist.Tag = &tagRef
			if from := ist.Tag.From; from != nil {
				copied := *from
				ist.Tag.From = &copied
			}
			if gen := ist.Tag.Generation; gen != nil {
				copied := *gen
				ist.Tag.Generation = &copied
			}

			// if the imageStream has Spec.Tags[tag].Annotations[k] = v, copy it to the image's annotations
			// and add them to the istag's annotations
			if image != nil && image.Annotations == nil {
				image.Annotations = make(map[string]string)
			}
			for k, v := range tagRef.Annotations {
				ist.Annotations[k] = v
				if image != nil {
					image.Annotations[k] = v
				}
			}
		}
	}

	if image != nil {
		if err := api.ImageWithMetadata(image); err != nil {
			return nil, err
		}
		image.DockerImageManifest = ""
		ist.Image = *image
	} else {
		ist.Image = api.Image{}
		ist.Image.Name = event.Image
	}

	// Replace the DockerImageReference with the value from event, which contains
	// real value from status. This should fix the problem for v1 registries,
	// where mutliple tags point to a single id and only the first image's metadata
	// is saved. This in turn will always return the pull spec from the first
	// imported image, which might be different than the requested tag.
	ist.Image.DockerImageReference = event.DockerImageReference

	return ist, nil
}