// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.31.1
// source: system_playlists.sql

package dbq

import (
	"context"

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

const clearStaleSystemPlaylistInFlight = `-- name: ClearStaleSystemPlaylistInFlight :exec
UPDATE system_playlist_runs SET in_flight = false WHERE in_flight = true
`

// Cron startup recovery: clear any in_flight=true rows from a previously
// crashed process. Safe because at startup, by definition, nothing is
// still building.
func (q *Queries) ClearStaleSystemPlaylistInFlight(ctx context.Context) error {
	_, err := q.db.Exec(ctx, clearStaleSystemPlaylistInFlight)
	return err
}

const createSystemPlaylist = `-- name: CreateSystemPlaylist :one
INSERT INTO playlists (
    user_id, name, description, is_public,
    kind, system_variant, seed_artist_id, cover_path
) VALUES ($1, $2, '', false, 'system', $3, $4, $5)
RETURNING id, user_id, name, description, is_public, cover_path, track_count, duration_sec, created_at, updated_at, kind, system_variant, seed_artist_id
`

type CreateSystemPlaylistParams struct {
	UserID        pgtype.UUID
	Name          string
	SystemVariant *string
	SeedArtistID  pgtype.UUID
	CoverPath     *string
}

// Inserts a system-generated playlist. Used by BuildSystemPlaylists.
// For 'for_you' variant, pass seed_artist_id as NULL (zero-value pgtype.UUID
// with Valid=false).
func (q *Queries) CreateSystemPlaylist(ctx context.Context, arg CreateSystemPlaylistParams) (Playlist, error) {
	row := q.db.QueryRow(ctx, createSystemPlaylist,
		arg.UserID,
		arg.Name,
		arg.SystemVariant,
		arg.SeedArtistID,
		arg.CoverPath,
	)
	var i Playlist
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.Name,
		&i.Description,
		&i.IsPublic,
		&i.CoverPath,
		&i.TrackCount,
		&i.DurationSec,
		&i.CreatedAt,
		&i.UpdatedAt,
		&i.Kind,
		&i.SystemVariant,
		&i.SeedArtistID,
	)
	return i, err
}

const deleteSystemPlaylistsForUser = `-- name: DeleteSystemPlaylistsForUser :exec
DELETE FROM playlists WHERE user_id = $1 AND kind = 'system'
`

// Atomic-replace step. CASCADE on playlist_tracks.playlist_id handles
// the join rows.
func (q *Queries) DeleteSystemPlaylistsForUser(ctx context.Context, userID pgtype.UUID) error {
	_, err := q.db.Exec(ctx, deleteSystemPlaylistsForUser, userID)
	return err
}

const failSystemPlaylistRun = `-- name: FailSystemPlaylistRun :exec
UPDATE system_playlist_runs
   SET in_flight  = false,
       last_error = $2
 WHERE user_id = $1
`

type FailSystemPlaylistRunParams struct {
	UserID    pgtype.UUID
	LastError *string
}

// Mark a failed build complete (in_flight=false, error captured).
func (q *Queries) FailSystemPlaylistRun(ctx context.Context, arg FailSystemPlaylistRunParams) error {
	_, err := q.db.Exec(ctx, failSystemPlaylistRun, arg.UserID, arg.LastError)
	return err
}

const finishSystemPlaylistRun = `-- name: FinishSystemPlaylistRun :exec
UPDATE system_playlist_runs
   SET last_run_at   = $2,
       last_run_date = $3,
       in_flight     = false,
       last_error    = NULL
 WHERE user_id = $1
`

type FinishSystemPlaylistRunParams struct {
	UserID      pgtype.UUID
	LastRunAt   pgtype.Timestamptz
	LastRunDate pgtype.Date
}

// Mark a successful build complete.
func (q *Queries) FinishSystemPlaylistRun(ctx context.Context, arg FinishSystemPlaylistRunParams) error {
	_, err := q.db.Exec(ctx, finishSystemPlaylistRun, arg.UserID, arg.LastRunAt, arg.LastRunDate)
	return err
}

