import 'dart:async';
import 'dart:convert';
import 'dart:ui';
import 'dart:io';

import 'package:sqflite/sqflite.dart';
import 'package:aves/model/entry/cache.dart';
import 'package:aves/model/entry/dirs.dart';
import 'package:aves/model/entry/extensions/keys.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:aves_model/aves_model.dart';
import 'package:aves_utils/aves_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:leak_tracker/leak_tracker.dart';

// ✅ NEW: Remote origin costante
import 'package:aves/remote/remote_origin.dart';

enum EntryDataType { basic, aspectRatio, catalog, address, references }

class AvesEntry with AvesEntryBase {
  // ============================================================
  //  CAMPI ORIGINALI AVES
  // ============================================================

  @override
  int id;

  @override
  String uri;

  @override
  int? pageId;

  @override
  int? sizeBytes;

  String? _path, _filename, _extension, _sourceTitle;
  EntryDir? _directory;
  int? contentId;
  final String sourceMimeType;
  int width, height, sourceRotationDegrees;
  int? dateAddedSecs, _dateModifiedMillis, sourceDateTakenMillis, _durationMillis;
  bool trashed;
  int origin;

  int? _catalogDateMillis;
  CatalogMetadata? _catalogMetadata;
  AddressDetails? _addressDetails;
  TrashDetails? trashDetails;

  List<AvesEntry>? stackedEntries;

  @override
  final AChangeNotifier visualChangeNotifier = AChangeNotifier();

  final AChangeNotifier metadataChangeNotifier = AChangeNotifier(),
      addressChangeNotifier = AChangeNotifier();

  // ============================================================
  //  CAMPI REMOTI (AGGIUNTI)
  // ============================================================

  String? remoteId;
  String? remotePath;
  String? remoteThumb1;
  String? remoteThumb2;
  String? provider;

  double? latitude;
  double? longitude;
  double? altitude;

  int? remoteWidth;
  int? remoteHeight;
  int? remoteRotation;

  // Toggle: se true il decoder remoto rispetta già l’EXIF → Aves NON ruota.
  static const bool kRemoteRespectsExifAtDecode = true;

  // Getter utili
  bool get isRemote => origin == RemoteOrigin.value; // EntryOrigins.remote == RemoteOrigin.value
  String? get remoteThumb => remoteThumb2 ?? remoteThumb1;

  // ============================================================
  //  COSTRUTTORE
  // ============================================================

  AvesEntry({
    required int? id,
    required this.uri,
    required String? path,
    required this.contentId,
    required this.pageId,
    required this.sourceMimeType,
    required this.width,
    required this.height,
    required this.sourceRotationDegrees,
    required this.sizeBytes,
    required String? sourceTitle,
    required this.dateAddedSecs,
    required int? dateModifiedMillis,
    required this.sourceDateTakenMillis,
    required int? durationMillis,
    required this.trashed,
    required this.origin,
    this.stackedEntries,
    this.remoteId,
    this.remotePath,
    this.remoteThumb1,
    this.remoteThumb2,
    this.provider,
    this.latitude,
    this.longitude,
    this.altitude,
    this.remoteWidth,
    this.remoteHeight,
    this.remoteRotation,
  }) : id = id ?? 0 {
    if (kFlutterMemoryAllocationsEnabled) {
      LeakTracking.dispatchObjectCreated(
        library: 'aves',
        className: '$AvesEntry',
        object: this,
      );
    }
    this.path = path;
    this.sourceTitle = sourceTitle;
    this.dateModifiedMillis = dateModifiedMillis;
    this.durationMillis = durationMillis;
  }

  // ============================================================
  //  COPY-WITH
  // ============================================================

