package tags

import (
	"context"
	"log/slog"
	"time"
)

// Worker periodically drains tracks needing tag enrichment. Unlike cover
// art (threaded through the file-scan chain because it needs track file
// paths), tag enrichment only needs data already in the DB — recording
// MBID / artist / title — so it runs as a standalone background worker,
// mirroring the ListenBrainz similarity worker. Rate limiting lives in the
// providers' httpClients, so a tick just drains a bounded batch and the
// external APIs pace themselves.
type Worker struct {
	enricher       *Enricher
	logger         *slog.Logger
	tick           time.Duration
	batch          int
	candidateBatch int
}

// NewWorker constructs a worker with production defaults: an initial drain
// shortly after boot, then every 30 minutes, up to 200 tracks per tick.
// MusicBrainz's 1 req/s ceiling is the real throttle, so the batch size
// mainly bounds how long one tick runs, not the request rate.
//
// candidateBatch is smaller than the track batch on purpose. Library tracks are
// a finite set that drains to completion and then costs nothing; out-of-library
// candidates (#2376) are effectively unbounded — every library artist's
// neighbours — so this arm would otherwise monopolise every tick forever and
// starve the track arm. 50/tick at ~1 req/s is roughly a minute of work, and
// the query hands back the highest-similarity candidates first so the ones that
// can actually reach a user's deck are enriched first.
func NewWorker(enricher *Enricher, logger *slog.Logger) *Worker {
	return &Worker{
		enricher:       enricher,
		logger:         logger,
		tick:           30 * time.Minute,
		batch:          200,
		candidateBatch: 50,
	}
}

// Run blocks until ctx is cancelled: an initial drain, then every w.tick.
func (w *Worker) Run(ctx context.Context) {
	w.tickOnce(ctx)
	t := time.NewTicker(w.tick)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-t.C:
			w.tickOnce(ctx)
		}
	}
}

// tickOnce drains one bounded batch of each kind. Both Enrich*Batch methods
// already log a category breakdown, so this only surfaces a fatal batch error.
//
// Tracks first: they back the taste profile the whole app reads from, whereas
// candidate tags only affect the Discover request surface. On a fresh install
// both are cold, and getting the profile warm matters more.
func (w *Worker) tickOnce(ctx context.Context) {
	if _, _, _, err := w.enricher.EnrichTrackBatch(ctx, w.batch, nil); err != nil {
		if ctx.Err() == nil {
			w.logger.Error("tags: enrichment tick failed", "err", err)
		}
	}
	// Not gated on the track arm's success: the two drains share nothing but a
	// provider chain, and a track-side failure says nothing about whether
	// candidate lookups will work.
	if ctx.Err() != nil {
		return
	}
	if _, _, _, err := w.enricher.EnrichCandidateArtistBatch(ctx, w.candidateBatch); err != nil {
		if ctx.Err() == nil {
			w.logger.Error("tags: candidate-artist enrichment tick failed", "err", err)
		}
	}
}
