package cmd

import (
	"errors"
	"fmt"
	"io"
	"strings"

	"github.com/golang/glog"
	"github.com/spf13/cobra"

	kapi "k8s.io/kubernetes/pkg/api"
	kerrors "k8s.io/kubernetes/pkg/api/errors"
	"k8s.io/kubernetes/pkg/api/unversioned"
	"k8s.io/kubernetes/pkg/client/retry"
	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
	"k8s.io/kubernetes/pkg/util/sets"

	"github.com/openshift/origin/pkg/client"
	"github.com/openshift/origin/pkg/cmd/templates"
	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
	imageapi "github.com/openshift/origin/pkg/image/api"
)

// TagOptions contains all the necessary options for the cli tag command.
type TagOptions struct {
	out      io.Writer
	osClient client.Interface

	deleteTag    bool
	aliasTag     bool
	scheduleTag  bool
	insecureTag  bool
	referenceTag bool
	namespace    string

	referencePolicy string

	ref            imageapi.DockerImageReference
	sourceKind     string
	destNamespace  []string
	destNameAndTag []string
}

var (
	tagLong = templates.LongDesc(`
		Tag existing images into image streams

		The tag command allows you to take an existing tag or image from an image
		stream, or a Docker image pull spec, and set it as the most recent image for a
		tag in 1 or more other image streams. It is similar to the 'docker tag'
		command, but it operates on image streams instead.

		Pass the --insecure flag if your external registry does not have a valid HTTPS
		certificate, or is only served over HTTP. Pass --scheduled to have the server
		regularly check the tag for updates and import the latest version (which can
		then trigger builds and deployments). Note that --scheduled is only allowed for
		Docker images.`)

	tagExample = templates.Examples(`
		# Tag the current image for the image stream 'openshift/ruby' and tag '2.0' into the image stream 'yourproject/ruby with tag 'tip'.
	  %[1]s tag openshift/ruby:2.0 yourproject/ruby:tip

	  # Tag a specific image.
	  %[1]s tag openshift/ruby@sha256:6b646fa6bf5e5e4c7fa41056c27910e679c03ebe7f93e361e6515a9da7e258cc yourproject/ruby:tip

	  # Tag an external Docker image.
	  %[1]s tag --source=docker openshift/origin:latest yourproject/ruby:tip

	  # Tag an external Docker image and request pullthrough for it.
	  %[1]s tag --source=docker openshift/origin:latest yourproject/ruby:tip --reference-policy=local


	  # Remove the specified spec tag from an image stream.
	  %[1]s tag openshift/origin:latest -d`)
)

const (
	sourceReferencePolicy = "source"
	localReferencePolicy  = "local"
)

// NewCmdTag implements the OpenShift cli tag command.
func NewCmdTag(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
	opts := &TagOptions{}

	cmd := &cobra.Command{
		Use:     "tag [--source=SOURCETYPE] SOURCE DEST [DEST ...]",
		Short:   "Tag existing images into image streams",
		Long:    tagLong,
		Example: fmt.Sprintf(tagExample, fullName),
		Run: func(cmd *cobra.Command, args []string) {
			kcmdutil.CheckErr(opts.Complete(f, cmd, args, out))
			kcmdutil.CheckErr(opts.Validate())
			kcmdutil.CheckErr(opts.RunTag())
		},
	}

	cmd.Flags().StringVar(&opts.sourceKind, "source", opts.sourceKind, "Optional hint for the source type; valid values are 'imagestreamtag', 'istag', 'imagestreamimage', 'isimage', and 'docker'.")
	cmd.Flags().BoolVarP(&opts.deleteTag, "delete", "d", opts.deleteTag, "Delete the provided spec tags.")
	cmd.Flags().BoolVar(&opts.aliasTag, "alias", false, "Should the destination tag be updated whenever the source tag changes. Defaults to false.")
	cmd.Flags().BoolVar(&opts.referenceTag, "reference", false, "Should the destination tag continue to pull from the source namespace. Defaults to false.")
	cmd.Flags().BoolVar(&opts.scheduleTag, "scheduled", false, "Set a Docker image to be periodically imported from a remote repository. Defaults to false.")
	cmd.Flags().BoolVar(&opts.insecureTag, "insecure", false, "Set to true if importing the specified Docker image requires HTTP or has a self-signed certificate. Defaults to false.")
	cmd.Flags().StringVar(&opts.referencePolicy, "reference-policy", sourceReferencePolicy, "Allow to request pullthrough for external image when set to 'local'. Defaults to 'source'.")

	return cmd
}

