package observability

import (
	"fmt"
	"net/http"
	"sort"
	"strings"
	"sync"
	"time"
)

type Registry struct {
	mu                   sync.RWMutex
	started              time.Time
	requests             map[string]int64
	latency              map[string][6]int64
	providerCalls        map[string]int64
	providerAvailability map[string]bool
}

func NewRegistry() *Registry {
	return &Registry{
		started:              time.Now().UTC(),
		requests:             map[string]int64{},
		latency:              map[string][6]int64{},
		providerCalls:        map[string]int64{},
		providerAvailability: map[string]bool{},
	}
}

func (r *Registry) RecordHTTPRequest(method, path string, status int, duration time.Duration) {
	if r == nil {
		return
	}
	route := normalizePath(path)
	requestKey := strings.Join([]string{method, route, fmt.Sprintf("%d", status)}, "\xff")
	latencyKey := strings.Join([]string{method, route}, "\xff")

	r.mu.Lock()
	defer r.mu.Unlock()
	r.requests[requestKey]++

	buckets := r.latency[latencyKey]
	seconds := duration.Seconds()
	for i, threshold := range []float64{0.05, 0.1, 0.25, 0.5, 1, 5} {
		if seconds <= threshold {
			buckets[i]++
		}
	}
	r.latency[latencyKey] = buckets
}

func (r *Registry) RecordProviderCall(providerType, providerName, status string) {
	if r == nil {
		return
	}
	key := strings.Join([]string{providerType, providerName, status}, "\xff")
	r.mu.Lock()
	defer r.mu.Unlock()
	r.providerCalls[key]++
}

func (r *Registry) SetProviderAvailability(providerType, providerName string, available bool) {
	if r == nil {
		return
	}
	key := strings.Join([]string{providerType, providerName}, "\xff")
	r.mu.Lock()
	defer r.mu.Unlock()
	r.providerAvailability[key] = available
}

func (r *Registry) Handler() http.HandlerFunc {
	return func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "text/plain; version=0.0.4")
		if r == nil {
			w.WriteHeader(http.StatusNotFound)
			return
		}
		_, _ = w.Write([]byte(r.Render()))
	}
}

func (r *Registry) Render() string {
	r.mu.RLock()
	defer r.mu.RUnlock()

	var out strings.Builder
	out.WriteString("# HELP banking_build_info Static application info.\n")
	out.WriteString("# TYPE banking_build_info gauge\n")
	out.WriteString("banking_build_info{service=\"banking-api\"} 1\n")
	out.WriteString("# HELP banking_uptime_seconds API process uptime in seconds.\n")
	out.WriteString("# TYPE banking_uptime_seconds gauge\n")
	out.WriteString(fmt.Sprintf("banking_uptime_seconds %.0f\n", time.Since(r.started).Seconds()))
	out.WriteString("# HELP banking_http_requests_total HTTP requests by method, route and status.\n")
	out.WriteString("# TYPE banking_http_requests_total counter\n")

	requestKeys := sortedKeys(r.requests)
	for _, key := range requestKeys {
		parts := strings.Split(key, "\xff")
		out.WriteString(fmt.Sprintf(
			"banking_http_requests_total{method=%q,route=%q,status=%q} %d\n",
			parts[0], parts[1], parts[2], r.requests[key],
		))
	}

	out.WriteString("# HELP banking_http_request_duration_seconds_bucket HTTP request duration buckets.\n")
	out.WriteString("# TYPE banking_http_request_duration_seconds_bucket histogram\n")
	latencyKeys := sortedKeys(r.latency)
	for _, key := range latencyKeys {
		parts := strings.Split(key, "\xff")
		thresholds := []string{"0.05", "0.1", "0.25", "0.5", "1", "5"}
		buckets := r.latency[key]
		for i, threshold := range thresholds {
			out.WriteString(fmt.Sprintf(
				"banking_http_request_duration_seconds_bucket{method=%q,route=%q,le=%q} %d\n",
				parts[0], parts[1], threshold, buckets[i],
			))
		}
	}
	out.WriteString("# HELP banking_provider_calls_total Provider calls by provider and status.\n")
	out.WriteString("# TYPE banking_provider_calls_total counter\n")
	providerCallKeys := sortedKeys(r.providerCalls)
	for _, key := range providerCallKeys {
		parts := strings.Split(key, "\xff")
		out.WriteString(fmt.Sprintf(
			"banking_provider_calls_total{provider_type=%q,provider=%q,status=%q} %d\n",
			parts[0], parts[1], parts[2], r.providerCalls[key],
		))
	}
	out.WriteString("# HELP banking_provider_availability Last known provider availability, 1 for available and 0 for unavailable.\n")
	out.WriteString("# TYPE banking_provider_availability gauge\n")
	providerAvailabilityKeys := sortedKeys(r.providerAvailability)
	for _, key := range providerAvailabilityKeys {
		parts := strings.Split(key, "\xff")
		value := 0
		if r.providerAvailability[key] {
			value = 1
		}
		out.WriteString(fmt.Sprintf(
			"banking_provider_availability{provider_type=%q,provider=%q} %d\n",
			parts[0], parts[1], value,
		))
	}
	return out.String()
}

func sortedKeys[V any](values map[string]V) []string {
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

func normalizePath(path string) string {
	parts := strings.Split(path, "/")
	for i, part := range parts {
		if looksLikeID(part) {
			parts[i] = "{id}"
		}
	}
	return strings.Join(parts, "/")
}

func looksLikeID(value string) bool {
	if len(value) == 36 && strings.Count(value, "-") == 4 {
		return true
	}
	if len(value) >= 24 {
		for _, char := range value {
			if (char < 'a' || char > 'f') && (char < 'A' || char > 'F') && (char < '0' || char > '9') && char != '-' {
				return false
			}
		}
		return true
	}
	return false
}
