package cmd

import (
	"bytes"
	"errors"
	"fmt"
	"io"

	kapi "k8s.io/kubernetes/pkg/api"
	kapierrors "k8s.io/kubernetes/pkg/api/errors"
	"k8s.io/kubernetes/pkg/client/restclient"
	kclient "k8s.io/kubernetes/pkg/client/unversioned"
	kclientcmd "k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
	clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"

	"github.com/openshift/origin/pkg/client"
	cliconfig "github.com/openshift/origin/pkg/cmd/cli/config"
	"github.com/openshift/origin/pkg/cmd/templates"
	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
	"github.com/openshift/origin/pkg/project/api"
	projectutil "github.com/openshift/origin/pkg/project/util"

	"github.com/spf13/cobra"
)

type ProjectOptions struct {
	Config       clientcmdapi.Config
	ClientConfig *restclient.Config
	ClientFn     func() (*client.Client, kclient.Interface, error)
	Out          io.Writer
	PathOptions  *kclientcmd.PathOptions

	ProjectName  string
	ProjectOnly  bool
	DisplayShort bool

	// SkipAccessValidation means that if a specific name is requested, don't bother checking for access to the project
	SkipAccessValidation bool
}

var (
	projectLong = templates.LongDesc(`
		Switch to another project and make it the default in your configuration

		If no project was specified on the command line, display information about the current active
		project. Since you can use this command to connect to projects on different servers, you will
		occasionally encounter projects of the same name on different servers. When switching to that
		project, a new local context will be created that will have a unique name - for instance,
		'myapp-2'. If you have previously created a context with a different name than the project
		name, this command will accept that context name instead.

		For advanced configuration, or to manage the contents of your config file, use the 'config'
		command.`)

	projectExample = templates.Examples(`
		# Switch to 'myapp' project
	  %[1]s myapp

	  # Display the project currently in use
	  %[1]s`)
)

// NewCmdProject implements the OpenShift cli rollback command
func NewCmdProject(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
	options := &ProjectOptions{}

	cmd := &cobra.Command{
		Use:     "project [NAME]",
		Short:   "Switch to another project",
		Long:    projectLong,
		Example: fmt.Sprintf(projectExample, fullName),
		Run: func(cmd *cobra.Command, args []string) {
			options.PathOptions = cliconfig.NewPathOptions(cmd)

			if err := options.Complete(f, args, out); err != nil {
				kcmdutil.CheckErr(kcmdutil.UsageError(cmd, err.Error()))
			}

			if err := options.RunProject(); err != nil {
				kcmdutil.CheckErr(err)
			}
		},
	}
	cmd.Flags().BoolVarP(&options.DisplayShort, "short", "q", false, "If true, display only the project name")
	return cmd
}

func (o *ProjectOptions) Complete(f *clientcmd.Factory, args []string, out io.Writer) error {
	var err error

	argsLength := len(args)
	switch {
	case argsLength > 1:
		return errors.New("Only one argument is supported (project name or context name).")
	case argsLength == 1:
		o.ProjectName = args[0]
	}

	o.Config, err = f.OpenShiftClientConfig.RawConfig()
	if err != nil {
		return err
	}

	o.ClientConfig, err = f.OpenShiftClientConfig.ClientConfig()
	if err != nil {
		return err
	}

	o.ClientFn = func() (*client.Client, kclient.Interface, error) {
		return f.Clients()
	}

	o.Out = out

	return nil
}
func (o ProjectOptions) Validate() error {
	return nil
}

