import 'dart:async';
import 'dart:collection';

import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/entry/sort.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/events.dart';
import 'package:aves/model/source/location/location.dart';
import 'package:aves/model/source/section_keys.dart';
import 'package:aves/model/source/tag.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/utils/collection_utils.dart';
import 'package:aves_model/aves_model.dart';
import 'package:aves_utils/aves_utils.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';

import 'package:aves/remote/collection_source_remote_ext.dart';
import 'package:aves/remote/remote_origin.dart';

class CollectionLens with ChangeNotifier {
  final CollectionSource source;
  final Set<CollectionFilter> filters;
  List<String> burstPatterns;
  EntrySectionFactor sectionFactor;
  EntrySortFactor sortFactor;
  bool sortReverse;
  final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
  final Set<StreamSubscription> _subscriptions = {};
  int? id;
  bool listenToSource, stackBursts, stackDevelopedRaws, fixedSort;
  List<AvesEntry>? fixedSelection;

  final Set<AvesEntry> _syntheticEntries = {};
  List<AvesEntry> _filteredSortedEntries = [];

  Map<SectionKey, List<AvesEntry>> sections = Map.unmodifiable({});
  Timer? _refreshTimer;

  CollectionLens({
    required this.source,
    Set<CollectionFilter?>? filters,
    this.id,
    this.listenToSource = true,
    this.stackBursts = true,
    this.stackDevelopedRaws = true,
    this.fixedSort = false,
    this.fixedSelection,
  }) : filters = (filters ?? {}).nonNulls.toSet(),
       burstPatterns = settings.collectionBurstPatterns,
       sectionFactor = settings.collectionSectionFactor,
       sortFactor = settings.collectionSortFactor,
       sortReverse = settings.collectionSortReverse {
    if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
    id ??= hashCode;
    if (listenToSource) {
      final sourceEvents = source.eventBus;
      _subscriptions.add(sourceEvents.on<EntryAddedEvent>().listen((e) => _onEntryAdded(e.entries)));
      _subscriptions.add(sourceEvents.on<EntryRemovedEvent>().listen((e) => _onEntryRemoved(e.entries)));
      _subscriptions.add(
        sourceEvents.on<EntryMovedEvent>().listen((e) {
          switch (e.type) {
            case MoveType.copy:
            case MoveType.export:
              // refreshing new items is already handled via `EntryAddedEvent`s
              break;
            case MoveType.move:
            case MoveType.fromBin:
              refresh();
            case MoveType.toBin:
              _onEntryRemoved(e.entries);
          }
        }),
      );
      _subscriptions.add(sourceEvents.on<EntryRefreshedEvent>().listen((e) => refresh()));
      _subscriptions.add(sourceEvents.on<FilterVisibilityChangedEvent>().listen((e) => refresh()));
      _subscriptions.add(sourceEvents.on<CatalogMetadataChangedEvent>().listen((e) => refresh()));
      _subscriptions.add(
        sourceEvents.on<AddressMetadataChangedEvent>().listen((e) {
          if (this.filters.any((filter) => filter is LocationFilter)) {
            refresh();
          }
        }),
      );
      favourites.addListener(_onFavouritesChanged);
    }
    _subscriptions.add(
      settings.updateStream
          .where(
            (event) => [
              SettingKeys.collectionBurstPatternsKey,
              SettingKeys.collectionSortFactorKey,
              SettingKeys.collectionGroupFactorKey,
              SettingKeys.collectionSortReverseKey,
            ].contains(event.key),
          )
          .listen((_) => _onSettingsChanged()),
    );
    refresh();
  }

@override
void dispose() {
  _subscriptions
    ..forEach((sub) => sub.cancel())
    ..clear();
  favourites.removeListener(_onFavouritesChanged);
  filterChangeNotifier.dispose();
  sortSectionChangeNotifier.dispose();
  _disposeSyntheticEntries();

  // cancel debounced refresh timer
  _refreshTimer?.cancel();
  _refreshTimer = null;

  super.dispose();
}

