// lib/model/source/media_store_source.dart
import 'dart:async';

import 'package:aves/model/covers.dart';
import 'package:aves/model/dynamic_albums.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/origins.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/grouping/common.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/debouncer.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';

// ⭐⭐⭐ (temporaneo) origine remota come nel tuo corrente.
// ATTENZIONE: poi la cambieremo perché colliderebbe con EntryOrigins.unknownContent (=1)
const int ORIGIN_REMOTE = 1;

class MediaStoreSource extends CollectionSource {
  final Debouncer _changeDebouncer = Debouncer(delay: ADurations.mediaContentChangeDebounceDelay);
  final Set<String> _changedUris = {};
  int? _lastGeneration;
  SourceScope _loadedScope, _targetScope;
  bool _canAnalyze = true;

  @override
  set canAnalyze(bool enabled) => _canAnalyze = enabled;

  @override
  SourceScope get loadedScope => _loadedScope;

  @override
  SourceScope get targetScope => _targetScope;

  @override
  Future<void> init({
    required SourceScope scope,
    AnalysisController? analysisController,
    bool loadTopEntriesFirst = false,
  }) async {
    _targetScope = scope;
    await reportService.log('$runtimeType init target scope=$scope');
    await _loadEssentials();

    addDirectories(albums: settings.pinnedFilters.whereType<StoredAlbumFilter>().map((v) => v.album).toSet());
    await updateGeneration();
    unawaited(
      _loadEntries(
        analysisController: analysisController,
        loadTopEntriesFirst: loadTopEntriesFirst,
      ),
    );
  }


Future<void> _loadEssentials() async {
  final stopwatch = Stopwatch()..start();
  state = SourceState.loading;

  // 1) Inizializza DB
  await localMediaDb.init();
  debugPrint('[MediaStoreSource] localMediaDb instance hashCode after init = ${localMediaDb.hashCode}');


  // 2) Inizializza tutto il resto
  await vaults.init();
  await favourites.init();

  albumGrouping.init();
  albumGrouping.setGroups(settings.albumGroups);
  albumGrouping.registerSource(this);

  tagGrouping.init();
  tagGrouping.setGroups(settings.tagGroups);
  tagGrouping.registerSource(this);

  await covers.init();
  await dynamicAlbums.init();

  // 3) Timezone fix
  final deviceOffset = DateTime.now().timeZoneOffset.inMilliseconds;
  final catalogOffset = settings.catalogTimeZoneOffsetMillis;
  if (deviceOffset != catalogOffset) {
    unawaited(reportService.log(
        'Time zone offset change: $catalogOffset -> $deviceOffset. Clear catalog metadata to get correct date/times.'));
    await localMediaDb.clearDates();
    await localMediaDb.clearCatalogMetadata();
    settings.catalogTimeZoneOffsetMillis = deviceOffset;
  }
  
  // ⭐⭐⭐ PATCH: loadDates SOLO ALLA FINE
debugPrint('[MediaStoreSource] calling loadDates()...');
await loadDates();
debugPrint('[MediaStoreSource] localMediaDb instance hashCode before loadDates = ${localMediaDb.hashCode}');
debugPrint('[MediaStoreSource] BEFORE loadDates: rawDb is null? ${localMediaDb.rawDb == null}');
debugPrint('[MediaStoreSource] loadDates() DONE');

  debugPrint('$runtimeType load essentials complete in ${stopwatch.elapsed.inMilliseconds}ms');
}

