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

import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../core/contracts/library.dart';
import '../shared/utils/app_logger.dart';
import '../shared/utils/tmdb_key_utils.dart';
import 'di_providers.dart';
import 'library_overview_provider.dart';
import 'playback_active_provider.dart'
    show playbackActiveProvider, detailPageActiveProvider;
import 'session_provider.dart';
import 'tmdb_settings_provider.dart';

final tmdbPrefetchProvider = NotifierProvider<TmdbPrefetchNotifier, void>(
  TmdbPrefetchNotifier.new,
);

class TmdbPrefetchNotifier extends Notifier<void> {
  static const _requestDelay = Duration(milliseconds: 200);
  static const _retryDelay = Duration(seconds: 2);
  static const _visibleItemCount = 6;
  static const _maxRetries = 2;
  static const _initialDelay = Duration(milliseconds: 1500);

  final Queue<TmdbMediaKey> _queue = Queue<TmdbMediaKey>();
  final Set<TmdbMediaKey> _enqueuedKeys = <TmdbMediaKey>{};
  final Map<TmdbMediaKey, int> _failureCount = <TmdbMediaKey, int>{};
  bool _disposeHookRegistered = false;
  bool _disposed = false;
  bool _isProcessing = false;
  bool _initialDelayElapsed = false;
  bool _initialDelayTimerStarted = false;
  bool _lastPlaybackActive = false;
  bool _lastDetailActive = false;
  String? _lastSessionServerId;
  List<EmbyRawItem>? _lastResumeRef;
  Map<String, List<EmbyRawItem>>? _lastLatestRef;

  @override
  void build() {
    _registerDisposeHook();

    final session = ref.watch(activeSessionProvider);
    final serverId = session?.serverId;
    if (_lastSessionServerId != serverId) {
      _lastSessionServerId = serverId;
      _resetQueue();
    }

    final playbackActive = ref.watch(playbackActiveProvider);
    final detailActive = ref.watch(detailPageActiveProvider);
    final resumedFromPlayback = _lastPlaybackActive && !playbackActive;
    _lastPlaybackActive = playbackActive;
    final resumedFromDetail = _lastDetailActive && !detailActive;
    _lastDetailActive = detailActive;

    final settings = ref.watch(tmdbSettingsProvider).value;
    if (!(settings?.canRequest ?? false)) return;

    final overview = ref.watch(libraryOverviewProvider).value;
    if (overview == null) return;

    final resumeSame = identical(_lastResumeRef, overview.resumeItems);
    final latestSame = identical(_lastLatestRef, overview.latestByLibrary);
    if (resumeSame && latestSame) {
      if (resumedFromPlayback || resumedFromDetail) {
        AppLogger.debug('prefetch', 'tmdb.build.resume_after_pause');
        _scheduleProcessing();
      } else {
        AppLogger.debug('prefetch', 'tmdb.build.skip');
      }
      return;
    }
    _lastResumeRef = overview.resumeItems;
    _lastLatestRef = overview.latestByLibrary;

    final visibleItems = <EmbyRawItem>[
      ...overview.resumeItems.take(_visibleItemCount),
    ];
    for (final items in overview.latestByLibrary.values) {
      visibleItems.addAll(items.take(_visibleItemCount));
    }
    _enqueueItems(visibleItems, priority: true);

    AppLogger.debug('prefetch', 'tmdb.build.enqueued', {
      'queue': _queue.length,
      'tracked': _enqueuedKeys.length,
    });
    _scheduleProcessing();
  }

  void prefetchItem(EmbyRawItem item, {bool priority = false}) {
    final key = tmdbMediaKeyFrom(item);
    if (key == null) return;
    _enqueueKey(key, priority: priority);
    _scheduleProcessing();
  }

  void _registerDisposeHook() {
    if (_disposeHookRegistered) return;
    _disposeHookRegistered = true;
    ref.onDispose(() {
      _disposed = true;
      _resetQueue();
    });
  }