const getSystemPlaylistByVariantForUser = `-- name: GetSystemPlaylistByVariantForUser :one
SELECT p.id, p.user_id, p.name, p.kind, p.system_variant,
       p.seed_artist_id, p.cover_path, p.track_count
  FROM playlists p
 WHERE p.user_id = $1
   AND p.kind = 'system'
   AND p.system_variant = $2
 ORDER BY p.created_at DESC
 LIMIT 1
`

type GetSystemPlaylistByVariantForUserParams struct {
	UserID        pgtype.UUID
	SystemVariant *string
}

type GetSystemPlaylistByVariantForUserRow struct {
	ID            pgtype.UUID
	UserID        pgtype.UUID
	Name          string
	Kind          string
	SystemVariant *string
	SeedArtistID  pgtype.UUID
	CoverPath     *string
	TrackCount    int32
}

// Returns the user's system playlist for the given variant
// ('for_you' / 'songs_like_artist' / 'discover'), or pgx.ErrNoRows
// when the user hasn't built that variant yet. For
// 'songs_like_artist' (which can have multiple rows per user, one
// per seed), this returns whichever LIMIT 1 picks — callers that
// need all instances should use a different query.
func (q *Queries) GetSystemPlaylistByVariantForUser(ctx context.Context, arg GetSystemPlaylistByVariantForUserParams) (GetSystemPlaylistByVariantForUserRow, error) {
	row := q.db.QueryRow(ctx, getSystemPlaylistByVariantForUser, arg.UserID, arg.SystemVariant)
	var i GetSystemPlaylistByVariantForUserRow
	err := row.Scan(
		&i.ID,
		&i.UserID,
		&i.Name,
		&i.Kind,
		&i.SystemVariant,
		&i.SeedArtistID,
		&i.CoverPath,
		&i.TrackCount,
	)
	return i, err
}

const getSystemPlaylistRun = `-- name: GetSystemPlaylistRun :one
SELECT user_id, last_run_at, last_run_date, in_flight, last_error
  FROM system_playlist_runs
 WHERE user_id = $1
`

func (q *Queries) GetSystemPlaylistRun(ctx context.Context, userID pgtype.UUID) (SystemPlaylistRun, error) {
	row := q.db.QueryRow(ctx, getSystemPlaylistRun, userID)
	var i SystemPlaylistRun
	err := row.Scan(
		&i.UserID,
		&i.LastRunAt,
		&i.LastRunDate,
		&i.InFlight,
		&i.LastError,
	)
	return i, err
}

const listActiveUsersForSystemPlaylists = `-- name: ListActiveUsersForSystemPlaylists :many


SELECT u.id FROM users u
 WHERE EXISTS (
     SELECT 1 FROM play_events pe
      WHERE pe.user_id = u.id
        AND pe.started_at > now() - INTERVAL '7 days'
   )
`

