import 'dart:async';

import '../../../shared/utils/app_logger.dart';
import '../../contracts/contracts.dart';
import '../ports/emby_gateway.dart';
import 'usecase_helpers.dart';

const _ucTag = 'UseCase';

class GetMediaDetailUseCase extends AuthedUseCase {
  GetMediaDetailUseCase({
    required super.sessionRepository,
    required super.serverRepository,
    required super.embyGateway,
  });

  Future<MediaDetailRes> execute(MediaDetailReq input) async {
    final base = await embyGateway.getItemDetail(await ctx(), input.itemId);
    return MediaDetailRes(base: base);
  }


  Future<MediaDetailRes> executeForServer({
    required String serverId,
    required String itemId,
  }) async {
    final ctx = await requireCtxForServer(serverId);
    final base = await embyGateway.getItemDetail(ctx, itemId);
    return MediaDetailRes(base: base);
  }
}

class SetFavoriteUseCase extends AuthedUseCase {
  SetFavoriteUseCase({
    required super.sessionRepository,
    required super.serverRepository,
    required super.embyGateway,
  });

  Future<FavoriteSetRes> execute(FavoriteSetReq input) async {
    final result = await embyGateway.setFavoriteStatus(
      ctx: await ctx(),
      itemId: input.itemId,
      isFavorite: input.isFavorite,
    );
    return FavoriteSetRes(itemId: input.itemId, isFavorite: result);
  }


  Future<FavoriteSetRes> executeForServer({
    required String serverId,
    required FavoriteSetReq input,
  }) async {
    final ctx = await requireCtxForServer(serverId);
    final result = await embyGateway.setFavoriteStatus(
      ctx: ctx,
      itemId: input.itemId,
      isFavorite: input.isFavorite,
    );
    return FavoriteSetRes(itemId: input.itemId, isFavorite: result);
  }
}

class SetPlayedUseCase extends AuthedUseCase {
  SetPlayedUseCase({
    required super.sessionRepository,
    required super.serverRepository,
    required super.embyGateway,
  });

  Future<PlayedSetRes> execute(PlayedSetReq input) async {
    final result = await embyGateway.setPlayedStatus(
      ctx: await ctx(),
      itemId: input.itemId,
      played: input.played,
    );
    return PlayedSetRes(itemId: input.itemId, played: result);
  }


  Future<PlayedSetRes> executeForServer({
    required String serverId,
    required PlayedSetReq input,
  }) async {
    final ctx = await requireCtxForServer(serverId);
    final result = await embyGateway.setPlayedStatus(
      ctx: ctx,
      itemId: input.itemId,
      played: input.played,
    );
    return PlayedSetRes(itemId: input.itemId, played: result);
  }
}

class HideFromResumeUseCase extends AuthedUseCase {
  HideFromResumeUseCase({
    required super.sessionRepository,
    required super.serverRepository,
    required super.embyGateway,
  });

  Future<HideFromResumeRes> execute(HideFromResumeReq input) async {
    final result = await embyGateway.hideFromResume(
      ctx: await ctx(),
      itemId: input.itemId,
      hide: input.hide,
    );
    return HideFromResumeRes(itemId: input.itemId, hide: result);
  }
}

class GetSeriesEpisodesUseCase extends AuthedUseCase {
  GetSeriesEpisodesUseCase({
    required super.sessionRepository,
    required super.serverRepository,
    required super.embyGateway,
  });

  Future<MediaEpisodesRes> execute(MediaEpisodesReq input) async {
    final items = await embyGateway.getSeriesEpisodes(
      await ctx(),
      input.seriesId,
      input.seasonId,
    );
    return MediaEpisodesRes(
      seriesId: input.seriesId,
      seasonId: input.seasonId,
      items: items,
    );
  }


