package httpapi

import (
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
)

type routeMetricSnapshot struct {
	Method       string  `json:"method"`
	Path         string  `json:"path"`
	Status       int     `json:"status"`
	Count        uint64  `json:"count"`
	AvgLatencyMs float64 `json:"avgLatencyMs"`
	MaxLatencyMs float64 `json:"maxLatencyMs"`
	LastSeenAt   string  `json:"lastSeenAt"`
}

type metricsSnapshot struct {
	GeneratedAt       string                `json:"generatedAt"`
	UptimeSeconds     int64                 `json:"uptimeSeconds"`
	RequestsTotal     uint64                `json:"requestsTotal"`
	StatusClassTotals map[string]uint64     `json:"statusClassTotals"`
	Routes            []routeMetricSnapshot `json:"routes"`
}

type routeMetricBucket struct {
	Method            string
	Path              string
	Status            int
	Count             uint64
	TotalLatencyNanos float64
	MaxLatencyNanos   float64
	LastSeenAt        time.Time
}

type requestMetrics struct {
	startedAt      time.Time
	requestsTotal  uint64
	status2xxTotal uint64
	status3xxTotal uint64
	status4xxTotal uint64
	status5xxTotal uint64
	statusOther    uint64

	mu      sync.RWMutex
	buckets map[string]*routeMetricBucket
}

func newRequestMetrics() *requestMetrics {
	return &requestMetrics{
		startedAt: time.Now().UTC(),
		buckets:   make(map[string]*routeMetricBucket),
	}
}

func (m *requestMetrics) observe(method, path string, status int, latency time.Duration) {
	if m == nil {
		return
	}
	if path == "" {
		path = "<unmatched>"
	}

	now := time.Now().UTC()
	latencyNanos := float64(latency.Nanoseconds())
	key := method + " " + path + " " + itoa(status)

	m.mu.Lock()
	defer m.mu.Unlock()

	m.requestsTotal++
	switch {
	case status >= 200 && status < 300:
		m.status2xxTotal++
	case status >= 300 && status < 400:
		m.status3xxTotal++
	case status >= 400 && status < 500:
		m.status4xxTotal++
	case status >= 500 && status < 600:
		m.status5xxTotal++
	default:
		m.statusOther++
	}

	bucket, exists := m.buckets[key]
	if !exists {
		bucket = &routeMetricBucket{
			Method:            method,
			Path:              path,
			Status:            status,
			Count:             1,
			TotalLatencyNanos: latencyNanos,
			MaxLatencyNanos:   latencyNanos,
			LastSeenAt:        now,
		}
		m.buckets[key] = bucket
		return
	}

	bucket.Count++
	bucket.TotalLatencyNanos += latencyNanos
	if latencyNanos > bucket.MaxLatencyNanos {
		bucket.MaxLatencyNanos = latencyNanos
	}
	bucket.LastSeenAt = now
}

func (m *requestMetrics) snapshot() metricsSnapshot {
	if m == nil {
		return metricsSnapshot{
			GeneratedAt:       time.Now().UTC().Format(time.RFC3339Nano),
			StatusClassTotals: map[string]uint64{},
			Routes:            []routeMetricSnapshot{},
		}
	}

	m.mu.RLock()
	defer m.mu.RUnlock()

	routes := make([]routeMetricSnapshot, 0, len(m.buckets))
	for _, bucket := range m.buckets {
		avgMs := 0.0
		if bucket.Count > 0 {
			avgMs = (bucket.TotalLatencyNanos / float64(bucket.Count)) / float64(time.Millisecond)
		}
		routes = append(routes, routeMetricSnapshot{
			Method:       bucket.Method,
			Path:         bucket.Path,
			Status:       bucket.Status,
			Count:        bucket.Count,
			AvgLatencyMs: avgMs,
			MaxLatencyMs: bucket.MaxLatencyNanos / float64(time.Millisecond),
			LastSeenAt:   bucket.LastSeenAt.Format(time.RFC3339Nano),
		})
	}

	sort.Slice(routes, func(i, j int) bool {
		if routes[i].Method != routes[j].Method {
			return routes[i].Method < routes[j].Method
		}
		if routes[i].Path != routes[j].Path {
			return routes[i].Path < routes[j].Path
		}
		return routes[i].Status < routes[j].Status
	})

	return metricsSnapshot{
		GeneratedAt:   time.Now().UTC().Format(time.RFC3339Nano),
		UptimeSeconds: int64(time.Since(m.startedAt).Seconds()),
		RequestsTotal: m.requestsTotal,
		StatusClassTotals: map[string]uint64{
			"2xx":   m.status2xxTotal,
			"3xx":   m.status3xxTotal,
			"4xx":   m.status4xxTotal,
			"5xx":   m.status5xxTotal,
			"other": m.statusOther,
		},
		Routes: routes,
	}
}

