// Copyright (C) 2025 wangyusong
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package metric

import (
	"context"
	"net/http"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/client_golang/prometheus/promhttp"

	"github.com/glidea/zenfeed/pkg/model"
	telemetrymodel "github.com/glidea/zenfeed/pkg/telemetry/model"
)

func Handler() http.Handler {
	return promhttp.Handler()
}

var (
	operationInFlight = promauto.NewGaugeVec(
		prometheus.GaugeOpts{
			Namespace: model.AppName,
			Name:      "operation_in_flight",
			Help:      "Number of operations in flight.",
		},
		[]string{
			telemetrymodel.KeyComponent,
			telemetrymodel.KeyComponentInstance,
			telemetrymodel.KeyOperation,
		},
	)

	operationTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: model.AppName,
			Name:      "operation_total",
			Help:      "Total number of operations.",
		},
		[]string{
			telemetrymodel.KeyComponent,
			telemetrymodel.KeyComponentInstance,
			telemetrymodel.KeyOperation,
			telemetrymodel.KeyResult,
		},
	)

	operationDuration = promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: model.AppName,
			Name:      "operation_duration_seconds",
			Help:      "Histogram of operation latencies in seconds.",
			Buckets:   []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 20},
		},
		[]string{
			telemetrymodel.KeyComponent,
			telemetrymodel.KeyComponentInstance,
			telemetrymodel.KeyOperation,
			telemetrymodel.KeyResult,
		},
	)
)

type ctxKey uint8

const (
	ctxKeyComponent ctxKey = iota
	ctxKeyInstance
	ctxKeyOperation
	ctxKeyStartTime
)

func StartWith(ctx context.Context, keyvals ...any) context.Context {
	// Extend from parent context.
	component, instance, operation, _ := parseFrom(ctx)

	// Parse component and operation... from keyvals.
	for i := 0; i < len(keyvals); i += 2 {
		if i+1 < len(keyvals) {
			switch keyvals[i] {
			case telemetrymodel.KeyComponent:
				component = keyvals[i+1].(string)
			case telemetrymodel.KeyComponentInstance:
				instance = keyvals[i+1].(string)
			case telemetrymodel.KeyOperation:
				operation = keyvals[i+1].(string)
			}
		}
	}
	if component == "" || operation == "" {
		panic("missing required keyvals")
	}

	// Record operation in flight.
	operationInFlight.WithLabelValues(component, instance, operation).Inc()

	// Add to context.
	ctx = context.WithValue(ctx, ctxKeyComponent, component)
	ctx = context.WithValue(ctx, ctxKeyInstance, instance)
	ctx = context.WithValue(ctx, ctxKeyOperation, operation)
	ctx = context.WithValue(ctx, ctxKeyStartTime, time.Now())

	return ctx
}

func RecordRED(ctx context.Context, err error) {
	// Parse component, instance, operation, and start time from context.
	component, instance, operation, startTime := parseFrom(ctx)
	duration := time.Since(startTime)

	// Determine result.
	result := telemetrymodel.ValResultSuccess
	if err != nil {
		result = telemetrymodel.ValResultError
	}

	// Record metrics.
	operationTotal.WithLabelValues(component, instance, operation, result).Inc()
	operationDuration.WithLabelValues(component, instance, operation, result).Observe(duration.Seconds())
	operationInFlight.WithLabelValues(component, instance, operation).Dec()
}

func Close(id prometheus.Labels) {
	operationInFlight.DeletePartialMatch(id)
	operationTotal.DeletePartialMatch(id)
	operationDuration.DeletePartialMatch(id)
}

func parseFrom(ctx context.Context) (component, instance, operation string, startTime time.Time) {
	if v := ctx.Value(ctxKeyComponent); v != nil {
		component = v.(string)
	}
	if v := ctx.Value(ctxKeyInstance); v != nil {
		instance = v.(string)
	}
	if v := ctx.Value(ctxKeyOperation); v != nil {
		operation = v.(string)
	}
	if v := ctx.Value(ctxKeyStartTime); v != nil {
		startTime = v.(time.Time)
	}

	return
}