  AvesEntry copyWith({
    int? id,
    String? uri,
    String? path,
    int? contentId,
    String? title,
    int? dateAddedSecs,
    int? dateModifiedMillis,
    int? origin,
    List<AvesEntry>? stackedEntries,
  }) {
    final copyEntryId = id ?? this.id;
    final copied = AvesEntry(
      id: copyEntryId,
      uri: uri ?? this.uri,
      path: path ?? this.path,
      contentId: contentId ?? this.contentId,
      pageId: null,
      sourceMimeType: sourceMimeType,
      width: width,
      height: height,
      sourceRotationDegrees: sourceRotationDegrees,
      sizeBytes: sizeBytes,
      sourceTitle: title ?? sourceTitle,
      dateAddedSecs: dateAddedSecs ?? this.dateAddedSecs,
      dateModifiedMillis: dateModifiedMillis ?? this.dateModifiedMillis,
      sourceDateTakenMillis: sourceDateTakenMillis,
      durationMillis: durationMillis,
      trashed: trashed,
      origin: origin ?? this.origin,
      stackedEntries: stackedEntries ?? this.stackedEntries,

      // campi remoti copiati
      remoteId: remoteId,
      remotePath: remotePath,
      remoteThumb1: remoteThumb1,
      remoteThumb2: remoteThumb2,
      provider: provider,
      latitude: latitude,
      longitude: longitude,
      altitude: altitude,
      remoteWidth: remoteWidth,
      remoteHeight: remoteHeight,
      remoteRotation: remoteRotation,
    )
      ..catalogMetadata = _catalogMetadata?.copyWith(id: copyEntryId)
      ..addressDetails = _addressDetails?.copyWith(id: copyEntryId)
      ..trashDetails = trashDetails?.copyWith(id: copyEntryId);

    return copied;
  }

  // ============================================================
  //  FROM MAP (DB → MODEL)  — REMOTE-FRIENDLY
  // ============================================================

factory AvesEntry.fromMap(Map map) {
  // origin/remoteId/uri → fallback corretti
  final origin = map[EntryFields.origin] as int? ?? 0;
  final rid = map['remoteId'] as String?;
  final rawUri = map[EntryFields.uri] as String?;
  final safeUri = rawUri ??
      ((origin == RemoteOrigin.value && rid != null)
          ? 'aves-remote://rid/$rid'
          : 'content://invalid');

  // MIME robusto
  final safeMime = (map[EntryFields.sourceMimeType] as String?) ??
      (map[EntryFields.mimeType] as String?) ??
      _inferMimeFromRemotePath(map) ??
      'image/jpeg';

  // Dimensioni
  final safeWidth =
      (map[EntryFields.width] as int?) ?? (map['remoteWidth'] as int?) ?? 0;
  final safeHeight =
      (map[EntryFields.height] as int?) ?? (map['remoteHeight'] as int?) ?? 0;

  // dateModified
  final safeDateModified = (map[EntryFields.dateModifiedMillis] as int?) ??
      (map[EntryFields.sourceDateTakenMillis] as int?) ??
      DateTime.now().millisecondsSinceEpoch;

  // contentId sintetico
  final safeContentId =
      (map[EntryFields.contentId] as int?) ?? _syntheticContentId(map);

  // --- CREA ENTRY ---
  final entry = AvesEntry(
    id: map[EntryFields.id] as int?,
    uri: safeUri,
    path: map[EntryFields.path] as String?,
    pageId: null,
    contentId: safeContentId,
    sourceMimeType: safeMime,
    width: safeWidth,
    height: safeHeight,
    sourceRotationDegrees:
        map[EntryFields.sourceRotationDegrees] as int? ?? 0,
    sizeBytes: map[EntryFields.sizeBytes] as int?,
    sourceTitle: map[EntryFields.title] as String?,
    dateAddedSecs: map[EntryFields.dateAddedSecs] as int?,
    dateModifiedMillis: safeDateModified,
    sourceDateTakenMillis:
        map[EntryFields.sourceDateTakenMillis] as int?,
    durationMillis: map[EntryFields.durationMillis] as int?,
    trashed: (map[EntryFields.trashed] as int? ?? 0) != 0,
    origin: origin,

    // --- REMOTE FIELDS ---
    remoteId: rid,
    remotePath: map['remotePath'] as String?,
    remoteThumb1: map['remoteThumb1'] as String?,
    remoteThumb2: map['remoteThumb2'] as String?,
    provider: map['provider'] as String?,
    remoteWidth: map['remoteWidth'] as int?,
    remoteHeight: map['remoteHeight'] as int?,
    remoteRotation: map['remoteRotation'] as int?,

    latitude: (map['latitude'] as num?)?.toDouble(),
    longitude: (map['longitude'] as num?)?.toDouble(),
    altitude: (map['altitude'] as num?)?.toDouble(),
  );

  // --- PATCH: GPS REMOTO → CATALOG METADATA ---
  if (entry.isRemote &&
      entry.latitude != null &&
      entry.longitude != null) {
    entry.catalogMetadata = CatalogMetadata(
      id: entry.id!,
      latitude: entry.latitude,
      longitude: entry.longitude,
    );
  }

  // --- PATCH: ADDRESS REMOTO ---
  if (map['countryName'] != null ||
      map['adminArea'] != null ||
      map['locality'] != null ||
      map['countryCode'] != null) {
    entry.addressDetails = AddressDetails(
      id: entry.id!,
      countryCode: map['countryCode'] as String?,
      countryName: map['countryName'] as String?,
      adminArea: map['adminArea'] as String?,
      locality: map['locality'] as String?,
    );
  }

  return entry;
}

Future<void> registerRemoteFolder() async {
  if (!isRemote) return;

  try {
    final cartella = remotePath ?? 'Remote';
    final user = provider ?? 'unknown';
    final dir = '$cartella@$user';

    final rows = await localMediaDb.rawDb.rawQuery(
      'SELECT hybrid_type FROM folders WHERE path = ?',
      [dir],
    );

    final localDirPath = '/storage/emulated/0/Pictures/$dir';
    final existsLocal = await Directory(localDirPath).exists();
    final hybridType = existsLocal ? 1 : 0;

    if (rows.isEmpty) {
      await localMediaDb.rawDb.insert(
        'folders',
        {
          'path': dir,
          'display_name': cartella,
          'user': user,
          'device_id': user,
          'is_remote': 1,
          'hybrid_type': hybridType,
          'album_type': 0,
          'priority': 0,
        },
        conflictAlgorithm: ConflictAlgorithm.ignore,
      );
    } else {
      final existingHybrid = rows.first['hybrid_type'] as int? ?? 0;
      if (existingHybrid != hybridType) {
        await localMediaDb.rawDb.update(
          'folders',
          {'hybrid_type': hybridType},
          where: 'path = ?',
          whereArgs: [dir],
        );
      }
    }
  } catch (e, st) {
    debugPrint('ERROR registering remote folder: $e\n$st');
  }
}

