import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../core/contracts/cancellation.dart';
import '../core/contracts/library.dart';
import '../core/infra/storage/home_page_cache_store.dart';
import '../shared/utils/app_logger.dart';
import 'di_providers.dart';
import 'playback_active_provider.dart';
import 'session_provider.dart';

class LibraryOverviewState {
  final List<EmbyRawLibrary> libraries;
  final List<EmbyRawItem> resumeItems;
  final Map<String, List<EmbyRawItem>> latestByLibrary;
  final Map<String, int> totalCountByLibrary;
  final Set<String> loadingLibraryIds;
  final bool resumeLoading;
  final Object? resumeError;
  final Map<String, Object?> latestErrorByLibrary;

  const LibraryOverviewState({
    this.libraries = const [],
    this.resumeItems = const [],
    this.latestByLibrary = const {},
    this.totalCountByLibrary = const {},
    this.loadingLibraryIds = const {},
    this.resumeLoading = false,
    this.resumeError,
    this.latestErrorByLibrary = const {},
  });

  LibraryOverviewState copyWith({
    List<EmbyRawLibrary>? libraries,
    List<EmbyRawItem>? resumeItems,
    Map<String, List<EmbyRawItem>>? latestByLibrary,
    Map<String, int>? totalCountByLibrary,
    Set<String>? loadingLibraryIds,
    bool? resumeLoading,
    Object? resumeError,
    bool clearResumeError = false,
    Map<String, Object?>? latestErrorByLibrary,
  }) => LibraryOverviewState(
    libraries: libraries ?? this.libraries,
    resumeItems: resumeItems ?? this.resumeItems,
    latestByLibrary: latestByLibrary ?? this.latestByLibrary,
    totalCountByLibrary: totalCountByLibrary ?? this.totalCountByLibrary,
    loadingLibraryIds: loadingLibraryIds ?? this.loadingLibraryIds,
    resumeLoading: resumeLoading ?? this.resumeLoading,
    resumeError: clearResumeError ? null : (resumeError ?? this.resumeError),
    latestErrorByLibrary: latestErrorByLibrary ?? this.latestErrorByLibrary,
  );
}

class LibraryOverviewNotifier extends AsyncNotifier<LibraryOverviewState> {
  CancellationToken? _latestBatchToken;
  int _latestRunId = 0;

  @override
  Future<LibraryOverviewState> build() async {
    final session = ref.watch(activeSessionProvider);
    if (session == null) return const LibraryOverviewState();

    final store = await ref.read(homePageCacheStoreProvider.future);
    final cache = await store.readOverview(session.serverId, session.userId);

    if (cache != null) {
      unawaited(_refreshFromNetwork(session.serverId, session.userId));
      return LibraryOverviewState(
        libraries: cache.libraries,
        resumeItems: cache.resumeItems,
        latestByLibrary: cache.latestByLibrary,
        loadingLibraryIds: const {},
        resumeLoading: false,
      );
    }

    return _loadPrimaryAndCache(session.serverId, session.userId);
  }

  Future<LibraryOverviewState> _loadPrimaryAndCache(
    String serverId,
    String userId,
  ) async {
    final store = await ref.read(homePageCacheStoreProvider.future);
    final viewsUc = await ref.read(libraryViewsUseCaseProvider.future);

    final sw = Stopwatch()..start();
    final views = await viewsUc.execute();
    AppLogger.debug('prefetch', 'overview.views', {
      'count': views.Items.length,
      'ms': sw.elapsedMilliseconds,
    });

    unawaited(
      store.updateOverviewFields(serverId, userId, libraries: views.Items),
    );

    final libraryIds = views.Items.map((lib) => lib.Id).toSet();
    final initialState = LibraryOverviewState(
      libraries: views.Items,
      resumeItems: const [],
      loadingLibraryIds: libraryIds,
      resumeLoading: true,
    );

    unawaited(_loadResumeAndCache(serverId, userId, store));
    unawaited(
      _loadLatestProgressivelyAndCache(views.Items, serverId, userId, store),
    );

    return initialState;
  }

