/*
Copyright 2022 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package collectors

import (
	"context"
	"fmt"
	"time"

	"k8s.io/component-base/metrics"
	runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
	"k8s.io/klog/v2"
)

type criMetricsCollector struct {
	metrics.BaseStableCollector
	// The descriptors structure will be populated by one call to ListMetricDescriptors from the runtime.
	// They will be saved in this map, where the key is the Name and the value is the Desc.
	descriptors             map[string]*metrics.Desc
	listPodSandboxMetricsFn func(context.Context) ([]*runtimeapi.PodSandboxMetrics, error)
}

// Check if criMetricsCollector implements necessary interface
var _ metrics.StableCollector = &criMetricsCollector{}

// NewCRIMetricsCollector implements the metrics.Collector interface
func NewCRIMetricsCollector(ctx context.Context, listPodSandboxMetricsFn func(context.Context) ([]*runtimeapi.PodSandboxMetrics, error), listMetricDescriptorsFn func(context.Context) ([]*runtimeapi.MetricDescriptor, error)) metrics.StableCollector {
	descs, err := listMetricDescriptorsFn(ctx)
	if err != nil {
		klog.ErrorS(err, "Error reading MetricDescriptors")
		return &criMetricsCollector{
			listPodSandboxMetricsFn: listPodSandboxMetricsFn,
		}
	}
	c := &criMetricsCollector{
		listPodSandboxMetricsFn: listPodSandboxMetricsFn,
		descriptors:             make(map[string]*metrics.Desc, len(descs)),
	}

	for _, desc := range descs {
		c.descriptors[desc.Name] = criDescToProm(desc)
	}

	return c
}

// Describe implements the metrics.DescribeWithStability interface.
func (c *criMetricsCollector) DescribeWithStability(ch chan<- *metrics.Desc) {
	for _, desc := range c.descriptors {
		ch <- desc
	}
}

// Collect implements the metrics.CollectWithStability interface.
// TODO(haircommander): would it be better if these were processed async?
func (c *criMetricsCollector) CollectWithStability(ch chan<- metrics.Metric) {
	podMetrics, err := c.listPodSandboxMetricsFn(context.Background())
	if err != nil {
		klog.ErrorS(err, "Failed to get pod metrics")
		return
	}

	for _, podMetric := range podMetrics {
		for _, metric := range podMetric.GetMetrics() {
			promMetric, err := c.criMetricToProm(metric)
			if err == nil {
				ch <- promMetric
			}
		}
		for _, ctrMetric := range podMetric.GetContainerMetrics() {
			for _, metric := range ctrMetric.GetMetrics() {
				promMetric, err := c.criMetricToProm(metric)
				if err == nil {
					ch <- promMetric
				}
			}
		}
	}
}

func criDescToProm(m *runtimeapi.MetricDescriptor) *metrics.Desc {
	// Labels in the translation are variableLabels, as opposed to constant labels.
	// This is because the values of the labels will be different for each container.
	return metrics.NewDesc(m.Name, m.Help, m.LabelKeys, nil, metrics.INTERNAL, "")
}

func (c *criMetricsCollector) criMetricToProm(m *runtimeapi.Metric) (metrics.Metric, error) {
	desc, ok := c.descriptors[m.Name]
	if !ok {
		err := fmt.Errorf("error converting CRI Metric to prometheus format")
		klog.V(5).ErrorS(err, "Descriptor not present in pre-populated list of descriptors", "name", m.Name)
		return nil, err
	}

	typ := criTypeToProm[m.MetricType]

	pm, err := metrics.NewConstMetric(desc, typ, float64(m.GetValue().Value), m.LabelValues...)
	if err != nil {
		klog.ErrorS(err, "Error getting CRI prometheus metric", "descriptor", desc.String())
		return nil, err
	}
	// If Timestamp is 0, then the runtime did not cache the result.
	// In this case, a cached result is a metric that was collected ahead of time,
	// as opposed to on-demand.
	// If the metric was requested as needed, then Timestamp==0.
	if m.Timestamp == 0 {
		return pm, nil
	}
	return metrics.NewLazyMetricWithTimestamp(time.Unix(0, m.Timestamp), pm), nil
}

var criTypeToProm = map[runtimeapi.MetricType]metrics.ValueType{
	runtimeapi.MetricType_COUNTER: metrics.CounterValue,
	runtimeapi.MetricType_GAUGE:   metrics.GaugeValue,
}
