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

package dbq

import (
	"context"
)

const countCandidateArtistTagCoverage = `-- name: CountCandidateArtistTagCoverage :one
SELECT count(*)::bigint AS processed,
       count(*) FILTER (WHERE tag_source <> 'none')::bigint AS with_tags
  FROM candidate_artist_tag_state
`

type CountCandidateArtistTagCoverageRow struct {
	Processed int64
	WithTags  int64
}

// Operator-facing coverage: how many distinct candidates have been processed,
// and how many of those actually yielded tags. The gap is the honest ceiling
// from the task — obscure artists with no MBID presence or no upstream tags
// stay thin no matter how long the worker runs, and that is worth being able
// to see rather than inferring from a silent surface.
func (q *Queries) CountCandidateArtistTagCoverage(ctx context.Context) (CountCandidateArtistTagCoverageRow, error) {
	row := q.db.QueryRow(ctx, countCandidateArtistTagCoverage)
	var i CountCandidateArtistTagCoverageRow
	err := row.Scan(&i.Processed, &i.WithTags)
	return i, err
}

const deleteCandidateArtistTags = `-- name: DeleteCandidateArtistTags :exec
DELETE FROM candidate_artist_tags WHERE candidate_mbid = $1
`

// Clear a candidate's cached tags before rewriting (atomic replace by the
// caller, same shape as DeleteTrackTags).
func (q *Queries) DeleteCandidateArtistTags(ctx context.Context, candidateMbid string) error {
	_, err := q.db.Exec(ctx, deleteCandidateArtistTags, candidateMbid)
	return err
}

const gcDeleteOrphanedCandidateArtistTagState = `-- name: GcDeleteOrphanedCandidateArtistTagState :execrows
DELETE FROM candidate_artist_tag_state s
 WHERE NOT EXISTS (
       SELECT 1 FROM artist_similarity_unmatched u
        WHERE u.candidate_mbid = s.candidate_mbid
   )
    OR EXISTS (SELECT 1 FROM artists a WHERE a.mbid = s.candidate_mbid)
`

// Same sweep for the bookkeeping rows. Kept as a separate statement rather
// than a cascade: the two tables are independent by design (a 'none' outcome
// has state but no tags), so neither can be the parent of the other.
func (q *Queries) GcDeleteOrphanedCandidateArtistTagState(ctx context.Context) (int64, error) {
	result, err := q.db.Exec(ctx, gcDeleteOrphanedCandidateArtistTagState)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected(), nil
}

const gcDeleteOrphanedCandidateArtistTags = `-- name: GcDeleteOrphanedCandidateArtistTags :execrows
DELETE FROM candidate_artist_tags t
 WHERE NOT EXISTS (
       SELECT 1 FROM artist_similarity_unmatched u
        WHERE u.candidate_mbid = t.candidate_mbid
   )
    OR EXISTS (SELECT 1 FROM artists a WHERE a.mbid = t.candidate_mbid)
`

// Drops cached tags for candidates that no longer appear in the similarity
// feed, or that have since been added to the library (their tags now live in
// track_tags). The feed is refetched periodically and churns, so without this
// the cache only ever grows.
func (q *Queries) GcDeleteOrphanedCandidateArtistTags(ctx context.Context) (int64, error) {
	result, err := q.db.Exec(ctx, gcDeleteOrphanedCandidateArtistTags)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected(), nil
}

const insertCandidateArtistTag = `-- name: InsertCandidateArtistTag :exec
INSERT INTO candidate_artist_tags (candidate_mbid, tag, weight)
VALUES ($1, $2, $3)
ON CONFLICT (candidate_mbid, tag)
    DO UPDATE SET weight = GREATEST(candidate_artist_tags.weight, EXCLUDED.weight)
`

type InsertCandidateArtistTagParams struct {
	CandidateMbid string
	Tag           string
	Weight        float64
}

// Upsert one (candidate, tag); keep the stronger weight when two providers
// agree on a tag with different folksonomy strengths.
func (q *Queries) InsertCandidateArtistTag(ctx context.Context, arg InsertCandidateArtistTagParams) error {
	_, err := q.db.Exec(ctx, insertCandidateArtistTag, arg.CandidateMbid, arg.Tag, arg.Weight)
	return err
}

const listCandidateArtistTagsForMbids = `-- name: ListCandidateArtistTagsForMbids :many
SELECT candidate_mbid, tag, weight
  FROM candidate_artist_tags
 WHERE candidate_mbid = ANY($1::text[])
`

