/*
Copyright 2017 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 provider

import (
	"fmt"
	"sync"

	pmodel "github.com/prometheus/common/model"

	apimeta "k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/klog/v2"

	"sigs.k8s.io/custom-metrics-apiserver/pkg/provider"

	prom "sigs.k8s.io/prometheus-adapter/pkg/client"
	"sigs.k8s.io/prometheus-adapter/pkg/naming"
)

// NB: container metrics sourced from cAdvisor don't consistently follow naming conventions,
// so we need to whitelist them and handle them on a case-by-case basis.  Metrics ending in `_total`
// *should* be counters, but may actually be guages in this case.

// SeriesType represents the kind of series backing a metric.
type SeriesType int

const (
	CounterSeries SeriesType = iota
	SecondsCounterSeries
	GaugeSeries
)

// SeriesRegistry provides conversions between Prometheus series and MetricInfo
type SeriesRegistry interface {
	// SetSeries replaces the known series in this registry.
	// Each slice in series should correspond to a MetricNamer in namers.
	SetSeries(series [][]prom.Series, namers []naming.MetricNamer) error
	// ListAllMetrics lists all metrics known to this registry
	ListAllMetrics() []provider.CustomMetricInfo
	// SeriesForMetric looks up the minimum required series information to make a query for the given metric
	// against the given resource (namespace may be empty for non-namespaced resources)
	QueryForMetric(info provider.CustomMetricInfo, namespace string, metricSelector labels.Selector, resourceNames ...string) (query prom.Selector, found bool)
	// MatchValuesToNames matches result values to resource names for the given metric and value set
	MatchValuesToNames(metricInfo provider.CustomMetricInfo, values pmodel.Vector) (matchedValues map[string]pmodel.SampleValue, found bool)
}

type seriesInfo struct {
	// seriesName is the name of the corresponding Prometheus series
	seriesName string

	// namer is the MetricNamer used to name this series
	namer naming.MetricNamer
}

// overridableSeriesRegistry is a basic SeriesRegistry
type basicSeriesRegistry struct {
	mu sync.RWMutex

	// info maps metric info to information about the corresponding series
	info map[provider.CustomMetricInfo]seriesInfo
	// metrics is the list of all known metrics
	metrics []provider.CustomMetricInfo

	mapper apimeta.RESTMapper
}

func (r *basicSeriesRegistry) SetSeries(newSeriesSlices [][]prom.Series, namers []naming.MetricNamer) error {
	if len(newSeriesSlices) != len(namers) {
		return fmt.Errorf("need one set of series per namer")
	}

	newInfo := make(map[provider.CustomMetricInfo]seriesInfo)
	for i, newSeries := range newSeriesSlices {
		namer := namers[i]
		for _, series := range newSeries {
			// TODO: warn if it doesn't match any resources
			resources, namespaced := namer.ResourcesForSeries(series)
			name, err := namer.MetricNameForSeries(series)
			if err != nil {
				klog.Errorf("unable to name series %q, skipping: %v", series.String(), err)
				continue
			}
			for _, resource := range resources {
				info := provider.CustomMetricInfo{
					GroupResource: resource,
					Namespaced:    namespaced,
					Metric:        name,
				}

				// some metrics aren't counted as namespaced
				if resource == naming.NsGroupResource || resource == naming.NodeGroupResource || resource == naming.PVGroupResource {
					info.Namespaced = false
				}

				// we don't need to re-normalize, because the metric namer should have already normalized for us
				newInfo[info] = seriesInfo{
					seriesName: series.Name,
					namer:      namer,
				}
			}
		}
	}

	// regenerate metrics
	newMetrics := make([]provider.CustomMetricInfo, 0, len(newInfo))
	for info := range newInfo {
		newMetrics = append(newMetrics, info)
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	r.info = newInfo
	r.metrics = newMetrics
	klog.Infof("Set series custom metrics from prometheus:%v", newMetrics)

	return nil
}

func (r *basicSeriesRegistry) ListAllMetrics() []provider.CustomMetricInfo {
	r.mu.RLock()
	defer r.mu.RUnlock()

	return r.metrics
}

func (r *basicSeriesRegistry) QueryForMetric(metricInfo provider.CustomMetricInfo, namespace string, metricSelector labels.Selector, resourceNames ...string) (prom.Selector, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	if len(resourceNames) == 0 {
		klog.Errorf("no resource names requested while producing a query for metric %s", metricInfo.String())
		return "", false
	}

	metricInfo, _, err := metricInfo.Normalized(r.mapper)
	if err != nil {
		klog.Errorf("unable to normalize group resource while producing a query: %v", err)
		return "", false
	}

	info, infoFound := r.info[metricInfo]
	if !infoFound {
		klog.V(10).Infof("metric %v not registered", metricInfo)
		return "", false
	}

	query, err := info.namer.QueryForSeries(info.seriesName, metricInfo.GroupResource, namespace, metricSelector, resourceNames...)
	if err != nil {
		klog.Errorf("unable to construct query for metric %s: %v", metricInfo.String(), err)
		return "", false
	}

	return query, true
}

func (r *basicSeriesRegistry) MatchValuesToNames(metricInfo provider.CustomMetricInfo, values pmodel.Vector) (matchedValues map[string]pmodel.SampleValue, found bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	metricInfo, _, err := metricInfo.Normalized(r.mapper)
	if err != nil {
		klog.Errorf("unable to normalize group resource while matching values to names: %v", err)
		return nil, false
	}

	info, infoFound := r.info[metricInfo]
	if !infoFound {
		return nil, false
	}

	resourceLbl, err := info.namer.LabelForResource(metricInfo.GroupResource)
	if err != nil {
		klog.Errorf("unable to construct resource label for metric %s: %v", metricInfo.String(), err)
		return nil, false
	}

	res := make(map[string]pmodel.SampleValue, len(values))
	for _, val := range values {
		if val == nil {
			// skip empty values
			continue
		}
		res[string(val.Metric[resourceLbl])] = val.Value
	}

	return res, true
}
