import 'dart:async';
import 'dart:io';
import 'dart:math' as math;

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:go_router/go_router.dart';
import 'package:canvas_danmaku/canvas_danmaku.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:window_manager/window_manager.dart';

import '../../core/contracts/cancellation.dart';
import '../../core/contracts/danmaku.dart';
import '../../core/contracts/auth.dart';
import '../../core/contracts/library.dart';
import '../../core/domain/ports/authed_trakt_gateway.dart';
import '../../core/infra/player_engine/media3_player_engine.dart';
import '../../core/infra/player_engine/player_engine.dart';
import '../../core/infra/system_audio/system_audio_controller.dart';
import '../../core/infra/trakt_api/trakt_pending_sync_queue.dart';
import '../../core/media/media_probe_service.dart';
import '../../core/media/media_source_metadata_resolver.dart';
import '../../providers/trakt_scrobble_status_provider.dart';
import '../../shared/app_info.dart';
import '../../shared/constants/breakpoints.dart';
import '../settings/settings_page.dart';
import '../../providers/audio_settings_provider.dart';
import '../../providers/danmaku_settings_provider.dart';
import '../../providers/di_providers.dart';
import '../../providers/last_played_version_store.dart';
import '../../providers/playback_active_provider.dart';
import '../../providers/player_engine_override_provider.dart';
import '../../providers/player_settings_provider.dart';
import '../../providers/emby_headers_provider.dart';
import '../../providers/playback_info_prefetch_provider.dart';
import '../../providers/session_provider.dart';
import '../../providers/subtitle_settings_provider.dart';
import '../../shared/utils/app_logger.dart';
import '../../shared/utils/cross_server_search_key.dart';
import '../../shared/utils/emby_ticks.dart';
import '../../shared/utils/format_utils.dart';
import '../../shared/utils/platform_device_name.dart';
import '../../shared/utils/user_error_formatter.dart';
import '../../shared/widgets/app_dialog.dart';
import '../../shared/widgets/app_loading_ring.dart';
import '../../shared/widgets/app_snack_bar.dart';
import '../../shared/widgets/version_source_card.dart';
import '../../shared/widgets/window_controls.dart';
import 'gestures/gesture_action.dart';
import 'gestures/gesture_bindings.dart';
import 'player_launcher.dart';
import 'playlist/cross_server_playlist.dart';
import 'playlist/pending_cross_server_sequence_provider.dart';
import 'playlist/playlist.dart';
import 'playlist/playlist_factory.dart';
import 'playlist/series_episode_playlist.dart';
import 'playlist/single_item_playlist.dart';
import 'services/danmaku/danmaku_feeder.dart';
import 'services/danmaku/danmaku_session_notifier.dart';
import 'services/frame_jank_monitor.dart';
import 'services/fullscreen_manager.dart';
import 'services/android_pip_manager.dart';
import 'services/pip_manager.dart';
import '../../core/infra/pip/android_pip_controller.dart';
import 'session/playback_phase.dart';
import 'session/playback_request.dart';
import 'session/playback_session.dart';
import 'session/playback_target.dart';
import 'session/playback_session_builder.dart';
import 'session/player_engine_holder.dart';
import 'session/player_engine_resolution.dart';
import 'session/relink_scheduler.dart';
import 'widgets/buffering_overlay.dart';
import 'widgets/danmaku_search_dialog.dart';
import 'widgets/debounced_seek_bar.dart';
import 'widgets/network_speed_indicator.dart';
import 'widgets/player_clock_text.dart';
import 'widgets/panel_toggle_affordance.dart';
import 'widgets/popover/panel_category_meta.dart';
import 'widgets/popover/player_panel_controller.dart';
import 'widgets/popover/player_panel_popover.dart';
import 'widgets/panels/danmaku_panel_body.dart';
import 'widgets/panels/episode_panel_body.dart';
import 'widgets/panels/panel_selection_list.dart';
import 'widgets/panels/selection_panel_entry.dart';
import 'widgets/player_panel_shell.dart';
import 'widgets/panels/tracks_panel_body.dart';
import 'widgets/fit_button.dart';
import 'widgets/player_debug_hud.dart';
import 'widgets/player_play_pause_button.dart';
import 'widgets/player_position_indicator.dart';
import 'widgets/player_video_widget.dart';
import 'widgets/player_volume_button.dart';
import 'widgets/player_status_scaffold.dart';
import 'widgets/quality_badge.dart';
import 'widgets/speed_button.dart';

import 'widgets/android/android_bottom_transport_controls.dart';
import 'widgets/android/android_gesture_feedback.dart';
import 'widgets/android/android_gesture_layer.dart';
import 'widgets/android/android_lock_overlay.dart';
import 'widgets/android/android_speed_strip.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'widgets/styled_subtitle_overlay.dart';
import 'widgets/track_selector_button.dart';
import 'widgets/version_panel.dart';

part 'player_page_bottom_bar.dart';
part 'player_page_danmaku.dart';
part 'player_page_lifecycle.dart';
part 'player_page_overlays.dart';
part 'player_page_relink.dart';
part 'player_page_session.dart';
part 'player_page_sources.dart';
part 'player_page_top_bar.dart';
part 'player_page_version_bar.dart';
part 'player_page_android_chrome.dart';

const _tag = 'PlayerPage';

class PlayerPage extends ConsumerStatefulWidget {
  final String itemId;
  final String? serverId;
  final String? mediaSourceId;
  final String? backdropUrl;
  final int? preferredSubtitleStreamIndex;
  final int? preferredAudioStreamIndex;
  final int? startPositionTicks;
  final bool fromStart;
  final DirectStreamLaunchData? directStream;