// RunProject contains all the necessary functionality for the OpenShift cli project command
func (o ProjectOptions) RunProject() error {
	config := o.Config
	clientCfg := o.ClientConfig
	out := o.Out

	var currentProject string
	currentContext := config.Contexts[config.CurrentContext]
	if currentContext != nil {
		currentProject = currentContext.Namespace
	}

	// No argument provided, we will just print info
	if len(o.ProjectName) == 0 {
		if len(currentProject) > 0 {
			if o.DisplayShort {
				fmt.Fprintln(out, currentProject)
				return nil
			}

			client, kubeclient, err := o.ClientFn()
			if err != nil {
				return err
			}

			switch err := confirmProjectAccess(currentProject, client, kubeclient); {
			case clientcmd.IsForbidden(err):
				return fmt.Errorf("you do not have rights to view project %q.", currentProject)
			case kapierrors.IsNotFound(err):
				return fmt.Errorf("the project %q specified in your config does not exist.", currentProject)
			case err != nil:
				return err
			}

			defaultContextName := cliconfig.GetContextNickname(currentContext.Namespace, currentContext.Cluster, currentContext.AuthInfo)

			// if they specified a project name and got a generated context, then only show the information they care about.  They won't recognize
			// a context name they didn't choose
			if config.CurrentContext == defaultContextName {
				fmt.Fprintf(out, "Using project %q on server %q.\n", currentProject, clientCfg.Host)

			} else {
				fmt.Fprintf(out, "Using project %q from context named %q on server %q.\n", currentProject, config.CurrentContext, clientCfg.Host)
			}

		} else {
			if o.DisplayShort {
				return fmt.Errorf("no project has been set")
			}
			fmt.Fprintf(out, "No project has been set. Pass a project name to make that the default.\n")
		}
		return nil
	}

	// We have an argument that can be either a context or project
	argument := o.ProjectName

	contextInUse := ""
	namespaceInUse := ""

	// Check if argument is an existing context, if so just set it as the context in use.
	// If not a context then we will try to handle it as a project.
	if context, contextExists := config.Contexts[argument]; !o.ProjectOnly && contextExists {
		contextInUse = argument
		namespaceInUse = context.Namespace

		config.CurrentContext = argument

	} else {
		if !o.SkipAccessValidation {
			client, kubeclient, err := o.ClientFn()
			if err != nil {
				return err
			}

			if err := confirmProjectAccess(argument, client, kubeclient); err != nil {
				if isNotFound, isForbidden := kapierrors.IsNotFound(err), clientcmd.IsForbidden(err); isNotFound || isForbidden {
					var msg string
					if isForbidden {
						msg = fmt.Sprintf("You are not a member of project %q.", argument)
					} else {
						msg = fmt.Sprintf("A project named %q does not exist on %q.", argument, clientCfg.Host)
					}

					projects, err := getProjects(client, kubeclient)
					if err == nil {
						switch len(projects) {
						case 0:
							msg += "\nYou are not a member of any projects. You can request a project to be created with the 'new-project' command."
						case 1:
							msg += fmt.Sprintf("\nYou have one project on this server: %s", api.DisplayNameAndNameForProject(&projects[0]))
						default:
							msg += "\nYour projects are:"
							for _, project := range projects {
								msg += fmt.Sprintf("\n* %s", api.DisplayNameAndNameForProject(&project))
							}
						}
					}

					if hasMultipleServers(config) {
						msg += "\nTo see projects on another server, pass '--server=<server>'."
					}
					return errors.New(msg)
				}
				return err
			}
		}
		projectName := argument

		kubeconfig, err := cliconfig.CreateConfig(projectName, o.ClientConfig)
		if err != nil {
			return err
		}

		merged, err := cliconfig.MergeConfig(config, *kubeconfig)
		if err != nil {
			return err
		}
		config = *merged

		namespaceInUse = projectName
		contextInUse = merged.CurrentContext
	}

	if err := kclientcmd.ModifyConfig(o.PathOptions, config, true); err != nil {
		return err
	}

	if o.DisplayShort {
		fmt.Fprintln(out, namespaceInUse)
		return nil
	}

	// calculate what name we'd generate for the context.  If the context has the same name, don't drop it into the output, because the user won't
	// recognize the name since they didn't choose it.
	defaultContextName := cliconfig.GetContextNickname(namespaceInUse, config.Contexts[contextInUse].Cluster, config.Contexts[contextInUse].AuthInfo)

	switch {
	// if there is no namespace, then the only information we can provide is the context and server
	case (len(namespaceInUse) == 0):
		fmt.Fprintf(out, "Now using context named %q on server %q.\n", contextInUse, clientCfg.Host)

	// inform them that they are already in the project they are trying to switch to
	case currentProject == namespaceInUse:
		fmt.Fprintf(out, "Already on project %q on server %q.\n", currentProject, clientCfg.Host)

	// if they specified a project name and got a generated context, then only show the information they care about.  They won't recognize
	// a context name they didn't choose
	case (argument == namespaceInUse) && (contextInUse == defaultContextName):
		fmt.Fprintf(out, "Now using project %q on server %q.\n", namespaceInUse, clientCfg.Host)

	// in all other cases, display all information
	default:
		fmt.Fprintf(out, "Now using project %q from context named %q on server %q.\n", namespaceInUse, contextInUse, clientCfg.Host)

	}

	return nil
}

func confirmProjectAccess(currentProject string, oClient *client.Client, kClient kclient.Interface) error {
	_, projectErr := oClient.Projects().Get(currentProject)
	if !kapierrors.IsNotFound(projectErr) {
		return projectErr
	}

	// at this point we know the error is a not found, but we'll test namespaces just in case we're running on kube
	if _, err := kClient.Namespaces().Get(currentProject); err == nil {
		return nil
	}

	// otherwise return the openshift error default
	return projectErr
}

func getProjects(oClient *client.Client, kClient kclient.Interface) ([]api.Project, error) {
	projects, err := oClient.Projects().List(kapi.ListOptions{})
	if err == nil {
		return projects.Items, nil
	}
	if err != nil && !kapierrors.IsNotFound(err) {
		return nil, err
	}

	namespaces, err := kClient.Namespaces().List(kapi.ListOptions{})
	if err != nil {
		return nil, err
	}
	projects = projectutil.ConvertNamespaceList(namespaces)
	return projects.Items, nil
}

func clusterAndAuthEquality(clientCfg *restclient.Config, cluster clientcmdapi.Cluster, authInfo clientcmdapi.AuthInfo) bool {
	return cluster.Server == clientCfg.Host &&
		cluster.InsecureSkipTLSVerify == clientCfg.Insecure &&
		cluster.CertificateAuthority == clientCfg.CAFile &&
		bytes.Equal(cluster.CertificateAuthorityData, clientCfg.CAData) &&
		authInfo.Token == clientCfg.BearerToken &&
		authInfo.ClientCertificate == clientCfg.TLSClientConfig.CertFile &&
		bytes.Equal(authInfo.ClientCertificateData, clientCfg.TLSClientConfig.CertData) &&
		authInfo.ClientKey == clientCfg.TLSClientConfig.KeyFile &&
		bytes.Equal(authInfo.ClientKeyData, clientCfg.TLSClientConfig.KeyData)
}

// TODO these kind of funcs could be moved to some kind of clientcmd util
func hasMultipleServers(config clientcmdapi.Config) bool {
	server := ""
	for _, cluster := range config.Clusters {
		if len(server) == 0 {
			server = cluster.Server
		}
		if server != cluster.Server {
			return true
		}
	}
	return false
}