import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:crypto/crypto.dart';
import 'package:path_provider/path_provider.dart';

/// Shared, bounded LRU cache for media bytes (chat images, document previews,
/// avatars). Eviction is driven by a total byte budget plus a hard entry cap,
/// so the most-recently-viewed media (i.e. your recent messages) stays instantly
/// available while older media is dropped and re-fetched on demand.
///
/// In addition to the in-memory layer there is a small disk layer for
/// persistent entries (avatars, banners), so they appear instantly after an
/// app restart instead of popping in once re-downloaded.
class MediaCache {
  MediaCache._();
  static final MediaCache instance = MediaCache._();

  // ~256 MB in memory, capped at 300 entries — comfortably covers the last
  // ~100 messages of media per active chat without unbounded growth.
  static const int _maxBytes = 256 * 1024 * 1024;
  static const int _maxEntries = 300;
  // Don't cache very large single files (videos etc.) — they'd evict everything.
  static const int _maxEntryBytes = 32 * 1024 * 1024;

  // Disk layer: only small entries (avatars, banners), bounded total size.
  static const int _maxDiskEntryBytes = 1024 * 1024; // 1 MB pro Eintrag
  static const int _maxDiskTotalBytes = 64 * 1024 * 1024; // 64 MB gesamt

  final LinkedHashMap<String, Uint8List> _entries = LinkedHashMap();
  int _totalBytes = 0;

  Directory? _diskDir;
  bool _pruneScheduled = false;

  Uint8List? get(String key) {
    final v = _entries.remove(key);
    if (v != null) _entries[key] = v; // move to most-recently-used position
    return v;
  }

  void put(String key, Uint8List bytes) {
    if (bytes.lengthInBytes > _maxEntryBytes) return;

    final existing = _entries.remove(key);
    if (existing != null) _totalBytes -= existing.lengthInBytes;

    _entries[key] = bytes;
    _totalBytes += bytes.lengthInBytes;

    _evict();
  }

  /// Memory lookup first, then disk. Disk hits are promoted back into memory.
  Future<Uint8List?> getPersistent(String key) async {
    final mem = get(key);
    if (mem != null) return mem;
    try {
      final file = await _diskFile(key);
      if (await file.exists()) {
        final bytes = await file.readAsBytes();
        if (bytes.isNotEmpty) {
          put(key, bytes);
          // Touch für LRU-Pruning nach mtime.
          file.setLastModified(DateTime.now()).catchError((_) {});
          return bytes;
        }
      }
    } catch (_) {}
    return null;
  }

  /// Stores in memory and (for small entries) on disk, surviving restarts.
  Future<void> putPersistent(String key, Uint8List bytes) async {
    put(key, bytes);
    if (bytes.lengthInBytes > _maxDiskEntryBytes) return;
    try {
      final file = await _diskFile(key);
      await file.writeAsBytes(bytes);
    } catch (_) {}
  }

  Future<File> _diskFile(String key) async {
    final dir = _diskDir ??= await _initDiskDir();
    final name = sha1.convert(utf8.encode(key)).toString();
    return File('${dir.path}${Platform.pathSeparator}$name.bin');
  }

  Future<Directory> _initDiskDir() async {
    final support = await getApplicationSupportDirectory();
    final dir = Directory(
        '${support.path}${Platform.pathSeparator}media_cache');
    await dir.create(recursive: true);
    _schedulePrune(dir);
    return dir;
  }

  /// Once per session: drop the oldest disk entries when over budget.
  void _schedulePrune(Directory dir) {
    if (_pruneScheduled) return;
    _pruneScheduled = true;
    Future(() async {
      try {
        final files = <File, FileStat>{};
        var total = 0;
        await for (final e in dir.list()) {
          if (e is! File) continue;
          final stat = await e.stat();
          files[e] = stat;
          total += stat.size;
        }
        if (total <= _maxDiskTotalBytes) return;
        final sorted = files.entries.toList()
          ..sort((a, b) => a.value.modified.compareTo(b.value.modified));
        for (final entry in sorted) {
          if (total <= _maxDiskTotalBytes) break;
          total -= entry.value.size;
          await entry.key.delete().catchError((_) => entry.key);
        }
      } catch (_) {}
    });
  }

  void _evict() {
    while ((_totalBytes > _maxBytes || _entries.length > _maxEntries) &&
        _entries.isNotEmpty) {
      final oldestKey = _entries.keys.first;
      final removed = _entries.remove(oldestKey);
      if (removed != null) _totalBytes -= removed.lengthInBytes;
    }
  }

  void clear() {
    _entries.clear();
    _totalBytes = 0;
  }
}