  Future<MediaEpisodesRes> executeForServer({
    required String serverId,
    required MediaEpisodesReq input,
  }) async {
    final ctx = await requireCtxForServer(serverId);
    final items = await embyGateway.getSeriesEpisodes(
      ctx,
      input.seriesId,
      input.seasonId,
    );
    return MediaEpisodesRes(
      seriesId: input.seriesId,
      seasonId: input.seasonId,
      items: items,
    );
  }
}

class GetSeriesSeasonsUseCase extends AuthedUseCase {
  GetSeriesSeasonsUseCase({
    required super.sessionRepository,
    required super.serverRepository,
    required super.embyGateway,
  });

  Future<SeriesSeasonsRes> execute(SeriesSeasonsReq input) async {
    final seasons = await embyGateway.getSeriesSeasons(
      await ctx(),
      input.seriesId,
    );
    return SeriesSeasonsRes(seriesId: input.seriesId, items: seasons);
  }


  Future<SeriesSeasonsRes> executeForServer({
    required String serverId,
    required SeriesSeasonsReq input,
  }) async {
    final ctx = await requireCtxForServer(serverId);
    final seasons = await embyGateway.getSeriesSeasons(ctx, input.seriesId);
    return SeriesSeasonsRes(seriesId: input.seriesId, items: seasons);
  }
}

class GetSimilarItemsUseCase extends AuthedUseCase {
  GetSimilarItemsUseCase({
    required super.sessionRepository,
    required super.serverRepository,
    required super.embyGateway,
  });

  Future<SimilarItemsRes> execute(SimilarItemsReq input) async {
    final items = await embyGateway.getSimilarItems(
      await ctx(),
      input.itemId,
      limit: input.limit,
    );
    return SimilarItemsRes(itemId: input.itemId, items: items);
  }
}

class GetAdditionalPartsUseCase extends AuthedUseCase {
  GetAdditionalPartsUseCase({
    required super.sessionRepository,
    required super.serverRepository,
    required super.embyGateway,
  });

  Future<AdditionalPartsRes> execute(AdditionalPartsReq input) async {
    final items = await embyGateway.getAdditionalParts(
      await ctx(),
      input.itemId,
    );
    return AdditionalPartsRes(itemId: input.itemId, items: items);
  }


  Future<AdditionalPartsRes> executeForServer({
    required String serverId,
    required String itemId,
  }) async {
    final ctx = await resolveAuthedContextForServer(
      sessionRepository: sessionRepository,
      serverRepository: serverRepository,
      serverId: serverId,
    );
    if (ctx == null) {
      return AdditionalPartsRes(itemId: itemId, items: const []);
    }
    final items = await embyGateway.getAdditionalParts(ctx, itemId);
    return AdditionalPartsRes(itemId: itemId, items: items);
  }
}

class GetSpecialFeaturesUseCase extends AuthedUseCase {
  GetSpecialFeaturesUseCase({
    required super.sessionRepository,
    required super.serverRepository,
    required super.embyGateway,
  });

  Future<List<EmbyRawItem>> execute(String itemId) async {
    return embyGateway.getSpecialFeatures(await ctx(), itemId);
  }
}

class CrossServerSearchUseCase extends AuthedUseCase {
  CrossServerSearchUseCase({
    required super.sessionRepository,
    required super.serverRepository,
    required super.embyGateway,
  });


  final Map<String, _SeriesIdentity?> _seriesIdentityCache = {};
  final Map<String, _EmptyServerSearchStreak> _emptyServerSearchStreaks = {};

  static const _emptyServerSkipThreshold = 3;
  static const _emptyServerSkipTtl = Duration(hours: 1);

  String _identityCacheKey(String serverId, String seriesPidQuery) =>
      '$serverId|$seriesPidQuery';


