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

extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState {
  void _enqueueLifecycleTransition(String label, Future<void> Function() transition) {
    _lifecycleTransition = _lifecycleTransition
        .catchError((Object error, StackTrace stackTrace) {
          appLogger.w('Previous lifecycle transition failed', error: error, stackTrace: stackTrace);
        })
        .then((_) async {
          if (!mounted) return;
          try {
            await transition();
          } catch (e, stackTrace) {
            appLogger.w('Lifecycle transition failed during $label', error: e, stackTrace: stackTrace);
          }
        });
  }

  void _recordLifecycleState(String state, {String? action}) {
    final isTv = PlatformDetector.isTV();
    final pipActive = PipService().isPipActive.value;
    final breadcrumbData = <String, dynamic>{
      'state': state,
      'isTv': isTv,
      'autoPipEnabled': _autoPipEnabled,
      'pipActive': pipActive,
      'pipTransitionInFlight': _androidAutoPipTransitionInFlight,
      'hiddenForBackground': _hiddenForBackground,
      'playerSuspendedForTvBackground': _playerSuspendedForTvBackground,
      'mediaControlsSuspendedForTvBackground': _mediaControlsSuspendedForTvBackground,
      'backend': _playerBackendLabel,
    };
    if (action != null) {
      breadcrumbData['action'] = action;
    }

    Sentry.addBreadcrumb(
      Breadcrumb(message: 'Player lifecycle $state', category: 'player.lifecycle', data: breadcrumbData),
    );

    appLogger.d(
      'Player lifecycle: state=$state'
      '${action != null ? ' action=$action' : ''}'
      ' isTv=$isTv'
      ' autoPipEnabled=$_autoPipEnabled'
      ' pipActive=$pipActive'
      ' pipTransitionInFlight=$_androidAutoPipTransitionInFlight'
      ' hiddenForBackground=$_hiddenForBackground'
      ' playerSuspendedForTvBackground=$_playerSuspendedForTvBackground'
      ' mediaControlsSuspendedForTvBackground=$_mediaControlsSuspendedForTvBackground'
      ' backend=$_playerBackendLabel',
    );
  }

  void _setAndroidAutoPipTransitionInFlight(bool value, {required String reason}) {
    if (!Platform.isAndroid || _androidAutoPipTransitionInFlight == value) return;
    _androidAutoPipTransitionInFlight = value;
    _recordLifecycleState('pip_transition', action: '${value ? 'started' : 'cleared'}:$reason');
  }

  void _suspendLiveTimelineForBackground() {
    _live.resumeTimelineOnResume = _live.timelineTimer != null;
    _stopLiveTimelineUpdates();
  }

  void _resumeLiveTimelineAfterBackgroundIfNeeded() {
    final shouldResume = _live.resumeTimelineOnResume;
    _live.resumeTimelineOnResume = false;
    if (shouldResume && _live.session != null) {
      _startLiveTimelineUpdates();
    }
  }

  Future<void> _handleAppHidden() async {
    if (_shouldSkipForPip) {
      _recordLifecycleState('hidden', action: 'skipped_for_pip');
      return;
    }

    // Suppress Watch Together heartbeats while backgrounded so App Nap
    // doesn't cause stale position broadcasts that make guests loop.
    _watchTogetherProvider?.setBackgrounded(true);

    final currentPlayer = player;
    if (currentPlayer == null || !_isPlayerInitialized) {
      _recordLifecycleState('hidden', action: 'skipped_no_player');
      return;
    }

    final isTv = PlatformDetector.isTV();
    if (widget.isLive && shouldStopLiveSessionForTvBackground(isTv: isTv, policy: _live.session?.backgroundPolicy)) {
      _live.exitOnResume = true;
      _live.resumeTimelineOnResume = false;
      _stopLiveTimelineUpdates();

      // Start the server cleanup before releasing the native stream. tvOS may
      // suspend the process shortly after this lifecycle callback returns.
      final stoppedReport = _sendStoppedProgressOnce();
      try {
        await currentPlayer.stop();
      } catch (e, stackTrace) {
        appLogger.w('Failed to stop live player while backgrounding', error: e, stackTrace: stackTrace);
      }
      await stoppedReport;
      if (!mounted || currentPlayer != player) return;
      await _suspendMediaControlsForTvBackground('hidden_live_stopped');
      _recordLifecycleState('hidden', action: 'live_stopped_exit_on_resume');
      return;
    }

    final shouldPauseForBackground = PlatformDetector.isHandheld(context) || isTv;

    // Pause first so Android MPV does not keep decoding against a transient
    // background surface while the app is locking or hiding.
    if (shouldPauseForBackground) {
      _wasPlayingBeforeInactive = currentPlayer.state.isActive;
      if (_wasPlayingBeforeInactive) {
        try {
          await _pauseWithPlaybackIntent(currentPlayer);
          appLogger.d('Video paused due to app being hidden (${isTv ? 'tv' : 'handheld'})');
        } catch (e) {
          appLogger.w('Failed to pause video before background transition', error: e);
        }
      }
    }

    if (!mounted || currentPlayer != player) return;

    _suspendLiveTimelineForBackground();

    if (isTv) {
      await _suspendMediaControlsForTvBackground('hidden');
      if (_armTvBackgroundPlayerSuspendTimer()) {
        _recordLifecycleState('hidden', action: 'tv_background_pause_suspend_armed');
      } else {
        _recordLifecycleState('hidden', action: 'tv_background_pause_only');
      }
      return;
    }

    _hiddenForBackground = true;
    await currentPlayer.setVisible(false, restoreOnWindowVisible: Platform.isMacOS);
    _recordLifecycleState('hidden', action: 'render_hidden');
  }

  Future<void> _handleAppResumed() async {
    _recordLifecycleState('resumed', action: 'begin');
    _watchTogetherProvider?.setBackgrounded(false);

    if (_live.exitOnResume) {
      _live.exitOnResume = false;
      _recordLifecycleState('resumed', action: 'exit_stopped_live_session');
      await _handleBackButton();
      return;
    }

    if (Platform.isAndroid && _androidAutoPipTransitionInFlight && !PipService().isPipActive.value) {
      _setAndroidAutoPipTransitionInFlight(false, reason: 'resume_without_pip');
    }

    final currentPlayer = player;

    // Restore render layer if it was hidden for background, then force a
    // video-output refresh before any auto-resume logic runs.
    if (_hiddenForBackground && currentPlayer != null && _isPlayerInitialized) {
      await currentPlayer.setVisible(true);
      if (!Platform.isMacOS) {
        await currentPlayer.updateFrame();
      }

      if (!mounted || currentPlayer != player) return;

      _hiddenForBackground = false;
      _recordLifecycleState('resumed', action: 'render_restored');
    }

    // A TV background suspend released the native pipeline via stop();
    // rebuild the playback session in place before the media-control restore
    // below can act on the stopped player.
    if (_playerSuspendedForTvBackground) {
      await _restorePlayerAfterTvBackgroundSuspend();
      if (!mounted || currentPlayer != player) return;
    }
    // TV never hides the render layer on background (_handleAppHidden returns
    // early without setting _hiddenForBackground), but the screensaver can
    // still destroy the surface. Kick the video output so a missed surface
    // callback can't leave the picture black: mpv re-attaches via
    // refreshVideoOutput, ExoPlayer just reapplies sizing/z-order.
    else if (!_hiddenForBackground &&
        Platform.isAndroid &&
        PlatformDetector.isTV() &&
        currentPlayer != null &&
        _isPlayerInitialized) {
      await currentPlayer.updateFrame();
      if (!mounted || currentPlayer != player) return;
      _recordLifecycleState('resumed', action: 'tv_video_output_kick');
    }

    // Restore media controls and wakelock when app is resumed.
    if (_isPlayerInitialized && mounted) {
      _resumeMediaControlsAfterTvBackground('app_resumed');
      await _restoreMediaControlsAfterResume();
    }

    _resumeLiveTimelineAfterBackgroundIfNeeded();
    _recordLifecycleState('resumed', action: 'complete');
  }

  /// Arm the grace timer that releases the native AV pipeline if the app
  /// stays backgrounded (Android TV only). Returns whether it was armed.
  bool _armTvBackgroundPlayerSuspendTimer() {
    if (!shouldSuspendPlayerForTvBackground(
      isAndroid: Platform.isAndroid,
      isTv: PlatformDetector.isTV(),
      isLive: widget.isLive,
      alreadySuspended: _playerSuspendedForTvBackground,
    )) {
      return false;
    }
    _tvBackgroundPlayerSuspendTimer?.cancel();
    _tvBackgroundPlayerSuspendTimer = Timer(VideoPlayerScreenState._tvBackgroundPlayerSuspendGrace, () {
      _tvBackgroundPlayerSuspendTimer = null;
      _enqueueLifecycleTransition('tv_background_suspend', _suspendPlayerForTvBackground);
    });
    return true;
  }

  void _cancelTvBackgroundPlayerSuspendTimer() {
    _tvBackgroundPlayerSuspendTimer?.cancel();
    _tvBackgroundPlayerSuspendTimer = null;
  }

  /// Grace expired while still backgrounded: release the native AV pipeline
  /// (MediaCodec decoders + AudioTrack, tunneled passthrough included) so a
  /// parked Plezy can't starve other apps on shared-hardware TV SoCs. stop()
  /// retains Dart-side position/duration/track state on both Android
  /// backends, and the progress tracker keeps sending paused heartbeats at
  /// the retained position, so the server session stays alive and resumable.
  /// Position and track selections are latched here because the reload on
  /// restore reads them after the native state is gone.
  Future<void> _suspendPlayerForTvBackground() async {
    final currentPlayer = player;
    if (!mounted || currentPlayer == null || !_isPlayerInitialized) return;
    // A live stream's tuned session is also its time-shift buffer. Stopping
    // it would force a re-tune at the live edge and discard pause state.
    if (widget.isLive) return;
    if (_playerSuspendedForTvBackground || _shouldSkipForPip) return;
    final lifecycleState = WidgetsBinding.instance.lifecycleState;
    if (lifecycleState == AppLifecycleState.resumed || lifecycleState == AppLifecycleState.inactive) return;
    if (_playbackTransition != _PlaybackTransition.idle || !_hasFirstFrame.value) {
      // A reload/zap/startup flow owns the player right now; stopping under
      // it would corrupt its open sequence. Retry after another grace.
      _armTvBackgroundPlayerSuspendTimer();
      return;
    }

    _tvBackgroundSuspendPosition = currentPlayer.state.position;
    _tvBackgroundSuspendAudioTrack = currentPlayer.state.track.audio;
    _tvBackgroundSuspendSubtitleTrack = currentPlayer.state.track.subtitle;
    _tvBackgroundSuspendSecondarySubtitleTrack = currentPlayer.state.track.secondarySubtitle;
    _playerSuspendedForTvBackground = true;
    try {
      await currentPlayer.stop();
      _recordLifecycleState('hidden', action: 'tv_background_suspend');
    } catch (e) {
      _playerSuspendedForTvBackground = false;
      _tvBackgroundSuspendPosition = null;
      _tvBackgroundSuspendAudioTrack = null;
      _tvBackgroundSuspendSubtitleTrack = null;
      _tvBackgroundSuspendSecondarySubtitleTrack = null;
      appLogger.w('TV background suspend failed; player left paused', error: e);
    }
  }

  /// Rebuild the playback session after a TV background suspend released the
  /// native pipeline. VOD reloads in place through the regular reload flow —
  /// a fresh playback decision, since the suspended stream URL may have
  /// expired server-side — and comes back paused; the caller's
  /// [_restoreMediaControlsAfterResume] then resumes it (with
  /// rewind-on-resume) exactly like a plain background pause. Live sessions
  /// never enter this flow because their tuned session and capture-buffer
  /// position must remain intact across backgrounding.
  Future<void> _restorePlayerAfterTvBackgroundSuspend() async {
    _playerSuspendedForTvBackground = false;
    final resumePosition = _tvBackgroundSuspendPosition;
    final audioTrack = _tvBackgroundSuspendAudioTrack;
    final subtitleTrack = _tvBackgroundSuspendSubtitleTrack;
    final secondarySubtitleTrack = _tvBackgroundSuspendSecondarySubtitleTrack;
    _tvBackgroundSuspendPosition = null;
    _tvBackgroundSuspendAudioTrack = null;
    _tvBackgroundSuspendSubtitleTrack = null;
    _tvBackgroundSuspendSecondarySubtitleTrack = null;

    final currentPlayer = player;
    if (!mounted || currentPlayer == null || !_isPlayerInitialized) return;

    _recordLifecycleState('resumed', action: 'tv_background_suspend_reload');
    final outcome = await _reloadMediaInPlace(
      metadata: _currentMetadata,
      resumePosition: resumePosition,
      preserveCurrentTrackSelection: true,
      preservedAudioTrack: audioTrack,
      preservedSubtitleTrack: subtitleTrack,
      preservedSecondarySubtitleTrack: secondarySubtitleTrack,
      startPaused: true,
      reason: 'TV background suspend restore',
    );
    if (outcome == _MediaReloadOutcome.rejected) {
      appLogger.w('TV background suspend restore: in-place reload rejected');
    } else if (outcome == _MediaReloadOutcome.failed) {
      appLogger.w('TV background suspend restore: in-place reload failed');
    }
  }
}
