// Package health implements the /healthz (liveness) and /readyz (readiness)
// endpoints for push-gateway.
//
// /healthz is a trivial liveness probe: it returns 200 + {status:ok} as long
// as the process is running and the Gin router is serving.
//
// /readyz reports readiness based on downstream dependency health. Per
// ARB-015 §17.4 / ISSUE-058 the readiness probe uses SOFT FAILURE semantics:
// when Redis or Kafka is unavailable the endpoint still returns HTTP 200 but
// carries `degraded: true` plus a `dependencies` block describing which
// component failed. This prevents Kubernetes from evicting the pod when a
// transient dependency blip occurs, at the cost of accepting some degraded
// behavior (no cross-instance fanout, no Kafka consumption) during the blip.
// Only the Hub being in a shutting-down state returns a non-200 (503).
//
// The hard-failure case is limited to:
//   - Hub.closing == true (process is shutting down) -> 503
//   - Internal misconfiguration (both Redis and Kafka missing in non-DevMode)
//     -> still 200 + degraded, since the pod can still serve local WebSocket
//     traffic.
package health

import (
	"context"
	"net/http"
	"time"

	"github.com/edu-cloud/push-gateway/internal/hub"
	"github.com/edu-cloud/push-gateway/internal/kafkaconsumer"
	"github.com/edu-cloud/push-gateway/internal/redisclient"
	"github.com/gin-gonic/gin"
)

// probeTimeout bounds each dependency check so a hung dependency cannot stall
// the readiness probe. Kubernetes typically expects /readyz to respond within
// 5s; we use 1s per probe to leave headroom.
const probeTimeout = 1 * time.Second

// dependencyStatus is the per-component health snapshot included in the
// /readyz response body.
type dependencyStatus struct {
	Ok      bool   `json:"ok"`
	Error   string `json:"error,omitempty"`
	Latency int64  `json:"latency_ms,omitempty"`
}

// readyzResponse is the ActionState-shaped envelope returned by /readyz.
// `degraded` is true when at least one non-critical dependency is unhealthy.
type readyzResponse struct {
	Status       string                      `json:"status"`
	Service      string                      `json:"service"`
	InstanceID   string                      `json:"instance_id"`
	Degraded     bool                        `json:"degraded"`
	Connections  int                         `json:"connections"`
	Users        int                         `json:"users"`
	Dependencies map[string]*dependencyStatus `json:"dependencies"`
}

// Healthz returns a trivial liveness handler: 200 + {status:ok, service}.
// Liveness never depends on downstream components — if the process can serve
// the request, it is alive.
func Healthz(service string) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"status":  "ok",
			"service": service,
		})
	}
}

// Readyzer builds the /readyz handler. It probes Redis (PING) and Kafka
// (reader lag / connectivity) and reports degraded state per ARB-015 §17.4.
//
// The Hub is used to report local connection counts and to detect the
// shutting-down state (which triggers a hard 503). The redisClient may be nil
// in DevMode; the kafkaConsumer may be nil when KAFKA_BROKERS is unset. Both
// nil cases are reported as degraded rather than failing the probe.
type Readyzer struct {
	hub      *hub.Hub
	redis    *redisclient.Client
	kafka    *kafkaconsumer.Consumer
	instance string
	service  string
}

// NewReadyzer constructs a Readyzer. redis and kafka may be nil; the resulting
// probe will mark the missing dependency as degraded.
func NewReadyzer(h *hub.Hub, r *redisclient.Client, k *kafkaconsumer.Consumer, service, instanceID string) *Readyzer {
	return &Readyzer{
		hub:      h,
		redis:    r,
		kafka:    k,
		instance: instanceID,
		service:  service,
	}
}

// Handler is the gin.HandlerFunc for GET /readyz.
func (rz *Readyzer) Handler(c *gin.Context) {
	// Hard failure: Hub is shutting down — reject new traffic.
	// (Hub.CloseAll sets closing=true; we treat this as 503 so the load
	// balancer stops sending WebSocket upgrades during drain.)
	if rz.hub.IsClosing() {
		c.JSON(http.StatusServiceUnavailable, readyzResponse{
			Status:      "shutting_down",
			Service:     rz.service,
			InstanceID:  rz.instance,
			Degraded:    true,
			Connections: rz.hub.ActiveConnections(),
			Users:       rz.hub.UserCount(),
			Dependencies: map[string]*dependencyStatus{
				"hub": {Ok: false, Error: "hub is closing"},
			},
		})
		return
	}

	deps := make(map[string]*dependencyStatus, 2)
	degraded := false

	// Redis probe (soft failure).
	if rz.redis == nil {
		deps["redis"] = &dependencyStatus{Ok: false, Error: "not configured"}
		degraded = true
	} else {
		deps["redis"] = probeRedis(rz.redis)
		if !deps["redis"].Ok {
			degraded = true
		}
	}

	// Kafka probe (soft failure).
	if rz.kafka == nil {
		deps["kafka"] = &dependencyStatus{Ok: false, Error: "not configured"}
		degraded = true
	} else {
		deps["kafka"] = probeKafka(rz.kafka)
		if !deps["kafka"].Ok {
			degraded = true
		}
	}

	// Always 200 (unless shutting down) per ISSUE-058/006 soft-failure rule.
	c.JSON(http.StatusOK, readyzResponse{
		Status:       statusText(degraded),
		Service:      rz.service,
		InstanceID:   rz.instance,
		Degraded:     degraded,
		Connections:  rz.hub.ActiveConnections(),
		Users:        rz.hub.UserCount(),
		Dependencies: deps,
	})
}

// statusText returns "ok" when healthy, "degraded" when at least one
// dependency is unhealthy.
func statusText(degraded bool) string {
	if degraded {
		return "degraded"
	}
	return "ok"
}

// probeRedis issues a PING with a short timeout and reports latency.
func probeRedis(r *redisclient.Client) *dependencyStatus {
	ctx, cancel := context.WithTimeout(context.Background(), probeTimeout)
	defer cancel()
	start := time.Now()
	if err := r.Ping(ctx); err != nil {
		return &dependencyStatus{Ok: false, Error: err.Error()}
	}
	return &dependencyStatus{Ok: true, Latency: time.Since(start).Milliseconds()}
}

// probeKafka checks that the consumer reader is still reachable. We use a
// lightweight Lag() call (segmentio/kafka-go Client API); on failure the
// consumer is marked degraded. Note: a degraded Kafka does NOT block WebSocket
// traffic — it only pauses notification consumption until recovery.
func probeKafka(k *kafkaconsumer.Consumer) *dependencyStatus {
	ctx, cancel := context.WithTimeout(context.Background(), probeTimeout)
	defer cancel()
	if err := k.HealthCheck(ctx); err != nil {
		return &dependencyStatus{Ok: false, Error: err.Error()}
	}
	return &dependencyStatus{Ok: true}
}
