package api

import (
	"context"
	"encoding/json"
	"net/http"
	"sync"
	"time"

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

// recommendationBuilds tracks in-flight rebuilds per user.
//
// Without this, four TVs waking up together would each kick off the same handful of Emby
// queries. The first one through does the work; the rest skip it and pick the rows up on
// their next home load.
type recommendationBuilds struct {
	mu      sync.Mutex
	running map[string]bool
}

func (b *recommendationBuilds) begin(userID string) bool {
	b.mu.Lock()
	defer b.mu.Unlock()
	if b.running == nil {
		b.running = map[string]bool{}
	}
	if b.running[userID] {
		return false
	}
	b.running[userID] = true
	return true
}

func (b *recommendationBuilds) done(userID string) {
	b.mu.Lock()
	defer b.mu.Unlock()
	delete(b.running, userID)
}

// cachedRecommendations returns the stored rows, or nil on a miss.
func (s *Server) cachedRecommendations(ctx context.Context, userID string) []recommend.Row {
	raw, err := s.cache.Get(ctx, cache.RecommendationsKey(userID))
	if err != nil {
		return nil
	}
	var rows []recommend.Row
	if err := json.Unmarshal(raw, &rows); err != nil {
		return nil
	}
	return rows
}

// buildRecommendations computes and caches rows for one user.
func (s *Server) buildRecommendations(ctx context.Context, sess store.Session) ([]recommend.Row, error) {
	rows, err := s.recommender.BuildRowsForUser(ctx, credentials(sess), sess.Username)
	if err != nil {
		return nil, err
	}
	// An empty result is cached too: a user with no history should not trigger a full
	// rebuild on every single home load.
	if raw, err := json.Marshal(rows); err == nil {
		if err := s.cache.Set(ctx, cache.RecommendationsKey(sess.EmbyUserID), raw, s.cfg.RecommendTTL); err != nil {
			s.loggerFor(ctx).Warn("recommendation cache write failed", "error", err)
		}
	}
	return rows, nil
}

// refreshRecommendationsInBackground rebuilds without holding up the caller.
//
// The home screen must stay fast, so a cold cache means "no recommendation rows this
// time" rather than "wait several seconds for Emby". The rows appear on the next load.
func (s *Server) refreshRecommendationsInBackground(sess store.Session) {
	if !s.recommendationBuilds.begin(sess.EmbyUserID) {
		return
	}
	go func() {
		defer s.recommendationBuilds.done(sess.EmbyUserID)

		// Detached from the request: the TV's connection is long gone by the time this
		// finishes, but the work is still worth completing.
		ctx, cancel := context.WithTimeout(context.WithoutCancel(context.Background()), s.cfg.RecommendTimeout)
		defer cancel()

		started := time.Now()
		rows, err := s.buildRecommendations(ctx, sess)
		if err != nil {
			s.loggerFor(ctx).Warn("recommendation build failed", "user", sess.EmbyUserID, "error", err)
			return
		}
		s.loggerFor(ctx).Info("recommendations rebuilt",
			"user", sess.EmbyUserID, "rows", len(rows), "ms", time.Since(started).Milliseconds())
	}()
}

// handleRecommendations serves the rows on their own, building synchronously when the
// cache is cold. `?refresh=1` forces a rebuild — useful for testing the engine without
// waiting out the TTL.
func (s *Server) handleRecommendations(w http.ResponseWriter, r *http.Request, sess store.Session) {
	ctx := r.Context()
	forceRefresh := r.URL.Query().Get("refresh") == "1"

	if !forceRefresh {
		if rows := s.cachedRecommendations(ctx, sess.EmbyUserID); rows != nil {
			w.Header().Set("X-Memby-Cache", "hit")
			s.decorateRowRatings(ctx, rows)
			writeJSON(w, http.StatusOK, map[string]any{"rows": rows})
			return
		}
	}

	buildCtx, cancel := context.WithTimeout(ctx, s.cfg.RecommendTimeout)
	defer cancel()

	rows, err := s.buildRecommendations(buildCtx, sess)
	if err != nil {
		s.writeUpstreamError(ctx, w, err, "could not build recommendations")
		return
	}
	w.Header().Set("X-Memby-Cache", "miss")
	s.decorateRowRatings(ctx, rows)
	writeJSON(w, http.StatusOK, map[string]any{"rows": nonNilRows(rows)})
}

// handleForYou is deliberately separate from /home. Tracearr and the richer scoring
// path may take longer than a launcher request, and the chosen time budget is local to
// this visit. The TV only calls this when the viewer enters the dedicated area.
func (s *Server) handleForYou(w http.ResponseWriter, r *http.Request, sess store.Session) {
	minutes := queryInt(r, "minutes", 0, 360)
	if s.forYou != nil && r.URL.Query().Get("refresh") != "1" {
		rows, hit, stale, err := s.forYou.PreparedRows(r.Context(), sess, minutes)
		if err != nil {
			s.loggerFor(r.Context()).Warn("prepared For You read failed; using live fallback",
				"user", sess.EmbyUserID, "error", err)
		} else if hit {
			rows = s.filterRecommendationPermissions(r.Context(), sess, rows)
			rows = s.personalizeTitles(r.Context(), sess, rows)
			rows = deduplicateRows(personalizeRowsByTitleScores(selectPersonalizedRows(rows)))
			w.Header().Set("X-Memby-For-You", "prepared")
			s.decorateRowRatings(r.Context(), rows)
			if stale {
				s.forYou.MarkDirty(r.Context(), sess)
				s.forYou.RefreshAsync(sess, false)
			}
			writeJSON(w, http.StatusOK, map[string]any{"rows": nonNilRows(rows)})
			return
		} else {
			s.forYou.MarkDirty(r.Context(), sess)
			s.forYou.RefreshAsync(sess, false)
		}
	}
	buildCtx, cancel := context.WithTimeout(r.Context(), s.cfg.RecommendTimeout)
	defer cancel()

	rows, err := s.recommender.BuildForYou(
		buildCtx,
		credentials(sess),
		sess.Username,
		recommend.ForYouOptions{AvailableMinutes: minutes},
	)
	if err != nil {
		s.writeUpstreamError(r.Context(), w, err, "could not build For You recommendations")
		return
	}
	rows = s.filterRecommendationPermissions(r.Context(), sess, rows)
	rows = s.personalizeTitles(r.Context(), sess, rows)
	rows = deduplicateRows(personalizeRowsByTitleScores(selectPersonalizedRows(rows)))
	w.Header().Set("X-Memby-For-You", "live-fallback")
	s.decorateRowRatings(r.Context(), rows)
	writeJSON(w, http.StatusOK, map[string]any{"rows": nonNilRows(rows)})
}

func nonNilRows(rows []recommend.Row) []recommend.Row {
	if rows == nil {
		return []recommend.Row{}
	}
	return rows
}
