package cmd

import (
	"errors"
	"fmt"
	"io"

	"github.com/linkerd/linkerd2/cli/table"
	"github.com/linkerd/linkerd2/controller/api/util"
	"github.com/linkerd/linkerd2/controller/gen/public"
	"github.com/linkerd/linkerd2/pkg/k8s"
	"github.com/linkerd/linkerd2/pkg/smimetrics"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/cobra"

	"github.com/servicemeshinterface/smi-sdk-go/pkg/apis/metrics/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
)

var allowedKinds = map[string]struct{}{
	k8s.CronJob:               struct{}{},
	k8s.DaemonSet:             struct{}{},
	k8s.Deployment:            struct{}{},
	k8s.Job:                   struct{}{},
	k8s.Namespace:             struct{}{},
	k8s.Pod:                   struct{}{},
	k8s.ReplicaSet:            struct{}{},
	k8s.ReplicationController: struct{}{},
	k8s.StatefulSet:           struct{}{},
}

type alphaStatOptions struct {
	namespace     string
	toResource    string
	allNamespaces bool
}

func newCmdAlphaStat() *cobra.Command {
	options := alphaStatOptions{
		namespace: "default",
	}

	statCmd := &cobra.Command{
		Use:   "stat [flags] (RESOURCE)",
		Short: "Display traffic stats about one or many resources",
		Long: `Display traffic stats about one or many resources
		
(RESOURCE) can be a resource kind; one of:
  * cronjobs
  * daemonsets
  * deployments
  * jobs
  * namespaces
  * pods
  * replicasets
  * replicationcontrollers
  * statefulsets
or it may be a specific named resource of one of the above kinds.

linkerd alpha stat will return a table of the requested resource or resources
showing the top-line metrics for those resources such as request rate, success
rate, and latency percentiles.  These values are measured on the server-side
unless the --to flag is specified.

The --to flag accepts a resource kind or a specific resource and instead
displays the metrics measured on the client side from the root resource to
the to-resource.  The root resource must be a specific named resource.

Examples:
  # Topline Resource Metrics
  linkerd alpha stat -n emojivoto deploy/web

  # Topline Resource Metrics for a whole Kind
  linkerd alpha stat -n emojivoto deploy

  # Outbound edges
  linkerd alpha stat -n emojivoto deploy/web --to=deploy

  # Outbound to a specific destination
  linkerd alpha stat -n emojivoto deploy/web --to=deploy/emoji`,
		Args: cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
			if err != nil {
				return err
			}

			namespace := options.namespace
			if options.allNamespaces {
				namespace = corev1.NamespaceAll
			}
			target, err := util.BuildResource(namespace, args[0])
			if err != nil {
				return err
			}
			if target.GetName() != "" && options.allNamespaces {
				// Getting a named resource from all-namespaces is not supported.
				return errors.New("cannot use --all-namespaces flag with a named target resource")
			}

			if _, ok := allowedKinds[target.GetType()]; !ok {
				return fmt.Errorf("%s is not a supported resource type", target.GetType())
			}
			kind, err := k8s.PluralResourceNameFromFriendlyName(target.GetType())
			if err != nil {
				return err
			}
			name := target.GetName()
			toResource := buildToResource("", options.toResource)
			// TODO: Lift this requirement once the API supports it.
			if toResource != nil && toResource.GetType() != target.GetType() {
				return errors.New("the --to resource must have the same kind as the target resource")
			}

			if name != "" {
				if toResource != nil {
					metrics, err := smimetrics.GetTrafficMetricsEdgesList(k8sAPI, target.GetNamespace(), kind, name, nil)
					if err != nil {
						return err
					}
					renderTrafficMetricsEdgesList(metrics, stdout, toResource, "to")
				} else {
					metrics, err := smimetrics.GetTrafficMetrics(k8sAPI, target.GetNamespace(), kind, name, nil)
					if err != nil {
						return err
					}
					renderTrafficMetrics(metrics, options.allNamespaces, stdout)
				}
			} else {
				if toResource != nil {
					return errors.New("the --to flag requires that the target resource name be specified")
				}
				metrics, err := smimetrics.GetTrafficMetricsList(k8sAPI, target.GetNamespace(), kind, nil)
				if err != nil {
					return err
				}
				renderTrafficMetricsList(metrics, options.allNamespaces, stdout)
			}

			return nil
		},
	}

	statCmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the specified resource")
	statCmd.PersistentFlags().StringVar(&options.toResource, "to", options.toResource, "If present, restricts outbound stats to the specified resource name")
	statCmd.PersistentFlags().BoolVarP(&options.allNamespaces, "all-namespaces", "A", options.allNamespaces, "Ignore the --namespace flag and fetches data from all namespaces")

	return statCmd
}

