package rollout

import (
	"errors"
	"fmt"
	"io"

	"github.com/openshift/origin/pkg/cmd/templates"
	"github.com/spf13/cobra"
	kerrors "k8s.io/kubernetes/pkg/api/errors"
	"k8s.io/kubernetes/pkg/api/meta"
	kclient "k8s.io/kubernetes/pkg/client/unversioned"
	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
	"k8s.io/kubernetes/pkg/kubectl/resource"
	"k8s.io/kubernetes/pkg/runtime"

	"github.com/openshift/origin/pkg/client"
	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
	deployapi "github.com/openshift/origin/pkg/deploy/api"
	deployutil "github.com/openshift/origin/pkg/deploy/util"
)

var (
	rolloutLatestLong = templates.LongDesc(`
		Start a new rollout for a deployment config with the latest state from its triggers

		This command is appropriate for running manual rollouts. If you want full control over
		running new rollouts, use "oc set triggers --manual" to disable all triggers in your
		deployment config and then whenever you want to run a new deployment process, use this
		command in order to pick up the latest images found in the cluster that are pointed by
		your image change triggers.`)

	rolloutLatestExample = templates.Examples(`
		# Start a new rollout based on the latest images defined in the image change triggers.
  	%[1]s rollout latest dc/nginx`)
)

// RolloutLatestOptions holds all the options for the `rollout latest` command.
// TODO: Support --dry-run
type RolloutLatestOptions struct {
	mapper meta.RESTMapper
	typer  runtime.ObjectTyper
	infos  []*resource.Info

	out         io.Writer
	shortOutput bool
	again       bool

	oc              client.Interface
	kc              kclient.Interface
	baseCommandName string
}

// NewCmdRolloutLatest implements the oc rollout latest subcommand.
func NewCmdRolloutLatest(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
	opts := &RolloutLatestOptions{
		baseCommandName: fullName,
	}

	cmd := &cobra.Command{
		Use:     "latest DEPLOYMENTCONFIG",
		Short:   "Start a new rollout for a deployment config with the latest state from its triggers",
		Long:    rolloutLatestLong,
		Example: fmt.Sprintf(rolloutLatestExample, fullName),
		Run: func(cmd *cobra.Command, args []string) {
			err := opts.Complete(f, cmd, args, out)
			kcmdutil.CheckErr(err)

			if err := opts.Validate(); err != nil {
				kcmdutil.CheckErr(kcmdutil.UsageError(cmd, err.Error()))
			}

			err = opts.RunRolloutLatest()
			kcmdutil.CheckErr(err)
		},
		ValidArgs: []string{"deploymentconfig"},
	}

	kcmdutil.AddOutputFlagsForMutation(cmd)
	cmd.Flags().Bool("again", false, "Deploy the current pod template without updating state from triggers")

	return cmd
}

func (o *RolloutLatestOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
	if len(args) != 1 {
		return errors.New("one deployment config name is needed as argument.")
	}

	namespace, _, err := f.DefaultNamespace()
	if err != nil {
		return err
	}

	o.oc, o.kc, err = f.Clients()
	if err != nil {
		return err
	}

	o.mapper, o.typer = f.Object(false)
	o.infos, err = resource.NewBuilder(o.mapper, o.typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)).
		ContinueOnError().
		NamespaceParam(namespace).
		ResourceNames("deploymentconfigs", args[0]).
		SingleResourceType().
		Do().Infos()
	if err != nil {
		return err
	}

	o.out = out
	o.shortOutput = kcmdutil.GetFlagString(cmd, "output") == "name"
	o.again = kcmdutil.GetFlagBool(cmd, "again")

	return nil
}

func (o RolloutLatestOptions) Validate() error {
	if len(o.infos) != 1 {
		return errors.New("a deployment config name is required.")
	}
	return nil
}

func (o RolloutLatestOptions) RunRolloutLatest() error {
	info := o.infos[0]
	config, ok := info.Object.(*deployapi.DeploymentConfig)
	if !ok {
		return fmt.Errorf("%s is not a deployment config", info.Name)
	}

	// TODO: Consider allowing one-off deployments for paused configs
	// See https://github.com/openshift/origin/issues/9903
	if config.Spec.Paused {
		return fmt.Errorf("cannot deploy a paused deployment config")
	}

	deploymentName := deployutil.LatestDeploymentNameForConfig(config)
	deployment, err := o.kc.ReplicationControllers(config.Namespace).Get(deploymentName)
	switch {
	case err == nil:
		// Reject attempts to start a concurrent deployment.
		if !deployutil.IsTerminatedDeployment(deployment) {
			status := deployutil.DeploymentStatusFor(deployment)
			return fmt.Errorf("#%d is already in progress (%s).", config.Status.LatestVersion, status)
		}
	case !kerrors.IsNotFound(err):
		return err
	}

	request := &deployapi.DeploymentRequest{
		Name:   config.Name,
		Latest: !o.again,
		Force:  true,
	}

	dc, err := o.oc.DeploymentConfigs(config.Namespace).Instantiate(request)
	// Pre 1.4 servers don't support the instantiate endpoint. Fallback to incrementing
	// latestVersion on them.
	if kerrors.IsNotFound(err) || kerrors.IsForbidden(err) {
		config.Status.LatestVersion++
		dc, err = o.oc.DeploymentConfigs(config.Namespace).Update(config)
	}
	if err != nil {
		return err
	}

	info.Refresh(dc, true)

	kcmdutil.PrintSuccess(o.mapper, o.shortOutput, o.out, info.Mapping.Resource, info.Name, "rolled out")
	return nil
}