// Track picks here join `tracks ... AND t.missing_since IS NULL` (#2523): a
// seed or For-You candidate has to be something that can actually play. Note
// this only affects newly GENERATED playlists — already-stored system
// playlists keep their rows until the next daily rebuild, which is why the
// shared ListPlaylistTracks read path is deliberately left unfiltered (it
// also serves user-curated playlists, where hiding a track the user added
// themselves would be wrong).
// M7 #352 slice 2: system-generated playlist queries.
// Active = had a play in the last 7 days. The cron iterates this list.
func (q *Queries) ListActiveUsersForSystemPlaylists(ctx context.Context) ([]pgtype.UUID, error) {
	rows, err := q.db.Query(ctx, listActiveUsersForSystemPlaylists)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []pgtype.UUID
	for rows.Next() {
		var id pgtype.UUID
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		items = append(items, id)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listPlaylistsByUserAndKind = `-- name: ListPlaylistsByUserAndKind :many
SELECT id, user_id, name, description, is_public, kind, system_variant,
       seed_artist_id, cover_path, track_count, duration_sec,
       created_at, updated_at
  FROM playlists
 WHERE user_id = $1
   AND ($2::text = 'all' OR kind = $2)
 ORDER BY updated_at DESC
`

type ListPlaylistsByUserAndKindParams struct {
	UserID  pgtype.UUID
	Column2 string
}

type ListPlaylistsByUserAndKindRow struct {
	ID            pgtype.UUID
	UserID        pgtype.UUID
	Name          string
	Description   string
	IsPublic      bool
	Kind          string
	SystemVariant *string
	SeedArtistID  pgtype.UUID
	CoverPath     *string
	TrackCount    int32
	DurationSec   int32
	CreatedAt     pgtype.Timestamptz
	UpdatedAt     pgtype.Timestamptz
}

// Used in integration tests; production filter applied in Go
// (api.handleListPlaylists fetches via Service.List and filters by Kind in
// memory so it can also surface other users' public playlists alongside
// the caller's own kind-filtered ones).
// Sorted by updated_at DESC.
func (q *Queries) ListPlaylistsByUserAndKind(ctx context.Context, arg ListPlaylistsByUserAndKindParams) ([]ListPlaylistsByUserAndKindRow, error) {
	rows, err := q.db.Query(ctx, listPlaylistsByUserAndKind, arg.UserID, arg.Column2)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListPlaylistsByUserAndKindRow
	for rows.Next() {
		var i ListPlaylistsByUserAndKindRow
		if err := rows.Scan(
			&i.ID,
			&i.UserID,
			&i.Name,
			&i.Description,
			&i.IsPublic,
			&i.Kind,
			&i.SystemVariant,
			&i.SeedArtistID,
			&i.CoverPath,
			&i.TrackCount,
			&i.DurationSec,
			&i.CreatedAt,
			&i.UpdatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const pickSeedArtists = `-- name: PickSeedArtists :many
WITH liked AS (
    SELECT gla.artist_id FROM general_likes_artists gla WHERE gla.user_id = $1
),
recent7 AS (
    SELECT t.artist_id,
           COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count,
           0 AS tier
      FROM play_events pe
      JOIN tracks t ON t.id = pe.track_id AND t.missing_since IS NULL
     WHERE pe.user_id = $1
       AND pe.started_at > now() - INTERVAL '7 days'
       AND t.artist_id IS NOT NULL
     GROUP BY t.artist_id
),
recent30 AS (
    SELECT t.artist_id,
           COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count,
           1 AS tier
      FROM play_events pe
      JOIN tracks t ON t.id = pe.track_id AND t.missing_since IS NULL
     WHERE pe.user_id = $1
       AND pe.started_at > now() - INTERVAL '30 days'
       AND t.artist_id IS NOT NULL
     GROUP BY t.artist_id
),
alltime AS (
    SELECT t.artist_id,
           COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count,
           2 AS tier
      FROM play_events pe
      JOIN tracks t ON t.id = pe.track_id AND t.missing_since IS NULL
     WHERE pe.user_id = $1
       AND t.artist_id IS NOT NULL
     GROUP BY t.artist_id
),
likedonly AS (
    SELECT l.artist_id, 0::bigint AS play_count, 3 AS tier
      FROM liked l
),
chosen AS (
    SELECT artist_id, play_count, tier FROM recent7
    UNION ALL
    SELECT artist_id, play_count, tier FROM recent30
     WHERE NOT EXISTS (SELECT 1 FROM recent7)
    UNION ALL
    SELECT artist_id, play_count, tier FROM alltime
     WHERE NOT EXISTS (SELECT 1 FROM recent7)
       AND NOT EXISTS (SELECT 1 FROM recent30)
    UNION ALL
    SELECT artist_id, play_count, tier FROM likedonly
     WHERE NOT EXISTS (SELECT 1 FROM recent7)
       AND NOT EXISTS (SELECT 1 FROM recent30)
       AND NOT EXISTS (SELECT 1 FROM alltime)
)
SELECT c.artist_id,
       (c.play_count + CASE WHEN l.artist_id IS NOT NULL THEN 5 ELSE 0 END)::bigint AS score,
       c.tier::int AS tier
  FROM chosen c
  LEFT JOIN liked l ON l.artist_id = c.artist_id
 ORDER BY score DESC, c.artist_id
 LIMIT 12
`

type PickSeedArtistsRow struct {
	ArtistID pgtype.UUID
	Score    int64
	Tier     int32
}

// Top-12 most-engaged distinct artist candidates, tiered so the
// "Songs like X" mixes never silently vanish (#1255): the old hard
// 7-day window emptied the seed pool after a quiet week, and the
// daily atomic-replace build then deleted every existing mix until
// the user played something again. Same fallback shape as
// PickTopPlayedTracksForUser (For You's seeds):
//
//	tier 0  engagement in the last 7 days
//	tier 1  (only if tier 0 empty) last 30 days
//	tier 2  (only if tiers 0+1 empty) all-time
//	tier 3  (only if 0-2 empty) liked artists with no play history
//
// All returned rows share one tier; produceSeedMixes maps it onto the
// rule-#131 pick-kind ladder and stamps the built tracks, so metrics
// can compare mixes seeded from fresh vs stale engagement.
// Score = unskipped-play count + 5 if user has liked the artist. The
// Go-side picker (pickSeedArtistsForDay) shuffles the 12
// daily-deterministically and takes songsLikeSeedCount (6) so the mix
// set both rotates day-to-day and fills the dedicated Home row (#1491).
func (q *Queries) PickSeedArtists(ctx context.Context, userID pgtype.UUID) ([]PickSeedArtistsRow, error) {
	rows, err := q.db.Query(ctx, pickSeedArtists, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []PickSeedArtistsRow
	for rows.Next() {
		var i PickSeedArtistsRow
		if err := rows.Scan(&i.ArtistID, &i.Score, &i.Tier); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const pickTopAlbumCoverForArtistByUser = `-- name: PickTopAlbumCoverForArtistByUser :one
SELECT a.cover_art_path
  FROM albums a
  LEFT JOIN tracks t ON t.album_id = a.id
  LEFT JOIN play_events pe
    ON pe.track_id = t.id
   AND pe.user_id = $1
   AND pe.was_skipped = false
 WHERE a.artist_id = $2
   AND a.cover_art_path IS NOT NULL
 GROUP BY a.id, a.release_date, a.cover_art_path
 ORDER BY COUNT(pe.id) DESC, a.release_date DESC NULLS LAST, a.id
 LIMIT 1
`

type PickTopAlbumCoverForArtistByUserParams struct {
	UserID   pgtype.UUID
	ArtistID pgtype.UUID
}

// "Songs like X" cover. Most-played album by user; tie-break by most-recent
// release. NULL if the artist has no albums or all covers are NULL.
// Note: albums uses cover_art_path (not cover_path).
func (q *Queries) PickTopAlbumCoverForArtistByUser(ctx context.Context, arg PickTopAlbumCoverForArtistByUserParams) (*string, error) {
	row := q.db.QueryRow(ctx, pickTopAlbumCoverForArtistByUser, arg.UserID, arg.ArtistID)
	var cover_art_path *string
	err := row.Scan(&cover_art_path)
	return cover_art_path, err
}

const pickTopPlayedTrackForArtistByUser = `-- name: PickTopPlayedTrackForArtistByUser :one
SELECT COALESCE(
    (SELECT t.id
       FROM play_events pe
       JOIN tracks t ON t.id = pe.track_id AND t.missing_since IS NULL
      WHERE pe.user_id = $1
        AND t.artist_id = $2
        AND pe.started_at > now() - INTERVAL '7 days'
        AND pe.was_skipped = false
      GROUP BY t.id
      ORDER BY COUNT(*) DESC, t.id
      LIMIT 1),
    (SELECT t.id
       FROM tracks t
       JOIN albums a ON a.id = t.album_id
      WHERE t.artist_id = $2
      ORDER BY a.release_date DESC NULLS LAST,
               t.disc_number NULLS LAST,
               t.track_number NULLS LAST,
               t.id
      LIMIT 1)
)::uuid AS id
`

type PickTopPlayedTrackForArtistByUserParams struct {
	UserID   pgtype.UUID
	ArtistID pgtype.UUID
}

// "Songs like X" seed selection. Returns the user's most-played non-skipped
// track by artist X in the last 7 days. Falls back to the artist's most-
// recent album's first track if no plays exist.
// Cast to uuid so sqlc infers pgtype.UUID rather than interface{}.
func (q *Queries) PickTopPlayedTrackForArtistByUser(ctx context.Context, arg PickTopPlayedTrackForArtistByUserParams) (pgtype.UUID, error) {
	row := q.db.QueryRow(ctx, pickTopPlayedTrackForArtistByUser, arg.UserID, arg.ArtistID)
	var id pgtype.UUID
	err := row.Scan(&id)
	return id, err
}

const pickTopPlayedTracksForUser = `-- name: PickTopPlayedTracksForUser :many
WITH recent AS (
    SELECT t.id, COUNT(*) AS c, 0 AS tier
      FROM play_events pe
      JOIN tracks t ON t.id = pe.track_id AND t.missing_since IS NULL
     WHERE pe.user_id = $1
       AND pe.started_at > now() - INTERVAL '30 days'
       AND pe.was_skipped = false
     GROUP BY t.id
),
alltime AS (
    SELECT t.id, COUNT(*) AS c, 1 AS tier
      FROM play_events pe
      JOIN tracks t ON t.id = pe.track_id AND t.missing_since IS NULL
     WHERE pe.user_id = $1
       AND pe.was_skipped = false
     GROUP BY t.id
),
liked AS (
    SELECT gl.track_id AS id, 0::bigint AS c, 2 AS tier
      FROM general_likes gl
     WHERE gl.user_id = $1
),
chosen AS (
    SELECT id, c, tier FROM recent
    UNION ALL
    SELECT id, c, tier FROM alltime
     WHERE NOT EXISTS (SELECT 1 FROM recent)
    UNION ALL
    SELECT id, c, tier FROM liked
     WHERE NOT EXISTS (SELECT 1 FROM recent)
       AND NOT EXISTS (SELECT 1 FROM alltime)
)
SELECT id
  FROM chosen
 ORDER BY tier, c DESC, id
 LIMIT 5
`

// For-You candidate seeds, tiered so For-You never silently vanishes:
//
//	tier 0  top non-skip plays in the last 30 days
//	tier 1  (only if tier 0 empty) all-time top non-skip plays
//	tier 2  (only if tiers 0+1 empty) liked tracks
//
// Returns up to 5 ids; tie-break by track_id for determinism. The
// Go-side picker (pickDailySeeds) draws the day's seeds from these.
// Widened from a hard 7-day window, which made For-You disappear
// after a week of not listening and never recover on a self-hosted
// library with sparse history.
func (q *Queries) PickTopPlayedTracksForUser(ctx context.Context, userID pgtype.UUID) ([]pgtype.UUID, error) {
	rows, err := q.db.Query(ctx, pickTopPlayedTracksForUser, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []pgtype.UUID
	for rows.Next() {
		var id pgtype.UUID
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		items = append(items, id)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const tryClaimSystemPlaylistRun = `-- name: TryClaimSystemPlaylistRun :one
INSERT INTO system_playlist_runs (user_id, in_flight)
VALUES ($1, true)
ON CONFLICT (user_id) DO UPDATE
   SET in_flight = true
   WHERE system_playlist_runs.in_flight = false
RETURNING user_id, last_run_at, last_run_date, in_flight, last_error
`

// Atomic test-and-set on in_flight. Returns the row if we won the claim,
// or pgx.ErrNoRows if someone else is already building.
func (q *Queries) TryClaimSystemPlaylistRun(ctx context.Context, userID pgtype.UUID) (SystemPlaylistRun, error) {
	row := q.db.QueryRow(ctx, tryClaimSystemPlaylistRun, userID)
	var i SystemPlaylistRun
	err := row.Scan(
		&i.UserID,
		&i.LastRunAt,
		&i.LastRunDate,
		&i.InFlight,
		&i.LastError,
	)
	return i, err
}
