package system

import (
	"fmt"
	"runtime"
	"sort"
	"text/template"
	"time"

	"github.com/docker/cli/cli"
	"github.com/docker/cli/cli/command"
	"github.com/docker/cli/kubernetes"
	"github.com/docker/cli/templates"
	"github.com/docker/docker/api/types"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"golang.org/x/net/context"
	kubernetesClient "k8s.io/client-go/kubernetes"
)

var versionTemplate = `{{with .Client -}}
Client:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}}
 Version:	{{.Version}}
 API version:	{{.APIVersion}}{{if ne .APIVersion .DefaultAPIVersion}} (downgraded from {{.DefaultAPIVersion}}){{end}}
 Go version:	{{.GoVersion}}
 Git commit:	{{.GitCommit}}
 Built:	{{.BuildTime}}
 OS/Arch:	{{.Os}}/{{.Arch}}
 Experimental:	{{.Experimental}}
 Orchestrator:	{{.Orchestrator}}
{{- end}}

{{- if .ServerOK}}{{with .Server}}

Server:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}}
 {{- range $component := .Components}}
 {{$component.Name}}:
  {{- if eq $component.Name "Engine" }}
  Version:	{{.Version}}
  API version:	{{index .Details "ApiVersion"}} (minimum version {{index .Details "MinAPIVersion"}})
  Go version:	{{index .Details "GoVersion"}}
  Git commit:	{{index .Details "GitCommit"}}
  Built:	{{index .Details "BuildTime"}}
  OS/Arch:	{{index .Details "Os"}}/{{index .Details "Arch"}}
  Experimental:	{{index .Details "Experimental"}}
  {{- else }}
  Version:	{{$component.Version}}
  {{- $detailsOrder := getDetailsOrder $component}}
  {{- range $key := $detailsOrder}}
  {{$key}}:		{{index $component.Details $key}}
   {{- end}}
  {{- end}}
 {{- end}}
 {{- end}}{{- end}}
 {{- if .KubernetesOK}}{{with .Kubernetes}}
 Kubernetes:
  Version:	{{.Kubernetes}}
  Stack API:	{{.StackAPI}}
{{- end}}{{end}}`

type versionOptions struct {
	format     string
	kubeConfig string
}

// versionInfo contains version information of both the Client, and Server
type versionInfo struct {
	Client     clientVersion
	Server     *types.Version
	Kubernetes *kubernetesVersion
}

type clientVersion struct {
	Platform struct{ Name string } `json:",omitempty"`

	Version           string
	APIVersion        string `json:"ApiVersion"`
	DefaultAPIVersion string `json:"DefaultAPIVersion,omitempty"`
	GitCommit         string
	GoVersion         string
	Os                string
	Arch              string
	BuildTime         string `json:",omitempty"`
	Experimental      bool
	Orchestrator      string `json:",omitempty"`
}

type kubernetesVersion struct {
	Kubernetes string
	StackAPI   string
}

// ServerOK returns true when the client could connect to the docker server
// and parse the information received. It returns false otherwise.
func (v versionInfo) ServerOK() bool {
	return v.Server != nil
}

func (v versionInfo) KubernetesOK() bool {
	return v.Kubernetes != nil
}

// NewVersionCommand creates a new cobra.Command for `docker version`
func NewVersionCommand(dockerCli command.Cli) *cobra.Command {
	var opts versionOptions

	cmd := &cobra.Command{
		Use:   "version [OPTIONS]",
		Short: "Show the Docker version information",
		Args:  cli.NoArgs,
		RunE: func(cmd *cobra.Command, args []string) error {
			return runVersion(dockerCli, &opts)
		},
	}

	flags := cmd.Flags()
	flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template")
	flags.StringVarP(&opts.kubeConfig, "kubeconfig", "k", "", "Kubernetes config file")
	flags.SetAnnotation("kubeconfig", "kubernetes", nil)
	flags.SetAnnotation("kubeconfig", "experimentalCLI", nil)

	return cmd
}

func reformatDate(buildTime string) string {
	t, errTime := time.Parse(time.RFC3339Nano, buildTime)
	if errTime == nil {
		return t.Format(time.ANSIC)
	}
	return buildTime
}

