package set

import (
	"fmt"
	"io"
	"os"

	"github.com/spf13/cobra"
	kapi "k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/api/meta"
	"k8s.io/kubernetes/pkg/api/unversioned"
	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
	"k8s.io/kubernetes/pkg/kubectl/resource"
	"k8s.io/kubernetes/pkg/runtime"
	"k8s.io/kubernetes/pkg/util/errors"

	buildapi "github.com/openshift/origin/pkg/build/api"
	"github.com/openshift/origin/pkg/cmd/templates"
	cmdutil "github.com/openshift/origin/pkg/cmd/util"
	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
)

var (
	buildSecretLong = templates.LongDesc(`
		Set or remove a build secret on a build config

		A build config can reference a secret to push or pull images from private registries or
		to access private source repositories.

		Specify the type of secret being set by using the --push, --pull, or --source flags.
		A secret reference can be removed by using --remove flag.

		A label selector may be specified with the --selector flag to select the build configs
		on which to set or remove secrets. Alternatively, all build configs in the namespace can
		be selected with the --all flag.`)

	buildSecretExample = templates.Examples(`  
		# Clear push secret on a build config
		%[1]s build-secret --push --remove bc/mybuild

		# Set the pull secret on a build config
		%[1]s build-secret --pull bc/mybuild mysecret

		# Set the push and pull secret on a build config
		%[1]s build-secret --push --pull bc/mybuild mysecret

		# Set the source secret on a set of build configs matching a selector
		%[1]s build-secret --source -l app=myapp gitsecret`)
)

type BuildSecretOptions struct {
	Out io.Writer
	Err io.Writer

	Builder *resource.Builder
	Infos   []*resource.Info

	Encoder       runtime.Encoder
	OutputVersion unversioned.GroupVersion

	Filenames []string
	Selector  string
	All       bool

	ShortOutput bool
	Local       bool
	Mapper      meta.RESTMapper

	PrintObject func(runtime.Object) error

	Secret string
	Push   bool
	Pull   bool
	Source bool
	Remove bool
}

// NewCmdBuildSecret implements the set build-secret command
func NewCmdBuildSecret(fullName string, f *clientcmd.Factory, out, errOut io.Writer) *cobra.Command {
	options := &BuildSecretOptions{
		Out: out,
		Err: errOut,
	}
	cmd := &cobra.Command{
		Use:     "build-secret BUILDCONFIG SECRETNAME",
		Short:   "Update a build secret on a build config",
		Long:    buildSecretLong,
		Example: fmt.Sprintf(buildSecretExample, fullName),
		Run: func(cmd *cobra.Command, args []string) {
			kcmdutil.CheckErr(options.Complete(f, cmd, args))
			kcmdutil.CheckErr(options.Validate())
			if err := options.Run(); err != nil {
				// TODO: move me to kcmdutil
				if err == cmdutil.ErrExit {
					os.Exit(1)
				}
				kcmdutil.CheckErr(err)
			}
		},
	}

	kcmdutil.AddPrinterFlags(cmd)
	cmd.Flags().StringVarP(&options.Selector, "selector", "l", options.Selector, "Selector (label query) to filter build configs")
	cmd.Flags().BoolVar(&options.All, "all", options.All, "Select all build configs in the namespace")
	cmd.Flags().StringSliceVarP(&options.Filenames, "filename", "f", options.Filenames, "Filename, directory, or URL to file to use to edit the resource.")

	cmd.Flags().BoolVar(&options.Push, "push", options.Push, "If true, set the push secret on a build config")
	cmd.Flags().BoolVar(&options.Pull, "pull", options.Pull, "If true, set the pull secret on a build config")
	cmd.Flags().BoolVar(&options.Source, "source", options.Source, "If true, set the source secret on a build config")
	cmd.Flags().BoolVar(&options.Remove, "remove", options.Remove, "If true, remove the build secret.")

	cmd.Flags().BoolVar(&options.Local, "local", false, "If true, set build-secret will NOT contact api-server but run locally.")

	cmd.MarkFlagFilename("filename", "yaml", "yml", "json")

	return cmd
}

var supportedBuildTypes = []string{"buildconfigs"}

func (o *BuildSecretOptions) secretFromArg(f *clientcmd.Factory, mapper meta.RESTMapper, typer runtime.ObjectTyper, namespace, arg string) (string, error) {
	builder := resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), kapi.Codecs.UniversalDecoder()).
		NamespaceParam(namespace).DefaultNamespace().
		RequireObject(false).
		ContinueOnError().
		ResourceNames("secrets", arg).
		Flatten()

	var secretName string
	err := builder.Do().Visit(func(info *resource.Info, err error) error {
		if err != nil {
			return err
		}
		if info.Mapping.Resource != "secrets" {
			return fmt.Errorf("please specify a secret")
		}
		secretName = info.Name
		return nil
	})
	if err != nil {
		return "", err
	}
	return secretName, nil
}