func buildToResource(namespace, to string) *public.Resource {
	toResource, err := util.BuildResource(namespace, to)
	if err != nil {
		log.Debugf("Invalid to resource: %s", err)
		return nil

	}
	log.Debugf("Using to resource: %v", toResource)
	return &toResource
}

func renderTrafficMetrics(metrics *v1alpha1.TrafficMetrics, allNamespaces bool, w io.Writer) {
	t := buildTable(false, allNamespaces)
	t.Data = []table.Row{metricsToRow(metrics, "")}
	t.Render(w)
}

func renderTrafficMetricsList(metrics *v1alpha1.TrafficMetricsList, allNamespaces bool, w io.Writer) {
	t := buildTable(false, allNamespaces)
	t.Data = []table.Row{}
	for _, row := range metrics.Items {
		row := row // Copy to satisfy golint.
		t.Data = append(t.Data, metricsToRow(row, ""))
	}
	t.Render(w)
}

func renderTrafficMetricsEdgesList(metrics *v1alpha1.TrafficMetricsList, w io.Writer, toResource *public.Resource, direction string) {
	t := buildTable(true, false)
	t.Data = []table.Row{}
	for _, row := range metrics.Items {
		row := row // Copy to satisfy golint.
		if string(row.Edge.Direction) != direction {
			continue
		}
		if toResource != nil {
			if toResource.GetName() != "" && row.Edge.Resource.Name != toResource.GetName() {
				log.Debugf("Skipping edge %v", row.Edge.Resource)
				continue
			}
		}
		t.Data = append(t.Data, metricsToRow(row, direction))
	}
	t.Render(w)
}

func getNumericMetric(metrics *v1alpha1.TrafficMetrics, name string) *resource.Quantity {
	for _, m := range metrics.Metrics {
		if m.Name == name {
			return m.Value
		}
	}
	return resource.NewQuantity(0, resource.DecimalSI)
}

func getNumericMetricWithUnit(metrics *v1alpha1.TrafficMetrics, name string) string {
	for _, m := range metrics.Metrics {
		if m.Name == name {
			value := m.Value.Value()
			return fmt.Sprintf("%d%s", value, m.Unit)
		}
	}
	return ""
}

func metricsToRow(metrics *v1alpha1.TrafficMetrics, direction string) []string {
	success := getNumericMetric(metrics, "success_count").MilliValue()
	failure := getNumericMetric(metrics, "failure_count").MilliValue()
	sr := "-"
	if success+failure > 0 {
		rate := float32(success) / float32(success+failure) * 100
		sr = fmt.Sprintf("%.2f%%", rate)
	}
	rate := float64(success+failure) / 1000.0 / metrics.Window.Seconds()
	rps := fmt.Sprintf("%.1frps", rate)

	var to string
	var from string
	if direction == "to" {
		to = metrics.Edge.Resource.String()
		from = metrics.Resource.Name
	} else if direction == "from" {
		to = metrics.Resource.Name
		from = metrics.Edge.Resource.String()
	}

	return []string{
		metrics.Resource.Namespace,
		metrics.Resource.Name,
		from,
		to,
		sr,
		rps,
		getNumericMetricWithUnit(metrics, "p50_response_latency"),
		getNumericMetricWithUnit(metrics, "p90_response_latency"),
		getNumericMetricWithUnit(metrics, "p99_response_latency"),
	}
}

func buildTable(outbound, allNamespaces bool) table.Table {
	columns := []table.Column{
		table.Column{
			Header:    "NAMESPACE",
			Width:     9,
			Hide:      !allNamespaces,
			Flexible:  true,
			LeftAlign: true,
		},
		table.Column{
			Header:    "NAME",
			Width:     4,
			Hide:      outbound,
			Flexible:  true,
			LeftAlign: true,
		},
		table.Column{
			Header:    "FROM",
			Width:     4,
			Hide:      !outbound,
			Flexible:  true,
			LeftAlign: true,
		},
		table.Column{
			Header:    "TO",
			Width:     2,
			Hide:      !outbound,
			Flexible:  true,
			LeftAlign: true,
		},
		table.Column{
			Header: "SUCCESS",
			Width:  7,
		},
		table.Column{
			Header: "RPS",
			Width:  9,
		},
		table.Column{
			Header: "LATENCY_P50",
			Width:  11,
		},
		table.Column{
			Header: "LATENCY_P90",
			Width:  11,
		},
		table.Column{
			Header: "LATENCY_P99",
			Width:  11,
		},
	}
	t := table.NewTable(columns, []table.Row{})
	t.Sort = []int{0, 1} // Sort by namespace, then name.
	return t
}
