package api

import (
	"context"
	"net/http"
	"strconv"
	"sync"

	"github.com/jackc/pgx/v5/pgtype"

	"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
	"git.fabledsword.com/bvandeusen/minstrel/internal/lidarr"
	"git.fabledsword.com/bvandeusen/minstrel/internal/recommendation"
)

// suggestionView is the wire shape returned by GET /api/discover/suggestions.
type suggestionView struct {
	MBID        string                 `json:"mbid"`
	Name        string                 `json:"name"`
	Score       float64                `json:"score"`
	Attribution []seedContributionView `json:"attribution"`
	// ImageURL is resolved on-demand from Lidarr (out-of-library
	// artists have no local art row). Omitted when Lidarr is disabled
	// or has no match — the client falls back to a placeholder. Not
	// cached: a remote URL Lidarr surfaced, fetched by the browser.
	ImageURL string `json:"image_url,omitempty"`
}

type seedContributionView struct {
	ArtistID     pgtype.UUID `json:"artist_id"`
	Name         string      `json:"name"`
	Contribution float64     `json:"contribution"`
	IsLiked      bool        `json:"is_liked"`
	PlayCount    int64       `json:"play_count"`
}

// handleListSuggestions implements GET /api/discover/suggestions.
//
// Query params:
//   - limit (default 12, capped at 50)
//   - half_life_days (default 30)
//
// Returns 200 with a JSON array (possibly empty). Read-only; no admin gate.
func (h *handlers) handleListSuggestions(w http.ResponseWriter, r *http.Request) {
	user, ok := requireUser(w, r)
	if !ok {
		return
	}
	limit := 12
	if v := r.URL.Query().Get("limit"); v != "" {
		n, err := strconv.Atoi(v)
		if err != nil || n < 1 {
			writeErr(w, apierror.BadRequest("bad_request", "invalid limit"))
			return
		}
		limit = n
	}
	halfLife := 30.0
	if v := r.URL.Query().Get("half_life_days"); v != "" {
		f, err := strconv.ParseFloat(v, 64)
		if err != nil || f <= 0 {
			writeErr(w, apierror.BadRequest("bad_request", "invalid half_life_days"))
			return
		}
		halfLife = f
	}

	suggestions, err := recommendation.SuggestArtists(r.Context(), h.pool, user.ID, halfLife, limit)
	if err != nil {
		h.logger.Error("api: list suggestions", "err", err)
		writeErr(w, apierror.InternalMsg("failed to load suggestions", err))
		return
	}

	out := make([]suggestionView, 0, len(suggestions))
	for _, s := range suggestions {
		attr := make([]seedContributionView, 0, len(s.Attribution))
		for _, a := range s.Attribution {
			attr = append(attr, seedContributionView{
				ArtistID:     a.ArtistID,
				Name:         a.Name,
				Contribution: a.Contribution,
				IsLiked:      a.IsLiked,
				PlayCount:    a.PlayCount,
			})
		}
		out = append(out, suggestionView{
			MBID: s.MBID, Name: s.Name, Score: s.Score, Attribution: attr,
		})
	}
	h.resolveSuggestionArt(r.Context(), out)
	writeJSON(w, http.StatusOK, out)
}

// resolveSuggestionArt fills ImageURL on-demand from Lidarr's artist
// lookup, matched by MBID (foreignArtistId). Best-effort and cache-free:
// Lidarr is the only source — when it's disabled, unreachable, or has
// no match for a candidate, that entry keeps an empty ImageURL and the
// client renders its placeholder. Lookups run with bounded concurrency
// so a full Discover page doesn't serialize ~12 round-trips. Never
// fails the request; the suggestions list is the contract, art is a
// nicety.
func (h *handlers) resolveSuggestionArt(ctx context.Context, views []suggestionView) {
	if len(views) == 0 {
		return
	}
	cfg, err := h.lidarrCfg.Get(ctx)
	if err != nil || !cfg.Enabled || cfg.BaseURL == "" || cfg.APIKey == "" {
		return // Lidarr off / unconfigured → placeholders only
	}
	client := lidarr.NewClient(cfg.BaseURL, cfg.APIKey)

	const maxConcurrent = 6
	sem := make(chan struct{}, maxConcurrent)
	var wg sync.WaitGroup
	for i := range views {
		if views[i].MBID == "" || views[i].Name == "" {
			continue
		}
		wg.Add(1)
		sem <- struct{}{}
		go func(idx int) {
			defer wg.Done()
			defer func() { <-sem }()
			results, lerr := client.LookupArtist(ctx, views[idx].Name)
			if lerr != nil {
				return // best-effort: no art on lookup failure
			}
			for _, res := range results {
				if res.MBID == views[idx].MBID && res.ImageURL != "" {
					// Distinct slice index per goroutine → race-free.
					views[idx].ImageURL = res.ImageURL
					return
				}
			}
		}(i)
	}
	wg.Wait()
}