  // ============================================================
  //  TO MAP (MODEL → DB)
  // ============================================================

  Map<String, dynamic> toDatabaseMap() {
    return {
      EntryFields.id: id,
      EntryFields.uri: uri,
      EntryFields.path: path,
      EntryFields.contentId: contentId,
      EntryFields.sourceMimeType: sourceMimeType,
      EntryFields.width: width,
      EntryFields.height: height,
      EntryFields.sourceRotationDegrees: sourceRotationDegrees,
      EntryFields.sizeBytes: sizeBytes,
      EntryFields.title: sourceTitle,
      EntryFields.dateAddedSecs: dateAddedSecs,
      EntryFields.dateModifiedMillis: dateModifiedMillis,
      EntryFields.sourceDateTakenMillis: sourceDateTakenMillis,
      EntryFields.durationMillis: durationMillis,
      EntryFields.trashed: trashed ? 1 : 0,
      EntryFields.origin: origin,

      // --- REMOTE FIELDS ---
      'remoteId': remoteId,
      'remotePath': remotePath,
      'remoteThumb1': remoteThumb1,
      'remoteThumb2': remoteThumb2,
      'provider': provider,
      'remoteWidth': remoteWidth,
      'remoteHeight': remoteHeight,
      'remoteRotation': remoteRotation,

      'latitude': latitude,
      'longitude': longitude,
      'altitude': altitude,
 
    };
  }

  // ============================================================
  //  GETTER “REMOTE-AWARE” (display/size/rotation + visibilità)
  // ============================================================

  @override
  int get rotationDegrees {
    if (isRemote) {
      // Decoder remoto rispetta già l'EXIF → Aves non ruota
      if (kRemoteRespectsExifAtDecode) return 0;
      // Altrimenti, usa la rotazione remota (se disponibile)
      return remoteRotation ?? 0;
    }
    return _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
  }

  @override
  set rotationDegrees(int rotationDegrees) {
    if (isRemote) {
      // Manteniamo il valore per future policy, ma non verrà applicato se kRemoteRespectsExifAtDecode=true
      remoteRotation = rotationDegrees;
    } else {
      sourceRotationDegrees = rotationDegrees;
      _catalogMetadata?.rotationDegrees = rotationDegrees;
    }
  }

  // 🔁 isFlipped (per i remoti: sempre false)
  @override
  bool get isFlipped => isRemote ? false : (_catalogMetadata?.isFlipped ?? false);

  @override
  set isFlipped(bool v) {
    if (!isRemote) {
      _catalogMetadata?.isFlipped = v;
    }
  }

