// Per-entity tile providers for the per-item rendering architecture
// (see docs/superpowers/specs/2026-05-13-per-item-rendering-design.md).
//
// Each provider is a StreamProvider.family<EntityRef?, String> keyed
// by entity id. The stream:
//   1. Watches the entity's drift row
//   2. Yields the populated row on every emission
//   3. Yields null while the row is absent (UI shows skeleton)
//   4. On the first missing-row emission, enqueues a hydration via
//      HydrationQueue so the row eventually lands
//
// The tile widget reacts to AsyncValue<EntityRef?>:
//   - loading or data:null → skeleton
//   - data:non-null        → real card
//   - error                → error placeholder
//
// Subscriptions are per-tile. A 50-tile home screen creates 50 drift
// subscriptions; drift handles this fine in practice but worth
// measuring if a future surface scales to hundreds.

import 'package:drift/drift.dart' as drift show OrderingTerm;
import 'package:drift/drift.dart' show leftOuterJoin;
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../cache/adapters.dart';
import '../cache/audio_cache_manager.dart' show appDbProvider;
import '../models/album.dart';
import '../models/artist.dart';
import '../models/track.dart';
import 'hydration_queue.dart';

/// Watches the cached_albums row for [id]. Triggers background
/// hydration on miss; yields the row once it lands.
final albumTileProvider =
    StreamProvider.family<AlbumRef?, String>((ref, id) async* {
  if (id.isEmpty) {
    yield null;
    return;
  }
  final db = ref.watch(appDbProvider);
  final query = (db.select(db.cachedAlbums)..where((t) => t.id.equals(id)))
      .join([
    leftOuterJoin(db.cachedArtists,
        db.cachedArtists.id.equalsExp(db.cachedAlbums.artistId)),
  ]);
  var enqueued = false;
  await for (final rows in query.watch()) {
    if (rows.isEmpty) {
      if (!enqueued) {
        enqueued = true;
        ref
            .read(hydrationQueueProvider)
            .enqueue(entityType: 'album', entityId: id);
      }
      yield null;
      continue;
    }
    final r = rows.first;
    final album = r.readTable(db.cachedAlbums);
    final artist = r.readTableOrNull(db.cachedArtists);
    yield album.toRef(artistName: artist?.name ?? '');
  }
});

/// Watches the cached_artists row for [id]. Triggers background
/// hydration on miss; yields the row once it lands.
///
/// LEFT JOIN cached_albums ordered by sort_title so the first row per
/// artist carries a representative album id; toRef() reconstructs
/// `/api/albums/<id>/cover` from it. Without this join the ArtistRef
/// has an empty coverUrl and tiles fall back to the music-notes
/// placeholder even though the server has the cover available.
final artistTileProvider =
    StreamProvider.family<ArtistRef?, String>((ref, id) async* {
  if (id.isEmpty) {
    yield null;
    return;
  }
  final db = ref.watch(appDbProvider);
  final query = (db.select(db.cachedArtists)..where((t) => t.id.equals(id)))
      .join([
    leftOuterJoin(db.cachedAlbums,
        db.cachedAlbums.artistId.equalsExp(db.cachedArtists.id)),
  ])
    ..orderBy([drift.OrderingTerm.asc(db.cachedAlbums.sortTitle)]);
  var enqueued = false;
  await for (final rows in query.watch()) {
    if (rows.isEmpty) {
      if (!enqueued) {
        enqueued = true;
        ref
            .read(hydrationQueueProvider)
            .enqueue(entityType: 'artist', entityId: id);
      }
      yield null;
      continue;
    }
    // First row has the alphabetically-first album (or null if artist
    // has no albums in drift yet — yields a coverless ArtistRef which
    // the UI falls back to the placeholder icon for).
    final artist = rows.first.readTable(db.cachedArtists);
    final firstAlbum = rows.first.readTableOrNull(db.cachedAlbums);
    yield artist.toRef(coverAlbumId: firstAlbum?.id ?? '');
  }
});

/// Watches the cached_tracks row for [id]. Triggers background
/// hydration on miss; yields the row once it lands.
final trackTileProvider =
    StreamProvider.family<TrackRef?, String>((ref, id) async* {
  if (id.isEmpty) {
    yield null;
    return;
  }
  final db = ref.watch(appDbProvider);
  final query = (db.select(db.cachedTracks)..where((t) => t.id.equals(id)))
      .join([
    leftOuterJoin(db.cachedArtists,
        db.cachedArtists.id.equalsExp(db.cachedTracks.artistId)),
    leftOuterJoin(db.cachedAlbums,
        db.cachedAlbums.id.equalsExp(db.cachedTracks.albumId)),
  ]);
  var enqueued = false;
  await for (final rows in query.watch()) {
    if (rows.isEmpty) {
      if (!enqueued) {
        enqueued = true;
        ref
            .read(hydrationQueueProvider)
            .enqueue(entityType: 'track', entityId: id);
      }
      yield null;
      continue;
    }
    final r = rows.first;
    final t = r.readTable(db.cachedTracks);
    final a = r.readTableOrNull(db.cachedArtists);
    final al = r.readTableOrNull(db.cachedAlbums);
    yield t.toRef(
      artistName: a?.name ?? '',
      albumTitle: al?.title ?? '',
    );
  }
});