func parseStreamName(defaultNamespace, name string) (string, string, error) {
	if !strings.Contains(name, "/") {
		return defaultNamespace, name, nil
	}

	parts := strings.Split(name, "/")
	if len(parts) != 2 {
		return "", "", fmt.Errorf("invalid image stream %q", name)
	}

	namespace := parts[0]
	if len(namespace) == 0 {
		return "", "", fmt.Errorf("invalid namespace %q for image stream %q", namespace, name)
	}

	streamName := parts[1]
	if len(streamName) == 0 {
		return "", "", fmt.Errorf("invalid name %q for image stream %q", streamName, name)
	}

	return namespace, streamName, nil
}

func determineSourceKind(f *clientcmd.Factory, input string) string {
	mapper, _ := f.Object()
	gvks, err := mapper.KindsFor(unversioned.GroupVersionResource{Group: imageapi.GroupName, Resource: input})
	if err == nil {
		return gvks[0].Kind
	}

	// DockerImage isn't in RESTMapper
	switch strings.ToLower(input) {
	case "docker", "dockerimage":
		return "DockerImage"
	}

	return input
}

// Complete completes all the required options for the tag command.
func (o *TagOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
	if len(args) < 2 && (len(args) < 1 && !o.deleteTag) {
		return kcmdutil.UsageError(cmd, "you must specify a source and at least one destination or one or more tags to delete")
	}

	// Setup writer.
	o.out = out

	// Setup client.
	var err error
	o.osClient, _, err = f.Clients()
	if err != nil {
		return err
	}

	// Setup namespace.
	if len(o.namespace) == 0 {
		o.namespace, _, err = f.DefaultNamespace()
		if err != nil {
			return err
		}
	}

	// Populate source.
	if !o.deleteTag {
		source := args[0]
		glog.V(3).Infof("Using %q as a source tag", source)

		sourceKind := o.sourceKind
		if len(sourceKind) > 0 {
			sourceKind = determineSourceKind(f, sourceKind)
		}
		if len(sourceKind) > 0 {
			validSources := sets.NewString("imagestreamtag", "istag", "imagestreamimage", "isimage", "docker", "dockerimage")
			if !validSources.Has(strings.ToLower(sourceKind)) {
				kcmdutil.CheckErr(kcmdutil.UsageError(cmd, "invalid source %q; valid values are %v", o.sourceKind, strings.Join(validSources.List(), ", ")))
			}
		}

		ref, err := imageapi.ParseDockerImageReference(source)
		if err != nil {
			return fmt.Errorf("invalid SOURCE: %v", err)
		}
		switch sourceKind {
		case "ImageStreamTag", "ImageStreamImage":
			if len(ref.Registry) > 0 {
				return fmt.Errorf("server in SOURCE is only allowed when providing a Docker image")
			}
			if ref.Namespace == imageapi.DockerDefaultNamespace {
				ref.Namespace = o.namespace
			}
			if sourceKind == "ImageStreamTag" {
				if len(ref.Tag) == 0 {
					return fmt.Errorf("--source=ImageStreamTag requires a valid <name>:<tag> in SOURCE")
				}
			} else {
				if len(ref.ID) == 0 {
					return fmt.Errorf("--source=ImageStreamImage requires a valid <name>@<id> in SOURCE")
				}
			}
		case "":
			if len(ref.Registry) > 0 {
				sourceKind = "DockerImage"
				break
			}
			if len(ref.ID) > 0 {
				sourceKind = "ImageStreamImage"
				break
			}
			if len(ref.Tag) > 0 {
				sourceKind = "ImageStreamTag"
				break
			}
			sourceKind = "DockerImage"
		}

		// if we are not aliasing the tag, specify the exact value to copy
		if sourceKind == "ImageStreamTag" && !o.aliasTag {
			srcNamespace := ref.Namespace
			if len(srcNamespace) == 0 {
				srcNamespace = o.namespace
			}
			is, err := o.osClient.ImageStreams(srcNamespace).Get(ref.Name)
			if err != nil {
				return err
			}
			event := imageapi.LatestTaggedImage(is, ref.Tag)
			if event == nil {
				return fmt.Errorf("%q is not currently pointing to an image, cannot use it as the source of a tag", args[0])
			}
			if len(event.Image) == 0 {
				imageRef, err := imageapi.ParseDockerImageReference(event.DockerImageReference)
				if err != nil {
					return fmt.Errorf("the image stream tag %q has an invalid pull spec and cannot be used to tag: %v", args[0], err)
				}
				ref = imageRef
				sourceKind = "DockerImage"
			} else {
				ref.ID = event.Image
				ref.Tag = ""
				sourceKind = "ImageStreamImage"
			}
		}

		args = args[1:]
		o.sourceKind = sourceKind
		o.ref = ref
		glog.V(3).Infof("Source tag %s %#v", o.sourceKind, o.ref)
	}

	// Populate destinations.
	for _, arg := range args {
		destNamespace, destNameAndTag, err := parseStreamName(o.namespace, arg)
		if err != nil {
			return err
		}
		o.destNamespace = append(o.destNamespace, destNamespace)
		o.destNameAndTag = append(o.destNameAndTag, destNameAndTag)
		glog.V(3).Infof("Using \"%s/%s\" as a destination tag", destNamespace, destNameAndTag)
	}

	return nil
}

