// Per-item hydration coordinator for the home/playlist/liked surfaces.
//
// Tile widgets are reactive to their per-entity drift row. On first
// build, if drift has no row for the requested id, the tile asks this
// queue to hydrate it. The queue dispatches one /api/<type>/:id
// request, writes the result to drift, and the drift watch() on the
// tile re-emits with the populated row.
//
// Concurrency cap: HydrationQueue limits the number of in-flight
// requests so a 50-tile home screen doesn't fire 50 parallel /api/
// calls and stall the auth-token-bound dio pool. In-flight dedup
// (keyed by `<type>:<id>`) ensures a single hydration when multiple
// tiles share an entity (rare on home, common when several screens
// reference the same album).
//
// Failures are swallowed — a single failed hydration leaves the tile
// in skeleton state. A future retry-on-visit pass can layer on top
// without changing the queue's contract.

import 'dart:async';
import 'dart:collection';

import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../cache/adapters.dart';
import '../cache/audio_cache_manager.dart' show appDbProvider;
import '../library/library_providers.dart' show libraryApiProvider;

/// Bounded-concurrency request queue for per-entity hydration. One
/// instance per Riverpod scope (provider singleton below).
class HydrationQueue {
  HydrationQueue(this._ref);
  final Ref _ref;

  /// Concurrent slot count. 4 keeps the dio token pool from saturating
  /// while still pulling tiles down faster than a serial loop. Bump
  /// if profiling shows the queue idle while the network is healthy.
  static const _maxConcurrent = 4;

  final Queue<_HydrationRequest> _pending = Queue();
  final Set<String> _inFlightKeys = {};
  int _inFlight = 0;

  /// Request hydration for (entityType, entityId). Idempotent — second
  /// call for the same key while the first is queued or in flight is
  /// dropped, so multiple tile rebuilds during a fast scroll don't
  /// inflate the queue.
  void enqueue({required String entityType, required String entityId}) {
    if (entityId.isEmpty) return;
    final key = '$entityType:$entityId';
    if (_inFlightKeys.contains(key)) return;
    if (_pending.any((r) => r.key == key)) return;
    _pending.add(_HydrationRequest(
        entityType: entityType, entityId: entityId, key: key));
    _pump();
  }

  void _pump() {
    while (_inFlight < _maxConcurrent && _pending.isNotEmpty) {
      final req = _pending.removeFirst();
      _inFlightKeys.add(req.key);
      _inFlight++;
      // Unawaited on purpose — _pump returns immediately so further
      // enqueues can fill remaining slots while this one runs.
      unawaited(_execute(req).whenComplete(() {
        _inFlight--;
        _inFlightKeys.remove(req.key);
        _pump();
      }));
    }
  }

  Future<void> _execute(_HydrationRequest req) async {
    try {
      switch (req.entityType) {
        case 'album':
          await _hydrateAlbum(req.entityId);
        case 'artist':
          await _hydrateArtist(req.entityId);
        case 'track':
          await _hydrateTrack(req.entityId);
      }
    } catch (_) {
      // Swallow — tile stays in skeleton. A retry-on-visit pass can
      // layer on top later without changing this contract.
    }
  }

  Future<void> _hydrateAlbum(String id) async {
    final api = await _ref.read(libraryApiProvider.future);
    final result = await api.getAlbum(id);
    final db = _ref.read(appDbProvider);
    // Album + tracks come bundled in the same response; persist both
    // so opening the album detail later is also a drift hit.
    await db.transaction(() async {
      await db
          .into(db.cachedAlbums)
          .insertOnConflictUpdate(result.album.toDrift());
      if (result.tracks.isNotEmpty) {
        await db.batch((b) {
          b.insertAllOnConflictUpdate(
              db.cachedTracks, result.tracks.map((t) => t.toDrift()).toList());
        });
      }
    });
  }

  Future<void> _hydrateArtist(String id) async {
    final api = await _ref.read(libraryApiProvider.future);
    final fresh = await api.getArtist(id);
    final db = _ref.read(appDbProvider);
    await db.into(db.cachedArtists).insertOnConflictUpdate(fresh.toDrift());
  }

  Future<void> _hydrateTrack(String id) async {
    final api = await _ref.read(libraryApiProvider.future);
    final fresh = await api.getTrack(id);
    final db = _ref.read(appDbProvider);
    await db.into(db.cachedTracks).insertOnConflictUpdate(fresh.toDrift());
  }
}

class _HydrationRequest {
  _HydrationRequest({
    required this.entityType,
    required this.entityId,
    required this.key,
  });
  final String entityType;
  final String entityId;
  final String key;
}

/// Singleton queue scoped to the Riverpod container. Kept as a plain
/// Provider (not StateProvider) since the queue's internal state is
/// pump-driven, not reactively observed.
final hydrationQueueProvider = Provider<HydrationQueue>((ref) {
  return HydrationQueue(ref);
});
