package api

import (
	"errors"
	"math/rand"
	"net/http"
	"time"

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

	"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
	"git.fabledsword.com/bvandeusen/minstrel/internal/playlists"
)

// Generic registry-driven system-playlist endpoints (#411 R2),
// replacing the former per-kind handlers. {kind} is the raw
// system_variant ('for_you' | 'discover' | future singleton kinds);
// only singleton kinds (playlists.RefreshableSystemKind) are
// addressable here — songs_like_artist is multi-per-user and played
// by playlist id, so it 404s.
//
// GET /api/playlists/system/{kind}/shuffle — the caller's playlist
// for that kind with tracks in rotation-aware order (#415): unheard
// this rotation first (shuffled), then heard; resets when exhausted.
// Same JSON shape as GET /api/playlists/{id} so clients reuse track
// parsing. Intentionally uncached — it varies per play, unlike the
// cached detail GET.
//
// POST /api/playlists/system/{kind}/refresh — rebuilds the caller's
// system playlists and returns that kind's fresh row.

func systemKindFromURL(w http.ResponseWriter, r *http.Request) (string, bool) {
	kind := chi.URLParam(r, "kind")
	if !playlists.RefreshableSystemKind(kind) {
		writeErr(w, &apierror.Error{Status: 404, Code: "not_found", Message: "unknown system playlist"})
		return "", false
	}
	return kind, true
}

func (h *handlers) handleSystemPlaylistShuffle(w http.ResponseWriter, r *http.Request) {
	kind, ok := systemKindFromURL(w, r)
	if !ok {
		return
	}
	h.serveSystemPlaylistShuffle(w, r, kind)
}

// systemRefreshResp unifies the prior for-you/discover shapes
// (for-you carried track_ids, discover didn't); clients ignore
// extra fields. playlist_id null + empty when the build produced no
// row for that kind (empty library).
type systemRefreshResp struct {
	PlaylistID *string  `json:"playlist_id"`
	TrackCount int32    `json:"track_count"`
	TrackIDs   []string `json:"track_ids"`
}

func (h *handlers) handleSystemPlaylistRefresh(w http.ResponseWriter, r *http.Request) {
	user, ok := requireUser(w, r)
	if !ok {
		return
	}
	kind, ok := systemKindFromURL(w, r)
	if !ok {
		return
	}
	if err := playlists.BuildSystemPlaylists(r.Context(), h.pool, h.logger, user.ID, time.Now(), h.dataDir); err != nil {
		h.logger.Error("system refresh: build failed",
			"kind", kind, "user_id", uuidToString(user.ID), "err", err)
		writeErr(w, apierror.InternalMsg("build failed", err))
		return
	}
	// #968: announce the rebuild so the user's other clients refresh.
	h.publishSystemRebuilt(user.ID)
	q := dbq.New(h.pool)
	v := kind
	pl, err := q.GetSystemPlaylistByVariantForUser(r.Context(),
		dbq.GetSystemPlaylistByVariantForUserParams{UserID: user.ID, SystemVariant: &v})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			writeJSON(w, http.StatusOK, systemRefreshResp{TrackIDs: []string{}})
			return
		}
		h.logger.Error("system refresh: lookup failed",
			"kind", kind, "user_id", uuidToString(user.ID), "err", err)
		writeErr(w, apierror.InternalMsg("lookup failed", err))
		return
	}
	rows, err := q.ListPlaylistTracks(r.Context(), pl.ID)
	if err != nil {
		h.logger.Error("system refresh: list tracks failed",
			"kind", kind, "playlist_id", uuidToString(pl.ID), "err", err)
		writeErr(w, apierror.InternalMsg("list tracks failed", err))
		return
	}
	trackIDs := make([]string, 0, len(rows))
	for _, row := range rows {
		if !row.LiveTrackID.Valid {
			continue
		}
		trackIDs = append(trackIDs, uuidToString(row.LiveTrackID))
	}
	idStr := uuidToString(pl.ID)
	writeJSON(w, http.StatusOK, systemRefreshResp{
		PlaylistID: &idStr,
		TrackCount: pl.TrackCount,
		TrackIDs:   trackIDs,
	})
}

func (h *handlers) serveSystemPlaylistShuffle(w http.ResponseWriter, r *http.Request, variant string) {
	user, ok := requireUser(w, r)
	if !ok {
		return
	}
	q := dbq.New(h.pool)

	v := variant
	pl, err := q.GetSystemPlaylistByVariantForUser(r.Context(),
		dbq.GetSystemPlaylistByVariantForUserParams{
			UserID:        user.ID,
			SystemVariant: &v,
		})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			// No snapshot yet (empty library / not built). Empty list,
			// not an error — the client just has nothing to play.
			writeJSON(w, http.StatusOK, playlistDetailView{
				Tracks: []playlistTrackView{},
			})
			return
		}
		h.logger.Error("system shuffle: lookup failed", "variant", variant, "err", err)
		writeErr(w, apierror.InternalMsg("lookup failed", err))
		return
	}

	detail, err := h.playlists.Get(r.Context(), user.ID, pl.ID)
	if err != nil {
		h.writePlaylistErr(w, err, "system shuffle")
		return
	}

	// Played set for the current rotation. ErrNoRows = nothing played
	// from this playlist yet → empty set, everything counts unplayed.
	played := map[[16]byte]bool{}
	st, rerr := q.GetRotationState(r.Context(), dbq.GetRotationStateParams{
		UserID: user.ID, PlaylistKind: variant,
	})
	if rerr != nil && !errors.Is(rerr, pgx.ErrNoRows) {
		h.logger.Error("system shuffle: rotation lookup failed", "variant", variant, "err", rerr)
		writeErr(w, apierror.InternalMsg("rotation lookup failed", rerr))
		return
	}
	if rerr == nil {
		for _, id := range st.PlayedTrackIds {
			if id.Valid {
				played[id.Bytes] = true
			}
		}
	}

	// Partition playable tracks. Rows whose upstream track was deleted
	// (TrackID nil) are dropped — nothing to enqueue.
	unplayed := make([]playlists.PlaylistTrack, 0, len(detail.Tracks))
	heard := make([]playlists.PlaylistTrack, 0)
	for _, t := range detail.Tracks {
		if t.TrackID == nil || !t.TrackID.Valid {
			continue
		}
		if played[t.TrackID.Bytes] {
			heard = append(heard, t)
		} else {
			unplayed = append(unplayed, t)
		}
	}

	// Whole snapshot heard → reset the rotation and treat everything
	// as fresh. Reset failure is non-fatal: we still return a
	// playable reshuffled list; the next play just re-resets.
	if len(unplayed) == 0 && len(heard) > 0 {
		if err := q.ResetRotationState(r.Context(), dbq.ResetRotationStateParams{
			UserID: user.ID, PlaylistKind: variant,
		}); err != nil {
			h.logger.Warn("system shuffle: rotation reset failed",
				"variant", variant, "err", err)
		}
		unplayed = heard
		heard = nil
	}

	rand.Shuffle(len(unplayed), func(i, j int) {
		unplayed[i], unplayed[j] = unplayed[j], unplayed[i]
	})
	rand.Shuffle(len(heard), func(i, j int) {
		heard[i], heard[j] = heard[j], heard[i]
	})

	detail.Tracks = append(unplayed, heard...)
	writeJSON(w, http.StatusOK, playlistDetailToView(detail))
}