  Future<void> _loadEntries({
    AnalysisController? analysisController,
    required bool loadTopEntriesFirst,
  }) async {
    unawaited(reportService.log('$runtimeType load (known) start'));
    final stopwatch = Stopwatch()..start();
    state = SourceState.loading;

    // ✅ STEP A: come Aves originale — pulizia SOLO in memoria, NON nel DB
    clearEntries();

    final scopeAlbumFilters = _targetScope?.whereType<StoredAlbumFilter>();
    final scopeDirectory =
        scopeAlbumFilters != null && scopeAlbumFilters.length == 1 ? scopeAlbumFilters.first.album : null;

    final Set<AvesEntry> topEntries = {};
    if (loadTopEntriesFirst) {
      final topIds = settings.topEntryIds?.toSet();
      if (topIds != null) {
        debugPrint('$runtimeType load ${stopwatch.elapsed} load ${topIds.length} top entries');
        topEntries.addAll(await localMediaDb.loadEntriesById(topIds));
        addEntries(topEntries);
      }
    }

    debugPrint('$runtimeType load ${stopwatch.elapsed} fetch known entries');
    final knownEntries =
        await localMediaDb.loadEntries(origin: EntryOrigins.mediaStoreContent, directory: scopeDirectory);
    final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet();
    final isLargeCollection = knownEntries.length > 80000;
    if (isLargeCollection && settings.isErrorReportingAllowed) {
      settings.isErrorReportingAllowed = false;
    }
    unawaited(reportService.setCustomKey('is_large_collection', isLargeCollection));
    unawaited(reportService.log('$runtimeType found ${knownEntries.length} known entries'));

    debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete entries');
    final knownDateByContentId =
        Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedMillis)));
    final knownContentIds = knownDateByContentId.keys.toList();

    final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet();
    debugPrint('[media_store_source] removedContentIds count=${removedContentIds.length} sample=${removedContentIds.take(10)}');

    if (topEntries.isNotEmpty) {
      final removedTopEntries = topEntries.where((entry) => removedContentIds.contains(entry.contentId));
      await removeEntries(removedTopEntries.map((entry) => entry.uri).toSet(), includeTrash: false);
    }
    final removedEntries = knownEntries.where((entry) => removedContentIds.contains(entry.contentId)).toSet();
    knownEntries.removeAll(removedEntries);

    // ⭐⭐⭐ PATCH: rimuovi solo locali, non remote ⭐⭐⭐
    final localRemovedEntries = removedEntries.where((e) => e.origin != ORIGIN_REMOTE).toSet();
    debugPrint('[media_store_source] removedEntries total=${removedEntries.length} localRemovedEntries=${localRemovedEntries.length}');

    if (localRemovedEntries.isNotEmpty) {
      await localMediaDb.removeIds(localRemovedEntries.map((entry) => entry.id).toSet());
    }

    // show known entries
    debugPrint('$runtimeType load ${stopwatch.elapsed} add known entries');
    addEntries(knownEntries, notify: false);
    notifyAlbumsChanged();

    await _loadVaultEntries(scopeDirectory);

    debugPrint('$runtimeType load ${stopwatch.elapsed} load metadata');
    if (scopeDirectory != null) {
      final ids = knownLiveEntries.map((entry) => entry.id).toSet();
      await loadCatalogMetadata(ids: ids);
      await loadAddresses(ids: ids);
    } else {
      await loadCatalogMetadata();
      await loadAddresses();

      await loadTrashDetails();
      unawaited(
        deleteExpiredTrash().then(
          (deletedUris) {
            if (deletedUris.isNotEmpty) {
              debugPrint('evicted ${deletedUris.length} expired items from the trash');
              removeEntries(deletedUris, includeTrash: true);
            }
          },
        ),
      );
    }
    updateDerivedFilters();

    _loadedScope = _targetScope;

    if (_canAnalyze) {
      await _loadNewEntries(
        analysisController: analysisController,
        directory: scopeDirectory,
        knownLiveEntries: knownLiveEntries,
        knownDateByContentId: knownDateByContentId,
      );
    } else {
      state = SourceState.ready;
    }
  }

  Future<void> _loadNewEntries({
    required AnalysisController? analysisController,
    required String? directory,
    required Set<AvesEntry> knownLiveEntries,
    required Map<int?, int?> knownDateByContentId,
  }) async {
    unawaited(reportService.log('$runtimeType load (new) start'));
    final stopwatch = Stopwatch()..start();

    final newEntries = <AvesEntry>{};
    final folderSet = <String>{};

    if (directory == null) {
      newEntries.addAll(await recoverUntrackedTrashItems());
    }

    debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete paths');

    // ⭐⭐⭐ PATCH: escludi remote ⭐⭐⭐
    final knownPathByContentId = Map.fromEntries(
      knownLiveEntries
          .where((entry) => entry.origin != ORIGIN_REMOTE)
          .map((entry) => MapEntry(entry.contentId, entry.path)),
    );

    final movedContentIds = (await mediaStoreService.checkObsoletePaths(knownPathByContentId)).toSet();
    movedContentIds.forEach((contentId) {
      knownDateByContentId[contentId] = 0;
    });

    debugPrint('$runtimeType load ${stopwatch.elapsed} fetch new entries');
    final knownContentIds = knownDateByContentId.keys.toSet();

    mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen(
      (entry) {
        final dir = entry.directory;
        if (dir != null) {
          folderSet.add(dir);
        }

        final contentId = entry.contentId;
        final existingEntry = knownContentIds.contains(contentId)
            ? knownLiveEntries.firstWhereOrNull((entry) => entry.contentId == contentId)
            : null;
        entry.id = existingEntry?.id ?? localMediaDb.nextId;

        newEntries.add(entry);
        setProgress(done: newEntries.length, total: 0);
      },


onDone: () async {
  // 🔹 Inserisci nuove entries nel DB
  if (newEntries.isNotEmpty) {
    await localMediaDb.insertEntries(newEntries);

    final duplicates = await localMediaDb.searchLiveDuplicates(
      EntryOrigins.mediaStoreContent,
      newEntries,
    );
    if (duplicates.isNotEmpty) {
      await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
      for (final duplicate in duplicates) {
        final duplicateId = duplicate.id;
        newEntries.removeWhere((v) => v.id == duplicateId);
      }
    }

    addEntries(newEntries);
    invalidateAlbumFilterSummary();
  }

  // 🔹 Analisi (come prima)
  Set<AvesEntry>? analysisEntries;
  final analysisIds = analysisController?.entryIds;
  if (analysisIds != null) {
    analysisEntries = allEntries
        .where((entry) => analysisIds.contains(entry.id))
        .toSet();
  }
  await analyze(analysisController, entries: analysisEntries);

  // 🔥 PATCH FOLDERS: comportamento incrementale, NON distruttivo

  // Se non ci sono nuove directory → NON toccare il DB
  if (folderSet.isEmpty) {
    debugPrint('[folders] Nessuna nuova cartella → DB invariato');
    state = SourceState.ready;
    return;
  }

  // Recupera cartelle già presenti nel DB
  final existing = await localMediaDb.getLocalFolderPaths();
  final newOnes = folderSet.difference(existing.toSet());

  if (newOnes.isEmpty) {
    debugPrint('[folders] Nessuna cartella nuova → DB invariato');
    state = SourceState.ready;
    return;
  }

  // Inserisci SOLO le nuove cartelle
  debugPrint('[folders] Aggiungo ${newOnes.length} nuove cartelle');
  //await localMediaDb.insertLocalFolders(newOnes.toList());

  // Aggiorna cache e UI
  await updateDirectories();
  notifyAlbumsChanged();

  state = SourceState.ready;
},

    );
  }

  @override
  Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController}) async {
    if (!canRefresh || !isReady) return changedUris;

    state = SourceState.loading;

    final changedUriByContentId = Map.fromEntries(
      changedUris.map((uri) {
        final pathSegments = Uri.parse(uri).pathSegments;
        if (pathSegments.isEmpty) return null;
        final idString = pathSegments.last;
        final contentId = int.tryParse(idString);
        if (contentId == null) return null;
        return MapEntry(contentId, uri);
      }).nonNulls,
    );

    final obsoleteContentIds =
        (await mediaStoreService.checkObsoleteContentIds(changedUriByContentId.keys.toList())).toSet();

    final obsoleteUris = obsoleteContentIds.map((contentId) => changedUriByContentId[contentId]).nonNulls.toSet();

    // ⭐⭐⭐ PATCH: rimuovi solo locali ⭐⭐⭐
    final localObsoleteUris = <String>{};
    for (final uri in obsoleteUris) {
      final existingEntry = allEntries.firstWhereOrNull((entry) => entry.uri == uri);
      if (existingEntry == null) {
        final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true);
        if (sourceEntry != null && sourceEntry.origin != ORIGIN_REMOTE) {
          localObsoleteUris.add(uri);
        }
      } else {
        if (existingEntry.origin != ORIGIN_REMOTE) {
          localObsoleteUris.add(uri);
        }
      }
    }

    if (localObsoleteUris.isNotEmpty) {
      await removeEntries(localObsoleteUris, includeTrash: false);
    }

    obsoleteContentIds.forEach(changedUriByContentId.remove);

    final tempUris = <String>{};
    final newEntries = <AvesEntry>{}, entriesToRefresh = <AvesEntry>{};
    final existingDirectories = <String>{};

    for (final kv in changedUriByContentId.entries) {
      final contentId = kv.key;
      final uri = kv.value;
      final sourceEntry = await mediaFetchService.getEntry(uri, null);
      if (sourceEntry != null) {
        final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
        if (existingEntry == null ||
            (sourceEntry.dateModifiedMillis ?? 0) > (existingEntry.dateModifiedMillis ?? 0) ||
            sourceEntry.path != existingEntry.path) {
          final newPath = sourceEntry.path;
          final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null;
          if (volume != null) {
            if (existingEntry != null) {
              entriesToRefresh.add(existingEntry);
            } else if (_canAnalyze) {
              sourceEntry.id = localMediaDb.nextId;
              newEntries.add(sourceEntry);
            }
            final existingDirectory = existingEntry?.directory;
            if (existingDirectory != null) {
              existingDirectories.add(existingDirectory);
            }
          } else {
            tempUris.add(uri);
          }
        }
      }
    }

    await _refreshVaultEntries(
      changedUris: changedUris.where(vaults.isVaultEntryUri).toSet(),
      newEntries: newEntries,
      entriesToRefresh: entriesToRefresh,
      existingDirectories: existingDirectories,
    );

    invalidateAlbumFilterSummary(directories: existingDirectories);

    if (newEntries.isNotEmpty) {
      await localMediaDb.insertEntries(newEntries);

      final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
      if (duplicates.isNotEmpty) {
        await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
        for (final duplicate in duplicates) {
          final duplicateId = duplicate.id;
          newEntries.removeWhere((v) => v.id == duplicateId);
          tempUris.add(duplicate.uri);
        }
      }

      addEntries(newEntries);
      await analyze(analysisController, entries: newEntries);
    }

    if (entriesToRefresh.isNotEmpty) {
      await refreshEntries(entriesToRefresh, EntryDataType.values.toSet());
    }

    state = SourceState.ready;