  Future<void> _loadResumeAndCache(
    String serverId,
    String userId,
    HomePageCacheStore store,
  ) async {
    final sw = Stopwatch()..start();
    try {
      final resumeUc = await ref.read(libraryResumeUseCaseProvider.future);
      final resume = await resumeUc.execute();
      final current = state.value;
      if (current == null) return;
      AppLogger.debug('prefetch', 'overview.resume.ok', {
        'count': resume.Items.length,
        'ms': sw.elapsedMilliseconds,
      });
      state = AsyncValue.data(
        current.copyWith(
          resumeItems: resume.Items,
          resumeLoading: false,
          clearResumeError: true,
        ),
      );
      unawaited(
        store.updateOverviewFields(serverId, userId, resumeItems: resume.Items),
      );
    } catch (e) {
      final current = state.value;
      if (current == null) return;
      AppLogger.debug('prefetch', 'overview.resume.err', {
        'error': e.toString(),
        'ms': sw.elapsedMilliseconds,
      });
      state = AsyncValue.data(
        current.copyWith(resumeLoading: false, resumeError: e),
      );
    }
  }

  static const _batchSize = 4;

  Future<void> _loadLatestProgressivelyAndCache(
    List<EmbyRawLibrary> libraries,
    String serverId,
    String userId,
    HomePageCacheStore store,
  ) async {
    await _runLatestBatches(
      libraries,
      serverId,
      userId,
      store,
      mode: _LatestUpdateMode.progressive,
    );
  }

  Future<void> _refreshFromNetwork(String serverId, String userId) async {
    final store = await ref.read(homePageCacheStoreProvider.future);

    try {
      final viewsUc = await ref.read(libraryViewsUseCaseProvider.future);
      final sw = Stopwatch()..start();
      final views = await viewsUc.execute();
      AppLogger.debug('prefetch', 'overview.views', {
        'count': views.Items.length,
        'ms': sw.elapsedMilliseconds,
      });
      if (!_sessionMatches(serverId, userId)) return;
      final current = state.value;
      if (current != null) {
        state = AsyncValue.data(current.copyWith(libraries: views.Items));
      }
      unawaited(
        store.updateOverviewFields(serverId, userId, libraries: views.Items),
      );

      unawaited(_refreshResumeFromNetwork(serverId, userId, store));

      unawaited(
        _refreshLatestFromNetwork(views.Items, serverId, userId, store),
      );
    } catch (_) {}
  }

  Future<void> _refreshResumeFromNetwork(
    String serverId,
    String userId,
    HomePageCacheStore store,
  ) async {
    final sw = Stopwatch()..start();
    try {
      final resumeUc = await ref.read(libraryResumeUseCaseProvider.future);
      final resume = await resumeUc.execute();
      AppLogger.debug('prefetch', 'overview.resume.ok', {
        'count': resume.Items.length,
        'ms': sw.elapsedMilliseconds,
      });
      if (!_sessionMatches(serverId, userId)) return;
      final current = state.value;
      if (current == null) return;
      state = AsyncValue.data(
        current.copyWith(
          resumeItems: resume.Items,
          resumeLoading: false,
          clearResumeError: true,
        ),
      );
      unawaited(
        store.updateOverviewFields(serverId, userId, resumeItems: resume.Items),
      );
    } catch (_) {}
  }

  Future<void> _refreshLatestFromNetwork(
    List<EmbyRawLibrary> libraries,
    String serverId,
    String userId,
    HomePageCacheStore store,
  ) async {
    await _runLatestBatches(
      libraries,
      serverId,
      userId,
      store,
      mode: _LatestUpdateMode.refresh,
    );
  }

  Future<void> _runLatestBatches(
    List<EmbyRawLibrary> libraries,
    String serverId,
    String userId,
    HomePageCacheStore store, {
    required _LatestUpdateMode mode,
  }) async {
    final runId = ++_latestRunId;
    _latestBatchToken?.cancel('latest_replaced');
    _latestBatchToken = null;
    final latestUc = await ref.read(libraryLatestUseCaseProvider.future);

    for (var i = 0; i < libraries.length;) {
      if (runId != _latestRunId || !_sessionMatches(serverId, userId)) return;

      await _waitWhileDetailActive();
      if (runId != _latestRunId || !_sessionMatches(serverId, userId)) return;

      final batch = libraries.sublist(
        i,
        (i + _batchSize).clamp(0, libraries.length),
      );
      final token = CancellationToken();
      _latestBatchToken = token;
      final sub = ref.listen<bool>(detailPageActiveProvider, (_, active) {
        if (active) token.cancel('detail_active');
      });

      try {
        if (ref.read(detailPageActiveProvider)) {
          token.cancel('detail_active');
        }
        token.throwIfCancelled();
        final results = await _fetchLatestBatch(batch, latestUc.execute, token);
        token.throwIfCancelled();

        if (runId != _latestRunId || !_sessionMatches(serverId, userId)) {
          return;
        }
        _applyLatestResults(results, serverId, userId, store, mode: mode);
        i += _batchSize;
      } on CancelledException catch (e) {
        if (runId != _latestRunId) return;
        AppLogger.debug('prefetch', 'overview.latest.cancel', {
          'index': i,
          'reason': e.reason ?? token.reason ?? 'cancelled',
        });
      } finally {
        sub.close();
        if (identical(_latestBatchToken, token)) {
          _latestBatchToken = null;
        }
      }
    }
  }

