// Package handler — k8s_resource_metrics.go: EPIC-4 Slice R5 (#1099)
// per-resource metrics endpoint.
//
// REST surface added by this slice:
//
//	GET /api/v1/sovereigns/{id}/k8s/metrics/{kind}/{ns}/{name}?window=1h
//
// Reads from `metrics.k8s.io/v1beta1` (kube-state-metrics + metrics-server)
// for the focused resource. Roll-ups for Deployment / StatefulSet sum
// metrics across owned Pods.
//
// The wire response bundles a "current" snapshot + a "series" sparkline
// frame so the UI can render a sparkline without a second round-trip.
// The series is populated from the in-process k8scache (PodMetrics is
// already a registered watch target — see kinds.go) so no additional
// scrape work happens here.
//
// Architecture rules:
//
//   - ADR-0001 §5: PodMetrics is watched via the same dynamicinformer
//     factory; the handler reads from the in-process Indexer (List()),
//     never directly from `metrics.k8s.io`.
//   - INVIOLABLE-PRINCIPLES.md #4 (never hardcode): the GVR comes from
//     the Registry; no hardcoded apiserver URLs.
package handler

import (
	"net/http"
	"strings"

	"github.com/go-chi/chi/v5"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// metricsResponse is the wire shape returned to the UI.
type metricsResponse struct {
	Kind      string                 `json:"kind"`
	Namespace string                 `json:"namespace,omitempty"`
	Name      string                 `json:"name"`
	// Current — point-in-time CPU / memory totals (across owned Pods
	// for workload kinds).
	Current map[string]any `json:"current"`
	// Series — sparkline samples. Single-frame today; future slices
	// can layer Prometheus-backed history when bp-prometheus ships.
	Series []map[string]any `json:"series"`
	// Source — "metrics.k8s.io" or "unavailable" when the API isn't
	// served on the target cluster (UI surfaces a friendly empty state).
	Source string `json:"source"`
}

// HandleK8sResourceMetrics — GET
//
//	/api/v1/sovereigns/{id}/k8s/metrics/{kind}/{ns}/{name}?window=1h
//
// Returns CPU / memory / disk for the focused resource. For
// Deployment / StatefulSet / DaemonSet the values are summed over the
// owned Pods (PodMetrics indexer + ownerReferences walk).
func (h *Handler) HandleK8sResourceMetrics(w http.ResponseWriter, r *http.Request) {
	if h.k8sCache == nil {
		http.Error(w, "k8scache disabled", http.StatusServiceUnavailable)
		return
	}
	clusterID := chi.URLParam(r, "id")
	kindName := chi.URLParam(r, "kind")
	ns := chi.URLParam(r, "ns")
	name := chi.URLParam(r, "name")
	if clusterID == "" || kindName == "" || name == "" {
		http.Error(w, "missing path parameters", http.StatusBadRequest)
		return
	}
	if ns == "_" {
		ns = ""
	}
	clusterID = h.resolveChrootClusterID(clusterID)
	if _, ok := h.k8sCache.Registry().Get(kindName); !ok {
		writeJSON(w, http.StatusNotFound, map[string]any{
			"error":          "unknown-kind",
			"kind":           kindName,
			"availableKinds": h.k8sCache.Registry().Names(),
		})
		return
	}

	resp := metricsResponse{
		Kind:      kindName,
		Namespace: ns,
		Name:      name,
		Source:    "metrics.k8s.io",
		Current:   map[string]any{},
		Series:    []map[string]any{},
	}

	// PodMetrics is registered as kind "podmetrics" (see kinds.go).
	pmItems, _, err := h.k8sCache.List(clusterID, "podmetrics", nil)
	if err != nil {
		// PodMetrics indexer absent → return an empty payload with
		// source=unavailable so the UI renders a friendly empty
		// state. NOT an error: clusters without metrics-server are a
		// real (if degraded) condition.
		resp.Source = "unavailable"
		writeJSON(w, http.StatusOK, resp)
		return
	}

	switch strings.ToLower(kindName) {
	case "pod":
		// Single Pod — find the matching PodMetrics by (ns,name).
		if pm := findPodMetrics(pmItems, ns, name); pm != nil {
			resp.Current = podMetricsTotals(pm)
			resp.Series = append(resp.Series, resp.Current)
		}
	case "deployment", "statefulset", "daemonset", "replicaset":
		// Workload — walk owned Pods via the indexer.
		ownedPods, _ := h.k8sCache.GetResourcesByOwner(clusterID, kindName, ns, name)
		// For Deployment we need a 2-hop walk (Deployment → ReplicaSet → Pod).
		if strings.EqualFold(kindName, "deployment") {
			rsList, _ := h.k8sCache.GetResourcesByOwner(clusterID, "deployment", ns, name)
			for _, rs := range rsList {
				pods, _ := h.k8sCache.GetResourcesByOwner(clusterID, "replicaset", ns, rs.GetName())
				ownedPods = append(ownedPods, pods...)
			}
		}
		totalCPU := int64(0)
		totalMem := int64(0)
		seenUIDs := map[string]bool{}
		for _, p := range ownedPods {
			uid := string(p.GetUID())
			if seenUIDs[uid] {
				continue
			}
			seenUIDs[uid] = true
			if p.GetKind() != "Pod" {
				continue
			}
			pm := findPodMetrics(pmItems, p.GetNamespace(), p.GetName())
			if pm == nil {
				continue
			}
			totals := podMetricsTotals(pm)
			if c, ok := totals["cpuMilli"].(int64); ok {
				totalCPU += c
			}
			if m, ok := totals["memBytes"].(int64); ok {
				totalMem += m
			}
		}
		resp.Current["cpuMilli"] = totalCPU
		resp.Current["memBytes"] = totalMem
		resp.Current["podCount"] = len(seenUIDs)
		resp.Series = append(resp.Series, resp.Current)
	case "node":
		// Node — would typically read from NodeMetrics; that GVR isn't
		// in the default registry. Surface as unavailable for now;
		// follow-up slice can register `nodemetrics` and reuse this
		// machinery.
		resp.Source = "unavailable"
	default:
		// Other kinds — no metric for now.
		resp.Source = "unavailable"
	}

	writeJSON(w, http.StatusOK, resp)
}

// findPodMetrics finds the PodMetrics matching (ns, name) in the
// indexer slice. PodMetrics objects mirror the Pod's metadata so a
// straightforward (namespace, name) match works.
func findPodMetrics(items []*unstructured.Unstructured, ns, name string) *unstructured.Unstructured {
	for _, it := range items {
		if it.GetName() == name && it.GetNamespace() == ns {
			return it
		}
	}
	return nil
}

// podMetricsTotals sums per-container CPU + memory from a PodMetrics
// object. Returns canonical keys cpuMilli + memBytes (int64).
//
// PodMetrics shape (metrics.k8s.io/v1beta1):
//
//	{
//	  containers: [
//	    { name: "...", usage: { cpu: "12m", memory: "256Mi" } },
//	  ],
//	  timestamp: "...",
//	  window: "30s",
//	}
func podMetricsTotals(pm *unstructured.Unstructured) map[string]any {
	out := map[string]any{
		"cpuMilli": int64(0),
		"memBytes": int64(0),
	}
	if pm == nil {
		return out
	}
	containers, ok, _ := unstructured.NestedSlice(pm.Object, "containers")
	if !ok {
		return out
	}
	totalCPU := int64(0)
	totalMem := int64(0)
	for _, c := range containers {
		cm, ok := c.(map[string]any)
		if !ok {
			continue
		}
		usage, ok := cm["usage"].(map[string]any)
		if !ok {
			continue
		}
		if cpu, ok := usage["cpu"].(string); ok {
			totalCPU += parseCPUMilli(cpu)
		}
		if mem, ok := usage["memory"].(string); ok {
			totalMem += parseMemBytes(mem)
		}
	}
	out["cpuMilli"] = totalCPU
	out["memBytes"] = totalMem
	return out
}

// parseCPUMilli parses a Kubernetes CPU quantity into milliCPU.
// Supports the common shapes ("12m", "0.5", "2", "100n").
func parseCPUMilli(s string) int64 {
	s = strings.TrimSpace(s)
	if s == "" {
		return 0
	}
	if strings.HasSuffix(s, "n") {
		// nanoCPU → milliCPU = / 1_000_000
		v := parseInt64(strings.TrimSuffix(s, "n"))
		return v / 1_000_000
	}
	if strings.HasSuffix(s, "u") {
		v := parseInt64(strings.TrimSuffix(s, "u"))
		return v / 1_000
	}
	if strings.HasSuffix(s, "m") {
		return parseInt64(strings.TrimSuffix(s, "m"))
	}
	// Whole or fractional cores → multiply by 1000.
	if v, err := parseFloat64(s); err == nil {
		return int64(v * 1000)
	}
	return 0
}

// parseMemBytes parses a Kubernetes memory quantity into bytes.
// Supports binary (Ki, Mi, Gi, Ti) and decimal (k, M, G, T) suffixes.
func parseMemBytes(s string) int64 {
	s = strings.TrimSpace(s)
	if s == "" {
		return 0
	}
	mult := int64(1)
	for suffix, m := range map[string]int64{
		"Ki": 1 << 10, "Mi": 1 << 20, "Gi": 1 << 30, "Ti": 1 << 40,
		"k": 1000, "M": 1_000_000, "G": 1_000_000_000, "T": 1_000_000_000_000,
	} {
		if strings.HasSuffix(s, suffix) {
			s = strings.TrimSuffix(s, suffix)
			mult = m
			break
		}
	}
	v := parseInt64(s)
	return v * mult
}

func parseInt64(s string) int64 {
	v := int64(0)
	for _, ch := range s {
		if ch < '0' || ch > '9' {
			return v
		}
		v = v*10 + int64(ch-'0')
	}
	return v
}

func parseFloat64(s string) (float64, error) {
	// Tiny float parser sufficient for K8s quantities like "0.5" / "2".
	parts := strings.SplitN(s, ".", 2)
	intPart := float64(parseInt64(parts[0]))
	if len(parts) == 1 {
		return intPart, nil
	}
	fracStr := parts[1]
	frac := float64(parseInt64(fracStr))
	denom := float64(1)
	for range fracStr {
		denom *= 10
	}
	return intPart + frac/denom, nil
}
