package metrics

import (
	"bytes"
	"errors"
	"fmt"
	"lunar/engine/utils/environment"
	"lunar/toolkit-core/configuration"
	"strings"

	"github.com/rs/zerolog/log"
	"go.opentelemetry.io/otel/metric"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/common/expfmt"
)

// EqualSystemMetrics compares only the SystemMetrics slice in Config.
func (c Config) EqualSystemMetrics(other Config) bool {
	if len(c.SystemMetrics) != len(other.SystemMetrics) {
		return false
	}
	for i := range c.SystemMetrics {
		if !c.SystemMetrics[i].Equal(other.SystemMetrics[i]) {
			return false
		}
	}
	return true
}

// Equal compares two GeneralMetrics structs.
func (g GeneralMetrics) Equal(other GeneralMetrics) bool {
	if len(g.LabelValue) != len(other.LabelValue) {
		return false
	}
	if len(g.MetricValue) != len(other.MetricValue) {
		return false
	}

	for i := range g.LabelValue {
		if g.LabelValue[i] != other.LabelValue[i] {
			return false
		}
	}

	for i := range g.MetricValue {
		if !g.MetricValue[i].Equal(other.MetricValue[i]) {
			return false
		}
	}

	return true
}

// Equal compares two MetricValue structs.
func (m MetricValue) Equal(other MetricValue) bool {
	if m.Name != other.Name || m.Description != other.Description || m.JSONPath != other.JSONPath {
		return false
	}

	if len(m.Buckets) != len(other.Buckets) {
		return false
	}
	for i := range m.Buckets {
		if m.Buckets[i] != other.Buckets[i] {
			return false
		}
	}

	return true
}

func (c Config) EqualLabeledEndpoints(other *Config) bool {
	if len(c.LabeledEndpoints) != len(other.LabeledEndpoints) {
		return false
	}
	for i := range c.LabeledEndpoints {
		if c.LabeledEndpoints[i] != other.LabeledEndpoints[i] {
			return false
		}
	}
	return true
}

// registerObservableMetric registers an observable metric with the given meter
func registerObservableMetric(
	meter metric.Meter,
	metricVal MetricValue,
) (meterObj metric.Observable, err error) {
	metricType, found := metricsObservableRegistry[metricVal.Name]
	if !found {
		return nil, fmt.Errorf("type unknown for metric: %s", metricVal.Name)
	}

	metricName := string(metricVal.Name)
	description := metric.WithDescription(metricVal.Description)

	switch metricType { //nolint:exhaustive
	case Int64ObservableCounter:
		meterObj, err = meter.Int64ObservableCounter(metricName, description)
	case Float64ObservableCounter:
		meterObj, err = meter.Float64ObservableCounter(metricName, description)
	case Int64ObservableUpDownCounter:
		meterObj, err = meter.Int64ObservableUpDownCounter(metricName, description)
	case Float64ObservableUpDownCounter:
		meterObj, err = meter.Float64ObservableUpDownCounter(metricName, description)
	case Int64ObservableGauge:
		meterObj, err = meter.Int64ObservableGauge(metricName, description)
	case Float64ObservableGauge:
		meterObj, err = meter.Float64ObservableGauge(metricName, description)
	default:
		return nil, fmt.Errorf("unknown metric type: %v", metricType)
	}
	return meterObj, err
}

// loadMetricsConfig loads and returns the metrics config
func loadMetricsConfig() (*Config, error) {
	log.Info().Msg("Loading metrics config")
	configFilePath := environment.GetMetricsConfigFilePathOrDefault()

	log.Info().Msgf("Metrics config file path: %s", configFilePath)
	result, err := configuration.DecodeYAML[Config](configFilePath)
	if err != nil {
		return nil, err
	}
	if len(result.Content) == 0 {
		return nil, errors.New("metrics config file is empty")
	}
	return result.UnmarshaledData, nil
}

func GatherLunarMetrics() (string, error) {
	mfs, err := prometheus.DefaultGatherer.Gather()
	if err != nil {
		return "", fmt.Errorf("error gathering metrics: %w", err)
	}

	var buf bytes.Buffer
	enc := expfmt.NewEncoder(&buf, expfmt.FmtText)
	for _, mf := range mfs {
		if isMetricLunarRelated(mf.GetName()) {
			if err := enc.Encode(mf); err != nil {
				return "", fmt.Errorf("error encoding metrics: %w", err)
			}
		}
	}

	return buf.String(), nil
}

func isMetricLunarRelated(metricName string) bool {
	if strings.HasPrefix(metricName, MetricPrefix) {
		return true
	}

	for _, label := range labelsToInclude {
		if strings.Contains(metricName, label) {
			return true
		}
	}
	return false
}