// Validate validates all the required options for the tag command.
func (o TagOptions) Validate() error {
	// Validate client and writer
	if o.osClient == nil {
		return errors.New("a client is required")
	}
	if o.out == nil {
		return errors.New("a writer interface is required")
	}

	if o.deleteTag && o.aliasTag {
		return errors.New("--alias and --delete may not be both specified")
	}

	if o.referencePolicy != sourceReferencePolicy && o.referencePolicy != localReferencePolicy {
		return errors.New("reference policy must be set to 'source' or 'local'")
	}

	// Validate source tag based on --delete usage.
	if o.deleteTag {
		if len(o.sourceKind) > 0 {
			return errors.New("cannot specify a source kind when deleting")
		}
		if len(o.ref.String()) > 0 {
			return errors.New("cannot specify a source when deleting")
		}
		if o.scheduleTag || o.insecureTag {
			return errors.New("cannot set flags for importing images when deleting a tag")
		}
	} else {
		if len(o.sourceKind) == 0 {
			return errors.New("a source kind is required")
		}
		if len(o.ref.String()) == 0 {
			return errors.New("a source is required")
		}
	}

	// Validate destination tags.
	if len(o.destNamespace) == 0 || len(o.destNameAndTag) == 0 {
		return errors.New("at least a destination is required")
	}
	if len(o.destNamespace) != len(o.destNameAndTag) {
		return errors.New("destination namespaces don't match with destination tags")
	}
	if o.sourceKind != "DockerImage" && (o.scheduleTag || o.insecureTag) {
		return errors.New("only Docker images can have importing flags set")
	}
	if o.aliasTag && (o.scheduleTag || o.insecureTag) {
		return errors.New("cannot set a Docker image tag as an alias and also set import flags")
	}

	return nil
}