// ⭐ PATCH: elimina cartelle vuote dal DB
cleanEmptyAlbums();

// ⭐ PATCH: aggiorna la lista album dopo la pulizia
await updateDirectories();
notifyAlbumsChanged();


    return tempUris;
  }

  void onStoreChanged(String? uri) {
    if (uri != null) _changedUris.add(uri);
    if (_changedUris.isNotEmpty) {
      _changeDebouncer(() async {
        final todo = _changedUris.toSet();
        _changedUris.clear();
        final tempUris = await refreshUris(todo);
        if (tempUris.isNotEmpty) {
          _changedUris.addAll(tempUris);
          onStoreChanged(null);
        }
      });
    }
  }

  Future<void> checkForChanges() async {
    final sinceGeneration = _lastGeneration;
    if (sinceGeneration != null) {
      _changedUris.addAll(await mediaStoreService.getChangedUris(sinceGeneration));
      onStoreChanged(null);
    }
    await updateGeneration();
  }

  Future<void> updateGeneration() async {
    _lastGeneration = await mediaStoreService.getGeneration();
  }

  // vault

  Future<void> _loadVaultEntries(String? directory) async {
    addEntries(await localMediaDb.loadEntries(origin: EntryOrigins.vault, directory: directory));
  }

  Future<void> _refreshVaultEntries({
    required Set<String> changedUris,
    required Set<AvesEntry> newEntries,
    required Set<AvesEntry> entriesToRefresh,
    required Set<String> existingDirectories,
  }) async {
    for (final uri in changedUris) {
      final existingEntry = allEntries.firstWhereOrNull((entry) => entry.uri == uri);
      if (existingEntry != null) {
        entriesToRefresh.add(existingEntry);
        final existingDirectory = existingEntry.directory;
        if (existingDirectory != null) {
          existingDirectories.add(existingDirectory);
        }
      } else {
        final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true);
        if (sourceEntry != null) {
          newEntries.add(
            sourceEntry.copyWith(
              id: localMediaDb.nextId,
              origin: EntryOrigins.vault,
            ),
          );
        }
      }
    }
  }
}
