package api

import (
	"context"
	"encoding/json"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"github.com/ponzischeme89/memby/server/internal/cache"
	"github.com/ponzischeme89/memby/server/internal/sonarr"
	"github.com/ponzischeme89/memby/server/internal/store"
)

type seriesEpisodesResponse struct {
	Items []json.RawMessage `json:"items"`
}

type flagRequest struct {
	Value bool `json:"value"`
}

type seasonFinaleResponse struct {
	SeasonFinale  bool   `json:"seasonFinale"`
	SeriesName    string `json:"seriesName,omitempty"`
	SeasonNumber  int    `json:"seasonNumber,omitempty"`
	EpisodeNumber int    `json:"episodeNumber,omitempty"`
}

type finaleEmbyItem struct {
	Type              string            `json:"Type"`
	SeriesID          string            `json:"SeriesId"`
	SeriesName        string            `json:"SeriesName"`
	ParentIndexNumber int               `json:"ParentIndexNumber"`
	IndexNumber       int               `json:"IndexNumber"`
	ProviderIDs       map[string]string `json:"ProviderIds"`
}

// handleItem serves full metadata for one item. The TV asks for this only after D-pad
// focus settles, so it is worth caching for longer than a home row.
func (s *Server) handleItem(w http.ResponseWriter, r *http.Request, sess store.Session) {
	ctx := r.Context()
	itemID := r.PathValue("id")
	if itemID == "" {
		writeError(w, http.StatusBadRequest, "item id is required")
		return
	}
	// Version the entry when the detail contract grows so older cached payloads cannot
	// hide newly requested fields such as People or the stored ratings.
	key := cache.UserKey(sess.EmbyUserID, "item:v5:"+itemID)

	if raw, err := s.cache.Get(ctx, key); err == nil {
		w.Header().Set("X-Memby-Cache", "hit")
		writeRaw(w, http.StatusOK, raw)
		return
	}

	item, err := s.emby.Item(ctx, credentials(sess), itemID, fieldsDetail)
	if err != nil {
		s.writeUpstreamError(ctx, w, err, "could not load the item")
		return
	}
	// A detail page can then draw its ratings with the rest of the hero rather than
	// after a second request. Anything not yet stored still arrives on /ratings.
	decorated := []json.RawMessage{item}
	s.decorateItemRatings(ctx, decorated)
	item = decorated[0]
	if err := s.cache.Set(ctx, key, item, s.cfg.ItemTTL); err != nil {
		s.loggerFor(ctx).Warn("item cache write failed", "error", err)
	}
	w.Header().Set("X-Memby-Cache", "miss")
	writeRaw(w, http.StatusOK, item)
}