  // =======================
  //  PATCH sui DUE GETTER
  // =======================

  @override
  double get displayAspectRatio {
    if (isRemote) {
      // dimensioni "naturali" dei remoti
      double w = (remoteWidth ?? width).toDouble();
      double h = (remoteHeight ?? height).toDouble();

      // Se il decoder remoto rispetta l’EXIF e l’originale era 90/270,
      // il bitmap decodificato è già portrait: per il FRAME swappiamo w/h.
      if (kRemoteRespectsExifAtDecode) {
        final rotated90 = ((remoteRotation ?? 0) % 180) == 90;
        if (rotated90) {
          final t = w;
          w = h;
          h = t;
        }
      }

      if (w == 0 || h == 0) return 1;
      return w / h;
    }

    // Locali: logica originale
    double w = width.toDouble();
    double h = height.toDouble();
    if (w == 0 || h == 0) return 1;
    return isRotated ? h / w : w / h;
  }

  @override
  Size get displaySize {
    if (isRemote) {
      double w = (remoteWidth ?? width).toDouble();
      double h = (remoteHeight ?? height).toDouble();

      if (kRemoteRespectsExifAtDecode) {
        final rotated90 = ((remoteRotation ?? 0) % 180) == 90;
        if (rotated90) {
          final t = w;
          w = h;
          h = t;
        }
      }

      if (w == 0 || h == 0) return const Size(1, 1);
      // Nessuno swap aggiuntivo lato Aves: il frame ora è coerente al bitmap
      return Size(w, h);
    }

    // Locali: logica originale
    final w = width.toDouble();
    final h = height.toDouble();
    return isRotated ? Size(h, w) : Size(w, h);
  }

  /// Presenza “logica”: i remoti non vivono sul FS locale → considera presenti se non cestinati.
  bool get isPresent {
    if (isRemote) return !trashed;
    return !trashed &&
        (uri.startsWith('content://') ||
            uri.startsWith('file://') ||
            path != null);
  }

  /// Visualizzabilità minima: MIME coerente + dimensioni non zero.
  bool get isDisplayable {
    if (trashed) return false;
    if (isRemote) {
      final m = mimeType;
      final supported = m.startsWith('image/') || m.startsWith('video/');
      final hasSize =
          (width > 0 && height > 0) || (remoteWidth != null && remoteHeight != null);
      return supported && hasSize;
    }
    final m = mimeType;
    return (m.startsWith('image/') || m.startsWith('video/')) && isPresent;
  }

  /// Le thumbs remote non dipendono dal canale nativo → basta isDisplayable.
  bool get canThumbnail => isDisplayable;

  // ============================================================
  //  (RESTO INVARIATO)
  // ============================================================

  Map<String, dynamic> toPlatformEntryMap() {
    return {
      EntryFields.uri: uri,
      EntryFields.path: path,
      EntryFields.pageId: pageId,
      EntryFields.mimeType: mimeType,
      EntryFields.width: width,
      EntryFields.height: height,
      EntryFields.rotationDegrees: rotationDegrees,
      EntryFields.isFlipped: isFlipped,
      EntryFields.dateModifiedMillis: dateModifiedMillis,
      EntryFields.sizeBytes: sizeBytes,
      EntryFields.trashed: trashed,
      EntryFields.trashPath: trashDetails?.path,
      EntryFields.origin: origin,
    };
  }

  void dispose() {
    if (kFlutterMemoryAllocationsEnabled) {
      LeakTracking.dispatchObjectDisposed(object: this);
    }
    visualChangeNotifier.dispose();
    metadataChangeNotifier.dispose();
    addressChangeNotifier.dispose();
  }

  @override
  String toString() => '$runtimeType#${shortHash(this)}{id=$id, uri=$uri, path=$path, pageId=$pageId}';

  set path(String? path) {
    _path = path;
    _directory = null;
    _filename = null;
    _extension = null;
    _bestTitle = null;
  }

  @override
  String? get path => _path;

  // directory path, without the trailing separator
  String? get directory {
    _directory ??= entryDirRepo.getOrCreate(path != null ? pContext.dirname(path!) : null);
    return _directory!.resolved;
  }

  String? get filenameWithoutExtension {
    _filename ??= path != null ? pContext.basenameWithoutExtension(path!) : null;
    return _filename;
  }

  // file extension, including the `.`
  String? get extension {
    _extension ??= path != null ? pContext.extension(path!) : null;
    return _extension;
  }

