

part of 'player_page.dart';

extension _PlayerPageSession on _PlayerPageState {
  Future<void> _switchTo(
    PlaybackRequest req, {
    PlayerEngineKind? forcedEngineKind,
  }) async {
    if (_switchInFlight) {
      _pendingResync = true;
      AppLogger.debug(_tag, '_switchTo busy → will resync to current');
      return;
    }
    final builder = _builder;
    if (builder == null) {
      AppLogger.warn(_tag, '_switchTo before initialize');
      return;
    }
    _switchInFlight = true;
    final stickyOverride =
        ref.read(playerEngineOverrideProvider).value ??
        PlayerEngineOverride.auto;
    final effectiveForcedEngineKind =
        forcedEngineKind ??
        (stickyOverride == PlayerEngineOverride.auto
            ? _forcedEngineBySource[_fallbackKeyForRequest(req)]
            : null);
    PlayerEngineKind? resolvedEngineKind;
    var resolvedIsDolbyVision = false;
    _relink.cancelPending();
    AppLogger.debug(
      _tag,
      '_switchTo itemId=${req.itemId} '
      'mediaSourceId=${req.mediaSourceId} '
      'forced=${effectiveForcedEngineKind?.name ?? '<none>'}',
    );

    final old = _session;
    if (old != null) {
      old.phase.removeListener(_onSessionPhaseChanged);
    }
    _attachWakelockSession(null);

    if (mounted) {
      setState(() {
        _session = null;


        _videoVisible = false;
        _errorMessage = null;
      });
      _drawer.close();
      _versionBarOpen.value = false;
      _invalidateBottomBar();
    }

    PlaybackSession? next;
    try {
      _lastRequest = req;
      final priorDisposeFuture = old?.dispose();
      if (!mounted) return;

      final prefetched = _prefetchedPlaybackInfo;
      _prefetchedPlaybackInfo = null;
      final expectedStartTimeTicks = _prefetchStartTimeTicksFor(req);
      final usablePrefetch =
          prefetched != null &&
              prefetched.info.isNotEmpty &&
              req.itemId == widget.itemId &&
              prefetched.startTimeTicks == expectedStartTimeTicks
          ? prefetched.info
          : null;
      if (prefetched != null && usablePrefetch == null) {
        AppLogger.debug(
          _tag,
          'drop prefetched PlaybackInfo: item=${req.itemId} '
          'prefetchTicks=${prefetched.startTimeTicks} '
          'expectedTicks=$expectedStartTimeTicks',
        );
      }

      next = await builder.build(
        request: req,
        feeder: _feeder,
        priorDisposeFuture: priorDisposeFuture,
        preloadedPlaybackInfo: usablePrefetch,
        forcedEngineKind: effectiveForcedEngineKind,
        onEngineResolved: (kind, isDolbyVision) {
          resolvedEngineKind = kind;
          resolvedIsDolbyVision = isDolbyVision;
        },
      );
      if (!mounted) {
        await next.dispose();
        return;
      }

      next.phase.addListener(_onSessionPhaseChanged);
      next.onStreamFailure = _handleStreamFailure;
      next.onExternalSubtitleError = _showExternalSubtitleError;
      setState(() => _session = next);
      unawaited(
        next.engine.stream.position
            .firstWhere((pos) => pos > Duration.zero)
            .then((_) {
              if (!mounted || !identical(_session, next)) return;
              setState(() => _videoVisible = true);
            })
            .catchError((_) {}),
      );
      _attachWakelockSession(next);
      _attachAndroidPipPlayingListener(next);

      unawaited(
        ref
            .read(danmakuSessionProvider.notifier)
            .loadFor(next.context.danmakuMatchContext),
      );

      await next.start();
      if (!mounted) return;

      final manuallyConfirmedMedia3 =
          stickyOverride == PlayerEngineOverride.media3 &&
          playerEngineKindFromDisplayName(next.engine.displayName) ==
              PlayerEngineKind.media3;
      if (manuallyConfirmedMedia3) {
        _forcedEngineBySource.remove(_fallbackKeyForRequest(req));
      }

      if (req.targetServer == null) {
        final ctx = next.context;
        final msid = ctx.mediaSourceId;
        if (msid != null && msid.isNotEmpty) {
          unawaited(
            _lastPlayedVersionStore.save(
              serverId: ctx.serverId,
              itemId: ctx.itemId,
              mediaSourceId: msid,
            ),
          );
        }
      }

      if (_playbackRate != 1.0) {
        await next.setRate(_playbackRate);
      }

      _configureAndroidPip();

      _armRelinkSustain();
    } catch (e, s) {


      if (e is CancelledException || !mounted) {
        AppLogger.debug(_tag, '_switchTo aborted (unmounted/cancelled)');
        if (next != null) {
          next.phase.removeListener(_onSessionPhaseChanged);
          try {
            await next.dispose();
          } catch (_) {}
        }
        return;
      }
      AppLogger.error(_tag, '_switchTo failed', e, s);
      final failedKind = next == null
          ? resolvedEngineKind
          : playerEngineKindFromDisplayName(next.engine.displayName);
      if (mounted &&
          await _tryFallbackFromSwitchFailure(
            request: req,
            failedKind: failedKind,
            failedSession: next,
            isDolbyVision: next?.context.isDolbyVision ?? resolvedIsDolbyVision,
          )) {
        return;
      }
      if (next != null) {
        next.phase.removeListener(_onSessionPhaseChanged);
        try {
          await next.dispose();
        } catch (disposeErr) {
          AppLogger.warn(
            _tag,
            '_switchTo failed: dispose next failed',
            disposeErr,
          );
        }
      }
      if (mounted) {
        setState(() {
          _session = null;
          _errorMessage = formatUserError(e);
        });
      }
    } finally {
      _switchInFlight = false;
      if (mounted && _session?.phase.value == PlaybackPhase.failed) {
        _onSessionPhaseChanged();
      }
      if (mounted) {
        setState(() {});
        _invalidateBottomBar();
      }
    }

    await _drainPendingResync();
  }


