import 'dart:io';
import 'dart:typed_data';

import 'package:path_provider/path_provider.dart';

/// Persistenter Datei-Cache für heruntergeladene Attachment-Vorschauen.
///
/// **Warum überhaupt ein Cache:** Vorschaubilder werden über
/// `GET /attachments/{id}` aus DOCUframe gerendert — das kostet Zeit und
/// Bandbreite und funktioniert offline gar nicht. Einmal geholte Varianten
/// landen deshalb auf der Platte und werden danach lokal bedient.
///
/// **Warum keine Content-Revalidierung:** Attachments sind unveränderlich.
/// Ein hochgeladenes Bild (DOCUframe-Objekt) ändert seinen Inhalt nie. Eine
/// einmal gecachte Variante ist daher dauerhaft gültig — kein ETag, kein
/// If-None-Match, kein HEAD nötig. Das Einzige, was den Cache betrifft, ist
/// das **Löschen** eines Attachments; dafür gibt es [retainOnly], das den
/// Cache auf die Menge noch gültiger Attachment-IDs eindampft.
///
/// **Datei-Layout:** Pro Attachment können mehrere *Varianten* liegen
/// (Thumbnail 600×600, Vollbild 2048×2048, …). Der Dateiname kodiert ID und
/// Render-Parameter:
///
///   `{attachmentId}__{w}x{h}_q{q}_{ext}`
///
/// Die ID steht vorne und ist als UUID frei von `__`, sodass [retainOnly] sie
/// zuverlässig wieder herausschneiden kann.
///
/// Der Cache ist durchweg **best-effort**: Lese-/Schreib-/Lösch-Fehler werden
/// geschluckt und führen höchstens zu einem erneuten Download, nie zu einem
/// Crash.
class AttachmentCache {
  AttachmentCache();

  static const _subdir = 'attachment_previews';
  static const _separator = '__';

  /// Einmal aufgelöstes Verzeichnis — `getApplicationCacheDirectory` nicht
  /// bei jedem Zugriff neu abfragen.
  Future<Directory>? _dirFuture;

  Future<Directory> _dir() => _dirFuture ??= _resolveDir();

  Future<Directory> _resolveDir() async {
    final base = await getApplicationCacheDirectory();
    final dir = Directory('${base.path}/$_subdir');
    if (!await dir.exists()) {
      await dir.create(recursive: true);
    }
    return dir;
  }

  String _fileName({
    required String attachmentId,
    required int w,
    required int h,
    required int q,
    required String ext,
  }) =>
      '$attachmentId$_separator${w}x${h}_q${q}_$ext';

  Future<File> _file({
    required String attachmentId,
    required int w,
    required int h,
    required int q,
    required String ext,
  }) async {
    final dir = await _dir();
    return File(
      '${dir.path}/${_fileName(attachmentId: attachmentId, w: w, h: h, q: q, ext: ext)}',
    );
  }

  /// Liest eine gecachte Variante. `null`, wenn nichts da ist oder das Lesen
  /// scheitert — der Aufrufer lädt dann frisch.
  Future<Uint8List?> read({
    required String attachmentId,
    required int w,
    required int h,
    required int q,
    required String ext,
  }) async {
    try {
      final f = await _file(
        attachmentId: attachmentId,
        w: w,
        h: h,
        q: q,
        ext: ext,
      );
      if (!await f.exists()) return null;
      final bytes = await f.readAsBytes();
      return bytes.isEmpty ? null : bytes;
    } catch (_) {
      return null;
    }
  }

  /// Schreibt eine Variante. Atomar via temp-Datei + rename, damit ein
  /// paralleler Read nie ein halb geschriebenes File sieht. Leere Bytes
  /// werden ignoriert (kaputter Download soll keinen leeren Cache-Eintrag
  /// hinterlassen).
  Future<void> write({
    required String attachmentId,
    required int w,
    required int h,
    required int q,
    required String ext,
    required Uint8List bytes,
  }) async {
    if (bytes.isEmpty) return;
    try {
      final f = await _file(
        attachmentId: attachmentId,
        w: w,
        h: h,
        q: q,
        ext: ext,
      );
      final tmp = File('${f.path}.tmp');
      await tmp.writeAsBytes(bytes, flush: true);
      await tmp.rename(f.path);
    } catch (_) {
      // best-effort
    }
  }

  /// Entfernt alle gecachten Dateien, deren Attachment-ID **nicht** in
  /// [validAttachmentIds] vorkommt. So verschwinden die Vorschauen gelöschter
  /// Foto-Notizen beim nächsten Tour-Load aus dem Cache. Verwaiste
  /// temp-Dateien (abgebrochene Writes) werden immer mit entfernt.
  Future<void> retainOnly(Set<String> validAttachmentIds) async {
    try {
      final dir = await _dir();
      if (!await dir.exists()) return;
      await for (final entity in dir.list()) {
        if (entity is! File) continue;
        final name = entity.uri.pathSegments.last;
        if (name.endsWith('.tmp')) {
          await _deleteQuietly(entity);
          continue;
        }
        final sepIdx = name.indexOf(_separator);
        final id = sepIdx == -1 ? name : name.substring(0, sepIdx);
        if (!validAttachmentIds.contains(id)) {
          await _deleteQuietly(entity);
        }
      }
    } catch (_) {
      // best-effort — Pruning darf nie den Tour-Load stören
    }
  }

  Future<void> _deleteQuietly(File f) async {
    try {
      await f.delete();
    } catch (_) {
      // ignore
    }
  }
}