  // the MIME type reported by the Media Store is unreliable
  // so we use the one found during cataloguing if possible
  @override
  String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;

  bool get isCatalogued => _catalogMetadata != null;

  DateTime? _bestDate;

  DateTime? get bestDate {
    _bestDate ??= dateTimeFromMillis(_catalogDateMillis) ??
        dateTimeFromMillis(sourceDateTakenMillis) ??
        dateTimeFromMillis(dateModifiedMillis ?? 0);
    return _bestDate;
  }

  @override
  bool get isAnimated => catalogMetadata?.isAnimated ?? false;

  bool get isHdr => _catalogMetadata?.isHdr ?? false;

  int get rating => _catalogMetadata?.rating ?? 0;

  bool get isRotated => rotationDegrees % 180 == 90;

  String? get sourceTitle => _sourceTitle;

  set sourceTitle(String? sourceTitle) {
    _sourceTitle = sourceTitle;
    _bestTitle = null;
  }

  int? get dateModifiedMillis => _dateModifiedMillis;

  set dateModifiedMillis(int? dateModifiedMillis) {
    _dateModifiedMillis = dateModifiedMillis;
    _bestDate = null;
  }

  DateTime? get monthTaken {
    final d = bestDate;
    return d == null ? null : DateTime(d.year, d.month);
  }

  DateTime? get dayTaken {
    final d = bestDate;
    return d == null ? null : DateTime(d.year, d.month, d.day);
  }

  @override
  int? get durationMillis => _durationMillis;

  set durationMillis(int? durationMillis) {
    _durationMillis = durationMillis;
    _durationText = null;
  }

  String? _durationText;

  String get durationText {
    _durationText ??= formatFriendlyDuration(Duration(milliseconds: durationMillis ?? 0));
    return _durationText!;
  }

  bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0;

  bool get hasAddress => _addressDetails != null;

  bool get hasFineAddress =>
      _addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3;

  Set<String>? _tags;

  Set<String> get tags {
    _tags ??= _catalogMetadata?.xmpSubjects?.split(';').where((tag) => tag.isNotEmpty).toSet() ?? {};
    return _tags!;
  }

  String? _bestTitle;

  @override
  String? get bestTitle {
    _bestTitle ??= _catalogMetadata?.xmpTitle?.isNotEmpty == true
        ? _catalogMetadata!.xmpTitle
        : (filenameWithoutExtension ?? sourceTitle);
    return _bestTitle;
  }

  int? get catalogDateMillis => _catalogDateMillis;

  set catalogDateMillis(int? dateMillis) {
    _catalogDateMillis = dateMillis;
    _bestDate = null;
  }

  CatalogMetadata? get catalogMetadata => _catalogMetadata;

  set catalogMetadata(CatalogMetadata? newMetadata) {
    final oldMimeType = mimeType;
    final oldDateModifiedMillis = dateModifiedMillis;
    final oldRotationDegrees = rotationDegrees;
    final oldIsFlipped = isFlipped;

    catalogDateMillis = newMetadata?.dateMillis;
    _catalogMetadata = newMetadata;
    _bestTitle = null;
    _tags = null;
    metadataChangeNotifier.notify();

    _onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
  }

  void clearMetadata() {
    catalogMetadata = null;
    addressDetails = null;
  }

  AddressDetails? get addressDetails => _addressDetails;

  set addressDetails(AddressDetails? newAddress) {
    _addressDetails = newAddress;
    addressChangeNotifier.notify();
  }

  String get shortAddress {
    return {
      _addressDetails?.countryName,
      _addressDetails?.adminArea,
      _addressDetails?.locality,
    }.nonNulls.where((v) => v.isNotEmpty).join(', ');
  }

  static void normalizeMimeTypeFields(Map fields) {
    final mimeType = fields[EntryFields.mimeType] as String?;
    if (mimeType != null) {
      fields[EntryFields.mimeType] = MimeTypes.normalize(mimeType);
    }
    final sourceMimeType = fields[EntryFields.sourceMimeType] as String?;
    if (sourceMimeType != null) {
      fields[EntryFields.sourceMimeType] = MimeTypes.normalize(sourceMimeType);
    }
  }