// handleSeasonFinale verifies an episode against Sonarr's complete season, including
// future episodes. Looking only at Emby's downloaded files would call every current
// weekly episode a finale until the following episode arrived.
func (s *Server) handleSeasonFinale(w http.ResponseWriter, r *http.Request, sess store.Session) {
	ctx := r.Context()
	itemID := r.PathValue("id")
	if itemID == "" {
		writeError(w, http.StatusBadRequest, "item id is required")
		return
	}
	empty := seasonFinaleResponse{}
	if s.sonarr == nil {
		writeJSON(w, http.StatusOK, empty)
		return
	}
	key := cache.UserKey(sess.EmbyUserID, "season-finale:v1:"+itemID)
	if raw, err := s.cache.Get(ctx, key); err == nil {
		writeRaw(w, http.StatusOK, raw)
		return
	}

	currentRaw, err := s.emby.Item(
		ctx, credentials(sess), itemID,
		"ProviderIds,SeriesId,SeriesName,ParentIndexNumber,IndexNumber",
	)
	if err != nil {
		s.writeUpstreamError(ctx, w, err, "could not inspect the playing episode")
		return
	}
	var current finaleEmbyItem
	if json.Unmarshal(currentRaw, &current) != nil ||
		!strings.EqualFold(current.Type, "Episode") || current.SeriesID == "" ||
		current.ParentIndexNumber <= 0 || current.IndexNumber <= 0 {
		s.writeSeasonFinaleResponse(ctx, key, empty, w)
		return
	}

	seriesRaw, err := s.emby.Item(ctx, credentials(sess), current.SeriesID, "ProviderIds")
	if err != nil {
		s.loggerFor(ctx).Warn("season finale series metadata unavailable", "item", itemID, "error", err)
		s.writeSeasonFinaleResponse(ctx, key, empty, w)
		return
	}
	var seriesItem finaleEmbyItem
	if json.Unmarshal(seriesRaw, &seriesItem) != nil {
		s.writeSeasonFinaleResponse(ctx, key, empty, w)
		return
	}
	tvdbID, err := strconv.Atoi(providerID(seriesItem.ProviderIDs, "tvdb"))
	if err != nil || tvdbID <= 0 {
		s.writeSeasonFinaleResponse(ctx, key, empty, w)
		return
	}

	series, err := s.sonarrSeriesCatalogue(ctx)
	if err != nil {
		s.loggerFor(ctx).Warn("season finale Sonarr series unavailable", "item", itemID, "error", err)
		s.writeSeasonFinaleResponse(ctx, key, empty, w)
		return
	}
	sonarrSeriesID := 0
	for _, candidate := range series {
		if candidate.TVDBID == tvdbID {
			sonarrSeriesID = candidate.ID
			break
		}
	}
	if sonarrSeriesID == 0 {
		s.writeSeasonFinaleResponse(ctx, key, empty, w)
		return
	}
	episodes, err := s.sonarr.Episodes(ctx, sonarrSeriesID)
	if err != nil {
		s.loggerFor(ctx).Warn("season finale Sonarr episodes unavailable", "item", itemID, "error", err)
		s.writeSeasonFinaleResponse(ctx, key, empty, w)
		return
	}
	result := seasonFinaleResponse{
		SeasonFinale:  isSeasonFinale(current.ParentIndexNumber, current.IndexNumber, episodes),
		SeriesName:    current.SeriesName,
		SeasonNumber:  current.ParentIndexNumber,
		EpisodeNumber: current.IndexNumber,
	}
	if !result.SeasonFinale {
		result = empty
	}
	s.writeSeasonFinaleResponse(ctx, key, result, w)
}

func (s *Server) writeSeasonFinaleResponse(
	ctx context.Context, key string, result seasonFinaleResponse, w http.ResponseWriter,
) {
	body, err := json.Marshal(result)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "could not encode finale status")
		return
	}
	if err := s.cache.Set(ctx, key, body, s.cfg.ItemTTL); err != nil {
		s.loggerFor(ctx).Warn("season finale cache write failed", "error", err)
	}
	writeRaw(w, http.StatusOK, body)
}

func providerID(ids map[string]string, wanted string) string {
	for key, value := range ids {
		if strings.EqualFold(key, wanted) {
			return value
		}
	}
	return ""
}

func isSeasonFinale(seasonNumber, episodeNumber int, episodes []sonarr.Episode) bool {
	if seasonNumber <= 0 || episodeNumber <= 0 {
		return false
	}
	lastEpisode := 0
	for _, episode := range episodes {
		if episode.SeasonNumber == seasonNumber && episode.EpisodeNumber > lastEpisode {
			lastEpisode = episode.EpisodeNumber
		}
	}
	return lastEpisode > 0 && episodeNumber == lastEpisode
}

// handleSeriesEpisodes supplies the complete episode browser in one cached response.
// The TV groups by ParentIndexNumber locally, so changing seasons never reaches Emby.
func (s *Server) handleSeriesEpisodes(w http.ResponseWriter, r *http.Request, sess store.Session) {
	ctx := r.Context()
	seriesID := r.PathValue("id")
	if seriesID == "" {
		writeError(w, http.StatusBadRequest, "series id is required")
		return
	}
	key := cache.UserKey(sess.EmbyUserID, "series-episodes:"+seriesID)
	if raw, err := s.cache.Get(ctx, key); err == nil {
		w.Header().Set("X-Memby-Cache", "hit")
		writeRaw(w, http.StatusOK, raw)
		return
	}

	result, err := s.emby.Episodes(ctx, credentials(sess), seriesID, url.Values{
		"UserId": {sess.EmbyUserID},
		// PremiereDate is the one fact that tells two episodes of a list apart, so the
		// episode browser asks for it by name — Emby does not return it otherwise.
		"Fields":           {"Overview,RunTimeTicks,SeriesName,PremiereDate,PrimaryImageAspectRatio"},
		"EnableUserData":   {"true"},
		"EnableImages":     {"true"},
		"EnableImageTypes": {"Primary,Thumb,Backdrop"},
		"ImageTypeLimit":   {"1"},
		"Limit":            {"1000"},
	})
	if err != nil {
		s.writeUpstreamError(ctx, w, err, "could not load series episodes")
		return
	}
	items := result.Items
	if items == nil {
		items = []json.RawMessage{}
	}
	body, err := json.Marshal(seriesEpisodesResponse{Items: items})
	if err != nil {
		writeError(w, http.StatusInternalServerError, "could not encode series episodes")
		return
	}
	if err := s.cache.Set(ctx, key, body, s.cfg.ItemTTL); err != nil {
		s.loggerFor(ctx).Warn("series episodes cache write failed", "error", err)
	}
	w.Header().Set("X-Memby-Cache", "miss")
	writeRaw(w, http.StatusOK, body)
}