func (o *BuildSecretOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error {
	var secretArg string
	if !o.Remove {
		if len(args) < 1 {
			return kcmdutil.UsageError(cmd, "a secret name must be specified")
		}
		secretArg = args[len(args)-1]
		args = args[:len(args)-1]
	}
	resources := args
	if len(resources) == 0 && len(o.Selector) == 0 && len(o.Filenames) == 0 && !o.All {
		return kcmdutil.UsageError(cmd, "one or more build configs must be specified as <name> or <resource>/<name>")
	}

	cmdNamespace, explicit, err := f.DefaultNamespace()
	if err != nil {
		return err
	}
	clientConfig, err := f.ClientConfig()
	if err != nil {
		return err
	}
	o.OutputVersion, err = kcmdutil.OutputVersion(cmd, clientConfig.GroupVersion)
	if err != nil {
		return err
	}

	mapper, typer := f.Object(false)
	if len(secretArg) > 0 {
		o.Secret, err = o.secretFromArg(f, mapper, typer, cmdNamespace, secretArg)
		if err != nil {
			return err
		}
	}
	o.Builder = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), kapi.Codecs.UniversalDecoder()).
		ContinueOnError().
		NamespaceParam(cmdNamespace).DefaultNamespace().
		FilenameParam(explicit, false, o.Filenames...).
		Flatten()

	if !o.Local {
		o.Builder = o.Builder.
			ResourceNames("buildconfigs", resources...).
			SelectorParam(o.Selector).
			Latest()

		if o.All {
			o.Builder.ResourceTypes(supportedBuildTypes...).SelectAllParam(o.All)
		}
	}

	output := kcmdutil.GetFlagString(cmd, "output")
	if len(output) != 0 || o.Local {
		o.PrintObject = func(obj runtime.Object) error { return f.PrintObject(cmd, mapper, obj, o.Out) }
	}

	o.Encoder = f.JSONEncoder()
	o.ShortOutput = kcmdutil.GetFlagString(cmd, "output") == "name"
	o.Mapper = mapper

	return nil
}

func (o *BuildSecretOptions) Validate() error {
	if !o.Pull && !o.Push && !o.Source {
		return fmt.Errorf("specify the type of secret to set (--push, --pull, or --source)")
	}
	if !o.Remove && len(o.Secret) == 0 {
		return fmt.Errorf("specify a secret to set")
	}
	if o.Remove && len(o.Secret) > 0 {
		return fmt.Errorf("a secret cannot be specified when using the --remove flag")
	}
	return nil
}

func (o *BuildSecretOptions) Run() error {
	infos := o.Infos
	singular := len(o.Infos) <= 1
	if o.Builder != nil {
		loaded, err := o.Builder.Do().IntoSingular(&singular).Infos()
		if err != nil {
			return err
		}
		infos = loaded
	}

	patches := CalculatePatches(infos, o.Encoder, func(info *resource.Info) (bool, error) {
		return o.setBuildSecret(info.Object)
	})

	if singular && len(patches) == 0 {
		return fmt.Errorf("cannot set a build secret on %s/%s", infos[0].Mapping.Resource, infos[0].Name)
	}

	if o.PrintObject != nil {
		object, err := resource.AsVersionedObject(infos, !singular, o.OutputVersion, kapi.Codecs.LegacyCodec(o.OutputVersion))
		if err != nil {
			return err
		}
		return o.PrintObject(object)
	}

	errs := []error{}
	for _, patch := range patches {
		info := patch.Info
		if patch.Err != nil {
			errs = append(errs, fmt.Errorf("%s/%s %v", info.Mapping.Resource, info.Name, patch.Err))
			continue
		}

		if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
			fmt.Fprintf(o.Err, "info: %s %q was not changed\n", info.Mapping.Resource, info.Name)
			continue
		}

		obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, kapi.StrategicMergePatchType, patch.Patch)
		if err != nil {
			errs = append(errs, fmt.Errorf("%s/%s %v", info.Mapping.Resource, info.Name, err))
			continue
		}

		info.Refresh(obj, true)
		kcmdutil.PrintSuccess(o.Mapper, o.ShortOutput, o.Out, info.Mapping.Resource, info.Name, "updated")
	}
	if len(errs) > 0 {
		return errors.NewAggregate(errs)
	}
	return nil
}

// setBuildSecret will set a secret on an object. For now the only supported
// object type is BuildConfig.
func (o *BuildSecretOptions) setBuildSecret(obj runtime.Object) (bool, error) {
	switch buildObj := obj.(type) {
	case *buildapi.BuildConfig:
		o.updateBuildConfig(buildObj)
		return true, nil
	default:
		return false, nil
	}
}

func (o *BuildSecretOptions) updateBuildConfig(bc *buildapi.BuildConfig) {
	if o.Push {
		if o.Remove {
			bc.Spec.Output.PushSecret = nil
		} else {
			bc.Spec.Output.PushSecret = &kapi.LocalObjectReference{
				Name: o.Secret,
			}
		}
	}

	if o.Pull {
		switch {
		case bc.Spec.Strategy.DockerStrategy != nil:
			if o.Remove {
				bc.Spec.Strategy.DockerStrategy.PullSecret = nil
			} else {
				bc.Spec.Strategy.DockerStrategy.PullSecret = &kapi.LocalObjectReference{
					Name: o.Secret,
				}
			}
		case bc.Spec.Strategy.SourceStrategy != nil:
			if o.Remove {
				bc.Spec.Strategy.SourceStrategy.PullSecret = nil
			} else {
				bc.Spec.Strategy.SourceStrategy.PullSecret = &kapi.LocalObjectReference{
					Name: o.Secret,
				}
			}
		case bc.Spec.Strategy.CustomStrategy != nil:
			if o.Remove {
				bc.Spec.Strategy.CustomStrategy.PullSecret = nil
			} else {
				bc.Spec.Strategy.CustomStrategy.PullSecret = &kapi.LocalObjectReference{
					Name: o.Secret,
				}
			}
		}
	}

	if o.Source {
		if o.Remove {
			bc.Spec.Source.SourceSecret = nil
		} else {
			bc.Spec.Source.SourceSecret = &kapi.LocalObjectReference{
				Name: o.Secret,
			}
		}
	}
}