package httpapi

import (
	"encoding/json"
	"log/slog"
	"net/http"

	"github.com/go-chi/chi/v5"

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

type deviceItem struct {
	DeviceID string `json:"device_id"`
	Name     string `json:"name"`
	Type     string `json:"type"`
	Tier     string `json:"tier"`
	Online   bool   `json:"online"`
	LastSeen int64  `json:"last_seen"`
}

// handleDevices returns the calling user's known devices with live online flags.
// Online = currently connected to /api/events; LastSeen = epoch from devices.last_seen.
func (s *Server) handleDevices(w http.ResponseWriter, r *http.Request) {
	claims, _ := jwtauth.ClaimsFromContext(r.Context())

	devs, err := s.queries.ListDevicesByUser(r.Context(), claims.UserID)
	if err != nil {
		slog.Error("list devices failed", "err", err, "user", claims.UserID)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
		return
	}

	out := make([]deviceItem, 0, len(devs))
	for _, d := range devs {
		out = append(out, deviceItem{
			DeviceID: d.DeviceID,
			Name:     d.Name,
			Type:     d.Type,
			Tier:     d.Tier,
			Online:   s.hub.Online(claims.UserID, d.DeviceID),
			LastSeen: d.LastSeen,
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{"devices": out})
}

// handleDeleteDevice removes a device: it revokes the device's broker session (so it
// can no longer refresh and dies within its short access TTL), then drops the local
// row, kicks its live SSE, and re-broadcasts presence. Identified by the stable
// device_id; the caller must own it. A full session is required (route-gated) — the
// old step-up requirement is gone, since a full-tier session is itself trusted.
func (s *Server) handleDeleteDevice(w http.ResponseWriter, r *http.Request) {
	claims, _ := jwtauth.ClaimsFromContext(r.Context())
	deviceID := chi.URLParam(r, "device_id")
	if deviceID == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing device id"})
		return
	}
	status, ok := s.revokeDevice(r, claims.UserID, deviceID)
	if !ok {
		writeJSON(w, status, map[string]string{"error": revokeErrorMsg(status)})
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

type renameDeviceReq struct {
	Name string `json:"name"`
}

// handleRenameDevice changes a device's human-readable name. The device's identity is
// device_id, so a rename never changes which session / row it is — fixing the old
// model where the name was the key and renaming meant losing the device. Full session.
func (s *Server) handleRenameDevice(w http.ResponseWriter, r *http.Request) {
	claims, _ := jwtauth.ClaimsFromContext(r.Context())
	deviceID := chi.URLParam(r, "device_id")
	if deviceID == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing device id"})
		return
	}
	var body renameDeviceReq
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&body); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
		return
	}
	name := jwtauth.SanitizeDeviceName(body.Name)
	if name == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "empty device name"})
		return
	}

	n, err := s.queries.RenameDevice(r.Context(), db.RenameDeviceParams{
		Name:     name,
		DeviceID: deviceID,
		UserID:   claims.UserID,
	})
	if err != nil {
		slog.Error("rename device failed", "err", err, "user", claims.UserID, "device", deviceID)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db"})
		return
	}
	if n == 0 {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "device not found"})
		return
	}
	// Decouple: the broker session (token) is untouched — only the device's display name
	// changes. Update the live hub entry's name (kept keyed by the stable device_id) and
	// re-broadcast presence so every peer sees the new name at once, with no re-login, no
	// session re-mint, and no duplicate device row.
	s.hub.Rename(claims.UserID, deviceID, name)
	s.hub.PublishPresence(r.Context(), claims.UserID)
	w.WriteHeader(http.StatusNoContent)
}