  Future<List<_LatestResult>> _fetchLatestBatch(
    List<EmbyRawLibrary> batch,
    Future<LibraryLatestRes> Function(
      LibraryLatestReq input, {
      CancellationToken? cancelToken,
    })
    execute,
    CancellationToken cancelToken,
  ) async {
    final results = <_LatestResult>[];
    final futures = batch.map((lib) async {
      cancelToken.throwIfCancelled();
      final sw = Stopwatch()..start();
      List<EmbyRawItem> items = const [];
      int totalCount = 0;
      Object? err;
      try {
        final res = await execute(
          LibraryLatestReq(parentId: lib.Id, limit: 16),
          cancelToken: cancelToken,
        );
        cancelToken.throwIfCancelled();
        items = res.items;
        totalCount = res.totalCount;
      } on CancelledException {
        rethrow;
      } catch (e) {
        err = e;
      }
      AppLogger.debug('prefetch', 
        err == null ? 'overview.latest.ok' : 'overview.latest.err',
        {
          'lib': lib.Name,
          'count': items.length,
          if (err != null) 'error': err.toString(),
          'ms': sw.elapsedMilliseconds,
        },
      );
      results.add(
        _LatestResult(
          libraryId: lib.Id,
          items: items,
          totalCount: totalCount,
          error: err,
        ),
      );
    }).toList();
    await Future.wait(futures);
    return results;
  }

  void _applyLatestResults(
    List<_LatestResult> results,
    String serverId,
    String userId,
    HomePageCacheStore store, {
    required _LatestUpdateMode mode,
  }) {
    final current = state.value;
    if (current == null) return;

    final newMap = Map<String, List<EmbyRawItem>>.from(current.latestByLibrary);
    final newCounts = Map<String, int>.from(current.totalCountByLibrary);
    var hasSuccess = false;

    if (mode == _LatestUpdateMode.refresh) {
      final newLoading = Set<String>.from(current.loadingLibraryIds);
      for (final r in results) {
        if (r.error != null) continue;
        newMap[r.libraryId] = r.items;
        if (r.totalCount > 0) newCounts[r.libraryId] = r.totalCount;
        newLoading.remove(r.libraryId);
        hasSuccess = true;
      }
      state = AsyncValue.data(
        current.copyWith(
          latestByLibrary: newMap,
          totalCountByLibrary: newCounts,
          loadingLibraryIds: newLoading,
        ),
      );
      if (hasSuccess) {
        unawaited(
          store.updateOverviewFields(
            serverId,
            userId,
            latestByLibrary: Map<String, List<EmbyRawItem>>.from(newMap),
          ),
        );
      }
      return;
    }

    final newLoading = Set<String>.from(current.loadingLibraryIds);
    final newErrors = Map<String, Object?>.from(current.latestErrorByLibrary);

    for (final r in results) {
      newMap[r.libraryId] = r.items;
      if (r.totalCount > 0) newCounts[r.libraryId] = r.totalCount;
      newLoading.remove(r.libraryId);
      if (r.error != null) {
        newErrors[r.libraryId] = r.error;
      } else {
        newErrors.remove(r.libraryId);
        hasSuccess = true;
      }
    }

    state = AsyncValue.data(
      current.copyWith(
        latestByLibrary: newMap,
        totalCountByLibrary: newCounts,
        loadingLibraryIds: newLoading,
        latestErrorByLibrary: newErrors,
      ),
    );

    if (hasSuccess) {
      unawaited(
        store.updateOverviewFields(
          serverId,
          userId,
          latestByLibrary: Map<String, List<EmbyRawItem>>.from(newMap),
        ),
      );
    }
  }