  Stream<List<CrossServerSourceCard>> executeStream(
    CrossServerSearchReq input, {
    CancellationToken? cancelToken,
  }) {
    final controller = StreamController<List<CrossServerSourceCard>>();
    final sw = Stopwatch()..start();
    () async {
      try {
        final activeServerId = await sessionRepository.getActiveServerId();
        final excludedServerId = input.excludedServerId ?? activeServerId;
        final allSessions = await sessionRepository.listAll();
        final allServers = await serverRepository.list();
        final serverMap = {for (final s in allServers) s.id: s};

        final acc = <CrossServerSourceCard>[];
        var probed = 0;
        var skipped = 0;
        final futures = <Future<void>>[];
        for (final session in allSessions) {
          if (session.serverId == excludedServerId) continue;
          final server = serverMap[session.serverId];
          if (server == null) continue;
          if (_shouldSkipEmptyServer(server)) {
            skipped++;
            final streak = _emptyServerSearchStreaks[server.id];
            AppLogger.debug(
              'DetailTiming',
              'xserver.server name=${server.name} skipped emptyStreak=${streak?.count ?? 0}',
            );
            continue;
          }
          probed++;
          futures.add(() async {
            final serverSw = Stopwatch()..start();
            final cards = await _buildCardsForServer(
              server,
              session,
              input,
              cancelToken: cancelToken,
            );
            AppLogger.debug(
              'DetailTiming',
              'xserver.server name=${server.name} cards=${cards.length} ms=${serverSw.elapsedMilliseconds}',
            );
            if (cancelToken?.isCancelled == true) return;
            _recordServerSearchResult(server, cards);
            if (cards.isEmpty) return;
            acc.addAll(cards);
            if (!controller.isClosed) {
              controller.add(List.unmodifiable(acc));
            }
          }());
        }
        await Future.wait(futures);
        AppLogger.debug(
          'DetailTiming',
          'xserver.total probed=$probed skipped=$skipped cards=${acc.length} ms=${sw.elapsedMilliseconds}',
        );
      } catch (e) {
        if (!controller.isClosed) controller.addError(e);
      } finally {
        if (!controller.isClosed) await controller.close();
      }
    }();
    return controller.stream;
  }

  bool _shouldSkipEmptyServer(EmbyServer server) {
    final streak = _emptyServerSearchStreaks[server.id];
    final skipUntil = streak?.skipUntil;
    if (skipUntil == null) return false;
    if (DateTime.now().isBefore(skipUntil)) return true;
    _emptyServerSearchStreaks.remove(server.id);
    return false;
  }

  void _recordServerSearchResult(
    EmbyServer server,
    List<CrossServerSourceCard> cards,
  ) {
    if (cards.isNotEmpty) {
      _emptyServerSearchStreaks.remove(server.id);
      return;
    }
    final streak = _emptyServerSearchStreaks.putIfAbsent(
      server.id,
      _EmptyServerSearchStreak.new,
    );
    streak.count++;
    if (streak.count < _emptyServerSkipThreshold) return;
    streak.skipUntil = DateTime.now().add(_emptyServerSkipTtl);
    AppLogger.debug(
      'DetailTiming',
      'xserver.server name=${server.name} emptyStreak=${streak.count} '
          'skipUntil=${streak.skipUntil!.toIso8601String()}',
    );
  }

  Future<List<CrossServerSourceCard>> _buildCardsForServer(
    EmbyServer server,
    SessionData session,
    CrossServerSearchReq req, {
    CancellationToken? cancelToken,
  }) async {
    final ctx = AuthedRequestContext(
      baseUrl: server.baseUrl,
      token: session.token,
      userId: session.userId,
    );
    try {
      if (req.anchorType == 'Movie') {
        return await _buildMovieCards(
          ctx,
          server,
          session,
          req,
          cancelToken: cancelToken,
        );
      }
      if (req.anchorType == 'Episode') {
        return await _buildEpisodeCards(
          ctx,
          server,
          session,
          req,
          cancelToken: cancelToken,
        );
      }
      return const [];
    } on CancelledException {
      return const [];
    } catch (e) {
      AppLogger.warn(_ucTag, 'CrossServerSearch on ${server.name} failed', e);
      return const [];
    }
  }

