// home.go is the M6a per-user composite home-page service. Reads five
// sections (recently added, rediscover albums, rediscover artists, most
// played tracks, last played artists) in parallel via goroutines and
// assembles them into a single payload for /api/home.
package recommendation

import (
	"context"
	"fmt"
	"sync"

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

	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)

// Section size caps. SPA splits these into rows on the page.
const (
	HomeRecentlyAddedLimit = 50
	HomeRediscoverLimit    = 25
	HomeMostPlayedLimit    = 75
	HomeLastPlayedLimit    = 25
)

// HomePayload is the composite returned by HomeData. All slices are
// non-nil at return time so the API handler can encode `[]` rather than
// `null` for empty sections.
type HomePayload struct {
	RecentlyAddedAlbums []dbq.ListRecentlyAddedAlbumsWithArtistRow
	RediscoverAlbums    []dbq.ListRediscoverAlbumsForUserRow
	RediscoverArtists   []dbq.ListRediscoverArtistsForUserRow
	MostPlayedTracks    []dbq.ListMostPlayedTracksForUserRow
	LastPlayedArtists   []dbq.ListLastPlayedArtistsForUserRow
}

// HomeData runs five queries in parallel and assembles the payload.
// Rediscover sections fall back to a random sample of liked entities
// when the eligibility query returns fewer than HomeRediscoverLimit rows.
// Any single query error fails the whole call (predictable empty-or-full
// UX for the SPA — partial payloads are not a feature).
func HomeData(ctx context.Context, pool *pgxpool.Pool, userID pgtype.UUID) (*HomePayload, error) {
	q := dbq.New(pool)

	var (
		wg       sync.WaitGroup
		mu       sync.Mutex
		firstErr error
	)
	fail := func(stage string, err error) {
		mu.Lock()
		defer mu.Unlock()
		if firstErr == nil {
			firstErr = fmt.Errorf("home: %s: %w", stage, err)
		}
	}

	out := &HomePayload{
		RecentlyAddedAlbums: []dbq.ListRecentlyAddedAlbumsWithArtistRow{},
		RediscoverAlbums:    []dbq.ListRediscoverAlbumsForUserRow{},
		RediscoverArtists:   []dbq.ListRediscoverArtistsForUserRow{},
		MostPlayedTracks:    []dbq.ListMostPlayedTracksForUserRow{},
		LastPlayedArtists:   []dbq.ListLastPlayedArtistsForUserRow{},
	}

	wg.Add(5)
	go func() {
		defer wg.Done()
		rows, err := q.ListRecentlyAddedAlbumsWithArtist(ctx, HomeRecentlyAddedLimit)
		if err != nil {
			fail("recently_added", err)
			return
		}
		out.RecentlyAddedAlbums = rows
	}()
	go func() {
		defer wg.Done()
		rows, err := q.ListMostPlayedTracksForUser(ctx, dbq.ListMostPlayedTracksForUserParams{
			UserID: userID, Limit: HomeMostPlayedLimit,
		})
		if err != nil {
			fail("most_played", err)
			return
		}
		out.MostPlayedTracks = rows
	}()
	go func() {
		defer wg.Done()
		rows, err := q.ListLastPlayedArtistsForUser(ctx, dbq.ListLastPlayedArtistsForUserParams{
			UserID: userID, Limit: HomeLastPlayedLimit,
		})
		if err != nil {
			fail("last_played", err)
			return
		}
		out.LastPlayedArtists = rows
	}()
	go func() {
		defer wg.Done()
		rows, err := loadRediscoverAlbums(ctx, q, userID)
		if err != nil {
			fail("rediscover_albums", err)
			return
		}
		out.RediscoverAlbums = rows
	}()
	go func() {
		defer wg.Done()
		rows, err := loadRediscoverArtists(ctx, q, userID)
		if err != nil {
			fail("rediscover_artists", err)
			return
		}
		out.RediscoverArtists = rows
	}()
	wg.Wait()

	if firstErr != nil {
		return nil, firstErr
	}
	return out, nil
}

func loadRediscoverAlbums(ctx context.Context, q *dbq.Queries, userID pgtype.UUID) ([]dbq.ListRediscoverAlbumsForUserRow, error) {
	primary, err := q.ListRediscoverAlbumsForUser(ctx, dbq.ListRediscoverAlbumsForUserParams{
		UserID: userID, Limit: HomeRediscoverLimit,
	})
	if err != nil {
		return nil, err
	}
	if len(primary) >= HomeRediscoverLimit {
		return primary, nil
	}
	fallback, err := q.ListRediscoverAlbumsFallbackForUser(ctx, dbq.ListRediscoverAlbumsFallbackForUserParams{
		UserID: userID, Limit: HomeRediscoverLimit,
	})
	if err != nil {
		return nil, err
	}
	seen := make(map[pgtype.UUID]struct{}, len(primary))
	for _, r := range primary {
		seen[r.Album.ID] = struct{}{}
	}
	for _, r := range fallback {
		if _, dup := seen[r.Album.ID]; dup {
			continue
		}
		primary = append(primary, dbq.ListRediscoverAlbumsForUserRow{
			Album:      r.Album,
			ArtistName: r.ArtistName,
		})
		if len(primary) >= HomeRediscoverLimit {
			break
		}
		seen[r.Album.ID] = struct{}{}
	}
	return primary, nil
}

func loadRediscoverArtists(ctx context.Context, q *dbq.Queries, userID pgtype.UUID) ([]dbq.ListRediscoverArtistsForUserRow, error) {
	primary, err := q.ListRediscoverArtistsForUser(ctx, dbq.ListRediscoverArtistsForUserParams{
		UserID: userID, Limit: HomeRediscoverLimit,
	})
	if err != nil {
		return nil, err
	}
	if len(primary) >= HomeRediscoverLimit {
		return primary, nil
	}
	fallback, err := q.ListRediscoverArtistsFallbackForUser(ctx, dbq.ListRediscoverArtistsFallbackForUserParams{
		UserID: userID, Limit: HomeRediscoverLimit,
	})
	if err != nil {
		return nil, err
	}
	seen := make(map[pgtype.UUID]struct{}, len(primary))
	for _, r := range primary {
		seen[r.Artist.ID] = struct{}{}
	}
	for _, r := range fallback {
		if _, dup := seen[r.Artist.ID]; dup {
			continue
		}
		primary = append(primary, dbq.ListRediscoverArtistsForUserRow{
			Artist:       r.Artist,
			CoverAlbumID: r.CoverAlbumID,
			AlbumCount:   r.AlbumCount,
		})
		if len(primary) >= HomeRediscoverLimit {
			break
		}
		seen[r.Artist.ID] = struct{}{}
	}
	return primary, nil
}
