import 'dart:async';

import 'package:collection/collection.dart';

import '../../../../../api/marianumcloud/webdav/queries/list_files/list_files_response.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
import '../repository/files_repository.dart';
import 'files_event.dart';
import 'files_state.dart';

class FilesBloc
    extends LoadableHydratedBloc<FilesEvent, FilesState, FilesRepository> {
  final List<String> initialPath;

  FilesBloc({this.initialPath = const []});

  @override
  FilesRepository repository() => FilesRepository();

  @override
  FilesState fromNothing() => FilesState(currentPath: initialPath);

  @override
  FilesState fromStorage(Map<String, dynamic> json) =>
      FilesState.fromJson(json);

  @override
  Map<String, dynamic>? toStorage(FilesState state) => null;

  @override
  Future<void> gatherData() async {
    final path = innerState?.currentPath ?? initialPath;
    await _query(path);
  }

  Future<void> refresh() async {
    add(RefetchStarted<FilesState>());
    final path = innerState?.currentPath ?? initialPath;
    // Explicit user action — bypass the cache TTL so the root listing also
    // refetches even though it is otherwise cached for a day.
    await _query(path, renew: true);
  }

  /// Pull-to-refresh and error-screen retry route through here. Unlike the
  /// inherited retry() (via gatherData(), which respects the cache TTL), this
  /// bypasses the TTL — otherwise the root listing returns its day-old cache.
  @override
  void retry() {
    unawaited(refresh());
  }

  Future<void> setPath(List<String> path) async {
    add(Emit((s) => s.copyWith(currentPath: path, listing: null)));
    add(RefetchStarted<FilesState>());
    await _query(path);
  }

  Future<void> createFolder(String name) async {
    final path = innerState?.currentPath ?? initialPath;
    await repo.data.createFolder('${path.join('/')}/$name');
    await refresh();
  }

  Future<void> _query(List<String> path, {bool renew = false}) async {
    final pathString = path.isEmpty ? '/' : path.join('/');

    // Drop late results when [setPath] has navigated elsewhere or when the
    // bloc has been disposed (e.g. share-flow picker closed mid-fetch). Both
    // would otherwise corrupt state or hit "add after close" on the stream.
    const pathEquality = ListEquality<String>();
    bool isStale() {
      if (isClosed) return true;
      final inner = innerState;
      if (inner == null) return false;
      return !pathEquality.equals(inner.currentPath, path);
    }

    Object? capturedError;
    ListFilesResponse? listing;
    try {
      listing = await repo.data.listFiles(
        pathString,
        renew: renew,
        onCacheData: (cached) {
          if (isStale()) return;
          // Cached payload arrives before the network call settles. Surface it
          // immediately via Emit so the listing is visible while isLoading
          // stays true and the top loading bar keeps spinning.
          cached.files.removeWhere(
            (file) => file.name.isEmpty || file.name == path.lastOrNull,
          );
          add(Emit((s) => s.copyWith(listing: cached)));
        },
        onRetry: (next, max) {
          if (isStale()) return;
          add(LoadingStatus('Erneuter Versuch ($next von $max) …'));
        },
        onError: (e) => capturedError = e,
      );
    } catch (e) {
      capturedError = e;
    }

    if (isStale()) return;

    if (listing != null) {
      listing.files.removeWhere(
        (file) => file.name.isEmpty || file.name == path.lastOrNull,
      );
      add(DataGathered((s) => s.copyWith(listing: listing)));
    }
    if (capturedError != null) addLoadingError(capturedError!);
  }
}
