package top

import (
	"fmt"
	"io"

	gonum "github.com/gonum/graph"
	"github.com/spf13/cobra"

	kapi "k8s.io/kubernetes/pkg/api"
	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
	"k8s.io/kubernetes/pkg/util/sets"

	"github.com/openshift/origin/pkg/api/graph"
	"github.com/openshift/origin/pkg/cmd/templates"

	"github.com/openshift/origin/pkg/client"
	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
	imageapi "github.com/openshift/origin/pkg/image/api"
	imagegraph "github.com/openshift/origin/pkg/image/graph/nodes"
)

const TopImageStreamsRecommendedName = "imagestreams"

var (
	topImageStreamsLong = templates.LongDesc(`
		Show usage statistics for ImageStreams

		This command analyzes all the ImageStreams managed by the platform and presents current
		usage statistics.`)

	topImageStreamsExample = templates.Examples(`
		# Show usage statistics for ImageStreams
  	%[1]s %[2]s`)
)

// NewCmdTopImageStreams implements the OpenShift cli top imagestreams command.
func NewCmdTopImageStreams(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command {
	opts := &TopImageStreamsOptions{}
	cmd := &cobra.Command{
		Use:     name,
		Short:   "Show usage statistics for ImageStreams",
		Long:    topImageStreamsLong,
		Example: fmt.Sprintf(topImageStreamsExample, parentName, name),
		Run: func(cmd *cobra.Command, args []string) {
			kcmdutil.CheckErr(opts.Complete(f, cmd, args, out))
			kcmdutil.CheckErr(opts.Validate(cmd))
			kcmdutil.CheckErr(opts.Run())
		},
	}

	return cmd
}

type TopImageStreamsOptions struct {
	// internal values
	Images  *imageapi.ImageList
	Streams *imageapi.ImageStreamList

	// helpers
	out      io.Writer
	osClient client.Interface
}

// Complete turns a partially defined TopImageStreamsOptions into a solvent structure
// which can be validated and used for showing limits usage.
func (o *TopImageStreamsOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
	osClient, _, _, err := f.Clients()
	if err != nil {
		return err
	}
	namespace := cmd.Flag("namespace").Value.String()
	if len(namespace) == 0 {
		namespace = kapi.NamespaceAll
	}
	o.out = out

	allImages, err := osClient.Images().List(kapi.ListOptions{})
	if err != nil {
		return err
	}
	o.Images = allImages

	allStreams, err := osClient.ImageStreams(namespace).List(kapi.ListOptions{})
	if err != nil {
		return err
	}
	o.Streams = allStreams

	return nil
}

// Validate ensures that a TopImageStreamsOptions is valid and can be used to execute command.
func (o TopImageStreamsOptions) Validate(cmd *cobra.Command) error {
	return nil
}

// Run contains all the necessary functionality to show current image references.
func (o TopImageStreamsOptions) Run() error {
	infos := o.imageStreamsTop()
	Print(o.out, ImageStreamColumns, infos)
	return nil
}

var ImageStreamColumns = []string{"NAME", "STORAGE", "IMAGES", "LAYERS"}

// imageStreamInfo contains contains statistic information about ImageStream usage.
type imageStreamInfo struct {
	ImageStream string
	Storage     int64
	Images      int
	Layers      int
}

var _ Info = &imageStreamInfo{}

func (i imageStreamInfo) PrintLine(out io.Writer) {
	printValue(out, i.ImageStream)
	printSize(out, i.Storage)
	printValue(out, i.Images)
	printValue(out, i.Layers)
}

// imageStreamsTop generates ImageStream information from a graph and
// returns this as a list of imageStreamInfo array.
func (o TopImageStreamsOptions) imageStreamsTop() []Info {
	g := graph.New()
	addImagesToGraph(g, o.Images)
	addImageStreamsToGraph(g, o.Streams)

	infos := []Info{}
	streamNodes := getImageStreamNodes(g.Nodes())
	for _, sn := range streamNodes {
		storage, images, layers := getImageStreamSize(g, sn)
		infos = append(infos, imageStreamInfo{
			ImageStream: fmt.Sprintf("%s/%s", sn.ImageStream.Namespace, sn.ImageStream.Name),
			Storage:     storage,
			Images:      images,
			Layers:      layers,
		})
	}

	return infos
}

func getImageStreamSize(g graph.Graph, node *imagegraph.ImageStreamNode) (int64, int, int) {
	imageEdges := g.OutboundEdges(node, ImageStreamImageEdgeKind)
	storage := int64(0)
	images := len(imageEdges)
	layers := 0
	blobSet := sets.NewString()
	for _, e := range imageEdges {
		imageNode, ok := e.To().(*imagegraph.ImageNode)
		if !ok {
			continue
		}
		image := imageNode.Image
		layers += len(image.DockerImageLayers)
		// we're counting only unique layers per the entire stream
		for _, layer := range image.DockerImageLayers {
			if blobSet.Has(layer.Name) {
				continue
			}
			blobSet.Insert(layer.Name)
			storage += layer.LayerSize
		}
		if len(image.DockerImageConfig) > 0 && !blobSet.Has(image.DockerImageMetadata.ID) {
			blobSet.Insert(image.DockerImageMetadata.ID)
			storage += int64(len(image.DockerImageConfig))
		}
	}

	return storage, images, layers
}

func getImageStreamNodes(nodes []gonum.Node) []*imagegraph.ImageStreamNode {
	ret := []*imagegraph.ImageStreamNode{}
	for i := range nodes {
		if node, ok := nodes[i].(*imagegraph.ImageStreamNode); ok {
			ret = append(ret, node)
		}
	}
	return ret
}