package recommendation

import (
	"context"

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

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

// contextAffinityShrinkage is the pseudo-count that pulls a low-play artist's
// cell-share toward the user's baseline, so an artist with one or two plays
// can't swing its affinity to ±1 on noise. At k plays the estimate sits
// halfway between the raw cell-share and the baseline.
const contextAffinityShrinkage = 5.0

// ContextAffinity is the read-side map of per-artist time-of-day/weekday
// affinity for the CURRENT context (#1531): artist_id → score in [-1, +1].
// Absent artists (no play history) score 0, so cold-start candidates stay
// neutral. The zero value is a valid all-neutral affinity.
type ContextAffinity struct {
	byArtist map[pgtype.UUID]float64
}

// Affinity returns the artist's current-context affinity, or 0 if unknown.
func (c ContextAffinity) Affinity(artistID pgtype.UUID) float64 {
	return c.byArtist[artistID]
}

// LoadContextAffinity computes each artist's affinity for the user's CURRENT
// context cell — daypart × weekday, narrowed by deviceClass when non-empty
// (#1551; radio passes the current device, the daily mixes pass "" for a
// device-agnostic cell). For every artist with completed plays in the window it
// compares the share of that artist's plays that fall in the current cell
// against the user's overall baseline share, shrinking sparse artists toward
// the baseline. Returns an empty (all-neutral) affinity when the user has no
// plays.
func LoadContextAffinity(
	ctx context.Context, q *dbq.Queries, userID pgtype.UUID, deviceClass string,
) (ContextAffinity, error) {
	rows, err := q.ListArtistContextPlayCountsForUser(ctx, dbq.ListArtistContextPlayCountsForUserParams{
		ID:      userID,
		Column2: deviceClass,
	})
	if err != nil {
		return ContextAffinity{}, err
	}
	var totalPlays, cellPlays int64
	for _, r := range rows {
		totalPlays += r.TotalPlays
		cellPlays += r.CellPlays
	}
	out := ContextAffinity{byArtist: make(map[pgtype.UUID]float64, len(rows))}
	if totalPlays == 0 {
		return out, nil
	}
	baseline := float64(cellPlays) / float64(totalPlays)
	for _, r := range rows {
		out.byArtist[r.ArtistID] = contextAffinity(
			float64(r.CellPlays), float64(r.TotalPlays), baseline, contextAffinityShrinkage)
	}
	return out, nil
}

// contextAffinity returns an artist's shrunk cell-share minus the user's
// baseline share, clamped to [-1, 1]. The shrinkage pseudo-count k pulls
// low-play artists toward the baseline (→ 0 affinity) so noise can't dominate;
// a heavily-played artist keeps close to its raw over/under-representation.
func contextAffinity(cellPlays, totalPlays, baseline, k float64) float64 {
	if totalPlays == 0 {
		return 0
	}
	shrunk := (cellPlays + baseline*k) / (totalPlays + k)
	return clampUnit(shrunk - baseline)
}
