package httpapi

import (
	"encoding/json"
	"net/http"
	"time"

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

// maxPushSubscribeBytes caps the subscribe/unsubscribe body. A PushSubscription
// JSON is well under 1 KB (endpoint URL + two short keys); 8 KB is generous
// headroom while still bounding per-request memory.
const maxPushSubscribeBytes = 8 * 1024

type vapidKeyResp struct {
	Enabled   bool   `json:"enabled"`
	PublicKey string `json:"public_key"`
}

// handlePushVAPIDKey hands the browser the VAPID public key it must pass as
// applicationServerKey when calling pushManager.subscribe. The public key is
// not a secret; enabled=false tells the client to hide the push UI entirely.
func (s *Server) handlePushVAPIDKey(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, vapidKeyResp{
		Enabled:   s.push.Enabled(),
		PublicKey: s.push.PublicKey(),
	})
}

// pushSubscribeReq mirrors PushSubscription.toJSON() plus the device's current
// UI locale, so the server can render notification text the device can read.
type pushSubscribeReq struct {
	Endpoint string `json:"endpoint"`
	Keys     struct {
		P256dh string `json:"p256dh"`
		Auth   string `json:"auth"`
	} `json:"keys"`
	Locale string `json:"locale"`
}

// knownLocales gates the locale we persist so a bogus value can't poison the
// stored row; unknown / empty falls back to the server default.
var knownLocales = map[string]bool{"zh-CN": true, "zh-TW": true, "en-US": true}

// handlePushSubscribe stores (or refreshes) this browser's Web Push endpoint for
// the calling device. Keyed by device_name so a notify-worthy event addressed to
// a specific offline device pushes only to that device. Upsert by endpoint hash
// makes a repeat subscribe idempotent.
func (s *Server) handlePushSubscribe(w http.ResponseWriter, r *http.Request) {
	if !s.push.Enabled() {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "push disabled"})
		return
	}
	claims, _ := jwtauth.ClaimsFromContext(r.Context())
	device, _ := jwtauth.DeviceNameFromContext(r.Context())
	if device == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing device"})
		return
	}

	r.Body = http.MaxBytesReader(w, r.Body, maxPushSubscribeBytes)
	var req pushSubscribeReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	if req.Endpoint == "" || req.Keys.P256dh == "" || req.Keys.Auth == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "incomplete subscription"})
		return
	}

	locale := req.Locale
	if !knownLocales[locale] {
		locale = "zh-CN"
	}

	now := time.Now().Unix()
	if err := s.queries.UpsertPushSubscription(r.Context(), db.UpsertPushSubscriptionParams{
		ID:         push.SubscriptionID(req.Endpoint),
		UserID:     claims.UserID,
		DeviceName: device,
		Endpoint:   req.Endpoint,
		P256dh:     req.Keys.P256dh,
		Auth:       req.Keys.Auth,
		UserAgent:  truncate(r.UserAgent(), 256),
		Locale:     locale,
		CreatedAt:  now,
		LastUsedAt: now,
	}); err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "store failed"})
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

type pushUnsubscribeReq struct {
	Endpoint string `json:"endpoint"`
}

// handlePushUnsubscribe drops a subscription the browser has revoked. Deleting by
// endpoint hash needs no auth coupling beyond the session — the worst a wrong
// endpoint does is delete a row the caller doesn't own, and endpoints are
// unguessable capabilities, so this is not a meaningful enumeration vector.
func (s *Server) handlePushUnsubscribe(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxPushSubscribeBytes)
	var req pushUnsubscribeReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Endpoint == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing endpoint"})
		return
	}
	if err := s.queries.DeletePushSubscription(r.Context(), push.SubscriptionID(req.Endpoint)); err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "delete failed"})
		return
	}
	w.WriteHeader(http.StatusNoContent)
}
