// Code generated by sqlc. DO NOT EDIT.
// versions:
//   sqlc v1.30.0
// source: tagging_items.sql

package sqlcgen

import (
	"context"
	"database/sql"
	"time"
)

const clearCompletedTaggingItems = `-- name: ClearCompletedTaggingItems :exec
UPDATE tagging_items
SET cleared_at = CURRENT_TIMESTAMP
WHERE status = 'confirmed'
  AND cleared_at IS NULL
  AND (CAST(?1 AS INTEGER) = 0 OR library_id = ?1)
`

func (q *Queries) ClearCompletedTaggingItems(ctx context.Context, libraryID int64) error {
	_, err := q.db.ExecContext(ctx, clearCompletedTaggingItems, libraryID)
	return err
}

const countPendingTaggingItems = `-- name: CountPendingTaggingItems :one
SELECT COUNT(*) FROM tagging_items ti
WHERE ti.status = 'pending'
  AND (CAST(?1 AS INTEGER) = 0 OR ti.library_id = ?1)
  AND EXISTS (
    SELECT 1 FROM audio_files af
    WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
  )
`

// "Needs tagging" is a question about the files, not about the row:
// every scanned folder gets a tagging_items row (see
// UpsertTaggingItemOnTrackAdd), including one whose files all arrived
// carrying a recording MBID.  Without the EXISTS a fully MB-tagged
// library reports its entire album count as pending work.  See the
// same predicate on the three list queries below.
func (q *Queries) CountPendingTaggingItems(ctx context.Context, libraryID int64) (int64, error) {
	row := q.db.QueryRowContext(ctx, countPendingTaggingItems, libraryID)
	var count int64
	err := row.Scan(&count)
	return count, err
}

const decrementTaggingItemTrackCount = `-- name: DecrementTaggingItemTrackCount :exec
UPDATE tagging_items
SET track_count = track_count - 1
WHERE group_key = ?
`

func (q *Queries) DecrementTaggingItemTrackCount(ctx context.Context, groupKey string) error {
	_, err := q.db.ExecContext(ctx, decrementTaggingItemTrackCount, groupKey)
	return err
}

const deleteTaggingItemIfEmpty = `-- name: DeleteTaggingItemIfEmpty :exec
DELETE FROM tagging_items
WHERE group_key = ? AND track_count <= 0
`

func (q *Queries) DeleteTaggingItemIfEmpty(ctx context.Context, groupKey string) error {
	_, err := q.db.ExecContext(ctx, deleteTaggingItemIfEmpty, groupKey)
	return err
}

const getNextPendingTaggingItem = `-- name: GetNextPendingTaggingItem :one
SELECT
  ti.group_key,
  ti.library_id,
  COALESCE(lb.name, '') AS library_name,
  ti.track_count,
  ti.album_name,
  ti.album_artist,
  ti.disc_number,
  ti.best_match_release_mbid,
  ti.score,
  ti.last_checked_at,
  ti.status,
  ti.created_at
FROM tagging_items ti
LEFT JOIN libraries lb ON lb.id = ti.library_id
WHERE ti.status = 'pending'
  AND (CAST(?1 AS INTEGER) = 0 OR ti.library_id = ?1)
  AND ti.group_key > ?2
  -- See CountPendingTaggingItems: the cursor must not stop on a
  -- folder the list query no longer shows, or "next" walks folders
  -- that are not in the sidebar.
  AND EXISTS (
    SELECT 1 FROM audio_files af
    WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
  )
ORDER BY ti.group_key
LIMIT 1
`

type GetNextPendingTaggingItemParams struct {
	LibraryID     int64
	AfterGroupKey string
}

type GetNextPendingTaggingItemRow struct {
	GroupKey             string
	LibraryID            int64
	LibraryName          string
	TrackCount           int64
	AlbumName            string
	AlbumArtist          string
	DiscNumber           int64
	BestMatchReleaseMbid sql.NullString
	Score                sql.NullFloat64
	LastCheckedAt        sql.NullTime
	Status               string
	CreatedAt            time.Time
}