func runVersion(dockerCli command.Cli, opts *versionOptions) error {
	templateFormat := versionTemplate
	tmpl := templates.New("version")
	if opts.format != "" {
		templateFormat = opts.format
	} else {
		tmpl = tmpl.Funcs(template.FuncMap{"getDetailsOrder": getDetailsOrder})
	}

	var err error
	tmpl, err = tmpl.Parse(templateFormat)
	if err != nil {
		return cli.StatusError{StatusCode: 64,
			Status: "Template parsing error: " + err.Error()}
	}

	vd := versionInfo{
		Client: clientVersion{
			Platform:          struct{ Name string }{cli.PlatformName},
			Version:           cli.Version,
			APIVersion:        dockerCli.Client().ClientVersion(),
			DefaultAPIVersion: dockerCli.DefaultVersion(),
			GoVersion:         runtime.Version(),
			GitCommit:         cli.GitCommit,
			BuildTime:         reformatDate(cli.BuildTime),
			Os:                runtime.GOOS,
			Arch:              runtime.GOARCH,
			Experimental:      dockerCli.ClientInfo().HasExperimental,
			Orchestrator:      string(dockerCli.ClientInfo().Orchestrator),
		},
		Kubernetes: getKubernetesVersion(dockerCli, opts.kubeConfig),
	}

	sv, err := dockerCli.Client().ServerVersion(context.Background())
	if err == nil {
		vd.Server = &sv
		foundEngine := false
		for _, component := range sv.Components {
			if component.Name == "Engine" {
				foundEngine = true
				buildTime, ok := component.Details["BuildTime"]
				if ok {
					component.Details["BuildTime"] = reformatDate(buildTime)
				}
				break
			}
		}

		if !foundEngine {
			vd.Server.Components = append(vd.Server.Components, types.ComponentVersion{
				Name:    "Engine",
				Version: sv.Version,
				Details: map[string]string{
					"ApiVersion":    sv.APIVersion,
					"MinAPIVersion": sv.MinAPIVersion,
					"GitCommit":     sv.GitCommit,
					"GoVersion":     sv.GoVersion,
					"Os":            sv.Os,
					"Arch":          sv.Arch,
					"BuildTime":     reformatDate(vd.Server.BuildTime),
					"Experimental":  fmt.Sprintf("%t", sv.Experimental),
				},
			})
		}
	}

	if err2 := tmpl.Execute(dockerCli.Out(), vd); err2 != nil && err == nil {
		err = err2
	}
	dockerCli.Out().Write([]byte{'\n'})
	return err
}

func getDetailsOrder(v types.ComponentVersion) []string {
	out := make([]string, 0, len(v.Details))
	for k := range v.Details {
		out = append(out, k)
	}
	sort.Strings(out)
	return out
}

func getKubernetesVersion(dockerCli command.Cli, kubeConfig string) *kubernetesVersion {
	if !dockerCli.ClientInfo().HasKubernetes() {
		return nil
	}

	version := kubernetesVersion{
		Kubernetes: "Unknown",
		StackAPI:   "Unknown",
	}
	config, err := kubernetes.NewKubernetesConfig(kubeConfig)
	if err != nil {
		logrus.Debugf("failed to get Kubernetes configuration: %s", err)
		return &version
	}
	kubeClient, err := kubernetesClient.NewForConfig(config)
	if err != nil {
		logrus.Debugf("failed to get Kubernetes client: %s", err)
		return &version
	}
	version.StackAPI = getStackVersion(kubeClient)
	version.Kubernetes = getKubernetesServerVersion(kubeClient)
	return &version
}

func getStackVersion(client *kubernetesClient.Clientset) string {
	apiVersion, err := kubernetes.GetStackAPIVersion(client)
	if err != nil {
		logrus.Debugf("failed to get Stack API version: %s", err)
		return "Unknown"
	}
	return string(apiVersion)
}

func getKubernetesServerVersion(client *kubernetesClient.Clientset) string {
	kubeVersion, err := client.DiscoveryClient.ServerVersion()
	if err != nil {
		logrus.Debugf("failed to get Kubernetes server version: %s", err)
		return "Unknown"
	}
	return kubeVersion.String()
}