  Future<List<CrossServerSourceCard>> _buildMovieCards(
    AuthedRequestContext ctx,
    EmbyServer server,
    SessionData session,
    CrossServerSearchReq req, {
    CancellationToken? cancelToken,
  }) async {
    const movieFields = 'ProviderIds,UserData,MediaSources,MediaStreams';
    final providerQuery = _buildProviderIdQuery(req.providerIds);
    var items = <EmbyRawItem>[];
    if (providerQuery != null && providerQuery.isNotEmpty) {
      final providerItems = await embyGateway.getLibraryItems(
        ctx: ctx,
        includeItemTypes: 'Movie',
        anyProviderIdEquals: providerQuery,
        fields: movieFields,
        limit: 10,
        recursive: true,
        cancelToken: cancelToken,
      );
      items = providerItems
          .where((item) => _itemMatchesAnyProviderId(item, req.providerIds))
          .toList(growable: false);
      final droppedItemCount = providerItems.length - items.length;
      if (droppedItemCount > 0) {
        AppLogger.debug(
          'DetailTiming',
          'xserver.provider-filter server=${server.name} '
              'dropped=$droppedItemCount returned=${providerItems.length}',
        );
      }
    }
    if (items.isEmpty && req.itemName.isNotEmpty) {
      final nameSearchItems = await embyGateway.getLibraryItems(
        ctx: ctx,
        includeItemTypes: 'Movie',
        searchTerm: req.itemName,
        fields: movieFields,
        limit: 20,
        recursive: true,
        cancelToken: cancelToken,
      );
      items = nameSearchItems
          .where(
            (item) => !_itemHasConflictingProviderId(item, req.providerIds),
          )
          .toList(growable: false);
    }
    final cards = <CrossServerSourceCard>[];
    if (cancelToken?.isCancelled == true) return List.unmodifiable(cards);
    final detailedList = await Future.wait(
      items.map(
        (it) => _fetchDetailedItem(ctx, server, it, cancelToken: cancelToken),
      ),
    );
    if (cancelToken?.isCancelled == true) return List.unmodifiable(cards);
    for (final detailed in detailedList) {
      cards.addAll(_expandToCards(server, session, detailed));
    }
    return List.unmodifiable(cards);
  }


  Future<EmbyRawItem> _fetchDetailedItem(
    AuthedRequestContext ctx,
    EmbyServer server,
    EmbyRawItem item, {
    CancellationToken? cancelToken,
  }) async {
    if (cancelToken?.isCancelled == true) return item;
    try {
      final full = await embyGateway.getItemDetail(ctx, item.Id);
      final fullSources = full.extra['MediaSources'];
      if (fullSources is List && fullSources.isNotEmpty) return full;
      return item;
    } catch (e) {
      AppLogger.warn(
        _ucTag,
        'CrossServer detail fetch failed on ${server.name} item=${item.Id}',
        e,
      );
      return item;
    }
  }

  Future<List<CrossServerSourceCard>> _buildEpisodeCards(
    AuthedRequestContext ctx,
    EmbyServer server,
    SessionData session,
    CrossServerSearchReq req, {
    CancellationToken? cancelToken,
  }) async {
    const episodeFields = 'ProviderIds,UserData,MediaSources,MediaStreams';

    final providerQuery = _buildProviderIdQuery(req.providerIds);
    if (providerQuery != null && providerQuery.isNotEmpty) {
      final episodes = await embyGateway.getLibraryItems(
        ctx: ctx,
        includeItemTypes: 'Episode',
        anyProviderIdEquals: providerQuery,
        fields: episodeFields,
        limit: 5,
        recursive: true,
        cancelToken: cancelToken,
      );
      final exact = _matchEpisodeBySE(
        episodes,
        req.parentIndexNumber,
        req.indexNumber,
      );
      if (exact != null) {
        final detailed = await _fetchDetailedItem(
          ctx,
          server,
          exact,
          cancelToken: cancelToken,
        );
        return _expandToCards(server, session, detailed);
      }
    }

    final seriesProviderIds = req.seriesProviderIds;
    if (seriesProviderIds == null || seriesProviderIds.isEmpty) {
      return const [];
    }

    final identity = await _resolveSeriesIdentity(
      ctx,
      server.id,
      seriesProviderIds,
      cancelToken: cancelToken,
    );
    if (identity == null) return const [];

    EmbyRawSeason? matchedSeason;
    for (final s in identity.seasons) {
      if (s.IndexNumber == req.parentIndexNumber) {
        matchedSeason = s;
        break;
      }
    }
    if (matchedSeason == null) return const [];

    final episodes = await embyGateway.getSeriesEpisodes(
      ctx,
      identity.remoteSeriesId,
      matchedSeason.Id,
      cancelToken: cancelToken,
    );
    final target = _matchEpisodeBySE(
      episodes,
      req.parentIndexNumber,
      req.indexNumber,
    );
    if (target == null) return const [];
    final detailed = await _fetchDetailedItem(
      ctx,
      server,
      target,
      cancelToken: cancelToken,
    );
    return _expandToCards(server, session, detailed);
  }