  Future<void> _waitWhileDetailActive() async {
    if (!ref.read(detailPageActiveProvider)) return;
    final completer = Completer<void>();
    final sub = ref.listen<bool>(detailPageActiveProvider, (_, active) {
      if (!active && !completer.isCompleted) {
        completer.complete();
      }
    });
    await completer.future;
    sub.close();
  }

  bool _sessionMatches(String serverId, String userId) {
    final s = ref.read(activeSessionProvider);
    return s != null && s.serverId == serverId && s.userId == userId;
  }

  Future<void> refresh() async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      final session = ref.read(activeSessionProvider);
      if (session == null) return const LibraryOverviewState();
      return _loadPrimaryAndCache(session.serverId, session.userId);
    });
  }


  Future<void> softRefresh() async {
    final session = ref.read(activeSessionProvider);
    if (session == null) return;
    if (state.value == null) {
      await refresh();
      return;
    }
    await _refreshFromNetwork(session.serverId, session.userId);
  }

  Future<void> retryResume() async {
    final session = ref.read(activeSessionProvider);
    if (session == null) return;
    final store = await ref.read(homePageCacheStoreProvider.future);
    final current = state.value;
    if (current == null) return;
    state = AsyncValue.data(
      current.copyWith(resumeLoading: true, clearResumeError: true),
    );
    await _loadResumeAndCache(session.serverId, session.userId, store);
  }

  Future<void> retryLatest(String libraryId) async {
    final session = ref.read(activeSessionProvider);
    if (session == null) return;
    final store = await ref.read(homePageCacheStoreProvider.future);
    final current = state.value;
    if (current == null) return;

    final newLoading = Set<String>.from(current.loadingLibraryIds)
      ..add(libraryId);
    final clearedErrors = Map<String, Object?>.from(
      current.latestErrorByLibrary,
    )..remove(libraryId);
    state = AsyncValue.data(
      current.copyWith(
        loadingLibraryIds: newLoading,
        latestErrorByLibrary: clearedErrors,
      ),
    );

    final uc = await ref.read(libraryLatestUseCaseProvider.future);
    final sw = Stopwatch()..start();
    try {
      final res = await uc.execute(
        LibraryLatestReq(parentId: libraryId, limit: 16),
      );
      AppLogger.debug('prefetch', 'overview.latest.retry.ok', {
        'lib': libraryId,
        'count': res.items.length,
        'ms': sw.elapsedMilliseconds,
      });
      final cur = state.value;
      if (cur == null) return;
      final newMap = Map<String, List<EmbyRawItem>>.from(cur.latestByLibrary);
      newMap[libraryId] = res.items;
      final newCounts = Map<String, int>.from(cur.totalCountByLibrary);
      if (res.totalCount > 0) newCounts[libraryId] = res.totalCount;
      final stillLoading = Set<String>.from(cur.loadingLibraryIds)
        ..remove(libraryId);
      state = AsyncValue.data(
        cur.copyWith(
          latestByLibrary: newMap,
          totalCountByLibrary: newCounts,
          loadingLibraryIds: stillLoading,
        ),
      );
      unawaited(
        store.updateOverviewFields(
          session.serverId,
          session.userId,
          latestByLibrary: Map<String, List<EmbyRawItem>>.from(newMap),
        ),
      );
    } catch (e) {
      AppLogger.debug('prefetch', 'overview.latest.retry.err', {
        'lib': libraryId,
        'error': e.toString(),
        'ms': sw.elapsedMilliseconds,
      });
      final cur = state.value;
      if (cur == null) return;
      final stillLoading = Set<String>.from(cur.loadingLibraryIds)
        ..remove(libraryId);
      final errs = Map<String, Object?>.from(cur.latestErrorByLibrary);
      errs[libraryId] = e;
      state = AsyncValue.data(
        cur.copyWith(
          loadingLibraryIds: stillLoading,
          latestErrorByLibrary: errs,
        ),
      );
    }
  }
}

final libraryOverviewProvider =
    AsyncNotifierProvider<LibraryOverviewNotifier, LibraryOverviewState>(
      LibraryOverviewNotifier.new,
    );

enum _LatestUpdateMode { progressive, refresh }

class _LatestResult {
  final String libraryId;
  final List<EmbyRawItem> items;
  final int totalCount;
  final Object? error;

  const _LatestResult({
    required this.libraryId,
    required this.items,
    required this.totalCount,
    this.error,
  });
}