  CollectionLens copyWith({
    CollectionSource? source,
    Set<CollectionFilter>? filters,
    bool? listenToSource,
    List<AvesEntry>? fixedSelection,
  }) => CollectionLens(
    source: source ?? this.source,
    filters: filters ?? this.filters,
    id: id,
    listenToSource: listenToSource ?? this.listenToSource,
    fixedSelection: fixedSelection ?? this.fixedSelection,
  );

  void _disposeSyntheticEntries() {
    _syntheticEntries.forEach((v) => v.dispose());
    _syntheticEntries.clear();
  }

  bool get isEmpty => _filteredSortedEntries.isEmpty;

  int get entryCount => _filteredSortedEntries.length;

  // sorted as displayed to the user, i.e. sorted then sectioned, not an absolute order on all entries
  List<AvesEntry>? _sortedEntries;

  List<AvesEntry> get sortedEntries {
    _sortedEntries ??= List.of(sections.entries.expand((kv) => kv.value));
    return _sortedEntries!;
  }

  bool get showHeaders {
    bool showAlbumHeaders() => !filters.any((v) => v is StoredAlbumFilter && !v.reversed);

    switch (sortFactor) {
      case EntrySortFactor.date:
        switch (sectionFactor) {
          case EntrySectionFactor.none:
            return false;
          case EntrySectionFactor.album:
            return showAlbumHeaders();
          case EntrySectionFactor.month:
            return true;
          case EntrySectionFactor.day:
            return true;
        }
      case EntrySortFactor.name:
      case EntrySortFactor.path:
        return showAlbumHeaders();
      case EntrySortFactor.rating:
        return !filters.any((f) => f is RatingFilter);
      case EntrySortFactor.size:
      case EntrySortFactor.duration:
        return false;
    }
  }

  void addFilters(Set<CollectionFilter> newFilters) {
    if (filters.containsAll(newFilters)) return;
    for (final filter in newFilters) {
      filters.removeWhere((other) => !filter.isCompatible(other));
    }
    filters.addAll(newFilters);
    _onFilterChanged();
  }

  void removeFilter(CollectionFilter filter) {
    if (!filters.contains(filter)) return;
    filters.remove(filter);
    _onFilterChanged();
  }

  void setLiveQuery(String query) {
    filters.removeWhere((v) => v is QueryFilter && v.live);
    if (query.isNotEmpty) {
      filters.add(QueryFilter(query, live: true));
    }
    _onFilterChanged();
  }

  void _onFilterChanged() {
    refresh();
    filterChangeNotifier.notifyListeners();
  }
/*
  void _applyFilters() {
    final entries = fixedSelection ?? (filters.contains(TrashFilter.instance) ? source.trashedEntries : source.visibleEntries);
    _disposeSyntheticEntries();
    _filteredSortedEntries = List.of(filters.isEmpty ? entries : entries.where((entry) => filters.every((filter) => filter.test(entry))));

    if (stackBursts) {
      _stackBursts();
    }
    if (stackDevelopedRaws) {
      _stackDevelopedRaws();
    }
  }
*/
void _applyFilters() {
  final entries = fixedSelection
      ?? (filters.contains(TrashFilter.instance)
          ? source.trashedEntries
          : source.visibleEntries);

  _disposeSyntheticEntries();

  // 🔥 1) Applica i filtri normali
  _filteredSortedEntries = List.of(
    filters.isEmpty
        ? entries
        : entries.where((entry) => filters.every((filter) => filter.test(entry))),
  );

  // 🔥 2) Nascondi completamente i remoti quando il server NON è OK
  if (!source.remoteVisible) {
    _filteredSortedEntries = _filteredSortedEntries
        .where((e) => e.origin != RemoteOrigin.value)
        .toList();
  }

  // 🔥 3) Stack burst e RAW come prima
  if (stackBursts) {
    _stackBursts();
  }
  if (stackDevelopedRaws) {
    _stackDevelopedRaws();
  }
}

  void _stackBursts() {
    final byBurstKey = groupBy<AvesEntry, String?>(_filteredSortedEntries, (entry) => entry.getBurstKey(burstPatterns)).whereNotNullKey();
    byBurstKey.forEach((burstKey, entries) {
      if (entries.length > 1) {
        entries.sort(AvesEntrySort.compareByName);
        final mainEntry = entries.first;
        final stackEntry = mainEntry.copyWith(stackedEntries: entries);
        _syntheticEntries.add(stackEntry);

        entries.skip(1).forEach((subEntry) {
          _filteredSortedEntries.remove(subEntry);
        });
        final index = _filteredSortedEntries.indexOf(mainEntry);
        _filteredSortedEntries.removeAt(index);
        _filteredSortedEntries.insert(index, stackEntry);
      }
    });
  }