  Future<_SeriesIdentity?> _resolveSeriesIdentity(
    AuthedRequestContext ctx,
    String serverId,
    Map<String, String> seriesProviderIds, {
    CancellationToken? cancelToken,
  }) async {
    final seriesPidQuery = _buildProviderIdQuery(seriesProviderIds);
    if (seriesPidQuery == null || seriesPidQuery.isEmpty) return null;
    final key = _identityCacheKey(serverId, seriesPidQuery);
    if (_seriesIdentityCache.containsKey(key)) {
      return _seriesIdentityCache[key];
    }
    final seriesList = await embyGateway.getLibraryItems(
      ctx: ctx,
      includeItemTypes: 'Series',
      anyProviderIdEquals: seriesPidQuery,
      fields: 'ProviderIds',
      limit: 1,
      recursive: true,
      cancelToken: cancelToken,
    );
    if (seriesList.isEmpty ||
        !_itemMatchesAnyProviderId(seriesList.first, seriesProviderIds)) {
      _seriesIdentityCache[key] = null;
      return null;
    }
    final remoteSeriesId = seriesList.first.Id;
    final seasons = await embyGateway.getSeriesSeasons(
      ctx,
      remoteSeriesId,
      cancelToken: cancelToken,
    );
    final identity = _SeriesIdentity(
      remoteSeriesId: remoteSeriesId,
      seasons: seasons,
    );
    _seriesIdentityCache[key] = identity;
    return identity;
  }


  Future<void> prewarmSeriesIdentity(
    Map<String, String> seriesProviderIds, {
    CancellationToken? cancelToken,
  }) async {
    if (seriesProviderIds.isEmpty) return;

    try {
      final activeServerId = await sessionRepository.getActiveServerId();
      final allSessions = await sessionRepository.listAll();
      final allServers = await serverRepository.list();
      final serverMap = {for (final s in allServers) s.id: s};

      final futures = <Future<void>>[];
      for (final session in allSessions) {
        if (session.serverId == activeServerId) continue;
        final server = serverMap[session.serverId];
        if (server == null) continue;
        final ctx = AuthedRequestContext(
          baseUrl: server.baseUrl,
          token: session.token,
          userId: session.userId,
        );
        futures.add(() async {
          try {
            await _resolveSeriesIdentity(
              ctx,
              server.id,
              seriesProviderIds,
              cancelToken: cancelToken,
            );
          } on CancelledException {
            return;
          } catch (e) {
            AppLogger.warn(
              _ucTag,
              'prewarmSeriesIdentity on ${server.name} failed',
              e,
            );
          }
        }());
      }
      await Future.wait(futures);
    } on CancelledException {

    } catch (e) {
      AppLogger.warn(_ucTag, 'prewarmSeriesIdentity setup failed', e);
    }
  }

