-- Auto re-acquisition of missing files (milestone #290). The unit is the
-- album: Lidarr acquires releases, and grouping collapses "40 missing files"
-- into "3 albums to ask for".

-- name: GetReacquisitionSettings :one
SELECT * FROM reacquisition_settings WHERE id = true;

-- name: UpdateReacquisitionSettings :one
-- Whole-row write from the admin card; the CHECKs in migration 0056 are the
-- validation, so a bad value fails loudly rather than being clamped silently.
UPDATE reacquisition_settings
   SET enabled            = sqlc.arg(enabled),
       grace_hours        = sqlc.arg(grace_hours),
       backoff_base_hours = sqlc.arg(backoff_base_hours),
       backoff_max_hours  = sqlc.arg(backoff_max_hours),
       max_attempts       = sqlc.arg(max_attempts),
       max_per_pass       = sqlc.arg(max_per_pass),
       auto_approve       = sqlc.arg(auto_approve)
 WHERE id = true
RETURNING *;

-- name: ListAlbumsDueReacquisition :many
-- The sweeper's selection. An album qualifies when:
--
--   * it still has at least one track whose file has been missing longer than
--     the grace window. Measured on missing_since, which reconcile never
--     re-stamps (#2523), so it is a genuine "gone since" clock rather than
--     "when we last noticed";
--   * MusicBrainz can name it. Both the album and its artist MBID are
--     required — Create rejects an album-kind request without them, and there
--     is nothing to ask Lidarr for anyway. Albums failing this are counted
--     separately (CountAlbumsMissingWithoutMbid) rather than vanishing;
--   * it has not spent its attempt budget (gave_up_at IS NULL);
--   * its backoff has elapsed: base * 2^(attempts-1) hours since the last
--     attempt, clamped to the configured maximum. First attempt (no row, or
--     last_attempt_at NULL) is always due.
--
-- Oldest attempt first, never-attempted first, so a large loss drains in a
-- stable order across passes instead of re-picking the same head each time.
SELECT albums.id           AS album_id,
       albums.title        AS album_title,
       albums.mbid         AS album_mbid,
       artists.id          AS artist_id,
       artists.name        AS artist_name,
       artists.mbid        AS artist_mbid,
       COUNT(tracks.id)::bigint AS missing_track_count,
       COALESCE(r.attempts, 0)::int AS attempts
  FROM albums
  JOIN artists ON artists.id = albums.artist_id
  JOIN tracks  ON tracks.album_id = albums.id
  LEFT JOIN missing_reacquisitions r ON r.album_id = albums.id
 WHERE tracks.missing_since IS NOT NULL
   AND tracks.missing_since <= now() - make_interval(hours => sqlc.arg(grace_hours)::int)
   AND albums.mbid IS NOT NULL
   AND artists.mbid IS NOT NULL
   AND (r.gave_up_at IS NULL)
   AND (
        r.last_attempt_at IS NULL
        OR r.last_attempt_at <= now() - make_interval(hours => LEAST(
             (sqlc.arg(backoff_base_hours)::int
              * POWER(2, GREATEST(COALESCE(r.attempts, 0) - 1, 0)))::int,
             sqlc.arg(backoff_max_hours)::int))
       )
 GROUP BY albums.id, albums.title, albums.mbid,
          artists.id, artists.name, artists.mbid, r.attempts, r.last_attempt_at
 ORDER BY r.last_attempt_at NULLS FIRST, albums.sort_title
 LIMIT sqlc.arg(page_limit);

-- name: CountAlbumsMissingWithoutMbid :one
-- Albums with missing files that can never be auto-requested because nothing
-- identifies them to MusicBrainz. Surfaced on the admin card so the gap is
-- visible: silently doing nothing for these would read as the feature being
-- broken.
SELECT COUNT(DISTINCT albums.id)::bigint
  FROM albums
  JOIN artists ON artists.id = albums.artist_id
  JOIN tracks  ON tracks.album_id = albums.id
 WHERE tracks.missing_since IS NOT NULL
   AND (albums.mbid IS NULL OR artists.mbid IS NULL);

-- name: RecordReacquisitionAttempt :one
-- Bumps the attempt counter and stamps the clock the backoff measures from.
-- Upsert because the first attempt has no row yet.
INSERT INTO missing_reacquisitions (album_id, attempts, last_attempt_at, last_request_id)
VALUES (sqlc.arg(album_id), 1, now(), sqlc.narg(last_request_id))
ON CONFLICT (album_id) DO UPDATE
   SET attempts        = missing_reacquisitions.attempts + 1,
       last_attempt_at = now(),
       last_request_id = COALESCE(EXCLUDED.last_request_id,
                                  missing_reacquisitions.last_request_id),
       updated_at      = now()
RETURNING *;

-- name: MarkReacquisitionGaveUp :exec
-- Stamped when the attempt budget is spent. Stored as a timestamp rather than
-- inferred from `attempts >= max_attempts` so the verdict survives an operator
-- later raising the maximum, and so the admin surface can say when.
UPDATE missing_reacquisitions
   SET gave_up_at = now(),
       updated_at = now()
 WHERE album_id = sqlc.arg(album_id)
   AND gave_up_at IS NULL;

-- name: ClearRecoveredReacquisitions :execrows
-- Drops state for albums that no longer have any missing track — the files
-- came back, or the scanner adopted them at a new path (#2528). Deleting
-- rather than resetting counters means a future loss starts from a clean
-- budget, which is right: it is a new problem, not a continuation.
DELETE FROM missing_reacquisitions r
 WHERE NOT EXISTS (
       SELECT 1 FROM tracks
        WHERE tracks.album_id = r.album_id
          AND tracks.missing_since IS NOT NULL
 );

-- name: GetReacquisitionForAlbums :many
-- State for the admin missing-files surface, so each directory group can say
-- whether a re-acquisition is in flight, waiting, or given up.
SELECT * FROM missing_reacquisitions WHERE album_id = ANY(sqlc.arg(album_ids)::uuid[]);
