part of '../../video_player_screen.dart';

extension _VideoPlayerEpisodeQueueMethods on VideoPlayerScreenState {
  /// Ensure a play queue exists for sequential episode playback
  Future<void> _ensurePlayQueue() async {
    if (!mounted) return;

    // Download/offline library mode uses the local downloaded queue instead.
    if (_offlineLibraryMode) return;

    // Skip play queue for live TV (would interfere with tuner session)
    if (widget.isLive) return;

    if (!_currentMetadata.isEpisode) {
      return;
    }

    // Plex-only — Jellyfin's local queue is published by
    // EpisodeNavigationService._ensureLocalEpisodeQueue from
    // _loadAdjacentEpisodes, so this method is a no-op for it.
    if (_currentMetadata.backend != MediaBackend.plex) return;

    try {
      final client = context.getPlexClientForServer(ServerId(_currentMetadata.serverId!));

      final playbackState = context.read<PlaybackStateProvider>();

      // For episodes, grandparentId points to the show
      final showRatingKey = _currentMetadata.grandparentId;
      if (showRatingKey == null) {
        appLogger.d('Episode missing grandparentId, skipping play queue creation');
        return;
      }

      // Preserve any queue this item belongs to — playlist, collection,
      // or same-show queue. `isItemInActiveQueue` is the same gate
      // VideoPlayerScreen.initState uses; a context-key check alone would
      // wipe a playlist queue (its key is the playlist id, not the show).
      // Only when the active queue is genuinely stale (item not in it)
      // do we clobber and create a fresh show queue.
      if (playbackState.isItemInActiveQueue(_currentMetadata)) {
        playbackState.setCurrentItem(_currentMetadata);
        appLogger.d('Using existing play queue (context: ${playbackState.shuffleContextKey})');
        return;
      }
      if (playbackState.isQueueActive) {
        appLogger.d('Resetting stale play queue (was: ${playbackState.shuffleContextKey}, now: $showRatingKey)');
        playbackState.clearShuffle();
      }

      appLogger.d('Creating sequential play queue for show $showRatingKey');
      final playQueue = await client.createShowPlayQueue(
        showRatingKey: showRatingKey,
        shuffle: 0,
        startingEpisodeKey: _currentMetadata.id,
        librarySectionID: _currentMetadata.libraryId,
        librarySectionTitle: _currentMetadata.libraryTitle,
      );

      if (playQueue != null && playQueue.items != null && playQueue.items!.isNotEmpty) {
        await playbackState.setPlaybackFromPlayQueue(playQueue, showRatingKey);
        playbackState.setPlayQueueWindowFetcher(
          (id, {center, window = 50}) => client.getPlayQueue(
            id,
            center: center,
            window: window,
            librarySectionID: _currentMetadata.libraryId,
            librarySectionTitle: _currentMetadata.libraryTitle,
          ),
        );

        appLogger.d('Sequential play queue created with ${playQueue.items!.length} items');
      } else {
        appLogger.w('Plex returned no usable sequential play queue; falling back to a local series queue');
      }
    } catch (e, st) {
      appLogger.w('Could not create Plex play queue; falling back to a local series queue', error: e, stackTrace: st);
    }
  }

  Future<AdjacentEpisodes> _loadAdjacentEpisodes({MediaItem? metadata, _PlaybackAttempt? attempt}) async {
    if (!mounted || widget.isLive) return const AdjacentEpisodes.unavailable();

    final targetMetadata = metadata ?? _currentMetadata;
    try {
      final adjacentEpisodes = _offlineLibraryMode
          ? _loadAdjacentEpisodesOffline(targetMetadata)
          : await _episodeNavigation.loadAdjacentEpisodes(
              context: context,
              metadata: targetMetadata,
              // The part actually being played, so the queue can skip sibling
              // entries of a Plex multi-episode file (#1500). MediaSourceInfo
              // carries the Plex numeric part id; MediaPart.id is its string form.
              playedPartId: _currentMediaInfo?.partId?.toString(),
            );
      _commitAdjacentEpisodes(targetMetadata, adjacentEpisodes, attempt);
      return adjacentEpisodes;
    } catch (e, st) {
      appLogger.w('Could not load adjacent episodes', error: e, stackTrace: st);
      const failed = AdjacentEpisodes.failed();
      _commitAdjacentEpisodes(targetMetadata, failed, attempt);
      return failed;
    }
  }

  /// Load next/previous episodes from locally downloaded content.
  AdjacentEpisodes _loadAdjacentEpisodesOffline(MediaItem metadata) {
    if (!metadata.isEpisode) return const AdjacentEpisodes.unavailable();

    final showKey = metadata.grandparentId;
    if (showKey == null) return const AdjacentEpisodes.unavailable();

    try {
      final downloadProvider = context.read<DownloadProvider>();
      final episodes = downloadProvider.getDownloadedEpisodesForShow(showKey);
      if (episodes.isEmpty) return const AdjacentEpisodes.failed();

      // Aired watch order (Specials interleaved by air date) — the shared
      // episode order, so offline next/prev matches streaming, what "download
      // next N" selects, and the offline OnDeck list (#1416/#1414). Copy first
      // so the provider's cached list isn't reordered.
      final sorted = List<MediaItem>.from(episodes)..sort(compareEpisodesByWatchOrder);
      final currentIdx = sorted.indexWhere((ep) => ep.id == metadata.id);
      if (currentIdx == -1) return const AdjacentEpisodes.failed();

      // Same-file siblings are skipped by file-path intersection of the
      // stored metadata (#1500) — offline media info doesn't carry the
      // server part id, so the helpers compare the items' own parts.
      final previous = previousEpisodeSkippingSameFile(sorted, currentIdx);
      final next = nextEpisodeSkippingSameFile(sorted, currentIdx);
      return AdjacentEpisodes(
        next: next,
        previous: previous,
        nextStatus: next == null ? QueueNavigationStatus.boundary : QueueNavigationStatus.found,
        previousStatus: previous == null ? QueueNavigationStatus.boundary : QueueNavigationStatus.found,
      );
    } catch (e, st) {
      appLogger.w('Could not load offline adjacent episodes', error: e, stackTrace: st);
      return const AdjacentEpisodes.failed();
    }
  }

  void _commitAdjacentEpisodes(MediaItem targetMetadata, AdjacentEpisodes adjacentEpisodes, _PlaybackAttempt? attempt) {
    if (!mounted || _currentMetadata.globalKey != targetMetadata.globalKey || (attempt != null && !attempt.isCurrent)) {
      return;
    }
    _setPlayerState(() {
      _nextEpisode = adjacentEpisodes.next;
      _previousEpisode = adjacentEpisodes.previous;
      _nextEpisodeStatus = adjacentEpisodes.nextStatus;
    });
  }
}