func (q *Queries) GetNextPendingTaggingItem(ctx context.Context, arg GetNextPendingTaggingItemParams) (GetNextPendingTaggingItemRow, error) {
	row := q.db.QueryRowContext(ctx, getNextPendingTaggingItem, arg.LibraryID, arg.AfterGroupKey)
	var i GetNextPendingTaggingItemRow
	err := row.Scan(
		&i.GroupKey,
		&i.LibraryID,
		&i.LibraryName,
		&i.TrackCount,
		&i.AlbumName,
		&i.AlbumArtist,
		&i.DiscNumber,
		&i.BestMatchReleaseMbid,
		&i.Score,
		&i.LastCheckedAt,
		&i.Status,
		&i.CreatedAt,
	)
	return i, err
}

const getPendingFolderDetail = `-- name: GetPendingFolderDetail :one
SELECT
  ti.group_key,
  ti.library_id,
  COALESCE(lb.name, '') AS library_name,
  COALESCE(lb.path, '') AS library_path,
  CAST(COALESCE((SELECT af.file_path FROM audio_files af WHERE af.group_key = ti.group_key AND af.group_key != '' LIMIT 1), '') AS TEXT) AS sample_file_path,
  ti.track_count,
  ti.album_name,
  ti.album_artist,
  ti.disc_number,
  ti.best_match_release_mbid,
  ti.score,
  ti.last_checked_at,
  ti.status,
  ti.created_at,
  ti.synthetic
FROM tagging_items ti
LEFT JOIN libraries lb ON lb.id = ti.library_id
WHERE ti.group_key = ?
LIMIT 1
`

type GetPendingFolderDetailRow struct {
	GroupKey             string
	LibraryID            int64
	LibraryName          string
	LibraryPath          string
	SampleFilePath       string
	TrackCount           int64
	AlbumName            string
	AlbumArtist          string
	DiscNumber           int64
	BestMatchReleaseMbid sql.NullString
	Score                sql.NullFloat64
	LastCheckedAt        sql.NullTime
	Status               string
	CreatedAt            time.Time
	Synthetic            int64
}

func (q *Queries) GetPendingFolderDetail(ctx context.Context, groupKey string) (GetPendingFolderDetailRow, error) {
	row := q.db.QueryRowContext(ctx, getPendingFolderDetail, groupKey)
	var i GetPendingFolderDetailRow
	err := row.Scan(
		&i.GroupKey,
		&i.LibraryID,
		&i.LibraryName,
		&i.LibraryPath,
		&i.SampleFilePath,
		&i.TrackCount,
		&i.AlbumName,
		&i.AlbumArtist,
		&i.DiscNumber,
		&i.BestMatchReleaseMbid,
		&i.Score,
		&i.LastCheckedAt,
		&i.Status,
		&i.CreatedAt,
		&i.Synthetic,
	)
	return i, err
}

const getTaggingItem = `-- name: GetTaggingItem :one
SELECT group_key, library_id, track_count, album_name, album_artist, disc_number, best_match_release_mbid, score, last_checked_at, status, cleared_at, created_at, synthetic, parent_group_key, album_artist_conflict FROM tagging_items
WHERE group_key = ?
LIMIT 1
`

func (q *Queries) GetTaggingItem(ctx context.Context, groupKey string) (TaggingItem, error) {
	row := q.db.QueryRowContext(ctx, getTaggingItem, groupKey)
	var i TaggingItem
	err := row.Scan(
		&i.GroupKey,
		&i.LibraryID,
		&i.TrackCount,
		&i.AlbumName,
		&i.AlbumArtist,
		&i.DiscNumber,
		&i.BestMatchReleaseMbid,
		&i.Score,
		&i.LastCheckedAt,
		&i.Status,
		&i.ClearedAt,
		&i.CreatedAt,
		&i.Synthetic,
		&i.ParentGroupKey,
		&i.AlbumArtistConflict,
	)
	return i, err
}

