package node

import (
	"fmt"
	"io"
	"sort"
	"strings"

	"github.com/docker/docker/api/types/swarm"
	"github.com/docker/docker/cli"
	"github.com/docker/docker/cli/command"
	"github.com/docker/docker/cli/command/inspect"
	"github.com/docker/docker/pkg/ioutils"
	"github.com/docker/go-units"
	"github.com/spf13/cobra"
	"golang.org/x/net/context"
)

type inspectOptions struct {
	nodeIds []string
	format  string
	pretty  bool
}

func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
	var opts inspectOptions

	cmd := &cobra.Command{
		Use:   "inspect [OPTIONS] self|NODE [NODE...]",
		Short: "Display detailed information on one or more nodes",
		Args:  cli.RequiresMinArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			opts.nodeIds = args
			return runInspect(dockerCli, opts)
		},
	}

	flags := cmd.Flags()
	flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template")
	flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format.")
	return cmd
}

func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
	client := dockerCli.Client()
	ctx := context.Background()
	getRef := func(ref string) (interface{}, []byte, error) {
		nodeRef, err := Reference(ctx, client, ref)
		if err != nil {
			return nil, nil, err
		}
		node, _, err := client.NodeInspectWithRaw(ctx, nodeRef)
		return node, nil, err
	}

	if !opts.pretty {
		return inspect.Inspect(dockerCli.Out(), opts.nodeIds, opts.format, getRef)
	}
	return printHumanFriendly(dockerCli.Out(), opts.nodeIds, getRef)
}

func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error {
	for idx, ref := range refs {
		obj, _, err := getRef(ref)
		if err != nil {
			return err
		}
		printNode(out, obj.(swarm.Node))

		// TODO: better way to do this?
		// print extra space between objects, but not after the last one
		if idx+1 != len(refs) {
			fmt.Fprintf(out, "\n\n")
		} else {
			fmt.Fprintf(out, "\n")
		}
	}
	return nil
}

// TODO: use a template
func printNode(out io.Writer, node swarm.Node) {
	fmt.Fprintf(out, "ID:\t\t\t%s\n", node.ID)
	ioutils.FprintfIfNotEmpty(out, "Name:\t\t\t%s\n", node.Spec.Name)
	if node.Spec.Labels != nil {
		fmt.Fprintln(out, "Labels:")
		for k, v := range node.Spec.Labels {
			fmt.Fprintf(out, " - %s = %s\n", k, v)
		}
	}

	ioutils.FprintfIfNotEmpty(out, "Hostname:\t\t%s\n", node.Description.Hostname)
	fmt.Fprintf(out, "Joined at:\t\t%s\n", command.PrettyPrint(node.CreatedAt))
	fmt.Fprintln(out, "Status:")
	fmt.Fprintf(out, " State:\t\t\t%s\n", command.PrettyPrint(node.Status.State))
	ioutils.FprintfIfNotEmpty(out, " Message:\t\t%s\n", command.PrettyPrint(node.Status.Message))
	fmt.Fprintf(out, " Availability:\t\t%s\n", command.PrettyPrint(node.Spec.Availability))
	ioutils.FprintfIfNotEmpty(out, " Address:\t\t%s\n", command.PrettyPrint(node.Status.Addr))

	if node.ManagerStatus != nil {
		fmt.Fprintln(out, "Manager Status:")
		fmt.Fprintf(out, " Address:\t\t%s\n", node.ManagerStatus.Addr)
		fmt.Fprintf(out, " Raft Status:\t\t%s\n", command.PrettyPrint(node.ManagerStatus.Reachability))
		leader := "No"
		if node.ManagerStatus.Leader {
			leader = "Yes"
		}
		fmt.Fprintf(out, " Leader:\t\t%s\n", leader)
	}

	fmt.Fprintln(out, "Platform:")
	fmt.Fprintf(out, " Operating System:\t%s\n", node.Description.Platform.OS)
	fmt.Fprintf(out, " Architecture:\t\t%s\n", node.Description.Platform.Architecture)

	fmt.Fprintln(out, "Resources:")
	fmt.Fprintf(out, " CPUs:\t\t\t%d\n", node.Description.Resources.NanoCPUs/1e9)
	fmt.Fprintf(out, " Memory:\t\t%s\n", units.BytesSize(float64(node.Description.Resources.MemoryBytes)))

	var pluginTypes []string
	pluginNamesByType := map[string][]string{}
	for _, p := range node.Description.Engine.Plugins {
		// append to pluginTypes only if not done previously
		if _, ok := pluginNamesByType[p.Type]; !ok {
			pluginTypes = append(pluginTypes, p.Type)
		}
		pluginNamesByType[p.Type] = append(pluginNamesByType[p.Type], p.Name)
	}

	if len(pluginTypes) > 0 {
		fmt.Fprintln(out, "Plugins:")
		sort.Strings(pluginTypes) // ensure stable output
		for _, pluginType := range pluginTypes {
			fmt.Fprintf(out, "  %s:\t\t%s\n", pluginType, strings.Join(pluginNamesByType[pluginType], ", "))
		}
	}
	fmt.Fprintf(out, "Engine Version:\t\t%s\n", node.Description.Engine.EngineVersion)

	if len(node.Description.Engine.Labels) != 0 {
		fmt.Fprintln(out, "Engine Labels:")
		for k, v := range node.Description.Engine.Labels {
			fmt.Fprintf(out, " - %s = %s\n", k, v)
		}
	}
}
