package jellyfin

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

	"github.com/go-chi/chi/v5"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/model/request"
	"github.com/navidrome/navidrome/server/events"
	"github.com/navidrome/navidrome/server/jellyfin/dto"
	"github.com/navidrome/navidrome/utils/req"
)

// resolveAnnotated finds which annotated repo owns id, returning the resource name used in
// refreshResource events. Albums and songs 404 when the user can't access their library; artists
// span libraries (library_artist), so have no single LibraryID to gate on and rely on list-time
// scoping. PlaylistRepository.Get enforces playlist visibility. When repo is nil the response has
// already been written, so callers must return without writing the annotation.
func (api *Router) resolveAnnotated(w http.ResponseWriter, r *http.Request, id string) (repo model.AnnotatedRepository, resource string) {
	ctx := r.Context()
	entity, err := model.GetEntityByID(ctx, api.ds, id)
	if err != nil && !errors.Is(err, model.ErrNotFound) {
		api.internalError(w, r, err)
		return nil, ""
	}
	u, _ := request.UserFrom(ctx)
	switch e := entity.(type) {
	case *model.Album:
		if u.HasLibraryAccess(e.LibraryID) {
			return api.ds.Album(ctx), "album"
		}
	case *model.Artist:
		return api.ds.Artist(ctx), "artist"
	case *model.MediaFile:
		if u.HasLibraryAccess(e.LibraryID) {
			return api.ds.MediaFile(ctx), "song"
		}
	case *model.Playlist:
		return api.ds.Playlist(ctx), "playlist"
	}
	// Unknown ids, inaccessible-library items and non-annotatable entities (radios) all read as absent.
	http.Error(w, "Not Found", http.StatusNotFound)
	return nil, ""
}

// getUserItemData returns the caller's play/favorite/rating state for a single item. Jellify
// fetches this per item to render played/favourite indicators; resolveItemByID enforces the
// library-access gate.
func (api *Router) getUserItemData(w http.ResponseWriter, r *http.Request) {
	id := api.resolveItemID(r.Context(), dto.DecodeID(chi.URLParam(r, "itemId")))
	item, ok := api.resolveItemByID(r.Context(), id, nil)
	if !ok {
		http.Error(w, "Not Found", http.StatusNotFound)
		return
	}
	data := item.UserData
	if data == nil {
		// Items without annotations still return a valid empty UserData.
		data = dto.UserData(model.Annotations{}, id)
	}
	api.ok(w, r, data)
}

func (api *Router) setFavorite(w http.ResponseWriter, r *http.Request, starred bool) {
	id := api.resolveItemID(r.Context(), dto.DecodeID(chi.URLParam(r, "itemId")))
	repo, resource := api.resolveAnnotated(w, r, id)
	if repo == nil {
		return
	}
	if err := repo.SetStar(starred, id); err != nil {
		api.internalError(w, r, err)
		return
	}
	api.broker.SendMessage(r.Context(), (&events.RefreshResource{}).With(resource, id))
	encodedID := dto.EncodeID(id)
	api.ok(w, r, &dto.UserItemDataDto{IsFavorite: starred, Key: encodedID, ItemId: encodedID})
}

func (api *Router) markFavorite(w http.ResponseWriter, r *http.Request) { api.setFavorite(w, r, true) }
func (api *Router) unmarkFavorite(w http.ResponseWriter, r *http.Request) {
	api.setFavorite(w, r, false)
}

func (api *Router) setItemRating(w http.ResponseWriter, r *http.Request, rating int) {
	id := api.resolveItemID(r.Context(), dto.DecodeID(chi.URLParam(r, "itemId")))
	repo, resource := api.resolveAnnotated(w, r, id)
	if repo == nil {
		return
	}
	if err := repo.SetRating(rating, id); err != nil {
		api.internalError(w, r, err)
		return
	}
	api.broker.SendMessage(r.Context(), (&events.RefreshResource{}).With(resource, id))
	encodedID := dto.EncodeID(id)
	d := &dto.UserItemDataDto{Key: encodedID, ItemId: encodedID}
	if rating > 0 {
		jfRating := float64(rating) * 2 // Navidrome 0-5 -> Jellyfin 0-10, mirrors dto.UserData
		d.Rating = &jfRating
	}
	api.ok(w, r, d)
}

// setRating maps Jellyfin's 0-10 rating (a nullable double, so fractional values are valid) to
// Navidrome's 0-5 stars. A nonzero rating floors at one star: rounding to 0 would clear it, since
// SetRating(0) is the delete path.
func (api *Router) setRating(w http.ResponseWriter, r *http.Request) {
	jfRating := req.Params(r).Float64Or("rating", 0)
	jfRating = min(max(jfRating, 0), 10) // clamp: a client sending e.g. Rating=100 must not write an out-of-domain rating
	rating := int(math.Round(jfRating / 2))
	if jfRating > 0 {
		rating = max(rating, 1)
	}
	api.setItemRating(w, r, rating)
}

func (api *Router) removeRating(w http.ResponseWriter, r *http.Request) {
	api.setItemRating(w, r, 0)
}