  void _stackDevelopedRaws() {
    final allRawEntries = _filteredSortedEntries.where((entry) => entry.isRaw).toSet();
    if (allRawEntries.isNotEmpty) {
      final allDevelopedEntries = _filteredSortedEntries.where((entry) => MimeTypes.developedRawImages.contains(entry.mimeType)).toSet();
      final rawEntriesByDir = groupBy<AvesEntry, String?>(allRawEntries, (entry) => entry.directory).whereNotNullKey();
      rawEntriesByDir.forEach((dir, dirRawEntries) {
        final dirDevelopedEntries = allDevelopedEntries.where((entry) => entry.directory == dir).toSet();
        for (final rawEntry in dirRawEntries) {
          final rawFilename = rawEntry.filenameWithoutExtension;
          final developedEntry = dirDevelopedEntries.firstWhereOrNull((entry) => entry.filenameWithoutExtension == rawFilename);
          if (developedEntry != null) {
            final mainEntry = developedEntry;
            final subEntry = rawEntry;

            final stackEntry = mainEntry.copyWith(stackedEntries: [mainEntry, subEntry]);
            _syntheticEntries.add(stackEntry);

            _filteredSortedEntries.remove(subEntry);
            final index = _filteredSortedEntries.indexOf(mainEntry);
            _filteredSortedEntries.removeAt(index);
            _filteredSortedEntries.insert(0, stackEntry);
          }
        }
      });
    }
  }

  void _applySort() {
    if (fixedSort) return;

    switch (sortFactor) {
      case EntrySortFactor.date:
        _filteredSortedEntries.sort(AvesEntrySort.compareByDate);
      case EntrySortFactor.name:
        _filteredSortedEntries.sort(AvesEntrySort.compareByName);
      case EntrySortFactor.rating:
        _filteredSortedEntries.sort(AvesEntrySort.compareByRating);
      case EntrySortFactor.size:
        _filteredSortedEntries.sort(AvesEntrySort.compareBySize);
      case EntrySortFactor.duration:
        _filteredSortedEntries.sort(AvesEntrySort.compareByDuration);
      case EntrySortFactor.path:
        _filteredSortedEntries.sort(AvesEntrySort.compareByPath);
    }
    if (sortReverse) {
      _filteredSortedEntries = _filteredSortedEntries.reversed.toList();
    }
  }

  void _applySection() {
    if (fixedSort) {
      sections = Map.fromEntries([
        MapEntry(const SectionKey(), _filteredSortedEntries),
      ]);
    } else {
      switch (sortFactor) {
        case EntrySortFactor.date:
          switch (sectionFactor) {
            case EntrySectionFactor.album:
              sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
            case EntrySectionFactor.month:
              sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken));
            case EntrySectionFactor.day:
              sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken));
            case EntrySectionFactor.none:
              sections = Map.fromEntries([
                MapEntry(const SectionKey(), _filteredSortedEntries),
              ]);
          }
        case EntrySortFactor.name:
          final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
          final int Function(EntryAlbumSectionKey, EntryAlbumSectionKey) compare = sortReverse ? (a, b) => source.compareAlbumsByName(b.directory, a.directory) : (a, b) => source.compareAlbumsByName(a.directory, b.directory);
          sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, compare);
        case EntrySortFactor.rating:
          sections = groupBy<AvesEntry, EntryRatingSectionKey>(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating));
        case EntrySortFactor.size:
        case EntrySortFactor.duration:
          sections = Map.fromEntries([
            MapEntry(const SectionKey(), _filteredSortedEntries),
          ]);
        case EntrySortFactor.path:
          final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
          final int Function(EntryAlbumSectionKey, EntryAlbumSectionKey) compare = sortReverse ? (a, b) => source.compareAlbumsByPath(b.directory, a.directory) : (a, b) => source.compareAlbumsByPath(a.directory, b.directory);
          sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, compare);
      }
    }
    sections = Map.unmodifiable(sections);
    _sortedEntries = null;
    notifyListeners();
  }

  // metadata change should also trigger a full refresh
  // as dates impact sorting and sectioning
  void refresh() {
    _applyFilters();
    _applySort();
    _applySection();
  }