const getTaggingItemsForAlbum = `-- name: GetTaggingItemsForAlbum :many
SELECT
  ti.group_key,
  ti.status,
  ti.score,
  ti.best_match_release_mbid,
  ti.track_count,
  ti.album_name,
  ti.album_artist,
  ti.synthetic
FROM tagging_items ti
WHERE ti.group_key IN (
    SELECT DISTINCT af.group_key
    FROM audio_files af
    WHERE af.album_id = ?1 AND af.group_key != ''
  )
  AND ti.cleared_at IS NULL
ORDER BY ti.score IS NULL, ti.score DESC, ti.group_key
`

type GetTaggingItemsForAlbumRow struct {
	GroupKey             string
	Status               string
	Score                sql.NullFloat64
	BestMatchReleaseMbid sql.NullString
	TrackCount           int64
	AlbumName            string
	AlbumArtist          string
	Synthetic            int64
}

// Every tagging group holding a file of this album.
//
// The join is `audio_files.group_key`, not a key derived from the
// album's folder path: a group carved out of a mixed-bag folder by
// SplitMixedFolder is keyed on its tags rather than on a directory,
// so a path-derived key finds nothing for exactly the messiest
// libraries this is meant to help.
//
// Usually one row. A multi-disc album is one group per disc, which
// the caller has to know about rather than average over -- applying
// to "the album" would silently retag one disc of three.
// Best first, with an unscored group last rather than first: NULL
// sorts low in SQLite and DESC would put it at the top.
func (q *Queries) GetTaggingItemsForAlbum(ctx context.Context, albumID sql.NullInt64) ([]GetTaggingItemsForAlbumRow, error) {
	rows, err := q.db.QueryContext(ctx, getTaggingItemsForAlbum, albumID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []GetTaggingItemsForAlbumRow
	for rows.Next() {
		var i GetTaggingItemsForAlbumRow
		if err := rows.Scan(
			&i.GroupKey,
			&i.Status,
			&i.Score,
			&i.BestMatchReleaseMbid,
			&i.TrackCount,
			&i.AlbumName,
			&i.AlbumArtist,
			&i.Synthetic,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listAudioFilesInTaggingGroup = `-- name: ListAudioFilesInTaggingGroup :many
SELECT
  af.id,
  af.file_path,
  af.basename,
  af.length_milliseconds,
  af.tag_status,
  COALESCE(af.track_number, 0) AS track_number,
  COALESCE(af.disc_number, 0) AS disc_number,
  af.title,
  af.artist_credit AS artist_name,
  COALESCE(af.recording_mbid, '') AS recording_mbid,
  COALESCE(al.name, '') AS album_name,
  COALESCE(al.artist_credit, '') AS album_artist
FROM audio_files af
LEFT JOIN albums al ON al.id = af.album_id
WHERE af.group_key = ?
ORDER BY COALESCE(af.disc_number, 0),
         COALESCE(af.track_number, 0),
         af.file_path
`

type ListAudioFilesInTaggingGroupRow struct {
	ID                 int64
	FilePath           string
	Basename           string
	LengthMilliseconds int64
	TagStatus          string
	TrackNumber        int64
	DiscNumber         int64
	Title              string
	ArtistName         string
	RecordingMbid      string
	AlbumName          string
	AlbumArtist        string
}

// album_name/album_artist are the PER-TRACK tags (each file's own
// album link), not the folder-level tagging_items values.
// SplitMixedFolder clusters on these to find sub-albums hiding inside
// a folder full of unrelated tracks.
func (q *Queries) ListAudioFilesInTaggingGroup(ctx context.Context, groupKey string) ([]ListAudioFilesInTaggingGroupRow, error) {
	rows, err := q.db.QueryContext(ctx, listAudioFilesInTaggingGroup, groupKey)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListAudioFilesInTaggingGroupRow
	for rows.Next() {
		var i ListAudioFilesInTaggingGroupRow
		if err := rows.Scan(
			&i.ID,
			&i.FilePath,
			&i.Basename,
			&i.LengthMilliseconds,
			&i.TagStatus,
			&i.TrackNumber,
			&i.DiscNumber,
			&i.Title,
			&i.ArtistName,
			&i.RecordingMbid,
			&i.AlbumName,
			&i.AlbumArtist,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listLikelyMixedBagGroupKeys = `-- name: ListLikelyMixedBagGroupKeys :many
SELECT ti.group_key
FROM tagging_items ti
JOIN audio_files af ON af.group_key = ti.group_key
LEFT JOIN albums rg ON rg.id = af.album_id
WHERE ti.synthetic = 0
  AND ti.track_count >= 4
  AND (
    ti.album_artist = ''
    OR LOWER(TRIM(ti.album_artist)) IN ('various artists', 'various', 'va', 'v.a.', 'v a', 'unknown')
  )
GROUP BY ti.group_key
HAVING COUNT(DISTINCT CASE WHEN af.artist_credit != '' THEN LOWER(TRIM(af.artist_credit)) END) > 1
   AND COUNT(DISTINCT CASE WHEN rg.name != '' THEN LOWER(TRIM(rg.name)) END) > 1
`

// Cheap, whole-library triage pass for autotag.IsMixedBag: one
// grouped scan over audio_files (indexed on group_key) rather than
// hydrating every group's full track list in Go.  LOWER/TRIM is an
// approximation of autotag.Normalize (no unicode fold, no qualifier
// stripping) so this can flag a false positive Normalize would
// clear, or miss a true one Normalize would catch.  Treat it as a
// triage filter for which groups are worth a real autotag.
// IsMixedBag check, or a badge at minimum, not the final word.
func (q *Queries) ListLikelyMixedBagGroupKeys(ctx context.Context) ([]string, error) {
	rows, err := q.db.QueryContext(ctx, listLikelyMixedBagGroupKeys)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []string
	for rows.Next() {
		var group_key string
		if err := rows.Scan(&group_key); err != nil {
			return nil, err
		}
		items = append(items, group_key)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listLocalAlbumCandidates = `-- name: ListLocalAlbumCandidates :many
SELECT
  al.id AS album_id,
  al.mbid AS album_mbid,
  al.name AS album_name,
  COALESCE(al.year, 0) AS year,
  al.artist_credit,
  COALESCE(af.track_number, 0) AS track_number,
  COALESCE(af.disc_number, 0) AS disc_number,
  af.title AS track_title,
  COALESCE(af.recording_mbid, '') AS recording_mbid,
  af.length_milliseconds
FROM albums al
JOIN audio_files af ON af.album_id = al.id
WHERE al.mbid IS NOT NULL
  AND al.mbid != ''
  AND af.recording_mbid IS NOT NULL
  AND af.recording_mbid != ''
  AND al.name = ? COLLATE NOCASE
ORDER BY al.id, af.disc_number, af.track_number
`

type ListLocalAlbumCandidatesRow struct {
	AlbumID            int64
	AlbumMbid          sql.NullString
	AlbumName          string
	Year               int64
	ArtistCredit       string
	TrackNumber        int64
	DiscNumber         int64
	TrackTitle         string
	RecordingMbid      string
	LengthMilliseconds int64
}

// One row per (album, track) for any local album carrying an MBID.
// Callers group these in Go and filter by normalized album-name match;
// the join is case-insensitive on name to pre-filter cheaply.
func (q *Queries) ListLocalAlbumCandidates(ctx context.Context, name string) ([]ListLocalAlbumCandidatesRow, error) {
	rows, err := q.db.QueryContext(ctx, listLocalAlbumCandidates, name)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListLocalAlbumCandidatesRow
	for rows.Next() {
		var i ListLocalAlbumCandidatesRow
		if err := rows.Scan(
			&i.AlbumID,
			&i.AlbumMbid,
			&i.AlbumName,
			&i.Year,
			&i.ArtistCredit,
			&i.TrackNumber,
			&i.DiscNumber,
			&i.TrackTitle,
			&i.RecordingMbid,
			&i.LengthMilliseconds,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listPendingTaggingItemsAlphabetical = `-- name: ListPendingTaggingItemsAlphabetical :many
SELECT
  ti.group_key,
  ti.library_id,
  COALESCE(lb.name, '') AS library_name,
  ti.track_count,
  ti.album_name,
  ti.album_artist,
  ti.disc_number,
  ti.best_match_release_mbid,
  ti.score,
  ti.last_checked_at,
  ti.status,
  ti.created_at
FROM tagging_items ti
LEFT JOIN libraries lb ON lb.id = ti.library_id
WHERE (CAST(?1 AS INTEGER) = 0 OR ti.library_id = ?1)
  AND (CAST(?2 AS TEXT) = 'all' OR ti.status = ?2)
  AND ti.cleared_at IS NULL
  -- Actionable rows must have something to act on: see
  -- CountPendingTaggingItems.  Reviewed rows (confirmed/skipped) are
  -- exempt because they are history, not work -- an applied folder is
  -- fully tagged by definition and would otherwise vanish from the
  -- sidebar's Completed section the instant it succeeded.
  AND (
    ti.status IN ('confirmed', 'skipped')
    OR EXISTS (
      SELECT 1 FROM audio_files af
      WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
    )
  )
ORDER BY LOWER(ti.album_artist), LOWER(ti.album_name), ti.disc_number
LIMIT ?4 OFFSET ?3
`

type ListPendingTaggingItemsAlphabeticalParams struct {
	LibraryID    int64
	StatusFilter string
	RowOffset    int64
	RowLimit     int64
}

type ListPendingTaggingItemsAlphabeticalRow struct {
	GroupKey             string
	LibraryID            int64
	LibraryName          string
	TrackCount           int64
	AlbumName            string
	AlbumArtist          string
	DiscNumber           int64
	BestMatchReleaseMbid sql.NullString
	Score                sql.NullFloat64
	LastCheckedAt        sql.NullTime
	Status               string
	CreatedAt            time.Time
}

func (q *Queries) ListPendingTaggingItemsAlphabetical(ctx context.Context, arg ListPendingTaggingItemsAlphabeticalParams) ([]ListPendingTaggingItemsAlphabeticalRow, error) {
	rows, err := q.db.QueryContext(ctx, listPendingTaggingItemsAlphabetical,
		arg.LibraryID,
		arg.StatusFilter,
		arg.RowOffset,
		arg.RowLimit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListPendingTaggingItemsAlphabeticalRow
	for rows.Next() {
		var i ListPendingTaggingItemsAlphabeticalRow
		if err := rows.Scan(
			&i.GroupKey,
			&i.LibraryID,
			&i.LibraryName,
			&i.TrackCount,
			&i.AlbumName,
			&i.AlbumArtist,
			&i.DiscNumber,
			&i.BestMatchReleaseMbid,
			&i.Score,
			&i.LastCheckedAt,
			&i.Status,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listPendingTaggingItemsByRecent = `-- name: ListPendingTaggingItemsByRecent :many
SELECT
  ti.group_key,
  ti.library_id,
  COALESCE(lb.name, '') AS library_name,
  ti.track_count,
  ti.album_name,
  ti.album_artist,
  ti.disc_number,
  ti.best_match_release_mbid,
  ti.score,
  ti.last_checked_at,
  ti.status,
  ti.created_at
FROM tagging_items ti
LEFT JOIN libraries lb ON lb.id = ti.library_id
WHERE (CAST(?1 AS INTEGER) = 0 OR ti.library_id = ?1)
  AND (CAST(?2 AS TEXT) = 'all' OR ti.status = ?2)
ORDER BY ti.created_at DESC, ti.group_key
LIMIT ?4 OFFSET ?3
`

type ListPendingTaggingItemsByRecentParams struct {
	LibraryID    int64
	StatusFilter string
	RowOffset    int64
	RowLimit     int64
}

type ListPendingTaggingItemsByRecentRow struct {
	GroupKey             string
	LibraryID            int64
	LibraryName          string
	TrackCount           int64
	AlbumName            string
	AlbumArtist          string
	DiscNumber           int64
	BestMatchReleaseMbid sql.NullString
	Score                sql.NullFloat64
	LastCheckedAt        sql.NullTime
	Status               string
	CreatedAt            time.Time
}

func (q *Queries) ListPendingTaggingItemsByRecent(ctx context.Context, arg ListPendingTaggingItemsByRecentParams) ([]ListPendingTaggingItemsByRecentRow, error) {
	rows, err := q.db.QueryContext(ctx, listPendingTaggingItemsByRecent,
		arg.LibraryID,
		arg.StatusFilter,
		arg.RowOffset,
		arg.RowLimit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListPendingTaggingItemsByRecentRow
	for rows.Next() {
		var i ListPendingTaggingItemsByRecentRow
		if err := rows.Scan(
			&i.GroupKey,
			&i.LibraryID,
			&i.LibraryName,
			&i.TrackCount,
			&i.AlbumName,
			&i.AlbumArtist,
			&i.DiscNumber,
			&i.BestMatchReleaseMbid,
			&i.Score,
			&i.LastCheckedAt,
			&i.Status,
			&i.CreatedAt,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listPendingTaggingItemsByScore = `-- name: ListPendingTaggingItemsByScore :many
SELECT
  ti.group_key,
  ti.library_id,
  COALESCE(lb.name, '') AS library_name,
  COALESCE(lb.path, '') AS library_path,
  CAST(COALESCE((SELECT af.file_path FROM audio_files af WHERE af.group_key = ti.group_key AND af.group_key != '' LIMIT 1), '') AS TEXT) AS sample_file_path,
  ti.track_count,
  ti.album_name,
  ti.album_artist,
  ti.disc_number,
  ti.best_match_release_mbid,
  ti.score,
  ti.last_checked_at,
  ti.status,
  ti.created_at,
  ti.synthetic
FROM tagging_items ti
LEFT JOIN libraries lb ON lb.id = ti.library_id
WHERE (CAST(?1 AS INTEGER) = 0 OR ti.library_id = ?1)
  AND (CAST(?2 AS TEXT) = 'all' OR ti.status = ?2)
  AND ti.cleared_at IS NULL
  -- See ListPendingTaggingItemsAlphabetical.
  AND (
    ti.status IN ('confirmed', 'skipped')
    OR EXISTS (
      SELECT 1 FROM audio_files af
      WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
    )
  )
ORDER BY ti.score IS NULL, ti.score DESC, LOWER(ti.album_artist), LOWER(ti.album_name)
LIMIT ?4 OFFSET ?3
`

type ListPendingTaggingItemsByScoreParams struct {
	LibraryID    int64
	StatusFilter string
	RowOffset    int64
	RowLimit     int64
}

type ListPendingTaggingItemsByScoreRow struct {
	GroupKey             string
	LibraryID            int64
	LibraryName          string
	LibraryPath          string
	SampleFilePath       string
	TrackCount           int64
	AlbumName            string
	AlbumArtist          string
	DiscNumber           int64
	BestMatchReleaseMbid sql.NullString
	Score                sql.NullFloat64
	LastCheckedAt        sql.NullTime
	Status               string
	CreatedAt            time.Time
	Synthetic            int64
}

func (q *Queries) ListPendingTaggingItemsByScore(ctx context.Context, arg ListPendingTaggingItemsByScoreParams) ([]ListPendingTaggingItemsByScoreRow, error) {
	rows, err := q.db.QueryContext(ctx, listPendingTaggingItemsByScore,
		arg.LibraryID,
		arg.StatusFilter,
		arg.RowOffset,
		arg.RowLimit,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListPendingTaggingItemsByScoreRow
	for rows.Next() {
		var i ListPendingTaggingItemsByScoreRow
		if err := rows.Scan(
			&i.GroupKey,
			&i.LibraryID,
			&i.LibraryName,
			&i.LibraryPath,
			&i.SampleFilePath,
			&i.TrackCount,
			&i.AlbumName,
			&i.AlbumArtist,
			&i.DiscNumber,
			&i.BestMatchReleaseMbid,
			&i.Score,
			&i.LastCheckedAt,
			&i.Status,
			&i.CreatedAt,
			&i.Synthetic,
		); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const markTaggingItemSynthetic = `-- name: MarkTaggingItemSynthetic :exec
UPDATE tagging_items
SET synthetic = 1,
    parent_group_key = ?
WHERE group_key = ?
`

type MarkTaggingItemSyntheticParams struct {
	ParentGroupKey string
	GroupKey       string
}

// Stamps a group as carved out of parent_group_key by
// SplitMixedFolder.  Idempotent: safe to call every time a track
// is migrated into the synthetic group, not just on first creation.
func (q *Queries) MarkTaggingItemSynthetic(ctx context.Context, arg MarkTaggingItemSyntheticParams) error {
	_, err := q.db.ExecContext(ctx, markTaggingItemSynthetic, arg.ParentGroupKey, arg.GroupKey)
	return err
}

const pruneOrphanedTaggingItems = `-- name: PruneOrphanedTaggingItems :exec
DELETE FROM tagging_items
WHERE NOT EXISTS (
  SELECT 1 FROM audio_files af WHERE af.group_key = tagging_items.group_key
)
`

// Self-healing sweep for rows whose track_count bookkeeping (scan
// orphan cleanup, maybeRebindTaggingGroup, SplitMixedFolder) never
// ran or drifted: a cancelled scan, a library move/rename the
// SoftScanAllLibraries disk-count/mtime heuristic did not catch, or
// a decrement that landed without its paired delete. Rather than
// trust track_count, this checks the ground truth directly: any
// group_key no audio_files row still points at is gone, and its
// tagging_items row (and cascaded tagging_candidates) should be too.
// Cheap: one indexed (idx_audio_files_group_key) existence check per
// row. Called opportunistically wherever the pending list is read,
// so stale entries cannot linger indefinitely between full rescans.
func (q *Queries) PruneOrphanedTaggingItems(ctx context.Context) error {
	_, err := q.db.ExecContext(ctx, pruneOrphanedTaggingItems)
	return err
}

const setAudioFileTagStatus = `-- name: SetAudioFileTagStatus :exec
UPDATE audio_files SET tag_status = ? WHERE id = ?
`

type SetAudioFileTagStatusParams struct {
	TagStatus string
	ID        int64
}

func (q *Queries) SetAudioFileTagStatus(ctx context.Context, arg SetAudioFileTagStatusParams) error {
	_, err := q.db.ExecContext(ctx, setAudioFileTagStatus, arg.TagStatus, arg.ID)
	return err
}

const setFileAlbumMBID = `-- name: SetFileAlbumMBID :exec
UPDATE albums SET mbid = ?
WHERE albums.id = (SELECT af.album_id FROM audio_files af WHERE af.id = ?)
`

type SetFileAlbumMBIDParams struct {
	Mbid sql.NullString
	ID   int64
}

// The album MBID for the album a file belongs to.  Keyed by file
// because that is what the autotag apply path holds; under the old
// schema it had to look the release group up through two join tables
// first (GetRecordingReleaseGroupID), which is gone.
func (q *Queries) SetFileAlbumMBID(ctx context.Context, arg SetFileAlbumMBIDParams) error {
	_, err := q.db.ExecContext(ctx, setFileAlbumMBID, arg.Mbid, arg.ID)
	return err
}

const setFileRecordingMBID = `-- name: SetFileRecordingMBID :exec
UPDATE audio_files SET recording_mbid = ? WHERE id = ?
`

type SetFileRecordingMBIDParams struct {
	RecordingMbid sql.NullString
	ID            int64
}

func (q *Queries) SetFileRecordingMBID(ctx context.Context, arg SetFileRecordingMBIDParams) error {
	_, err := q.db.ExecContext(ctx, setFileRecordingMBID, arg.RecordingMbid, arg.ID)
	return err
}

const setTaggingItemBestMatch = `-- name: SetTaggingItemBestMatch :exec
UPDATE tagging_items
SET best_match_release_mbid = ?,
    score = ?,
    status = ?,
    last_checked_at = CURRENT_TIMESTAMP
WHERE group_key = ?
`

type SetTaggingItemBestMatchParams struct {
	BestMatchReleaseMbid sql.NullString
	Score                sql.NullFloat64
	Status               string
	GroupKey             string
}

func (q *Queries) SetTaggingItemBestMatch(ctx context.Context, arg SetTaggingItemBestMatchParams) error {
	_, err := q.db.ExecContext(ctx, setTaggingItemBestMatch,
		arg.BestMatchReleaseMbid,
		arg.Score,
		arg.Status,
		arg.GroupKey,
	)
	return err
}

const setTaggingItemScore = `-- name: SetTaggingItemScore :exec
UPDATE tagging_items
SET best_match_release_mbid = ?,
    score = ?,
    last_checked_at = CURRENT_TIMESTAMP
WHERE group_key = ?
`

type SetTaggingItemScoreParams struct {
	BestMatchReleaseMbid sql.NullString
	Score                sql.NullFloat64
	GroupKey             string
}

func (q *Queries) SetTaggingItemScore(ctx context.Context, arg SetTaggingItemScoreParams) error {
	_, err := q.db.ExecContext(ctx, setTaggingItemScore, arg.BestMatchReleaseMbid, arg.Score, arg.GroupKey)
	return err
}

const setTaggingItemStatus = `-- name: SetTaggingItemStatus :exec
UPDATE tagging_items
SET status = ?,
    last_checked_at = CURRENT_TIMESTAMP
WHERE group_key = ?
`

type SetTaggingItemStatusParams struct {
	Status   string
	GroupKey string
}

func (q *Queries) SetTaggingItemStatus(ctx context.Context, arg SetTaggingItemStatusParams) error {
	_, err := q.db.ExecContext(ctx, setTaggingItemStatus, arg.Status, arg.GroupKey)
	return err
}

const upsertTaggingItemOnTrackAdd = `-- name: UpsertTaggingItemOnTrackAdd :exec
INSERT INTO tagging_items (
  group_key, library_id, track_count,
  album_name, album_artist, disc_number, status
)
VALUES (?, ?, 1, ?, ?, ?, 'pending')
ON CONFLICT(group_key) DO UPDATE SET
  track_count = tagging_items.track_count + 1,
  album_name = CASE
    WHEN tagging_items.album_name = '' THEN excluded.album_name
    ELSE tagging_items.album_name
  END,
  -- Tracks real consensus, not first-write-wins: stays set only
  -- while every track that has contributed a non-empty value agrees.
  -- A later track with a *different* non-empty value clears it back
  -- to '' and latches album_artist_conflict, since a single
  -- disagreeing tag means the folder no longer has one authoritative
  -- album-artist -- IsMixedBag (backend/autotag) treats a non-empty
  -- value here as trusted, so leaving a stale first-seen value in
  -- place would let one track's tag silently blind mixed-bag
  -- detection for the whole folder. The latch (rather than just
  -- clearing the text column) stops a later track from coincidentally
  -- repeating an already-disputed value and resurrecting trust in it.
  album_artist_conflict = CASE
    WHEN tagging_items.album_artist_conflict = 1 THEN 1
    WHEN tagging_items.album_artist != '' AND excluded.album_artist != ''
      AND tagging_items.album_artist != excluded.album_artist THEN 1
    ELSE 0
  END,
  album_artist = CASE
    WHEN tagging_items.album_artist_conflict = 1 THEN ''
    WHEN tagging_items.album_artist != '' AND excluded.album_artist != ''
      AND tagging_items.album_artist != excluded.album_artist THEN ''
    WHEN tagging_items.album_artist = '' THEN excluded.album_artist
    ELSE tagging_items.album_artist
  END
`

type UpsertTaggingItemOnTrackAddParams struct {
	GroupKey    string
	LibraryID   int64
	AlbumName   string
	AlbumArtist string
	DiscNumber  int64
}

func (q *Queries) UpsertTaggingItemOnTrackAdd(ctx context.Context, arg UpsertTaggingItemOnTrackAddParams) error {
	_, err := q.db.ExecContext(ctx, upsertTaggingItemOnTrackAdd,
		arg.GroupKey,
		arg.LibraryID,
		arg.AlbumName,
		arg.AlbumArtist,
		arg.DiscNumber,
	)
	return err
}
