package metrics

import (
	"sync"
	"time"
)

// Metrics holds all observability metrics for the trip planner service.
type Metrics struct {
	// Cache metrics per layer
	CacheHits   map[string]int64 // per-layer hit counts (city, station, search)
	CacheMisses map[string]int64 // per-layer miss counts

	// API quota remaining (per key or global)
	APIQuotaRemaining int64

	// Circuit breaker metrics
	CircuitBreakerTrips int64 // total circuit breaker trips (opened)

	// Search metrics
	SearchCount    int64      // total number of searches
	SearchDuration *histogram // distribution of search durations

	// Internal counters
	mu        sync.Mutex
	layerTTLs map[string]time.Duration
}

// histogram tracks duration values and computes simple stats.
type histogram struct {
	values    []int64 // nanoseconds
	maxValues int
}

// New creates a new Metrics instance with initialized maps.
func New() *Metrics {
	return &Metrics{
		CacheHits:      make(map[string]int64),
		CacheMisses:    make(map[string]int64),
		layerTTLs:      make(map[string]time.Duration),
		SearchDuration: &histogram{maxValues: 1000},
	}
}

// RecordCacheHit records a cache hit for the given layer.
func (m *Metrics) RecordCacheHit(layer string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.CacheHits[layer]++
}

// RecordCacheMiss records a cache miss for the given layer.
func (m *Metrics) RecordCacheMiss(layer string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.CacheMisses[layer]++
}

// RecordAPIQuota records the remaining API quota.
func (m *Metrics) RecordAPIQuota(remaining int64) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.APIQuotaRemaining = remaining
}

// RecordCircuitBreakerTrip records a circuit breaker trip.
func (m *Metrics) RecordCircuitBreakerTrip() {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.CircuitBreakerTrips++
}

// RecordSearch records a completed search with its duration in nanoseconds.
func (m *Metrics) RecordSearch(durationNS int64) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.SearchCount++
	m.SearchDuration.values = append(m.SearchDuration.values, durationNS)
	// Trim if exceeding max
	if len(m.SearchDuration.values) > m.SearchDuration.maxValues {
		m.SearchDuration.values = m.SearchDuration.values[len(m.SearchDuration.values)-m.SearchDuration.maxValues:]
	}
}

// GetCacheHitRate returns the hit rate (hits / (hits + misses)) for a layer.
func (m *Metrics) GetCacheHitRate(layer string) float64 {
	m.mu.Lock()
	defer m.mu.Unlock()
	hits := m.CacheHits[layer]
	misses := m.CacheMisses[layer]
	total := hits + misses
	if total == 0 {
		return 0
	}
	return float64(hits) / float64(total)
}

// GetMetricsJSON returns all metrics as a JSON-friendly map.
func (m *Metrics) GetMetricsJSON() map[string]interface{} {
	m.mu.Lock()
	defer m.mu.Unlock()

	avgSearchDuration := 0.0
	if m.SearchCount > 0 && len(m.SearchDuration.values) > 0 {
		var total int64
		for _, v := range m.SearchDuration.values {
			total += v
		}
		avgSearchDuration = float64(total) / float64(len(m.SearchDuration.values)) / 1e6 // convert to milliseconds
	}

	result := map[string]interface{}{
		"cache_hits":             m.CacheHits,
		"cache_misses":           m.CacheMisses,
		"cache_hit_rate":         m.getOverallHitRate(),
		"api_quota_remaining":    m.APIQuotaRemaining,
		"circuit_breaker_trips":  m.CircuitBreakerTrips,
		"search_count":           m.SearchCount,
		"avg_search_duration_ms": avgSearchDuration,
		"layer_ttls":             m.layerTTLs,
	}
	return result
}

// getOverallHitRate calculates overall hit rate across all layers.
func (m *Metrics) getOverallHitRate() float64 {
	var totalHits, totalMisses int64
	for _, hits := range m.CacheHits {
		totalHits += hits
	}
	for _, misses := range m.CacheMisses {
		totalMisses += misses
	}
	total := totalHits + totalMisses
	if total == 0 {
		return 0
	}
	return float64(totalHits) / float64(total)
}

// SetLayerTTL sets the TTL for a cache layer (for documentation/observability).
func (m *Metrics) SetLayerTTL(layer string, ttl time.Duration) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.layerTTLs[layer] = ttl
}

// GetLayerTTL returns the TTL for a cache layer.
func (m *Metrics) GetLayerTTL(layer string) (time.Duration, bool) {
	m.mu.Lock()
	defer m.mu.Unlock()
	ttl, ok := m.layerTTLs[layer]
	return ttl, ok
}