/// Refresh debounced per evitare troppi rebuild quando arrivano batch di upsert
void refreshDebounced([int ms = 250]) {
  _refreshTimer?.cancel();
  _refreshTimer = Timer(Duration(milliseconds: ms), () => refresh());
}


  void _onFavouritesChanged() {
    if (filters.any((filter) => filter is FavouriteFilter)) {
      refresh();
    }
  }

  void _onSettingsChanged() {
    final newBurstPatterns = settings.collectionBurstPatterns;
    final newSortFactor = settings.collectionSortFactor;
    final newSectionFactor = settings.collectionSectionFactor;
    final newSortReverse = settings.collectionSortReverse;

    final needFilter = burstPatterns != newBurstPatterns;
    final needSort = needFilter || sortFactor != newSortFactor || sortReverse != newSortReverse;
    final needSection = needSort || sectionFactor != newSectionFactor;

    if (needFilter) {
      burstPatterns = newBurstPatterns;
      _applyFilters();
    }
    if (needSort) {
      sortFactor = newSortFactor;
      sortReverse = newSortReverse;
      _applySort();
    }
    if (needSection) {
      sectionFactor = newSectionFactor;
      _applySection();
    }

    if (needFilter) {
      filterChangeNotifier.notifyListeners();
    }
    if (needSort || needSection) {
      sortSectionChangeNotifier.notifyListeners();
    }
  }

  void _onEntryAdded(Set<AvesEntry>? entries) {
    refreshDebounced();
  }

  void _onEntryRemoved(Set<AvesEntry> entries) {
    if (_syntheticEntries.isNotEmpty) {
      // find impacted stacks
      final obsoleteStacks = <AvesEntry>{};

      void _replaceStack(AvesEntry stackEntry, AvesEntry entry) {
        obsoleteStacks.add(stackEntry);
        fixedSelection?.replace(stackEntry, entry);
        _filteredSortedEntries.replace(stackEntry, entry);
        _sortedEntries?.replace(stackEntry, entry);
        sections.forEach((key, sectionEntries) => sectionEntries.replace(stackEntry, entry));
      }

      final stacks = _filteredSortedEntries.where((entry) => entry.isStack).toSet();
      stacks.forEach((stackEntry) {
        final subEntries = stackEntry.stackedEntries!;
        if (subEntries.any(entries.contains)) {
          final mainEntry = subEntries.first;

          // remove the deleted sub-entries
          subEntries.removeWhere(entries.contains);

          switch (subEntries.length) {
            case 0:
              // remove the stack itself
              obsoleteStacks.add(stackEntry);
              break;
            case 1:
              // replace the stack by the last remaining sub-entry
              _replaceStack(stackEntry, subEntries.first);
              break;
            default:
              // keep the stack with the remaining sub-entries
              if (!subEntries.contains(mainEntry)) {
                // recreate the stack with the correct main entry
                _replaceStack(stackEntry, subEntries.first.copyWith(stackedEntries: subEntries));
              }
              break;
          }
        }
      });

      obsoleteStacks.forEach((stackEntry) {
        _syntheticEntries.remove(stackEntry);
        stackEntry.dispose();
      });
      entries.addAll(obsoleteStacks);
    }

    // we should remove obsolete entries and sections
    // but do not apply sort/section
    // as section order change would surprise the user while browsing
    fixedSelection?.removeWhere(entries.contains);
    _filteredSortedEntries.removeWhere(entries.contains);
    _sortedEntries?.removeWhere(entries.contains);
    sections.forEach((key, sectionEntries) => sectionEntries.removeWhere(entries.contains));
    sections = Map.unmodifiable(Map.fromEntries(sections.entries.where((kv) => kv.value.isNotEmpty)));
    notifyListeners();
  }

  @override
  String toString() => '$runtimeType#${shortHash(this)}{id=$id, source=$source, filters=$filters, entryCount=$entryCount}';
}