func requestMetricsMiddleware(metrics *requestMetrics) gin.HandlerFunc {
	return func(c *gin.Context) {
		startedAt := time.Now()
		c.Next()

		path := c.FullPath()
		if path == "" {
			path = c.Request.URL.Path
		}
		if path == "/v1/metrics" || path == "/v1/metrics/prometheus" {
			return
		}
		metrics.observe(c.Request.Method, path, c.Writer.Status(), time.Since(startedAt))
	}
}

func (m *requestMetrics) snapshotPrometheus() string {
	snapshot := m.snapshot()
	var builder strings.Builder

	builder.WriteString("# HELP productier_http_uptime_seconds Process uptime in seconds.\n")
	builder.WriteString("# TYPE productier_http_uptime_seconds gauge\n")
	builder.WriteString("productier_http_uptime_seconds ")
	builder.WriteString(strconv.FormatInt(snapshot.UptimeSeconds, 10))
	builder.WriteByte('\n')

	builder.WriteString("# HELP productier_http_requests_total Total HTTP requests by status class.\n")
	builder.WriteString("# TYPE productier_http_requests_total counter\n")
	statusClasses := []string{"2xx", "3xx", "4xx", "5xx", "other"}
	for _, statusClass := range statusClasses {
		builder.WriteString(`productier_http_requests_total{status_class="`)
		builder.WriteString(escapePrometheusLabelValue(statusClass))
		builder.WriteString(`"} `)
		builder.WriteString(strconv.FormatUint(snapshot.StatusClassTotals[statusClass], 10))
		builder.WriteByte('\n')
	}

	builder.WriteString("# HELP productier_http_requests_route_total Total HTTP requests by route and status code.\n")
	builder.WriteString("# TYPE productier_http_requests_route_total counter\n")
	builder.WriteString("# HELP productier_http_request_latency_avg_ms Average request latency in milliseconds by route and status code.\n")
	builder.WriteString("# TYPE productier_http_request_latency_avg_ms gauge\n")
	builder.WriteString("# HELP productier_http_request_latency_max_ms Max request latency in milliseconds by route and status code.\n")
	builder.WriteString("# TYPE productier_http_request_latency_max_ms gauge\n")
	for _, route := range snapshot.Routes {
		labels := `method="` + escapePrometheusLabelValue(route.Method) +
			`",path="` + escapePrometheusLabelValue(route.Path) +
			`",status="` + strconv.Itoa(route.Status) + `"`
		builder.WriteString("productier_http_requests_route_total{")
		builder.WriteString(labels)
		builder.WriteString("} ")
		builder.WriteString(strconv.FormatUint(route.Count, 10))
		builder.WriteByte('\n')

		builder.WriteString("productier_http_request_latency_avg_ms{")
		builder.WriteString(labels)
		builder.WriteString("} ")
		builder.WriteString(strconv.FormatFloat(route.AvgLatencyMs, 'f', 3, 64))
		builder.WriteByte('\n')

		builder.WriteString("productier_http_request_latency_max_ms{")
		builder.WriteString(labels)
		builder.WriteString("} ")
		builder.WriteString(strconv.FormatFloat(route.MaxLatencyMs, 'f', 3, 64))
		builder.WriteByte('\n')
	}

	return builder.String()
}

func escapePrometheusLabelValue(value string) string {
	escaped := strings.ReplaceAll(value, `\`, `\\`)
	escaped = strings.ReplaceAll(escaped, "\n", `\n`)
	escaped = strings.ReplaceAll(escaped, `"`, `\"`)
	return escaped
}

func itoa(value int) string {
	if value == 0 {
		return "0"
	}
	isNegative := value < 0
	if isNegative {
		value = -value
	}
	var digits [20]byte
	index := len(digits)
	for value > 0 {
		index--
		digits[index] = byte('0' + (value % 10))
		value /= 10
	}
	if isNegative {
		index--
		digits[index] = '-'
	}
	return string(digits[index:])
}
