import 'package:flutter/foundation.dart';

import '../../../../api/marianumcloud/search/search_files.dart';
import '../../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
import '../../../../utils/debouncer.dart';
import 'local_cache_search.dart';

/// Holds the live state of a Files-search session: current query, the latest
/// local-cache hits (synchronous), the latest server hits (asynchronous,
/// debounced), and loading/error flags. Notifies listeners whenever any of
/// these change so the UI can rebuild incrementally as results stream in.
class FilesSearchController extends ChangeNotifier {
  FilesSearchController({List<String>? initialPathScope})
    : _pathScope = List<String>.from(initialPathScope ?? const []);

  static const Duration _serverDebounce = Duration(seconds: 1);
  final String _debounceTag =
      'files-search-${DateTime.now().microsecondsSinceEpoch}';
  final SearchFiles _api = SearchFiles();

  String _query = '';
  List<String> _pathScope;
  List<CacheableFile> _cacheResults = const [];
  List<CacheableFile> _serverResults = const [];
  bool _serverLoading = false;
  Object? _serverError;
  int _serverEpoch = 0;
  bool _disposed = false;

  /// Guards against the race where the search delegate is closed (and the
  /// controller disposed) while a debounced cache scan or server call is
  /// still in flight: their late `notifyListeners()` would otherwise throw
  /// on a disposed `ChangeNotifier`.
  void _safeNotify() {
    if (_disposed) return;
    notifyListeners();
  }

  String get query => _query;
  List<String> get pathScope => List.unmodifiable(_pathScope);
  bool get isScoped => _pathScope.isNotEmpty;
  List<CacheableFile> get cacheResults => _cacheResults;
  List<CacheableFile> get serverResults => _serverResults;
  bool get serverLoading => _serverLoading;
  Object? get serverError => _serverError;

  /// Combined, deduplicated result list (cache hits first, then any
  /// server-only hits) — handy for empty-state checks. Dedup key is the
  /// WebDAV path.
  List<CacheableFile> get combinedResults {
    if (_cacheResults.isEmpty) return _serverResults;
    if (_serverResults.isEmpty) return _cacheResults;
    final seen = <String>{for (final f in _cacheResults) f.path};
    return [
      ..._cacheResults,
      ..._serverResults.where((f) => seen.add(f.path)),
    ];
  }

  Future<void> setQuery(String value) async {
    if (value == _query) return;
    _query = value;
    // Bumping the epoch up front invalidates any in-flight server call from
    // a previous query, so its late response cannot toggle `_serverLoading`
    // off while a fresh search is queued behind the debounce.
    final epoch = ++_serverEpoch;
    if (_query.trim().isEmpty) {
      Debouncer.cancel(_debounceTag);
      _cacheResults = const [];
      _serverResults = const [];
      _serverLoading = false;
      _serverError = null;
      _safeNotify();
      return;
    }
    // Show loading immediately — even before the (typically fast) cache
    // scan resolves — so the indicator is visible the moment the user
    // starts typing rather than after the first await hop.
    _serverLoading = true;
    _serverError = null;
    _safeNotify();

    final cacheHits = await searchLocalCaches(_query, pathScope: _pathScope);
    if (epoch != _serverEpoch) return;
    _cacheResults = cacheHits;
    _safeNotify();
    _scheduleServerCall();
  }

  /// Drops the path filter and re-runs the current search globally. Used by
  /// the empty-state "Im Hauptverzeichnis suchen" button.
  Future<void> searchEverywhere() async {
    if (!isScoped) return;
    _pathScope = const [];
    final epoch = ++_serverEpoch;
    if (_query.trim().isEmpty) {
      _safeNotify();
      return;
    }
    _serverLoading = true;
    _serverError = null;
    _safeNotify();

    final cacheHits = await searchLocalCaches(_query);
    if (epoch != _serverEpoch) return;
    _cacheResults = cacheHits;
    _safeNotify();
    _scheduleServerCall();
  }

  /// Re-runs the current server query immediately, bypassing the debounce.
  /// Wired to the `LoadableStateErrorScreen` "Erneut versuchen" button.
  void retry() {
    if (_query.trim().isEmpty) return;
    ++_serverEpoch;
    Debouncer.cancel(_debounceTag);
    _serverLoading = true;
    _serverError = null;
    _safeNotify();
    _runServerCall();
  }

  void _scheduleServerCall() {
    Debouncer.debounce(_debounceTag, _serverDebounce, _runServerCall);
  }

  Future<void> _runServerCall() async {
    final epoch = _serverEpoch;
    final term = _query;
    final scopePrefix = _pathScope.isEmpty ? '' : '${_pathScope.join('/')}/';
    try {
      final response = await _api.run(term: term);
      if (epoch != _serverEpoch) return;
      _serverResults = response.entries
          .map((e) => e.toCacheable())
          .whereType<CacheableFile>()
          .where((f) => scopePrefix.isEmpty || f.path.startsWith(scopePrefix))
          .toList();
      _serverLoading = false;
      _serverError = null;
      _safeNotify();
    } on Object catch (e) {
      if (epoch != _serverEpoch) return;
      _serverResults = const [];
      _serverLoading = false;
      _serverError = e;
      _safeNotify();
    }
  }

  @override
  void dispose() {
    _disposed = true;
    Debouncer.cancel(_debounceTag);
    super.dispose();
  }
}