  static List<CrossServerSourceCard> _expandToCards(
    EmbyServer server,
    SessionData session,
    EmbyRawItem item,
  ) {
    final raw = item.extra['MediaSources'];
    if (raw is! List) return const [];
    final sources = raw
        .whereType<Map<String, dynamic>>()
        .map(EmbyRawMediaSource.fromJson)
        .toList();
    final cards = <CrossServerSourceCard>[];
    for (final s in sources) {
      final id = s.Id;
      if (id == null) continue;
      cards.add(
        CrossServerSourceCard(
          serverId: server.id,
          serverName: server.name,
          serverUrl: server.baseUrl,
          token: session.token,
          userId: session.userId,
          landingItemId: item.Id,
          mediaSourceId: id,
          item: item,
          mediaSource: s,
          quality: _extractQualityFromSource(s),
        ),
      );
    }
    return cards;
  }

  static EmbyRawItem? _matchEpisodeBySE(
    List<EmbyRawItem> episodes,
    int? parentIndex,
    int? index,
  ) {
    if (parentIndex == null || index == null) return null;
    for (final it in episodes) {
      final ps = (it.extra['ParentIndexNumber'] as num?)?.toInt();
      if (ps == parentIndex && it.IndexNumber == index) return it;
    }
    return null;
  }

  static bool _itemMatchesAnyProviderId(
    EmbyRawItem item,
    Map<String, String> requestedProviderIds,
  ) {
    final normalizedRequestedProviderIds = normalizeProviderIds(
      requestedProviderIds,
    );
    if (normalizedRequestedProviderIds.isEmpty) return false;

    final normalizedItemProviderIds = normalizedProviderIdsOfItem(item);
    for (final requestedEntry in normalizedRequestedProviderIds.entries) {
      if (normalizedItemProviderIds[requestedEntry.key] ==
          requestedEntry.value) {
        return true;
      }
    }
    return false;
  }

  static bool _itemHasConflictingProviderId(
    EmbyRawItem item,
    Map<String, String> requestedProviderIds,
  ) {
    final normalizedRequestedProviderIds = normalizeProviderIds(
      requestedProviderIds,
    );
    if (normalizedRequestedProviderIds.isEmpty) return false;

    final normalizedItemProviderIds = normalizedProviderIdsOfItem(item);
    for (final requestedEntry in normalizedRequestedProviderIds.entries) {
      final itemProviderId = normalizedItemProviderIds[requestedEntry.key];
      if (itemProviderId != null && itemProviderId != requestedEntry.value) {
        return true;
      }
    }
    return false;
  }

  static MediaQualityInfo? _extractQualityFromSource(EmbyRawMediaSource src) {
    final streams = src.MediaStreams ?? const [];
    if (streams.isEmpty || !streams.any((s) => s.Type == 'Video')) {
      final fallback = MediaQualityInfo.fromSourceName(
        src.Name,
        fallbackHeight: src.Height,
        container: src.Container,
      );
      AppLogger.debug(
        'DetailQuality',
        'name-fallback sourceId=${src.Id ?? 'null'} '
            'name="${src.Name ?? 'null'}" -> '
            '${fallback == null ? 'null' : '"${fallback.resolutionLabel} ${fallback.dynamicRangeLabel}"'}',
      );
      return fallback;
    }
    return MediaQualityInfo.fromMediaSource(src);
  }

  static String? _buildProviderIdQuery(Map<String, String> providerIds) {
    if (providerIds.isEmpty) return null;
    final parts = <String>[];
    for (final entry in providerIds.entries) {
      if (entry.value.isNotEmpty) {
        parts.add('${entry.key}.${entry.value}');
      }
    }
    if (parts.isEmpty) return null;
    return parts.join(',');
  }
}

class _SeriesIdentity {
  final String remoteSeriesId;
  final List<EmbyRawSeason> seasons;
  const _SeriesIdentity({required this.remoteSeriesId, required this.seasons});
}

class _EmptyServerSearchStreak {
  int count = 0;
  DateTime? skipUntil;
}