  const PlayerPage({
    super.key,
    required this.itemId,
    this.serverId,
    this.mediaSourceId,
    this.backdropUrl,
    this.preferredSubtitleStreamIndex,
    this.preferredAudioStreamIndex,
    this.startPositionTicks,
    this.fromStart = false,
    this.directStream,
  });

  @override
  ConsumerState<PlayerPage> createState() => _PlayerPageState();
}

class _PlayerPageState extends ConsumerState<PlayerPage>
    with WidgetsBindingObserver {
  int _maxVolumeLevel = 15;
  double _volumeFraction = 0.0;

  PlaybackSession? _session;
  PlaybackRequest? _lastRequest;
  bool _videoVisible = false;
  PlaybackSessionBuilder? _builder;
  Playlist? _playlist;
  ({int startTimeTicks, Map<String, dynamic> info})? _prefetchedPlaybackInfo;
  bool _switchInFlight = false;
  bool _engineFallbackInFlight = false;


  final Map<String, PlayerEngineKind> _forcedEngineBySource =
      <String, PlayerEngineKind>{};


  final PlayerEngineHolder _engineHolder = PlayerEngineHolder();


  bool _pendingResync = false;
  String? _errorMessage;


  final RelinkScheduler _relink = RelinkScheduler();


  final FrameJankMonitor _frameJankMonitor = FrameJankMonitor();

  late final FullscreenManager _fullscreen = FullscreenManager(
    onStateChanged: () {
      if (mounted) {
        setState(() {});
        _invalidateBottomBar();
      }
    },
  );
  late final PipManager _pip = PipManager(
    onStateChanged: () {
      if (mounted) {
        setState(() {});
        _invalidateBottomBar();
      }
    },
  );


  late final AndroidPipManager _androidPip = AndroidPipManager(
    onStateChanged: () {
      if (mounted) {
        setState(() {});
        _invalidateBottomBar();
      }
    },
  );
  StreamSubscription<bool>? _androidPipPlayingSub;


  bool get _isAndroidPip => Platform.isAndroid && _androidPip.isInPip;

  final ValueNotifier<BoxFit> _videoFit = ValueNotifier<BoxFit>(BoxFit.contain);
  final ValueNotifier<int> _barVersion = ValueNotifier<int>(0);


  final ValueNotifier<bool> _debugHudVisible = ValueNotifier<bool>(false);


  final PlayerPanelController _drawer = PlayerPanelController();


  final Map<PlayerPanelCategory, LayerLink> _panelLinks = {
    for (final c in PlayerPanelCategory.values) c: LayerLink(),
  };


  final GlobalKey<EpisodePanelBodyState> _episodeBodyKey =
      GlobalKey<EpisodePanelBodyState>();


  final ValueNotifier<bool> _versionBarOpen = ValueNotifier<bool>(false);


  final ValueNotifier<Duration?> _seekPreview = ValueNotifier<Duration?>(null);
  final ValueNotifier<int> _danmakuOverlayVersion = ValueNotifier<int>(0);
  final ValueNotifier<int> _subtitleOverlayVersion = ValueNotifier<int>(0);
  final ValueNotifier<bool> _bufferingOverlayVisible = ValueNotifier<bool>(
    false,
  );


  List<CrossServerSourceCard> _crossServerCards = const [];


  CrossServerSearchKey? _lastCrossServerSearchKey;

  bool _isDanmakuSearchOpen = false;
  bool _backNavigationInFlight = false;


  bool _playerStartupBegun = false;
  bool _isAndroidLocked = false;
  int _brightnessGen = 0;
  double? _lastSetBrightness;
  GestureFeedbackData _androidFeedbackData = const GestureFeedbackData.none();
  StreamSubscription<SystemVolumeEvent>? _systemVolumeSub;
  Timer? _systemVolumeOsdTimer;
  double? _savedRateBeforeLongPress;
  double _playbackRate = 1.0;
  SubtitleStyleSettings _subtitleSettings = const SubtitleStyleSettings();
  PlayerSettingsState _initialPlayerSettings = const PlayerSettingsState();
  DanmakuSettingsState _initialDanmakuSettings = const DanmakuSettingsState();
  AudioSettingsState _initialAudioSettings = const AudioSettingsState();

  final DanmakuFeeder _feeder = DanmakuFeeder();

  DanmakuSessionState _sessionState = const DanmakuSessionIdle();
  DanmakuPanelSettings _panelSettings = const DanmakuPanelSettings();
  double? _danmakuOverlayHeight;
  double? _danmakuOverlaySyncedHeight;
  DanmakuPanelSettings? _danmakuOverlaySyncedSettings;
  Timer? _danmakuPanelPersistTimer;
  DanmakuPanelSettings? _pendingDanmakuPanelSettings;
  DanmakuSessionState? _lastSyncedDanmakuSession;

  late final StateController<bool> _playbackActiveNotifier;
  late final PlayerSettingsNotifier _playerSettingsNotifier;
  late final AudioSettingsNotifier _audioSettingsNotifier;
  late final SubtitleSettingsNotifier _subtitleSettingsNotifier;
  late final DanmakuSettingsNotifier _danmakuSettingsNotifier;
  late final LastPlayedVersionStore _lastPlayedVersionStore;
  StreamSubscription<bool>? _wakelockPlayingSub;
  PlaybackSession? _wakelockSession;
  bool _wakelockEnabled = false;

  bool get _isDesktopWindow => Platform.isWindows || Platform.isMacOS;

  double get _topBarLeftInset => _fullscreen.isFullscreen ? 8 : 12;
  double get _topBarTopInset => _fullscreen.isFullscreen ? 8 : 12;


  String get _fallbackTitle => _playlist?.current.value.displayLabel ?? '';

  bool get _showFallbackWindowControls =>
      !_fullscreen.isFullscreen && !_pip.isPip && _isDesktopWindow;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _playbackActiveNotifier = ref.read(playbackActiveProvider.notifier);
    _playerSettingsNotifier = ref.read(playerSettingsProvider.notifier);
    _audioSettingsNotifier = ref.read(audioSettingsProvider.notifier);
    _subtitleSettingsNotifier = ref.read(subtitleSettingsProvider.notifier);
    _danmakuSettingsNotifier = ref.read(danmakuSettingsProvider.notifier);
    _lastPlayedVersionStore = ref.read(lastPlayedVersionStoreProvider);

    _drawer.addListener(_onPanelChanged);
    _frameJankMonitor.start();

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted) return;
      ref.read(playbackActiveProvider.notifier).state = true;
    });


    if (Platform.isAndroid || Platform.isIOS) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (!mounted) return;
        _beginStartupAfterRouteAnimation();
      });
    } else {
      _beginPlayerStartup();
    }
  }


  void _beginStartupAfterRouteAnimation() {
    final routeAnimation = ModalRoute.of(context)?.animation;
    if (routeAnimation == null ||
        routeAnimation.status == AnimationStatus.completed) {
      _beginPlayerStartup();
      return;
    }
    late final AnimationStatusListener statusListener;
    statusListener = (AnimationStatus status) {
      if (status == AnimationStatus.completed ||
          status == AnimationStatus.dismissed) {
        routeAnimation.removeStatusListener(statusListener);
        if (mounted && status == AnimationStatus.completed) {
          _beginPlayerStartup();
        }
      }
    };
    routeAnimation.addStatusListener(statusListener);
  }


  void _beginPlayerStartup() {
    if (_playerStartupBegun || !mounted) return;
    _playerStartupBegun = true;

    if (Platform.isAndroid) {
      unawaited(_beginAndroidPlayerStartup());
      return;
    }

    unawaited(_initialize());
  }

  Future<void> _beginAndroidPlayerStartup() async {
    try {
      await SystemChrome.setPreferredOrientations(const <DeviceOrientation>[
        DeviceOrientation.landscapeLeft,
        DeviceOrientation.landscapeRight,
      ]);
      if (!mounted || _backNavigationInFlight) return;
      await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
      if (!mounted || _backNavigationInFlight) return;
    } catch (error, stack) {
      AppLogger.warn(_tag, 'configure Android immersive mode failed', error);
      AppLogger.debug(_tag, stack.toString());
    }

    if (!mounted || _backNavigationInFlight) return;
    _enableAndroidSystemAudio();
    _restoreAndroidBrightness();
    _androidPip.onAction = _handleAndroidPipAction;
    unawaited(
      _androidPip
          .initialize()
          .then((_) {
            if (!mounted) return;
            _configureAndroidPip();
          })
          .catchError((Object error, StackTrace stack) {
            AppLogger.warn(_tag, 'initialize Android PiP failed', error);
            AppLogger.debug(_tag, stack.toString());
          }),
    );
    unawaited(_initialize());
  }

  void _restoreAndroidBrightness() {
    final savedBrightness = ref
        .read(playerSettingsProvider)
        .value
        ?.androidBrightness;
    if (savedBrightness == null) return;
    unawaited(
      ScreenBrightness.instance
          .setApplicationScreenBrightness(savedBrightness)
          .catchError((_) {}),
    );
  }

  void _enableAndroidSystemAudio() {
    unawaited(
      SystemAudioController.instance
          .getVolume()
          .then((event) {
            _volumeFraction = event.percent;
            _maxVolumeLevel = event.max;
          })
          .catchError((Object error, StackTrace stack) {
            AppLogger.warn(_tag, 'initial volume fetch failed', error);
          }),
    );
    unawaited(
      SystemAudioController.instance.setKeyIntercept(true).catchError((
        Object error,
        StackTrace stack,
      ) {
        AppLogger.warn(_tag, 'enable volume key intercept failed', error);
        AppLogger.debug(_tag, stack.toString());
      }),
    );
    _systemVolumeSub = SystemAudioController.instance.volumeChanges.listen(
      _showAndroidSystemVolumeOsd,
      onError: (Object error, StackTrace stack) {
        AppLogger.warn(_tag, 'system volume event failed', error);
        AppLogger.debug(_tag, stack.toString());
      },
    );
  }

  void _showAndroidSystemVolumeOsd(SystemVolumeEvent event) {
    if (!mounted) return;
    _volumeFraction = event.percent;
    _maxVolumeLevel = event.max;
    _systemVolumeOsdTimer?.cancel();
    setState(
      () => _androidFeedbackData = GestureFeedbackData(
        kind: GestureFeedbackKind.volume,
        value: event.percent,
      ),
    );
    _systemVolumeOsdTimer = Timer(const Duration(milliseconds: 800), () {
      if (!mounted || _androidFeedbackData.kind != GestureFeedbackKind.volume) {
        return;
      }
      setState(() => _androidFeedbackData = const GestureFeedbackData.none());
    });
  }

  void _disableAndroidSystemAudio() {
    if (!Platform.isAndroid) return;
    unawaited(
      SystemAudioController.instance.setKeyIntercept(false).catchError((
        Object error,
        StackTrace stack,
      ) {
        AppLogger.warn(_tag, 'disable volume key intercept failed', error);
        AppLogger.debug(_tag, stack.toString());
      }),
    );
    unawaited(_systemVolumeSub?.cancel());
    _systemVolumeSub = null;
    _systemVolumeOsdTimer?.cancel();
    _systemVolumeOsdTimer = null;
  }

  void _onMatchSelected(int index) {
    if (index == _danmakuSelectedMatchIndex) return;
    unawaited(ref.read(danmakuSessionProvider.notifier).selectMatch(index));
  }

  Future<void> _openDanmakuSearchDialog() async {
    final session = _session;
    if (session == null) return;
    if (_sessionState is DanmakuSessionIdle) return;
    final dialogItemId = session.context.itemId;
    final matches = _danmakuMatches;
    final dCtx = session.context.danmakuMatchContext;
    final initialQuery =
        dCtx.type == 'Episode' && (dCtx.seriesName?.isNotEmpty ?? false)
        ? dCtx.seriesName!
        : session.context.itemName;

    setState(() => _isDanmakuSearchOpen = true);
    _invalidateBottomBar();
    try {
      final size = MediaQuery.sizeOf(context);
      await showAppRawDialog<void>(
        context,
        useRootNavigator: true,
        constraints: BoxConstraints(
          maxWidth: math.min(size.width - 48, 520.0),
          maxHeight: math.min(size.height - 112, 560.0),
        ),
        builder: (_) => DanmakuSearchDialog(
          currentItemId: dialogItemId,
          isItemStillCurrent: () =>
              mounted && _session?.context.itemId == dialogItemId,
          initialQuery: initialQuery,
          initialResults: matches,
          initialSelectedIndex: _danmakuSelectedMatchIndex,
          targetEpisode: dCtx.episode,
        ),
      );
    } finally {
      if (mounted) {
        setState(() => _isDanmakuSearchOpen = false);
        _invalidateBottomBar();
      }
    }
  }

  void _attachWakelockSession(PlaybackSession? session) {
    if (identical(_wakelockSession, session)) {
      _syncWakelockForSession(session);
      return;
    }

    unawaited(_wakelockPlayingSub?.cancel());
    _wakelockPlayingSub = null;
    _wakelockSession = session;

    if (session == null) {
      _setWakelockEnabled(false);
      return;
    }

    _wakelockPlayingSub = session.engine.stream.playing.listen((_) {
      _syncWakelockForSession(session);
    });
    _syncWakelockForSession(session);
  }

  void _syncWakelockForSession(PlaybackSession? session) {
    if (!identical(_wakelockSession, session)) return;
    final shouldEnable =
        session != null &&
        session.phase.value == PlaybackPhase.playing &&
        session.engine.state.playing;
    _setWakelockEnabled(shouldEnable);
  }

  void _setWakelockEnabled(bool enabled) {
    if (_wakelockEnabled == enabled) return;
    _wakelockEnabled = enabled;

    unawaited(
      (enabled ? WakelockPlus.enable() : WakelockPlus.disable()).catchError((
        Object error,
        StackTrace stack,
      ) {
        AppLogger.warn(
          _tag,
          'wakelock ${enabled ? 'enable' : 'disable'} failed',
          error,
        );
        AppLogger.debug(_tag, stack.toString());
      }),
    );
  }

  void _onDanmakuPanelChanged(DanmakuPanelSettings next) {
    final enabledChanged = _panelSettings.enabled != next.enabled;
    final offsetChanged = _panelSettings.offset != next.offset;
    final layoutChanged = _isDanmakuLayoutChange(_panelSettings, next);
    _applyDanmakuPanelSettings(next);
    if (enabledChanged) {
      _invalidateBottomBar();
    }
    if (enabledChanged || layoutChanged) {
      _invalidateDanmakuOverlay();
    }
    if (enabledChanged || offsetChanged || layoutChanged) {
      final s = _session;
      if (s != null) {
        final posSec = s.engine.state.position.inMilliseconds / 1000.0;
        _syncDanmakuFeederForSession(_feeder, s, posSec, clear: true);
      }
    }
    _refreshPanel();
    _danmakuSettingsNotifier.setPanelSettings(next);
    _scheduleDanmakuPanelSettingsPersist(next);
  }

  void _scheduleDanmakuPanelSettingsPersist(DanmakuPanelSettings next) {
    _pendingDanmakuPanelSettings = next;
    _danmakuPanelPersistTimer?.cancel();
    _danmakuPanelPersistTimer = Timer(const Duration(milliseconds: 250), () {
      final pending = _pendingDanmakuPanelSettings;
      _pendingDanmakuPanelSettings = null;
      if (pending == null) return;
      unawaited(_danmakuSettingsNotifier.persistPanelSettings(pending));
    });
  }

  void _flushDanmakuPanelSettingsPersist() {
    _danmakuPanelPersistTimer?.cancel();
    _danmakuPanelPersistTimer = null;
    final pending = _pendingDanmakuPanelSettings;
    _pendingDanmakuPanelSettings = null;
    if (pending == null) return;
    unawaited(_danmakuSettingsNotifier.persistPanelSettings(pending));
  }

  void _retryDanmaku() {
    final session = _session;
    if (session == null) return;
    unawaited(
      ref
          .read(danmakuSessionProvider.notifier)
          .loadFor(session.context.danmakuMatchContext),
    );
  }

  void _goToDanmakuSettings() {
    _drawer.close();
    showSettingsDialog(context, initialTab: 'danmaku');
  }

  void _toggleDanmakuGlobalEnabled() {
    unawaited(_enableDanmakuForCurrentSession());
  }

  Future<void> _enableDanmakuForCurrentSession() async {
    final notifier = ref.read(danmakuSettingsProvider.notifier);
    await notifier.setEnabled(true);
    if (!mounted) return;
    final session = _session;
    if (session == null) return;
    await ref
        .read(danmakuSessionProvider.notifier)
        .loadFor(session.context.danmakuMatchContext);
  }

  void _onSubtitleSettingsChanged(SubtitleStyleSettings next) {
    _subtitleSettings = next;
    _refreshPanel();
    _invalidateSubtitleOverlay();
    _session?.applySubtitleStyle(next);
    ref.read(subtitleSettingsProvider.notifier).setSettings(next);
  }

  void _setPlaybackRate(double rate) {
    _playbackRate = rate;
    unawaited(_session?.setRate(rate) ?? Future<void>.value());
    _feeder.setPanelSettings(
      _panelSettings,
      option: _buildDanmakuOption(_panelSettings),
    );
    _invalidateBottomBar();
    _refreshPanel();
  }

  void _setVideoFit(BoxFit fit) {
    _videoFit.value = fit;
    _invalidateBottomBar();
    _refreshPanel();
  }


  void _togglePanel(PlayerPanelCategory c) {
    _versionBarOpen.value = false;
    _drawer.toggle(c);
  }


  Widget _panelIcon(PlayerPanelCategory c) {
    return CompositedTransformTarget(
      link: _panelLinks[c]!,
      child: PanelToggleAffordance(
        isOpen: _drawer.category == c,
        onTap: () => _togglePanel(c),
        tooltip: panelCategoryLabel(c),
        child: Icon(panelCategoryIcon(c), color: Colors.white, size: 26),
      ),
    );
  }


  Widget _panelBody(PlaybackSession session, PlayerPanelCategory c) {
    switch (c) {
      case PlayerPanelCategory.subtitle:
        session.prefetchExternalSubtitles();
        return SubtitlePanelBody(
          subtitleSettings: _subtitleSettings,
          onSubtitleSettingsChanged: _onSubtitleSettingsChanged,
          subtitles: session.context.embySubtitles,
          activeSubtitleStreamIndex: session.activeSubtitleStreamIndex,
          subtitleLoading: session.subtitleLoading,
          onSubtitleSelected: (streamIndex) =>
              _onSubtitleSelected(session, streamIndex),
        );
      case PlayerPanelCategory.audio:
        return AudioPanelBody(
          player: session.engine,
          onAudioTrackSelected: (track) =>
              _onAudioTrackSelected(session, track),
        );
      case PlayerPanelCategory.speed:
        return PanelSelectionList<double>(
          entries: [
            for (final speed in SpeedButton.speeds)
              SelectionPanelEntry.item(
                value: speed,
                label: SpeedButton.formatRate(speed),
                isActive: speed == _playbackRate,
              ),
          ],
          onSelected: _setPlaybackRate,
        );
      case PlayerPanelCategory.fit:
        return PanelSelectionList<BoxFit>(
          entries: [
            for (final option in FitButton.options)
              SelectionPanelEntry.item(
                value: option.fit,
                label: option.label,
                subtitle: option.subtitle,
                isActive: option.fit == _videoFit.value,
              ),
          ],
          onSelected: _setVideoFit,
        );
      case PlayerPanelCategory.episode:
        return EpisodePanelBody(
          key: _episodeBodyKey,
          seriesId: session.context.seriesId!,
          initialSeasonId: session.context.seasonId,
          currentEpisodeId: session.context.itemId,
          serverId: session.context.serverId,
          baseUrl: session.context.baseUrl,
          token: session.context.token,
          onEpisodeSelected: _onEpisodePicked,
        );
      case PlayerPanelCategory.danmaku:
        final danmakuState = ref.read(danmakuSettingsProvider).value;
        return DanmakuPanelBody(
          settings: _panelSettings,
          onChanged: _onDanmakuPanelChanged,
          sessionState: _sessionState,
          danmakuGlobalEnabled: danmakuState?.enabled ?? false,
          danmakuSourceConfigured:
              (danmakuState?.enabledSources ?? const []).isNotEmpty,
          matches: _danmakuMatches,
          selectedMatchIndex: _danmakuSelectedMatchIndex,
          onMatchSelected: _onMatchSelected,
          onOpenSearch: _openDanmakuSearchDialog,
          isLoading: _danmakuSearching,
          commentCount: _danmakuCommentCount,
          blockedKeywords: danmakuState?.blockedKeywords ?? const [],
          onAddKeyword: (kw) =>
              ref.read(danmakuSettingsProvider.notifier).addBlockedKeyword(kw),
          onRemoveKeyword: (kw) => ref
              .read(danmakuSettingsProvider.notifier)
              .removeBlockedKeyword(kw),
          onRetry: _retryDanmaku,
          onGoToSettings: _goToDanmakuSettings,
          onToggleDanmakuEnabled: _toggleDanmakuGlobalEnabled,
        );
    }
  }

  Future<void> _toggleFullscreen() async {
    if (_pip.isPip) {
      await _pip.leave();
    }
    await _fullscreen.toggle();
  }

  Future<void> _togglePip() async {
    if (!_isDesktopWindow) return;
    if (_fullscreen.isFullscreen) {
      await _fullscreen.leave();
    }
    await _pip.toggle();
  }

  void _goBack() {
    if (_backNavigationInFlight) return;
    _backNavigationInFlight = true;
    unawaited(_goBackAfterLeavingPip());
  }

  Future<void> _goBackAfterLeavingPip() async {
    try {
      try {
        unawaited(_session?.pause() ?? Future<void>.value());
      } catch (_) {}

      if (_pip.isPip) {
        await _pip.leave();
        if (!mounted) return;
      }

      if (_fullscreen.isFullscreen) {
        await _fullscreen.leave();
        if (!mounted) return;
      }

      if (context.canPop()) {
        final session = _session;
        final ticks = session != null
            ? ticksFromDuration(session.engine.state.position)
            : null;
        final result = session != null
            ? PlayerExitResult(
                positionTicks: ticks,
                serverId: session.context.serverId,
                itemId: session.context.itemId,
                mediaSourceId: session.context.mediaSourceId,
              )
            : null;
        context.pop(result);
      } else {
        context.go('/');
      }
    } catch (error, stack) {
      AppLogger.warn(_tag, 'goBack failed', error);
      AppLogger.debug(_tag, stack.toString());
    } finally {
      _backNavigationInFlight = false;
    }
  }


  Future<void> _showPlayerContextMenu(Offset globalPosition) async {
    final overlay =
        Overlay.of(context).context.findRenderObject() as RenderBox?;
    if (overlay == null) return;
    final selected = await showMenu<String>(
      context: context,
      position: RelativeRect.fromRect(
        globalPosition & const Size(40, 40),
        Offset.zero & overlay.size,
      ),
      items: [
        CheckedPopupMenuItem<String>(
          value: 'hud',
          checked: _debugHudVisible.value,
          child: const Text('调试信息 HUD'),
        ),
      ],
    );
    if (selected == 'hud') {
      _debugHudVisible.value = !_debugHudVisible.value;
    }
  }

  void _invalidateBottomBar() => _barVersion.value++;

  void _setAndroidLocked(bool locked) {
    if (!mounted || _isAndroidLocked == locked) return;
    setState(() => _isAndroidLocked = locked);
    _configureAndroidPip();
  }

  void _handleAndroidPipAction(PipAction action) {
    final session = _session;
    if (session == null) return;
    switch (action) {
      case PipAction.play:
        unawaited(session.engine.play());
      case PipAction.pause:
        unawaited(session.engine.pause());
      case PipAction.next:
        final pl = _playlist;
        if (pl != null && pl.hasNext) unawaited(pl.next());
      case PipAction.prev:
        final pl = _playlist;
        if (pl != null && pl.hasPrev) unawaited(pl.prev());
      case PipAction.seekForward:
        final secs =
            ref.read(playerSettingsProvider).value?.seekForwardSeconds ?? 15;
        final target = session.engine.state.position + Duration(seconds: secs);
        unawaited(session.engine.seek(target));
      case PipAction.seekBackward:
        final secs =
            ref.read(playerSettingsProvider).value?.seekBackwardSeconds ?? 15;
        final target = session.engine.state.position - Duration(seconds: secs);
        unawaited(
          session.engine.seek(target < Duration.zero ? Duration.zero : target),
        );
    }
  }

  void _configureAndroidPip() {
    if (!Platform.isAndroid) return;
    final session = _session;
    final settings = ref.read(playerSettingsProvider).value;

    const int aspectX = 16;
    const int aspectY = 9;

    unawaited(
      _androidPip.configure(
        aspectX: aspectX,
        aspectY: aspectY,
        autoEnterOnLeave:
            (settings?.autoPip ?? true) &&
            session != null &&
            session.engine.state.playing &&
            !_isAndroidLocked,
        isPlaying: session?.engine.state.playing ?? false,
        hasNext: _playlist?.hasNext ?? false,
        hasPrev: _playlist?.hasPrev ?? false,
        useEpisodeActions: settings?.pipShowEpisodeActions ?? true,
      ),
    );
  }

  void _attachAndroidPipPlayingListener(PlaybackSession? session) {
    unawaited(_androidPipPlayingSub?.cancel());
    _androidPipPlayingSub = null;
    if (session == null || !Platform.isAndroid) return;
    _androidPipPlayingSub = session.engine.stream.playing.listen((_) {
      _configureAndroidPip();
      if (_androidPip.isInPip) {
        final settings = ref.read(playerSettingsProvider).value;
        unawaited(
          _androidPip.updatePlaybackState(
            isPlaying: session.engine.state.playing,
            hasNext: _playlist?.hasNext ?? false,
            hasPrev: _playlist?.hasPrev ?? false,
            useEpisodeActions: settings?.pipShowEpisodeActions ?? true,
          ),
        );
      }
    });
  }

  Future<void> _enterAndroidPip() async {
    if (!Platform.isAndroid || !_androidPip.isSupported) return;
    _drawer.close();
    _versionBarOpen.value = false;
    await _androidPip.enter(aspectX: 16, aspectY: 9);
  }


  void _refreshPanel() {
    if (mounted) setState(() {});
  }

  void _invalidateDanmakuOverlay() => _danmakuOverlayVersion.value++;
  void _invalidateSubtitleOverlay() => _subtitleOverlayVersion.value++;

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (Platform.isAndroid && state == AppLifecycleState.paused) {
      _session?.engine.pause();
    }
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _disableAndroidSystemAudio();

    if (Platform.isAndroid) {
      SystemChrome.setPreferredOrientations(const <DeviceOrientation>[]);
      SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
      unawaited(
        ScreenBrightness.instance.resetApplicationScreenBrightness().catchError(
          (_) {},
        ),
      );
    }

    _flushDanmakuPanelSettingsPersist();
    _relink.dispose();
    _frameJankMonitor.dispose();
    unawaited(_androidPipPlayingSub?.cancel());
    _androidPipPlayingSub = null;

    Future<void>.microtask(() {
      try {
        _playbackActiveNotifier.state = false;
      } catch (_) {}
    });

    unawaited(_wakelockPlayingSub?.cancel());
    _wakelockPlayingSub = null;
    _wakelockSession = null;
    _setWakelockEnabled(false);

    _session?.phase.removeListener(_onSessionPhaseChanged);
    _playlist?.current.removeListener(_onCurrentItemChanged);
    final playlist = _playlist;
    _playlist = null;
    try {
      playlist?.dispose();
    } catch (e) {
      AppLogger.warn(_tag, 'dispose: playlist failed', e);
    }

    _drawer.removeListener(_onPanelChanged);

    for (final step in <(String, void Function())>[
      ('fullscreen', _fullscreen.dispose),
      ('pip', _pip.dispose),
      ('androidPip', _androidPip.dispose),
      ('feederDetach', _feeder.detach),
      ('videoFit', _videoFit.dispose),
      ('barVersion', _barVersion.dispose),
      ('debugHudVisible', _debugHudVisible.dispose),
      ('drawer', _drawer.dispose),
      ('versionBarOpen', _versionBarOpen.dispose),
      ('seekPreview', _seekPreview.dispose),
      ('danmakuOverlayVer', _danmakuOverlayVersion.dispose),
      ('subtitleOverlayVer', _subtitleOverlayVersion.dispose),
      ('bufferingOverlayVisible', _bufferingOverlayVisible.dispose),
    ]) {
      try {
        step.$2();
      } catch (e) {
        AppLogger.warn(_tag, 'dispose: ${step.$1} failed', e);
      }
    }

    final session = _session;
    _session = null;
    final holder = _engineHolder;
    if (session != null) {
      unawaited(session.dispose().whenComplete(holder.dispose));
    } else {
      unawaited(holder.dispose());
    }

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _wireDanmakuListeners();

    ref.listen<bool>(
      audioSettingsProvider.select((s) => s.value?.volumeBoost ?? false),
      (prev, next) {
        if (prev == true && next == false) {
          final s = _session;
          if (s != null && s.engine.state.volume > 100) {
            unawaited(s.engine.setVolume(100));
          }
        }
      },
    );

    final session = _session;
    final error = _errorMessage;
    final currentCrossServerSearchKey = session == null
        ? null
        : _buildCrossServerKey(session.context);
    if (currentCrossServerSearchKey != null) {
      _lastCrossServerSearchKey = currentCrossServerSearchKey;
    }
    final retainedCrossServerSearchKey = session == null
        ? _lastCrossServerSearchKey
        : currentCrossServerSearchKey;
    final asyncCrossServerCards = retainedCrossServerSearchKey == null
        ? null
        : ref.watch(
            crossServerSearchDataProvider(retainedCrossServerSearchKey),
          );
    if (session != null) {
      if (currentCrossServerSearchKey != null &&
          asyncCrossServerCards != null) {
        _crossServerCards = _filteredCrossServerCards(
          asyncCrossServerCards.maybeWhen(
            data: (list) => list,
            orElse: () =>
                asyncCrossServerCards.value ?? const <CrossServerSourceCard>[],
          ),
          session.context,
        );
      } else {
        _crossServerCards = const [];
      }
    }

    return PopScope(
      canPop: false,
      onPopInvokedWithResult: (didPop, _) {
        if (!didPop) _goBack();
      },
      child: Scaffold(
        backgroundColor: Colors.black,
        body: Stack(
          fit: StackFit.expand,
          children: [
            if (session != null)
              Positioned.fill(child: _buildPlayingStack(session)),
            if (session == null || !_videoVisible) ...[
              PlayerLoadingBackdrop(backdropUrl: widget.backdropUrl),
              PlayerStatusTopBar(
                title: _fallbackTitle,
                onBack: _goBack,
                topInset: _topBarTopInset,
                leftInset: _topBarLeftInset,
                showWindowControls: _showFallbackWindowControls,
                enableWindowDrag: _showFallbackWindowControls,
              ),
            ],
            if ((session == null || !_videoVisible) && error == null)
              const PlayerLoadingIndicator(),
            if (error != null && session == null) ...[
              const Positioned.fill(child: ColoredBox(color: Colors.black)),
              PlayerStatusTopBar(
                title: _fallbackTitle,
                onBack: _goBack,
                topInset: _topBarTopInset,
                leftInset: _topBarLeftInset,
                showWindowControls: _showFallbackWindowControls,
                enableWindowDrag: _showFallbackWindowControls,
              ),
              PlayerErrorCard(message: error, onBack: _goBack, onRetry: _retry),
            ],
          ],
        ),
      ),
    );
  }


  Widget _buildPlayingStack(PlaybackSession session) {
    final seekSettings = ref.watch(
      playerSettingsProvider.select(
        (s) => s.whenData(
          (d) => (fwd: d.seekForwardSeconds, bwd: d.seekBackwardSeconds),
        ),
      ),
    );
    final seekFwd = seekSettings.value?.fwd ?? 15;
    final seekBwd = seekSettings.value?.bwd ?? 15;
    final volumeBoost = ref.watch(
      audioSettingsProvider.select((s) => s.value?.volumeBoost ?? false),
    );

    final playerSettings = ref.watch(playerSettingsProvider).value;
    final desktopBindings = playerSettings == null
        ? GestureBindings.defaults
        : GestureBindings(
            keyboardHoldLeftSpeed: playerSettings.keyboardHoldLeftSpeed,
            keyboardHoldRightSpeed: playerSettings.keyboardHoldRightSpeed,
            mouseLeftClickAction: playerSettings.mouseLeftClickAction,
            mouseLeftDoubleClickAction:
                playerSettings.mouseLeftDoubleClickAction,
            mouseRightClickAction: playerSettings.mouseRightClickAction,
            mouseRightDoubleClickAction:
                playerSettings.mouseRightDoubleClickAction,
          );
    final gestureBindings = _isDesktopWindow
        ? desktopBindings
        : GestureBindings(
            keyboardHoldLeftSpeed: desktopBindings.keyboardHoldLeftSpeed,
            keyboardHoldRightSpeed: desktopBindings.keyboardHoldRightSpeed,
            mouseLeftClickAction: GestureAction.toggleControls,
            mouseLeftDoubleClickAction: GestureAction.playPause,
            mouseRightClickAction: desktopBindings.mouseRightClickAction,
            mouseRightDoubleClickAction:
                desktopBindings.mouseRightDoubleClickAction,
          );

    return Stack(
      fit: StackFit.expand,
      children: [
        AnimatedBuilder(
          animation: _drawer,
          builder: (context, _) {
            final panelOpen = _drawer.isOpen;
            return PlayerVideoWidget(
              key: ValueKey<String>(
                'mpv:${session.context.itemId}:'
                '${session.context.mediaSourceId ?? ''}',
              ),
              player: session.engine,
              fit: _videoFit,
              topBar: Platform.isAndroid
                  ? (_isAndroidPip
                        ? const <Widget>[]
                        : _buildAndroidTopBar(session))
                  : _buildTopBar(session),
              topBarLeftInset: _topBarLeftInset,
              topBarTopInset: _topBarTopInset,
              bottomBar: Platform.isAndroid
                  ? (_isAndroidPip
                        ? const SizedBox.shrink()
                        : _buildAndroidBottomBar(session))
                  : _buildBottomBar(session),
              centerBar: Platform.isAndroid
                  ? null
                  : (_pip.isPip ? _buildPipCenterControls(session) : null),
              leftBar: Platform.isAndroid && !_isAndroidLocked && !_isAndroidPip
                  ? _buildAndroidLeftBar()
                  : null,
              rightBar:
                  Platform.isAndroid && !_isAndroidLocked && !_isAndroidPip
                  ? _buildAndroidRightBar(session)
                  : null,
              mobileGestureBuilder:
                  Platform.isAndroid && !_isAndroidLocked && !_isAndroidPip
                  ? (toggleControls, showControls) => _buildAndroidGestureLayer(
                      session,
                      toggleControls,
                      showControls,
                    )
                  : null,
              gestureFeedback: Platform.isAndroid && !_isAndroidPip
                  ? _buildAndroidGestureFeedback()
                  : null,
              overlays: _isAndroidPip
                  ? _buildPipOverlays(session)
                  : _buildVideoOverlays(session),
              onControlsAreaTap: panelOpen ? _drawer.close : null,
              isFullscreen: _fullscreen.isFullscreen,
              onToggleFullscreen: _toggleFullscreen,
              isPip: _pip.isPip,
              onTogglePip: _isDesktopWindow ? _togglePip : null,
              lockControls: panelOpen,
              suppressControlsChrome:
                  Platform.isAndroid && (_isAndroidLocked || _isAndroidPip),
              keyboardEnabled: !_isDanmakuSearchOpen && !_drawer.isOpen,
              seekForwardSeconds: seekFwd,
              seekBackwardSeconds: seekBwd,
              volumeBoost: volumeBoost,
              gestureBindings: gestureBindings,
              currentRate: _playbackRate,
              onSetRate: _setPlaybackRate,
              onPrevItem: () {
                final pl = _playlist;
                if (pl != null && pl.hasPrev) unawaited(pl.prev());
              },
              onNextItem: () {
                final pl = _playlist;
                if (pl != null && pl.hasNext) unawaited(pl.next());
              },
              onExit: _goBack,
              onSecondaryContextMenu: kDebugMode
                  ? _showPlayerContextMenu
                  : null,
            );
          },
        ),
        _buildVersionBarLayer(session),
        if (!Platform.isAndroid)
          PlayerPanelPopover(
            controller: _drawer,
            anchorLinks: _panelLinks,
            bodyBuilder: (c) => _panelBody(session, c),
          ),
        if (Platform.isAndroid && !_isAndroidPip)
          _buildAndroidDrawerLayer(session),
        if (Platform.isAndroid && _isAndroidLocked && !_isAndroidPip)
          AndroidLockedOverlay(onUnlock: () => _setAndroidLocked(false)),
        if (_switchInFlight)
          const Positioned.fill(
            child: IgnorePointer(
              child: ColoredBox(
                color: Colors.black54,
                child: Center(
                  child: AppLoadingRing(size: 44, color: Colors.white),
                ),
              ),
            ),
          ),
      ],
    );
  }
}