// RunTag contains all the necessary functionality for the OpenShift cli tag command.
func (o TagOptions) RunTag() error {
	var tagReferencePolicy imageapi.TagReferencePolicyType
	switch o.referencePolicy {
	case sourceReferencePolicy:
		tagReferencePolicy = imageapi.SourceTagReferencePolicy
	case localReferencePolicy:
		tagReferencePolicy = imageapi.LocalTagReferencePolicy
	}
	for i, destNameAndTag := range o.destNameAndTag {
		destName, destTag, ok := imageapi.SplitImageStreamTag(destNameAndTag)
		if !ok {
			return fmt.Errorf("%q must be of the form <stream_name>:<tag>", destNameAndTag)
		}

		err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
			isc := o.osClient.ImageStreams(o.destNamespace[i])

			if o.deleteTag {
				// new server support
				err := o.osClient.ImageStreamTags(o.destNamespace[i]).Delete(destName, destTag)
				switch {
				case err == nil:
					fmt.Fprintf(o.out, "Deleted tag %s/%s.\n", o.destNamespace[i], destNameAndTag)
					return nil

				case kerrors.IsMethodNotSupported(err), kerrors.IsForbidden(err):
					// fall back to legacy behavior
				default:
					//  error that isn't whitelisted: fail
					return err
				}

				// try the old way
				target, err := isc.Get(destName)
				if err != nil {
					if !kerrors.IsNotFound(err) {
						return err
					}

					// Nothing to do here, continue to the next dest tag
					// if there is any.
					fmt.Fprintf(o.out, "Image stream %q does not exist.\n", destName)
					return nil
				}

				// The user wants to delete a spec tag.
				if _, ok := target.Spec.Tags[destTag]; !ok {
					return fmt.Errorf("destination tag %s/%s does not exist.\n", o.destNamespace[i], destNameAndTag)
				}
				delete(target.Spec.Tags, destTag)

				if _, err = isc.Update(target); err != nil {
					return err
				}

				fmt.Fprintf(o.out, "Deleted tag %s/%s.\n", o.destNamespace[i], destNameAndTag)
				return nil
			}

			// The user wants to symlink a tag.
			istag := &imageapi.ImageStreamTag{
				ObjectMeta: kapi.ObjectMeta{
					Name:      destNameAndTag,
					Namespace: o.destNamespace[i],
				},
				Tag: &imageapi.TagReference{
					Reference: o.referenceTag,
					ImportPolicy: imageapi.TagImportPolicy{
						Insecure:  o.insecureTag,
						Scheduled: o.scheduleTag,
					},
					ReferencePolicy: imageapi.TagReferencePolicy{
						Type: tagReferencePolicy,
					},
					From: &kapi.ObjectReference{
						Kind: o.sourceKind,
					},
				},
			}
			localRef := o.ref
			switch o.sourceKind {
			case "DockerImage":
				istag.Tag.From.Name = localRef.Exact()
				gen := int64(0)
				istag.Tag.Generation = &gen

			default:
				istag.Tag.From.Name = localRef.NameString()
				istag.Tag.From.Namespace = o.ref.Namespace
				if len(o.ref.Namespace) == 0 && o.destNamespace[i] != o.namespace {
					istag.Tag.From.Namespace = o.namespace
				}
			}

			msg := ""
			sameNamespace := o.namespace == o.destNamespace[i]
			if o.aliasTag {
				if sameNamespace {
					msg = fmt.Sprintf("Tag %s set up to track %s.", destNameAndTag, o.ref.Exact())
				} else {
					msg = fmt.Sprintf("Tag %s/%s set up to track %s.", o.destNamespace[i], destNameAndTag, o.ref.Exact())
				}
			} else {
				if istag.Tag.ImportPolicy.Scheduled {
					if sameNamespace {
						msg = fmt.Sprintf("Tag %s set to import %s periodically.", destNameAndTag, o.ref.Exact())
					} else {
						msg = fmt.Sprintf("Tag %s/%s set to %s periodically.", o.destNamespace[i], destNameAndTag, o.ref.Exact())
					}
				} else {
					if sameNamespace {
						msg = fmt.Sprintf("Tag %s set to %s.", destNameAndTag, o.ref.Exact())
					} else {
						msg = fmt.Sprintf("Tag %s/%s set to %s.", o.destNamespace[i], destNameAndTag, o.ref.Exact())
					}
				}
			}

			// supported by new servers.
			_, err := o.osClient.ImageStreamTags(o.destNamespace[i]).Update(istag)
			switch {
			case err == nil:
				fmt.Fprintln(o.out, msg)
				return nil

			case kerrors.IsMethodNotSupported(err), kerrors.IsForbidden(err), kerrors.IsNotFound(err):
				// if we got one of these errors, it possible that a Create will do what we need.  Try that
				_, err := o.osClient.ImageStreamTags(o.destNamespace[i]).Create(istag)
				switch {
				case err == nil:
					fmt.Fprintln(o.out, msg)
					return nil

				case kerrors.IsMethodNotSupported(err), kerrors.IsForbidden(err):
					// fall back to legacy behavior
				default:
					//  error that isn't whitelisted: fail
					return err
				}

			default:
				//  error that isn't whitelisted: fail
				return err

			}

			target, err := isc.Get(destName)
			if kerrors.IsNotFound(err) {
				target = &imageapi.ImageStream{
					ObjectMeta: kapi.ObjectMeta{
						Name: destName,
					},
				}
			} else if err != nil {
				return err
			}

			if target.Spec.Tags == nil {
				target.Spec.Tags = make(map[string]imageapi.TagReference)
			}

			if oldTargetTag, exists := target.Spec.Tags[destTag]; exists {
				if oldTargetTag.Generation == nil {
					// for servers that do not support tag generations, we need to force re-import to fetch its metadata
					delete(target.Annotations, imageapi.DockerImageRepositoryCheckAnnotation)
					istag.Tag.Generation = nil
				}
			}
			target.Spec.Tags[destTag] = *istag.Tag

			// Check the stream creation timestamp and make sure we will not
			// create a new image stream while deleting.
			if target.CreationTimestamp.IsZero() && !o.deleteTag {
				_, err = isc.Create(target)
			} else {
				_, err = isc.Update(target)
			}
			if err != nil {
				return err
			}

			fmt.Fprintln(o.out, msg)
			return nil
		})
		if err != nil {
			return err
		}
	}

	return nil
}