  Future<void> _drainPendingResync() async {
    if (!_pendingResync || !mounted) return;
    _pendingResync = false;
    final current = _playlist?.current.value;
    if (current == null || current.itemId.isEmpty) return;
    if (current.itemId == _session?.context.itemId) return;
    AppLogger.debug(_tag, '_drainPendingResync → realign to ${current.itemId}');
    _onCurrentItemChanged();
  }

  void _onSessionPhaseChanged() {
    final phase = _session?.phase.value;
    AppLogger.debug(_tag, 'session phase: $phase');
    _syncWakelockForSession(_session);
    if (phase == PlaybackPhase.ended && !_switchInFlight) {
      final pl = _playlist;
      if (pl != null && pl.hasNext) {
        unawaited(pl.next());
      }
    } else if (phase == PlaybackPhase.failed && mounted) {
      if (_switchInFlight) return;
      final failed = _session;
      failed?.phase.removeListener(_onSessionPhaseChanged);
      _attachWakelockSession(null);
      if (_tryFallbackFromFailedSession(failed)) {
        return;
      }
      if (failed != null) {
        _rememberFailurePosition(ticksFromDuration(failed.engine.state.position));
      }
      setState(() {
        _session = null;
        _errorMessage = formatUserError(failed?.lastError);
      });
      if (failed != null) {
        unawaited(failed.dispose());
      }
      _invalidateBottomBar();
    }
  }

  Future<void> _retry() async {
    final req = _lastRequest;
    if (req == null) return;


    _relink.reset();
    if (mounted) setState(() => _errorMessage = null);
    await _switchTo(req);
  }

  Future<bool> _tryFallbackFromSwitchFailure({
    required PlaybackRequest request,
    required PlayerEngineKind? failedKind,
    required bool isDolbyVision,
    PlaybackSession? failedSession,
  }) async {
    if (_engineFallbackInFlight || failedKind == null) return false;
    final override =
        ref.read(playerEngineOverrideProvider).value ??
        PlayerEngineOverride.auto;
    final key = _fallbackKeyForRequest(request);
    final fallbackTarget = nextAutoFallbackEngine(
      override: override,
      failedEngineKind: failedKind,
      isDolbyVision: isDolbyVision,
    );
    if (fallbackTarget == null) return false;

    _forcedEngineBySource[key] = fallbackTarget;
    _engineFallbackInFlight = true;
    _relink.cancelPending();
    failedSession?.phase.removeListener(_onSessionPhaseChanged);
    AppLogger.warn(
      _tag,
      '${failedKind.name} switch failed for ${request.itemId} '
      'mediaSource=${request.mediaSourceId ?? '<auto>'}; '
      'fallback to ${fallbackTarget.name}',
    );
    if (failedSession != null) {
      await failedSession.dispose();
    }
    if (!mounted) {
      _engineFallbackInFlight = false;
      return true;
    }
    setState(() {
      _session = null;
      _errorMessage = null;
    });
    unawaited(
      Future<void>.microtask(() async {
        try {
          await _switchTo(request, forcedEngineKind: fallbackTarget);
        } finally {
          _engineFallbackInFlight = false;
        }
      }),
    );
    _invalidateBottomBar();
    return true;
  }

  bool _tryFallbackFromFailedSession(PlaybackSession? failed) {
    if (failed == null || _switchInFlight || _engineFallbackInFlight) {
      return false;
    }
    final base = _lastRequest;
    if (base == null) return false;
    final override =
        ref.read(playerEngineOverrideProvider).value ??
        PlayerEngineOverride.auto;
    final failedKind = playerEngineKindFromDisplayName(
      failed.engine.displayName,
    );
    final key = _fallbackKeyForRequest(base);
    final fallbackTarget = nextAutoFallbackEngine(
      override: override,
      failedEngineKind: failedKind,
      isDolbyVision: failed.context.isDolbyVision,
    );
    if (fallbackTarget == null) return false;

    _forcedEngineBySource[key] = fallbackTarget;
    _engineFallbackInFlight = true;
    _relink.cancelPending();

    final ticks = ticksFromDuration(failed.engine.state.position);
    final req = ticks > 0
        ? base.copyWith(startPositionTicks: ticks, playFromStart: false)
        : base;
    AppLogger.warn(
      _tag,
      '${failedKind?.name ?? "unknown"} failed for ${failed.context.itemId} '
      'mediaSource=${failed.context.mediaSourceId ?? '<auto>'}; '
      'fallback to ${fallbackTarget.name}',
    );
    setState(() {
      _session = null;
      _errorMessage = null;
    });
    unawaited(
      failed.dispose().whenComplete(() async {
        try {
          await _switchTo(req, forcedEngineKind: fallbackTarget);
        } finally {
          _engineFallbackInFlight = false;
        }
      }),
    );
    _invalidateBottomBar();
    return true;
  }

  String _fallbackKeyForRequest(PlaybackRequest request) {
    return [
      request.targetServer?.serverId ?? '',
      request.itemId,
      request.mediaSourceId ?? '',
    ].join(':');
  }


  void _showExternalSubtitleError(String message) {
    if (!mounted) return;
    showAppSnackBar(context, message, tone: AppSnackTone.warning);
  }


}
