package naming

import (
	"bytes"
	"fmt"
	"strings"
	"sync"
	"text/template"

	apimeta "k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/runtime/schema"

	prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
	"github.com/directxman12/k8s-prometheus-adapter/pkg/config"
	"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
	pmodel "github.com/prometheus/common/model"
	"k8s.io/klog"
)

var (
	groupNameSanitizer = strings.NewReplacer(".", "_", "-", "_")
	nsGroupResource    = schema.GroupResource{Resource: "namespaces"}
)

// ResourceConverter knows the relationship between Kubernetes group-resources and Prometheus labels,
// and can convert between the two for any given label or series.
type ResourceConverter interface {
	// ResourcesForSeries returns the group-resources associated with the given series,
	// as well as whether or not the given series has the "namespace" resource).
	ResourcesForSeries(series prom.Series) (res []schema.GroupResource, namespaced bool)
	// LabelForResource returns the appropriate label for the given resource.
	LabelForResource(resource schema.GroupResource) (pmodel.LabelName, error)
}

type resourceConverter struct {
	labelResourceMu   sync.RWMutex
	labelToResource   map[pmodel.LabelName]schema.GroupResource
	resourceToLabel   map[schema.GroupResource]pmodel.LabelName
	labelResExtractor *labelGroupResExtractor
	mapper            apimeta.RESTMapper
	labelTemplate     *template.Template
}

// NewResourceConverter creates a ResourceConverter based on a generic template plus any overrides.
// Either overrides or the template may be empty, but not both.
func NewResourceConverter(resourceTemplate string, overrides map[string]config.GroupResource, mapper apimeta.RESTMapper) (ResourceConverter, error) {
	converter := &resourceConverter{
		labelToResource: make(map[pmodel.LabelName]schema.GroupResource),
		resourceToLabel: make(map[schema.GroupResource]pmodel.LabelName),
		mapper:          mapper,
	}

	if resourceTemplate != "" {
		labelTemplate, err := template.New("resource-label").Delims("<<", ">>").Parse(resourceTemplate)
		if err != nil {
			return converter, fmt.Errorf("unable to parse label template %q: %v", resourceTemplate, err)
		}
		converter.labelTemplate = labelTemplate

		labelResExtractor, err := newLabelGroupResExtractor(labelTemplate)
		if err != nil {
			return converter, fmt.Errorf("unable to generate label format from template %q: %v", resourceTemplate, err)
		}
		converter.labelResExtractor = labelResExtractor
	}

	// invert the structure for consistency with the template
	for lbl, groupRes := range overrides {
		infoRaw := provider.CustomMetricInfo{
			GroupResource: schema.GroupResource{
				Group:    groupRes.Group,
				Resource: groupRes.Resource,
			},
		}
		info, _, err := infoRaw.Normalized(converter.mapper)
		if err != nil {
			return nil, fmt.Errorf("unable to normalize group-resource %v: %v", groupRes, err)
		}

		converter.labelToResource[pmodel.LabelName(lbl)] = info.GroupResource
		converter.resourceToLabel[info.GroupResource] = pmodel.LabelName(lbl)
	}

	return converter, nil
}

func (r *resourceConverter) LabelForResource(resource schema.GroupResource) (pmodel.LabelName, error) {
	r.labelResourceMu.RLock()
	// check if we have a cached copy or override
	lbl, ok := r.resourceToLabel[resource]
	r.labelResourceMu.RUnlock() // release before we call makeLabelForResource
	if ok {
		return lbl, nil
	}

	// NB: we don't actually care about the gap between releasing read lock
	// and acquiring the write lock -- if we do duplicate work sometimes, so be
	// it, as long as we're correct.

	// otherwise, use the template and save the result
	lbl, err := r.makeLabelForResource(resource)
	if err != nil {
		return "", fmt.Errorf("unable to convert resource %s into label: %v", resource.String(), err)
	}
	return lbl, nil
}

// makeLabelForResource constructs a label name for the given resource, and saves the result.
// It must *not* be called under an existing lock.
func (r *resourceConverter) makeLabelForResource(resource schema.GroupResource) (pmodel.LabelName, error) {
	if r.labelTemplate == nil {
		return "", fmt.Errorf("no generic resource label form specified for this metric")
	}
	buff := new(bytes.Buffer)

	singularRes, err := r.mapper.ResourceSingularizer(resource.Resource)
	if err != nil {
		return "", fmt.Errorf("unable to singularize resource %s: %v", resource.String(), err)
	}
	convResource := schema.GroupResource{
		Group:    groupNameSanitizer.Replace(resource.Group),
		Resource: singularRes,
	}

	if err := r.labelTemplate.Execute(buff, convResource); err != nil {
		return "", err
	}
	if buff.Len() == 0 {
		return "", fmt.Errorf("empty label produced by label template")
	}
	lbl := pmodel.LabelName(buff.String())

	r.labelResourceMu.Lock()
	defer r.labelResourceMu.Unlock()

	r.resourceToLabel[resource] = lbl
	r.labelToResource[lbl] = resource
	return lbl, nil
}

func (r *resourceConverter) ResourcesForSeries(series prom.Series) ([]schema.GroupResource, bool) {
	// use an updates map to avoid having to drop the read lock to update the cache
	// until the end.  Since we'll probably have few updates after the first run,
	// this should mean that we rarely have to hold the write lock.
	var resources []schema.GroupResource
	updates := make(map[pmodel.LabelName]schema.GroupResource)
	namespaced := false

	// use an anon func to get the right defer behavior
	func() {
		r.labelResourceMu.RLock()
		defer r.labelResourceMu.RUnlock()

		for lbl := range series.Labels {
			var groupRes schema.GroupResource
			var ok bool

			// check if we have an override
			if groupRes, ok = r.labelToResource[lbl]; ok {
				resources = append(resources, groupRes)
			} else if groupRes, ok = updates[lbl]; ok {
				resources = append(resources, groupRes)
			} else if r.labelResExtractor != nil {
				// if not, check if it matches the form we expect, and if so,
				// convert to a group-resource.
				if groupRes, ok = r.labelResExtractor.GroupResourceForLabel(lbl); ok {
					info, _, err := provider.CustomMetricInfo{GroupResource: groupRes}.Normalized(r.mapper)
					if err != nil {
						// this is likely to show up for a lot of labels, so make it a verbose info log
						klog.V(9).Infof("unable to normalize group-resource %s from label %q, skipping: %v", groupRes.String(), lbl, err)
						continue
					}

					groupRes = info.GroupResource
					resources = append(resources, groupRes)
					updates[lbl] = groupRes
				}
			}

			if groupRes == nsGroupResource {
				namespaced = true
			}
		}
	}()

	// update the cache for next time.  This should only be called by discovery,
	// so we don't really have to worry about the gap between read and write locks
	// (plus, we don't care if someone else updates the cache first, since the results
	// are necessarily the same, so at most we've done extra work).
	if len(updates) > 0 {
		r.labelResourceMu.Lock()
		defer r.labelResourceMu.Unlock()

		for lbl, groupRes := range updates {
			r.labelToResource[lbl] = groupRes
		}
	}

	return resources, namespaced
}