// Cached tags for a set of candidates, for slice 6's taste-overlap ranking.
// One row per (candidate, tag).
func (q *Queries) ListCandidateArtistTagsForMbids(ctx context.Context, dollar_1 []string) ([]CandidateArtistTag, error) {
	rows, err := q.db.Query(ctx, listCandidateArtistTagsForMbids, dollar_1)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []CandidateArtistTag
	for rows.Next() {
		var i CandidateArtistTag
		if err := rows.Scan(&i.CandidateMbid, &i.Tag, &i.Weight); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const listCandidateArtistsMissingTags = `-- name: ListCandidateArtistsMissingTags :many

SELECT u.candidate_mbid,
       coalesce(max(u.candidate_name), '')::text AS candidate_name,
       sum(u.score)::float8                      AS total_score
  FROM artist_similarity_unmatched u
  LEFT JOIN candidate_artist_tag_state s ON s.candidate_mbid = u.candidate_mbid
 WHERE NOT EXISTS (SELECT 1 FROM artists a WHERE a.mbid = u.candidate_mbid)
   AND (
       s.candidate_mbid IS NULL
       OR (s.tag_source = 'none' AND s.tag_sources_version < $1)
   )
 GROUP BY u.candidate_mbid
 ORDER BY total_score DESC, u.candidate_mbid
 LIMIT $2
`

type ListCandidateArtistsMissingTagsParams struct {
	TagSourcesVersion int32
	Limit             int32
}

type ListCandidateArtistsMissingTagsRow struct {
	CandidateMbid string
	CandidateName string
	TotalScore    float64
}

// Folksonomy tags for out-of-library Discover candidates (#2376). Parallel to
// track_tags.sql, but keyed by MBID because the artist has no local row. See
// 0050_candidate_artist_tags.up.sql for why the bookkeeping is its own table.
// Candidates eligible for tag enrichment: never processed (no state row) or
// settled 'none' under an older provider version.
//
// artist_similarity_unmatched holds one row per (seed, candidate, source), so
// this GROUPs to one row per candidate — enriching the same MBID once per seed
// that pointed at it would multiply the API calls for no gain.
//
// ORDER BY summed similarity DESC is the load-bearing part. The candidate pool
// is O(library artists x neighbours per artist) — thousands — and MusicBrainz
// allows ~1 req/s, so it can NEVER be fully enriched in one pass. Draining in
// strength order means the candidates most likely to actually reach a user's
// deck get tags first, and the long tail fills in over subsequent ticks
// instead of starving behind it.
//
// Already-in-library candidates are skipped: they have an artists row, so
// their tags belong in track_tags, and the suggestion query filters them out
// anyway. $1 = current tag_sources_version, $2 = limit.
// candidate_name is coalesced to the empty string so it lands non-nullable in
// Go: the name is only a Last.fm lookup key, and empty simply means "MBID-keyed
// providers only", which the provider chain already handles. max() is an
// arbitrary-but-deterministic pick when several seeds spell one MBID
// differently.
func (q *Queries) ListCandidateArtistsMissingTags(ctx context.Context, arg ListCandidateArtistsMissingTagsParams) ([]ListCandidateArtistsMissingTagsRow, error) {
	rows, err := q.db.Query(ctx, listCandidateArtistsMissingTags, arg.TagSourcesVersion, arg.Limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []ListCandidateArtistsMissingTagsRow
	for rows.Next() {
		var i ListCandidateArtistsMissingTagsRow
		if err := rows.Scan(&i.CandidateMbid, &i.CandidateName, &i.TotalScore); err != nil {
			return nil, err
		}
		items = append(items, i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const setCandidateArtistTagState = `-- name: SetCandidateArtistTagState :exec
INSERT INTO candidate_artist_tag_state (candidate_mbid, tag_source, tag_sources_version)
VALUES ($1, $2, $3)
ON CONFLICT (candidate_mbid) DO UPDATE
   SET tag_source          = EXCLUDED.tag_source,
       tag_sources_version = EXCLUDED.tag_sources_version,
       updated_at          = now()
`

type SetCandidateArtistTagStateParams struct {
	CandidateMbid     string
	TagSource         string
	TagSourcesVersion int32
}

// Stamp the enrichment outcome so the drainer skips settled candidates.
// $2 = 'musicbrainz' | 'lastfm' | 'mixed' | 'none', $3 = current version.
func (q *Queries) SetCandidateArtistTagState(ctx context.Context, arg SetCandidateArtistTagStateParams) error {
	_, err := q.db.Exec(ctx, setCandidateArtistTagState, arg.CandidateMbid, arg.TagSource, arg.TagSourcesVersion)
	return err
}
