package httpapi

import (
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"time"

	"commilitia.net/cdrop/internal/db"
	"commilitia.net/cdrop/internal/hub"
	"commilitia.net/cdrop/internal/jwtauth"
)

const (
	keepaliveInterval = 15 * time.Second
	probeFrame        = ": hi\n\n"
)

// handleEvents serves the long-lived SSE channel mandated by brief §5.
// Sets X-Accel-Buffering / Cache-Control / flush to defeat reverse-proxy buffering.
// Sends a probe frame immediately so the client can detect first-byte latency.
func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) {
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "streaming unsupported", http.StatusInternalServerError)
		return
	}

	claims, ok := jwtauth.ClaimsFromContext(r.Context())
	if !ok {
		http.Error(w, "missing claims", http.StatusUnauthorized)
		return
	}
	deviceName, _ := jwtauth.DeviceNameFromContext(r.Context())
	if deviceName == "" {
		http.Error(w, "missing device", http.StatusBadRequest)
		return
	}

	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("X-Accel-Buffering", "no")
	w.Header().Set("Connection", "keep-alive")
	w.WriteHeader(http.StatusOK)

	if _, err := io.WriteString(w, probeFrame); err != nil {
		return
	}
	flusher.Flush()

	client := s.hub.Connect(r.Context(), claims.UserID, deviceName)
	defer s.hub.Disconnect(client)

	slog.Debug("sse connected", "user", claims.UserID, "device", deviceName)

	ping := time.NewTicker(keepaliveInterval)
	defer ping.Stop()

	for {
		select {
		case ev, ok := <-client.Events():
			if !ok {
				return
			}
			if err := writeSSEEvent(w, ev); err != nil {
				return
			}
			flusher.Flush()
		case <-ping.C:
			if _, err := io.WriteString(w, "event: ping\ndata: {}\n\n"); err != nil {
				return
			}
			flusher.Flush()
			// Refresh last_seen so a connection-only client (no other API calls)
			// keeps its device row alive past TTL — matches the sliding
			// refresh_token semantics requested for device persistence.
			_ = s.queries.UpsertDevice(r.Context(), db.UpsertDeviceParams{
				UserID:   claims.UserID,
				Name:     deviceName,
				Type:     jwtauth.DeviceTypeFromContext(r.Context()),
				LastSeen: time.Now().Unix(),
			})
		case <-r.Context().Done():
			return
		}
	}
}

func writeSSEEvent(w io.Writer, ev hub.Event) error {
	payload, err := json.Marshal(ev.Data)
	if err != nil {
		return fmt.Errorf("marshal sse data: %w", err)
	}
	// SSE frame: "event: <type>\ndata: <json>\n\n".
	// JSON is single-line via json.Marshal so no `data:` continuation needed.
	_, err = fmt.Fprintf(w, "event: %s\ndata: %s\n\n", ev.Type, payload)
	return err
}