  void _resetQueue() {
    _queue.clear();
    _enqueuedKeys.clear();
    _failureCount.clear();
    _lastResumeRef = null;
    _lastLatestRef = null;
  }

  void _enqueueItems(Iterable<EmbyRawItem> items, {bool priority = false}) {
    final keys = <TmdbMediaKey>[];
    for (final item in items) {
      final key = tmdbMediaKeyFrom(item);
      if (key != null) keys.add(key);
    }

    if (priority) {
      for (final key in keys.reversed) {
        _enqueueKey(key, priority: true);
      }
    } else {
      for (final key in keys) {
        _enqueueKey(key);
      }
    }
  }

  void _enqueueKey(TmdbMediaKey key, {bool priority = false}) {
    if (_enqueuedKeys.contains(key)) {
      if (priority && _queue.remove(key)) {
        _queue.addFirst(key);
      }
      return;
    }

    _enqueuedKeys.add(key);
    if (priority) {
      _queue.addFirst(key);
    } else {
      _queue.add(key);
    }
  }

  bool get _shouldPause =>
      ref.read(playbackActiveProvider) || ref.read(detailPageActiveProvider);

  void _scheduleProcessing() {
    if (_disposed || _isProcessing || _queue.isEmpty) return;
    if (_shouldPause) return;
    if (!_initialDelayElapsed) {
      _startInitialDelay();
      return;
    }
    unawaited(_processQueue());
  }

  void _startInitialDelay() {
    if (_initialDelayTimerStarted) return;
    _initialDelayTimerStarted = true;
    Future<void>.delayed(_initialDelay, () {
      _initialDelayElapsed = true;
      if (_disposed) return;
      _scheduleProcessing();
    });
  }

  Future<void> _processQueue() async {
    if (_isProcessing) return;
    _isProcessing = true;
    try {
      while (!_disposed && _queue.isNotEmpty) {
        if (!_canRequestTmdb()) break;
        if (_shouldPause) {
          AppLogger.debug('prefetch', 'tmdb.queue.suspended', {
            'queue': _queue.length,
          });
          break;
        }

        final key = _queue.removeFirst();
        _enqueuedKeys.remove(key);

        bool requeued = false;
        try {
          final detail = await ref.read(tmdbEnrichedDetailProvider(key).future);
          if (_disposed) break;
          if (detail == null) {
            ref.invalidate(tmdbEnrichedDetailProvider(key));
            _failureCount.remove(key);
            AppLogger.debug('prefetch', 'tmdb.fetch.empty', {'key': key});
          } else {
            _failureCount.remove(key);
            AppLogger.debug('prefetch', 'tmdb.fetch.ok', {'key': key});
          }
        } catch (e) {
          if (_disposed) break;
          ref.invalidate(tmdbEnrichedDetailProvider(key));
          final attempts = (_failureCount[key] ?? 0) + 1;
          if (attempts <= _maxRetries) {
            _failureCount[key] = attempts;
            _enqueuedKeys.add(key);
            _queue.addLast(key);
            requeued = true;
            AppLogger.debug('prefetch', 'tmdb.fetch.retry', {
              'key': key,
              'attempts': attempts,
              'error': e.toString(),
            });
          } else {
            _failureCount.remove(key);
            AppLogger.debug('prefetch', 'tmdb.fetch.giveup', {
              'key': key,
              'error': e.toString(),
            });
          }
        }

        if (!_disposed && _queue.isNotEmpty) {
          await Future<void>.delayed(requeued ? _retryDelay : _requestDelay);
        }
      }
    } finally {
      _isProcessing = false;
      if (!_disposed &&
          _queue.isNotEmpty &&
          _canRequestTmdb() &&
          !_shouldPause) {
        _scheduleProcessing();
      }
    }
  }

  bool _canRequestTmdb() {
    return ref.read(tmdbSettingsProvider).value?.canRequest ?? false;
  }
}