  Future<void> applyNewFields(Map newFields, {required bool persist}) async {
    final oldMimeType = mimeType;
    final oldDateModifiedMillis = this.dateModifiedMillis;
    final oldRotationDegrees = this.rotationDegrees;
    final oldIsFlipped = this.isFlipped;

    final uri = newFields[EntryFields.uri];
    if (uri is String) this.uri = uri;
    final path = newFields[EntryFields.path];
    if (path is String) this.path = path;
    final contentId = newFields[EntryFields.contentId];
    if (contentId is int) this.contentId = contentId;

    final sourceTitle = newFields[EntryFields.title];
    if (sourceTitle is String) this.sourceTitle = sourceTitle;
    final sourceRotationDegrees = newFields[EntryFields.sourceRotationDegrees];
    if (sourceRotationDegrees is int) this.sourceRotationDegrees = sourceRotationDegrees;
    final sourceDateTakenMillis = newFields[EntryFields.sourceDateTakenMillis];
    if (sourceDateTakenMillis is int) this.sourceDateTakenMillis = sourceDateTakenMillis;

    final width = newFields[EntryFields.width];
    if (width is int) this.width = width;
    final height = newFields[EntryFields.height];
    if (height is int) this.height = height;
    final durationMillis = newFields[EntryFields.durationMillis];
    if (durationMillis is int) this.durationMillis = durationMillis;

    final sizeBytes = newFields[EntryFields.sizeBytes];
    if (sizeBytes is int) this.sizeBytes = sizeBytes;
    final dateModifiedMillis = newFields[EntryFields.dateModifiedMillis];
    if (dateModifiedMillis is int) this.dateModifiedMillis = dateModifiedMillis;
    final rotationDegrees = newFields[EntryFields.rotationDegrees];
    if (rotationDegrees is int) this.rotationDegrees = rotationDegrees;
    final isFlipped = newFields[EntryFields.isFlipped];
    if (isFlipped is bool) this.isFlipped = isFlipped;

    if (persist) {
      await localMediaDb.updateEntry(id, this);
      if (catalogMetadata != null) await localMediaDb.saveCatalogMetadata({catalogMetadata!});
    }

    await _onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
    metadataChangeNotifier.notify();
  }

  Future<void> refresh({
    required bool background,
    required bool persist,
    required Set<EntryDataType> dataTypes,
  }) async {
    _bestDate = null;
    _bestTitle = null;
    _tags = null;

    if (persist) {
      await localMediaDb.removeIds({id}, dataTypes: dataTypes);
    }

    final updatedEntry = await mediaFetchService.getEntry(uri, mimeType);
    if (updatedEntry != null) {
      await applyNewFields(updatedEntry.toDatabaseMap(), persist: persist);
    }
  }

  Future<bool> delete() {
    final opCompleter = Completer<bool>();
    mediaEditService
        .delete(entries: {this})
        .listen(
          (event) => opCompleter.complete(event.success && !event.skipped),
          onError: opCompleter.completeError,
          onDone: () {
            if (!opCompleter.isCompleted) {
              opCompleter.complete(false);
            }
          },
        );
    return opCompleter.future;
  }

  Future<void> _onVisualFieldChanged(
    String oldMimeType,
    int? oldDateModifiedMillis,
    int oldRotationDegrees,
    bool oldIsFlipped,
  ) async {
    if ((!MimeTypes.refersToSameType(oldMimeType, mimeType) && !MimeTypes.isVideo(oldMimeType)) ||
        oldDateModifiedMillis != dateModifiedMillis ||
        oldRotationDegrees != rotationDegrees ||
        oldIsFlipped != isFlipped) {
      await EntryCache.evict(uri, oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped, isAnimated);
      visualChangeNotifier.notify();
    }
  }

  // ------------------------------------------------------------
  // Helpers “remote-friendly”
  // ------------------------------------------------------------

  static String? _inferMimeFromRemotePath(Map map) {
    final path = (map['remotePath'] as String?) ?? (map[EntryFields.path] as String?);
    if (path == null) return null;
    final lower = path.toLowerCase();
    if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
    if (lower.endsWith('.png')) return 'image/png';
    if (lower.endsWith('.webp')) return 'image/webp';
    if (lower.endsWith('.gif')) return 'image/gif';
    if (lower.endsWith('.mp4')) return 'video/mp4';
    if (lower.endsWith('.mov')) return 'video/quicktime';
    if (lower.endsWith('.mkv')) return 'video/x-matroska';
    return null;
  }

  static int? _syntheticContentId(Map map) {
    final id = map[EntryFields.id] as int?;
    if (id == null) return null;
    return 1000000000 + id; // disgiunto dai locali, >0
  }
}
