package web

import (
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"strconv"
	"strings"

	"pete/internal/storage"
)

// maxStateBodyBytes caps read/bookmark request bodies. These carry a single id
// and a boolean, so this is generous headroom.
const maxStateBodyBytes = 1024

// maxStateIDs bounds how many ids /api/state will look up in one call. A page
// renders a few dozen cards; this is well clear of that.
const maxStateIDs = 500

// requireUser resolves the signed-in user or writes the appropriate error and
// returns nil. It mirrors handlePrefs: 404 when auth is disabled entirely, 401
// for anonymous callers (the client's cue to stay on localStorage).
func (s *Server) requireUser(w http.ResponseWriter, r *http.Request) *SessionUser {
	if s.auth == nil {
		http.Error(w, "auth disabled", http.StatusNotFound)
		return nil
	}
	u := s.auth.userFromRequest(r)
	if u == nil {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		http.Error(w, `{"error":"not signed in"}`, http.StatusUnauthorized)
		return nil
	}
	return u
}

// decodeStateBody reads a small JSON body into dst, writing a 400 on failure.
func decodeStateBody(w http.ResponseWriter, r *http.Request, dst any) bool {
	return decodeStateBodyN(w, r, dst, maxStateBodyBytes)
}

// decodeStateBodyN is decodeStateBody with an explicit byte cap, for endpoints
// (like push subscribe) whose payloads run larger than a single id + flag.
func decodeStateBodyN(w http.ResponseWriter, r *http.Request, dst any, limit int64) bool {
	body, err := io.ReadAll(io.LimitReader(r.Body, limit+1))
	if err != nil || int64(len(body)) > limit {
		http.Error(w, `{"error":"bad request"}`, http.StatusBadRequest)
		return false
	}
	if err := json.Unmarshal(body, dst); err != nil {
		http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest)
		return false
	}
	return true
}

// handleRead records or clears a story's read flag for the signed-in user.
func (s *Server) handleRead(w http.ResponseWriter, r *http.Request) {
	u := s.requireUser(w, r)
	if u == nil {
		return
	}
	var req struct {
		ID   int64 `json:"id"`
		Read bool  `json:"read"`
	}
	if !decodeStateBody(w, r, &req) {
		return
	}
	if req.ID <= 0 {
		http.Error(w, `{"error":"bad id"}`, http.StatusBadRequest)
		return
	}
	if err := storage.SetRead(u.Sub, req.ID, req.Read); err != nil {
		slog.Error("state: set read failed", "sub", u.Sub, "id", req.ID, "err", err)
		http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// handleBookmark adds or removes a bookmark for the signed-in user.
func (s *Server) handleBookmark(w http.ResponseWriter, r *http.Request) {
	u := s.requireUser(w, r)
	if u == nil {
		return
	}
	var req struct {
		ID int64 `json:"id"`
		On bool  `json:"on"`
	}
	if !decodeStateBody(w, r, &req) {
		return
	}
	if req.ID <= 0 {
		http.Error(w, `{"error":"bad id"}`, http.StatusBadRequest)
		return
	}
	if err := storage.SetBookmark(u.Sub, req.ID, req.On); err != nil {
		slog.Error("state: set bookmark failed", "sub", u.Sub, "id", req.ID, "err", err)
		http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// handleState returns which of the given story ids are read and bookmarked, so
// the client can paint a freshly rendered page in one round-trip.
func (s *Server) handleState(w http.ResponseWriter, r *http.Request) {
	u := s.requireUser(w, r)
	if u == nil {
		return
	}
	ids := parseIDList(r.URL.Query().Get("ids"))
	read, bookmarked, err := storage.UserStoryState(u.Sub, ids)
	if err != nil {
		slog.Error("state: lookup failed", "sub", u.Sub, "err", err)
		http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	_ = json.NewEncoder(w).Encode(map[string]any{
		"read":       keysOf(read),
		"bookmarked": keysOf(bookmarked),
	})
}

// parseIDList turns "1,2,3" into a bounded, deduplicated slice of positive ids.
func parseIDList(raw string) []int64 {
	if raw == "" {
		return nil
	}
	seen := make(map[int64]bool)
	out := make([]int64, 0)
	for _, part := range strings.Split(raw, ",") {
		id, err := strconv.ParseInt(strings.TrimSpace(part), 10, 64)
		if err != nil || id <= 0 || seen[id] {
			continue
		}
		seen[id] = true
		out = append(out, id)
		if len(out) >= maxStateIDs {
			break
		}
	}
	return out
}

// keysOf returns the keys of a set as a slice (order unspecified).
func keysOf(m map[int64]bool) []int64 {
	out := make([]int64, 0, len(m))
	for k := range m {
		out = append(out, k)
	}
	return out
}
