import 'dart:async';

import 'package:audio_service/audio_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../likes/likes_provider.dart' show likedIdsProvider;
import '../player/album_color_extractor.dart';
import '../player/player_provider.dart';
import 'audio_cache_manager.dart';
import 'cache_settings_provider.dart';
import 'db.dart';

/// Listens to the player's currently-playing track. When it changes,
/// computes the next-N tracks ahead in the queue and pins them via
/// AudioCacheManager (source: autoPrefetch). After pinning, runs an
/// eviction pass against the operator-set cap.
///
/// The window N comes from cacheSettingsProvider.prefetchWindow
/// (default 5, configurable in Settings).
class Prefetcher {
  Prefetcher(this._ref) {
    // The mediaItem stream changes whenever the active track changes
    // (skipNext/skipPrev or natural progression). Settings changes (e.g.
    // operator bumps prefetch window) also trigger a reconcile.
    _ref.listen<AsyncValue<MediaItem?>>(mediaItemProvider, (_, __) => _reconcile());
    _ref.listen<AsyncValue<CacheSettings>>(cacheSettingsProvider, (_, __) => _reconcile());
  }

  final Ref _ref;

  Future<void> _reconcile() async {
    final settings = _ref.read(cacheSettingsProvider).value;
    if (settings == null) return;

    final queue = _ref.read(queueProvider).value ?? const <MediaItem>[];
    final current = _ref.read(mediaItemProvider).value;
    if (queue.isEmpty || current == null) return;

    final currentIdx = queue.indexWhere((m) => m.id == current.id);
    if (currentIdx < 0) return;

    final endIdx =
        (currentIdx + settings.prefetchWindow).clamp(0, queue.length - 1);

    final mgr = _ref.read(audioCacheManagerProvider);
    final coverCache = _ref.read(albumCoverCacheProvider);
    final colorCache = _ref.read(albumColorCacheProvider);

    // Walk the window. For each upcoming track we want THREE things
    // ready when the player transitions into it:
    //
    //   1. Audio file on disk (otherwise playback stalls on stream
    //      load — the original prefetcher concern).
    //   2. Cover bytes on disk under AlbumCoverCache (otherwise
    //      _toMediaItem's peekCached returns null, mediaItem
    //      broadcasts with artUri=null, and the now-playing screen
    //      stalls in _scheduleSwap awaiting precacheImage of a file
    //      that doesn't exist yet).
    //   3. Palette color extracted and memoized in AlbumColorCache
    //      (otherwise the gradient backdrop has to wait for
    //      PaletteGenerator to run after the cover lands —
    //      visible as the cover snapping in before the gradient).
    //
    // Each call is idempotent (cache-aware): if already cached, it's
    // a no-op. Everything fire-and-forget so the reconcile completes
    // quickly even on a fresh queue.
    for (var i = currentIdx; i <= endIdx; i++) {
      final media = queue[i];
      final trackId = media.id;
      final albumId = media.extras?['album_id'] as String?;

      if (!await mgr.isCached(trackId)) {
        // ignore: unawaited_futures
        mgr.pin(trackId, source: CacheSource.autoPrefetch);
      }

      if (albumId != null && albumId.isNotEmpty) {
        // Cover bytes: getOrFetch returns the file path; the side
        // effect (writing to disk) is what we care about.
        // ignore: unawaited_futures
        coverCache.getOrFetch(albumId);
        // Palette: getOrExtract chains off coverCache.getOrFetch so
        // it'll wait for the cover before sampling — safe to call
        // in parallel here.
        // ignore: unawaited_futures
        colorCache.getOrExtract(albumId);
      }
    }

    // Eviction pass after pinning new files. Per-bucket (#427 S2):
    // liked-ness decides the bucket, so a track that's both liked
    // and recently played is protected by the Liked cap, not the
    // rolling LRU.
    final liked =
        _ref.read(likedIdsProvider).value?.tracks ?? const <String>{};
    await mgr.evictBuckets(
      likedCap: settings.likedCapBytes,
      rollingCap: settings.rollingCapBytes,
      liked: liked,
    );
  }
}

/// Read this provider once at app start to activate the prefetcher.
/// Constructor wires the listeners.
final prefetcherProvider = Provider<Prefetcher>((ref) {
  return Prefetcher(ref);
});