// handleTrailer answers with the item's first local trailer, or 404 when it has none.
// The screensaver's Play action uses this before asking for a playback URL.
func (s *Server) handleTrailer(w http.ResponseWriter, r *http.Request, sess store.Session) {
	itemID := r.PathValue("id")
	if itemID == "" {
		writeError(w, http.StatusBadRequest, "item id is required")
		return
	}
	result, err := s.emby.LocalTrailers(r.Context(), credentials(sess), itemID)
	if err != nil {
		s.writeUpstreamError(r.Context(), w, err, "could not load trailers")
		return
	}
	if len(result.Items) == 0 {
		writeError(w, http.StatusNotFound, "no trailer available")
		return
	}
	writeRaw(w, http.StatusOK, result.Items[0])
}

func (s *Server) handleFavorite(w http.ResponseWriter, r *http.Request, sess store.Session) {
	s.setFlag(w, r, sess, func(itemID string, value bool) (json.RawMessage, error) {
		return s.emby.SetFavorite(r.Context(), credentials(sess), itemID, value)
	})
}

func (s *Server) handlePlayed(w http.ResponseWriter, r *http.Request, sess store.Session) {
	s.setFlag(w, r, sess, func(itemID string, value bool) (json.RawMessage, error) {
		return s.emby.SetPlayed(r.Context(), credentials(sess), itemID, value)
	})
}

func (s *Server) handleHideFromResume(w http.ResponseWriter, r *http.Request, sess store.Session) {
	itemID := r.PathValue("id")
	if itemID == "" {
		writeError(w, http.StatusBadRequest, "item id is required")
		return
	}
	userData, err := s.emby.HideFromResume(r.Context(), credentials(sess), itemID)
	if err != nil {
		s.writeUpstreamError(r.Context(), w, err, "could not remove the item from Continue Watching")
		return
	}
	if err := s.cache.InvalidateUser(r.Context(), sess.EmbyUserID); err != nil {
		s.loggerFor(r.Context()).Warn("cache invalidation failed", "error", err)
	}
	if s.forYou != nil {
		s.forYou.MarkDirty(r.Context(), sess)
	}
	writeRaw(w, http.StatusOK, userData)
}

// setFlag applies a user-data mutation and drops this user's cached views, so the next
// home request reflects it rather than serving the row it just contradicted.
func (s *Server) setFlag(
	w http.ResponseWriter,
	r *http.Request,
	sess store.Session,
	apply func(itemID string, value bool) (json.RawMessage, error),
) {
	itemID := r.PathValue("id")
	if itemID == "" {
		writeError(w, http.StatusBadRequest, "item id is required")
		return
	}
	var req flagRequest
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<10)).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "malformed request body")
		return
	}

	userData, err := apply(itemID, req.Value)
	if err != nil {
		s.writeUpstreamError(r.Context(), w, err, "could not update the item")
		return
	}
	if err := s.cache.InvalidateUser(r.Context(), sess.EmbyUserID); err != nil {
		s.loggerFor(r.Context()).Warn("cache invalidation failed", "error", err)
	}
	if s.forYou != nil {
		s.forYou.MarkDirty(r.Context(), sess)
	}
	writeRaw(w, http.StatusOK, userData)
}
