import 'dart:async';
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';

/// Caches album cover bytes to disk so MediaItem.artUri can point at a
/// file:// URI. Android's MediaSession framework fetches artUri itself
/// and doesn't carry our Bearer header, so we pre-fetch via the
/// authenticated dio and hand the system a local file path instead.
///
/// Cache layout: `{applicationCacheDirectory}/album_covers/{albumId}.jpg`.
/// No explicit eviction — covers are tiny and OS clears app cache when
/// space is tight.
class AlbumCoverCache {
  AlbumCoverCache({
    required Future<Dio> Function() dioFactory,
    Future<Directory> Function()? cacheDirFactory,
  })  : _dioFactory = dioFactory,
        _cacheDirFactory = cacheDirFactory ?? getApplicationCacheDirectory;

  final Future<Dio> Function() _dioFactory;
  final Future<Directory> Function() _cacheDirFactory;

  /// In-flight requests keyed by albumId so concurrent callers for the
  /// same album dedupe to one fetch.
  final Map<String, Future<String?>> _inflight = {};

  /// Cached covers directory path resolved once on the first
  /// async cacheDir call, then reused for sync existsSync() checks
  /// in [peekCached]. Without this every "is the cover already on
  /// disk?" check would have to await path_provider, blocking the
  /// MediaItem broadcast on every track change.
  String? _coversDirPath;

  /// Returns the local file path for [albumId]'s cover if it's
  /// already cached on disk, or null otherwise. Synchronous — uses
  /// File.existsSync() against a path computed from the directory
  /// resolved by an earlier async [getOrFetch] call. Returns null
  /// until at least one getOrFetch has populated _coversDirPath.
  ///
  /// The audio handler uses this to seed MediaItem.artUri on the
  /// initial broadcast so external media controllers (Wear, Android
  /// Auto, Bluetooth) see the cover on the first frame for warm-cache
  /// tracks. Cold-cache tracks still fall back to the async
  /// _loadArtForCurrentItem path.
  String? peekCached(String albumId) {
    if (albumId.isEmpty) return null;
    final dir = _coversDirPath;
    if (dir == null) return null;
    final path = '$dir/$albumId.jpg';
    return File(path).existsSync() ? path : null;
  }

  /// Returns local file path to the album cover, or null on any
  /// failure (network, 4xx/5xx, disk full, empty albumId).
  Future<String?> getOrFetch(String albumId) {
    if (albumId.isEmpty) return Future.value(null);
    final existing = _inflight[albumId];
    if (existing != null) return existing;
    final fut = _doFetch(albumId);
    _inflight[albumId] = fut;
    fut.whenComplete(() => _inflight.remove(albumId));
    return fut;
  }

  Future<String?> _doFetch(String albumId) async {
    try {
      final dir = await _cacheDirFactory();
      final coversDir = Directory('${dir.path}/album_covers');
      await coversDir.create(recursive: true);
      // Cache the resolved directory so [peekCached] can do its
      // existsSync() check without re-awaiting path_provider.
      _coversDirPath = coversDir.path;
      final filePath = '${coversDir.path}/$albumId.jpg';
      final file = File(filePath);
      if (await file.exists()) return filePath;

      final dio = await _dioFactory();
      final r = await dio.get<List<int>>(
        '/api/albums/$albumId/cover',
        options: Options(responseType: ResponseType.bytes),
      );
      final bytes = r.data;
      if (bytes == null || bytes.isEmpty) return null;
      await file.writeAsBytes(bytes, flush: true);
      return filePath;
    } catch (e) {
      debugPrint('AlbumCoverCache: fetch failed for $albumId: $e');
      return null;
    }
  }
}