String _buildAvailabilityProviderQuery({
  required String tmdbId,
  String? imdbId,
}) {
  final parts = <String>['Tmdb.${tmdbId.trim()}'];
  final imdb = imdbId?.trim() ?? '';
  if (imdb.isNotEmpty) parts.add('Imdb.$imdb');
  return parts.join(',');
}


class TmdbAvailabilityUseCase extends AuthedUseCase {
  TmdbAvailabilityUseCase({
    required super.sessionRepository,
    required super.serverRepository,
    required super.embyGateway,
    this.settleTimeout = const Duration(seconds: 3),
  });

  final Duration settleTimeout;

  Stream<List<TmdbLibraryHit>> executeStream(
    TmdbAvailabilityReq input, {
    CancellationToken? cancelToken,
  }) {
    final controller = StreamController<List<TmdbLibraryHit>>();
    Timer? settleTimer;
    () async {
      try {
        final tmdbId = input.tmdbId.trim();
        if (tmdbId.isEmpty) {
          if (!controller.isClosed) controller.add(const []);
          return;
        }
        final isMovie = input.mediaType == 'movie';
        final includeItemTypes = isMovie ? 'Movie' : 'Series';
        final providerQuery = _buildAvailabilityProviderQuery(
          tmdbId: tmdbId,
          imdbId: isMovie ? input.imdbId : null,
        );

        final allSessions = await sessionRepository.listAll();
        final allServers = await serverRepository.list();
        final serverMap = {for (final s in allServers) s.id: s};

        final acc = <TmdbLibraryHit>[];
        var settled = false;
        void emit() {
          if (!controller.isClosed) controller.add(List.unmodifiable(acc));
        }

        var pending = 0;
        final futures = <Future<void>>[];
        for (final session in allSessions) {
          final server = serverMap[session.serverId];
          if (server == null) continue;
          pending++;
          futures.add(
            _probeServer(
              server,
              session,
              includeItemTypes,
              providerQuery,
              cancelToken: cancelToken,
            ).then((hit) {
              pending--;
              if (hit != null) acc.add(hit);
              if (pending == 0) {
                settleTimer?.cancel();
                if (!settled || hit != null) emit();
              } else if (settled && hit != null) {
                emit();
              }
            }),
          );
        }
        if (pending == 0) {
          emit();
          return;
        }
        settleTimer = Timer(settleTimeout, () {
          settled = true;
          emit();
        });
        await Future.wait(futures);
      } catch (e) {
        if (!controller.isClosed) controller.addError(e);
      } finally {
        settleTimer?.cancel();
        if (!controller.isClosed) await controller.close();
      }
    }();
    return controller.stream;
  }

  Future<TmdbLibraryHit?> _probeServer(
    EmbyServer server,
    SessionData session,
    String includeItemTypes,
    String providerQuery, {
    CancellationToken? cancelToken,
  }) async {
    final ctx = AuthedRequestContext(
      baseUrl: server.baseUrl,
      token: session.token,
      userId: session.userId,
    );
    try {
      final items = await embyGateway.getLibraryItems(
        ctx: ctx,
        includeItemTypes: includeItemTypes,
        anyProviderIdEquals: providerQuery,
        fields: 'ProviderIds,ProductionYear,UserData',
        limit: 1,
        recursive: true,
        cancelToken: cancelToken,
      );
      if (items.isEmpty) return null;
      final item = items.first;
      return TmdbLibraryHit(
        serverId: server.id,
        serverName: server.name,
        itemId: item.Id,
        itemType: item.Type ?? includeItemTypes,
        name: item.Name,
        productionYear: item.ProductionYear,
        playbackPositionTicks: item.UserData?.PlaybackPositionTicks,
        runTimeTicks: item.RunTimeTicks,
      );
    } on CancelledException {
      return null;
    } catch (e) {
      AppLogger.warn(_ucTag, 'TmdbAvailability on ${server.name} failed', e);
      return null;
    }
  }
}
