// Copyright 2025 TiKV 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.

//go:build vm_histogram

package metrics

import (
	"fmt"
	"sort"
	"strings"
	"sync"

	vmetrics "github.com/VictoriaMetrics/metrics"
	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
)

type vmHistogram struct {
	h    *vmetrics.Histogram
	desc *prometheus.Desc
}

func (h *vmHistogram) Observe(v float64) {
	// VictoriaMetrics uses `Update` for recording observations.
	h.h.Update(v)
}

func (h *vmHistogram) Desc() *prometheus.Desc {
	return h.desc
}

func (h *vmHistogram) Write(out *dto.Metric) error {
	out.Histogram = &dto.Histogram{}
	return nil
}

func (h *vmHistogram) Describe(ch chan<- *prometheus.Desc) {
	ch <- h.desc
}

func (h *vmHistogram) Collect(ch chan<- prometheus.Metric) {
	ch <- prometheus.NewInvalidMetric(h.desc, fmt.Errorf("VM histogram metrics are exported via VictoriaMetrics exposition format"))
}

type vmHistogramVec struct {
	opts       prometheus.HistogramOpts
	labelNames []string

	mu       sync.RWMutex
	children map[string]*vmHistogram
}

func newHistogram(opts prometheus.HistogramOpts) Histogram {
	fqName := prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name)
	constKeys := sortedLabelKeys(opts.ConstLabels)
	metric := buildVMMetricName(fqName, nil, nil, constKeys, opts.ConstLabels)

	labelNames := make([]string, 0, len(opts.ConstLabels))
	for k := range opts.ConstLabels {
		labelNames = append(labelNames, k)
	}
	sort.Strings(labelNames)

	desc := prometheus.NewDesc(
		fqName,
		opts.Help,
		labelNames,
		opts.ConstLabels,
	)

	return &vmHistogram{h: vmetrics.GetOrCreateHistogram(metric), desc: desc}
}

func newHistogramVec(opts prometheus.HistogramOpts, labelNames []string) HistogramVec {
	return &vmHistogramVec{
		opts:       opts,
		labelNames: append([]string(nil), labelNames...),
		children:   make(map[string]*vmHistogram),
	}
}

func (v *vmHistogramVec) With(labels prometheus.Labels) prometheus.Observer {
	lvs := make([]string, len(v.labelNames))
	for i, name := range v.labelNames {
		lvs[i] = labels[name]
	}
	return v.WithLabelValues(lvs...)
}

func (v *vmHistogramVec) WithLabelValues(lvs ...string) prometheus.Observer {
	if len(lvs) != len(v.labelNames) {
		panic("inconsistent label cardinality")
	}
	key := strings.Join(lvs, "\xff")

	v.mu.RLock()
	child := v.children[key]
	v.mu.RUnlock()
	if child != nil {
		return child
	}

	v.mu.Lock()
	defer v.mu.Unlock()
	child = v.children[key]
	if child != nil {
		return child
	}

	fqName := prometheus.BuildFQName(v.opts.Namespace, v.opts.Subsystem, v.opts.Name)
	constKeys := sortedLabelKeys(v.opts.ConstLabels)
	metric := buildVMMetricName(fqName, v.labelNames, lvs, constKeys, v.opts.ConstLabels)

	labelNames := make([]string, 0, len(v.labelNames)+len(v.opts.ConstLabels))
	labelNames = append(labelNames, v.labelNames...)
	for k := range v.opts.ConstLabels {
		labelNames = append(labelNames, k)
	}
	sort.Strings(labelNames)

	desc := prometheus.NewDesc(
		fqName,
		v.opts.Help,
		labelNames,
		v.opts.ConstLabels,
	)

	child = &vmHistogram{h: vmetrics.GetOrCreateHistogram(metric), desc: desc}
	v.children[key] = child
	return child
}

func (v *vmHistogramVec) Describe(ch chan<- *prometheus.Desc) {
	v.mu.RLock()
	defer v.mu.RUnlock()
	for _, h := range v.children {
		ch <- h.desc
	}
}

func (v *vmHistogramVec) Collect(ch chan<- prometheus.Metric) {
	v.mu.RLock()
	defer v.mu.RUnlock()
	for _, h := range v.children {
		h.Collect(ch)
	}
}

func registerHistograms() {
	// No-op in vm_histogram mode.
	// Histograms are stored in VictoriaMetrics registry and exported via exposition helper.
}

func sortedLabelKeys(labels prometheus.Labels) []string {
	if len(labels) == 0 {
		return nil
	}
	keys := make([]string, 0, len(labels))
	for k := range labels {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys
}

func buildVMMetricName(
	fqName string,
	labelNames []string,
	labelValues []string,
	constLabelKeys []string,
	constLabels prometheus.Labels,
) string {
	// VM `metrics` identifies a metric with the full string like:
	//   name{a="b",c="d"}
	// so we must keep label ordering deterministic to avoid duplicates.
	var b strings.Builder
	b.WriteString(fqName)

	totalLabels := len(labelNames) + len(constLabelKeys)
	if totalLabels == 0 {
		return b.String()
	}

	b.WriteByte('{')
	first := true

	for i, k := range labelNames {
		if !first {
			b.WriteByte(',')
		}
		first = false
		b.WriteString(k)
		b.WriteString(`="`)
		b.WriteString(escapeLabelValue(labelValues[i]))
		b.WriteByte('"')
	}

	for _, k := range constLabelKeys {
		if !first {
			b.WriteByte(',')
		}
		first = false
		b.WriteString(k)
		b.WriteString(`="`)
		b.WriteString(escapeLabelValue(constLabels[k]))
		b.WriteByte('"')
	}

	b.WriteByte('}')
	return b.String()
}

func escapeLabelValue(v string) string {
	// Minimal escaping for Prometheus exposition style used by VM:
	// - backslash
	// - double quote
	// - newline
	v = strings.ReplaceAll(v, `\`, `\\`)
	v = strings.ReplaceAll(v, `"`, `\"`)
	v = strings.ReplaceAll(v, "\n", `\n`)
	return v
}
