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

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:fvp/mdk.dart' as mdk;

import '../../contracts/cancellation.dart';
import '../../media/embedded_subtitle_cues.dart';
import '../../../shared/utils/app_logger.dart';
import 'player_engine.dart';
import 'seek_guard.dart';
import 'texture_video_view.dart';

const _tag = 'FvpPlayer';
const _androidTlsChannel = MethodChannel('smplayer/android_tls');

Future<String>? _androidTrustedCaBundlePath;

Future<String> _loadAndroidTrustedCaBundlePath() async {
  final existing = _androidTrustedCaBundlePath;
  if (existing != null) return existing;

  final loading = _androidTlsChannel
      .invokeMethod<String>('exportTrustedCaBundle')
      .then((path) {
        if (path == null || path.isEmpty) {
          throw StateError('Android 系统 CA 导出返回了空路径');
        }
        return path;
      });
  _androidTrustedCaBundlePath = loading;
  try {
    return await loading;
  } catch (_) {
    if (identical(_androidTrustedCaBundlePath, loading)) {
      _androidTrustedCaBundlePath = null;
    }
    rethrow;
  }
}


bool isFvpNetworkError(String error) {
  final lower = error.toLowerCase();
  if (lower.startsWith('tcp:') || lower.startsWith('tls:')) return true;
  const patterns = <String>[
    'tls',
    'ssl',
    'handshake',
    'connection refused',
    'connection reset',
    'connection timed out',
    'network is unreachable',
    'host not found',
    'name resolution',
    'no route to host',
    'http error 4',
    'http error 5',
    'server returned 4',
    'server returned 5',
    'protocol not found',
    'no address',
  ];
  for (final pattern in patterns) {
    if (lower.contains(pattern)) return true;
  }
  return false;
}


@visibleForTesting
String? fvpNetworkErrorPayload({
  required String category,
  required int errorCode,
  required String detail,
}) {
  if (errorCode >= 0 || detail.isEmpty) return null;

  final isIoLike =
      category.startsWith('reader') ||
      category.startsWith('io') ||
      category.startsWith('demuxer') ||
      category == 'source';
  if (!isIoLike) return null;

  final normalizedCategory = category.startsWith('reader')
      ? 'reader'
      : category;
  final payload = '$normalizedCategory: $detail (err=$errorCode)';
  return isFvpNetworkError(payload) ? payload : null;
}


const bool kDvForceSoftwareDecode = true;


const List<String> kDvSoftwareDecoders = ['FFmpeg:threads=0'];


({int min, int max}) bufferRangeForRate(double rate) {
  final factor = rate > 1 ? rate : 1.0;
  return (min: (1000 * factor).round(), max: (15000 * factor).round());
}


const int kDvPlaybackMinBufferMs = 3000;
({int min, int max}) playbackBufferRangeForRate(
  double rate, {
  bool isDolbyVision = false,
}) {
  final base = bufferRangeForRate(rate);
  if (!isDolbyVision || base.min >= kDvPlaybackMinBufferMs) return base;
  return (min: kDvPlaybackMinBufferMs, max: base.max);
}


List<String>? decoderListForMode(String mode) {
  if (mode == 'no') {
    return const ['FFmpeg:threads=0', 'dav1d'];
  }
  if (Platform.isWindows) {
    return const [
      'MFT:d3d=11',
      'D3D11',
      'DXVA',
      'CUDA',
      'hap',
      'FFmpeg',
      'dav1d',
    ];
  }
  if (Platform.isMacOS || Platform.isIOS) {
    return const ['hap', 'FFmpeg', 'dav1d'];
  }
  if (Platform.isAndroid) {
    return const ['AMediaCodec', 'FFmpeg', 'dav1d'];
  }
  return null;
}


class FvpPlayerEngine implements PlayerEngine {
  FvpPlayerEngine() {
    _configureBaseProperties();
    _stateSub = _player.onStateChanged.listen(_handleStateChanged);
    _statusSub = _player.onMediaStatus.listen(_handleMediaStatusChanged);
    _eventSub = _player.onEvent.listen(_handleEvent);
    _player.onSubtitleText(_handleSubtitleText);
    _player.textureId.addListener(_bumpTextureVersion);
  }

  final mdk.Player _player = mdk.Player();

  @override
  final PlayerStreams stream = PlayerStreams();
  @override
  final PlayerState state = PlayerState();
  @override
  final ValueNotifier<ui.Size?> videoSize = ValueNotifier<ui.Size?>(null);
  @override
  final ValueNotifier<int> textureVersion = ValueNotifier<int>(0);

  @override
  bool get usesPlatformSurface => false;


  double _estimatedRxBytes = 0;
  int? _lastDownloadFrontierMs;

  @override
  Future<int?> totalRxBytes() async {
    if (_disposed) return null;
    final bitRate = _player.mediaInfo.bitRate;
    if (bitRate <= 0) return null;
    final frontierMs = _player.position + _player.buffered();
    final lastFrontierMs = _lastDownloadFrontierMs;
    _lastDownloadFrontierMs = frontierMs;
    if (lastFrontierMs != null) {
      final deltaMediaMs = frontierMs - lastFrontierMs;
      if (deltaMediaMs > 0) {
        _estimatedRxBytes += deltaMediaMs / 1000.0 * bitRate / 8.0;
      }
    }
    return _estimatedRxBytes.round();
  }


  void _resetRxEstimator() {
    _estimatedRxBytes = 0;
    _lastDownloadFrontierMs = null;
  }

  @override
  bool isNetworkStreamError(String error) => isFvpNetworkError(error);

  StreamSubscription<
    ({mdk.PlaybackState oldValue, mdk.PlaybackState newValue})
  >?
  _stateSub;
  StreamSubscription<({mdk.MediaStatus oldValue, mdk.MediaStatus newValue})>?
  _statusSub;
  StreamSubscription<mdk.MediaEvent>? _eventSub;
  Timer? _positionTimer;
  Timer? _statsTimer;

  bool _disposed = false;
  bool _manualStop = false;
  bool _completedEmitted = false;
  double _lastNonZeroVolume = 100.0;
  String _hwdecMode = 'auto-copy';


  String _activeVideoDecoder = '?';


  ui.Size? _textureNaturalSize;


  String? _lastColorSignature;


  final List<String> _recentEvents = <String>[];


  bool _isDolbyVision = false;


  bool _dvSoftwareDecodeFailed = false;


  final EmbeddedSubtitleCues _primaryCues = EmbeddedSubtitleCues();
  List<String> _lastEmittedSubtitle = const [];


  bool _subtitleCueReceived = false;


  int? _desiredSubtitleRel;
  int? _desiredAudioRel;


  bool _reassertTracksPending = false;

  final SeekGuard _seekGuard = SeekGuard();


  final ValueNotifier<PlaybackStats?> _debugStats =
      ValueNotifier<PlaybackStats?>(null);

  @override
  ValueListenable<PlaybackStats?> get debugStats => _debugStats;

  @override
  ValueListenable<int?> get textureId => _player.textureId;


  void setDolbyVision(bool isDv) {
    _isDolbyVision = isDv;
  }

  @override
  String get displayName => 'fvp';

  @override
  void configureHardwareDecoding(String mode) {
    _hwdecMode = mode;
    final List<String>? decoders;
    if (kDvForceSoftwareDecode &&
        _isDolbyVision &&
        mode != 'no' &&
        !_dvSoftwareDecodeFailed) {
      decoders = kDvSoftwareDecoders;
    } else {
      decoders = decoderListForMode(mode);
    }
    if (decoders != null) {
      _player.videoDecoders = decoders;
    }
  }

  @override
  Future<void> open(
    String url, {
    Duration start = Duration.zero,
    bool play = true,
    Map<String, String>? headers,
    CancellationToken? cancel,
  }) async {
    _ensureNotDisposed();
    cancel?.throwIfCancelled();
    await _stopForMediaChange();
    if (_disposed) return;
    cancel?.throwIfCancelled();

    _recentEvents.clear();
    _completedEmitted = false;
    _dvSoftwareDecodeFailed = false;
    _desiredSubtitleRel = null;
    _desiredAudioRel = null;
    _reassertTracksPending = false;
    _resetRxEstimator();
    videoSize.value = null;
    _seekGuard.arm(start);
    state
      ..position = start
      ..buffer = start
      ..duration = Duration.zero
      ..playing = false
      ..completed = false
      ..tracks = const PlayerTracks()
      ..track = const PlayerTrack()
      ..subtitle = const []
      ..error = null;
    _emitStateSnapshot();

    configureHardwareDecoding(_hwdecMode);
    _setHttpHeaders(headers);
    await _configureAndroidTls(url);
    if (_disposed) return;
    cancel?.throwIfCancelled();
    _player.media = url;
    _logSchemeIfDirectConnect(url);
    _player.onSubtitleText(_handleSubtitleText);
    _player.activeSubtitleTracks = const [];
    _player.activeAudioTracks = const [];

    var ret = await _runCancellable(_player.prepare(position: 0), cancel);
    if (_disposed) return;
    cancel?.throwIfCancelled();
    var loaded = ret >= 0 || _player.mediaStatus.test(mdk.MediaStatus.loaded);

    if (!loaded &&
        kDvForceSoftwareDecode &&
        _isDolbyVision &&
        _hwdecMode != 'no') {
      await Future<void>.delayed(const Duration(milliseconds: 200));
      if (_disposed) return;
      cancel?.throwIfCancelled();
      AppLogger.warn(
        _tag,
        'DV software decode failed (ret=$ret); retrying with hardware decoders',
      );
      _dvSoftwareDecodeFailed = true;
      final hwDecoders = decoderListForMode(_hwdecMode);
      if (hwDecoders != null) {
        _player.videoDecoders = hwDecoders;
      }
      await _stopForMediaChange();
      if (_disposed) return;
      cancel?.throwIfCancelled();
      _recentEvents.clear();
      await _configureAndroidTls(url);
      if (_disposed) return;
      cancel?.throwIfCancelled();
      _player.media = url;
      _player.onSubtitleText(_handleSubtitleText);
      _player.activeSubtitleTracks = const [];
      _player.activeAudioTracks = const [];
      ret = await _runCancellable(_player.prepare(position: 0), cancel);
      if (_disposed) return;
      cancel?.throwIfCancelled();
      loaded = ret >= 0 || _player.mediaStatus.test(mdk.MediaStatus.loaded);
    }

    if (!loaded) {
      await Future<void>.delayed(const Duration(milliseconds: 500));
      if (_disposed) return;
      _logOpenFailureDiagnostics(url, start, ret);
      throw StateError('MDK 打开媒体失败：$ret');
    }
    if (start > Duration.zero) {
      _applyPlaybackBufferRange();
      await _primeResumeSeek(start, cancel);
      if (_disposed) return;
      cancel?.throwIfCancelled();
    }

    _refreshMediaInfo();
    _logDvDiagnostics();
    await _refreshTexture();
    if (_disposed) return;
    cancel?.throwIfCancelled();
    _player
      ..volume = state.volume / 100.0
      ..mute = state.volume <= 0;
    if (play) {
      await this.play();
    }
  }


  Future<T> _runCancellable<T>(Future<T> future, CancellationToken? cancel) {
    if (cancel == null) return future;
    return Future.any<T>([
      future,
      cancel.whenCancelled.then<T>(
        (_) => Future<T>.error(CancelledException(cancel.reason)),
      ),
    ]);
  }

  @override
  Future<void> play() async {
    _ensureNotDisposed();
    _manualStop = false;
    final tex = _player.textureId.value;
    if (tex == null || tex < 0) {
      try {
        await _refreshTexture();
      } catch (e) {
        AppLogger.warn(_tag, 'play: texture refresh failed', e);
      }
      if (_disposed) return;
    }
    _player.state = mdk.PlaybackState.playing;
    _setPlaying(true);
    _startPositionTimer();
  }

  @override
  Future<void> pause() async {
    if (_disposed) return;
    _player.state = mdk.PlaybackState.paused;
    _setPlaying(false);
  }

  @override
  Future<void> stop() async {
    if (_disposed) return;
    await _stopForMediaChange();
  }

  @override
  Future<void> seek(Duration position) async {
    if (_disposed) return;
    final durationMs = state.duration.inMilliseconds;
    final maxMs = durationMs <= 0 ? 1 << 62 : durationMs;
    final target = Duration(
      milliseconds: position.inMilliseconds.clamp(0, maxMs).toInt(),
    );
    _seekGuard.arm(target);
    _resetRxEstimator();
    state.position = target;
    stream.addPosition(target);
    await _player.seek(position: target.inMilliseconds);
    _pollPosition();
  }

  @override
  Future<void> setRate(double rate) async {
    if (_disposed) return;
    state.rate = rate;
    _player.playbackRate = rate;
    _applyPlaybackBufferRange();
  }

  @override
  Future<void> setVolume(double volume) async {
    if (_disposed) return;
    final next = volume.clamp(0.0, 200.0);
    state.volume = next;
    if (next > 0) _lastNonZeroVolume = next;
    _player
      ..volume = next / 100.0
      ..mute = next <= 0;
    stream.addVolume(next);
  }

  @override
  Future<void> toggleMute() async {
    if (state.volume <= 0) {
      await setVolume(_lastNonZeroVolume <= 0 ? 100 : _lastNonZeroVolume);
    } else {
      await setVolume(0);
    }
  }

  @override
  Future<void> setAudioTrack(AudioTrack track) async {
    if (_disposed) return;
    if (track.no) {
      _desiredAudioRel = -1;
      _player.activeAudioTracks = const [];
    } else if (track.index != null) {
      final rel = _relativeAudioIndex(track.index!);
      if (rel >= 0) {
        _desiredAudioRel = rel;
        _player.activeAudioTracks = [rel];
      }
    }
    _reassertTracksPending = true;
    state.track = state.track.copyWith(audio: track);
    stream.addTrack(state.track);
  }

  @override
  Future<void> setSubtitleTrack(SubtitleTrack track) async {
    if (_disposed) return;
    _primaryCues.clear();
    _lastEmittedSubtitle = const [];
    _subtitleCueReceived = false;
    if (track.no) {
      _desiredSubtitleRel = -1;
      _player.activeSubtitleTracks = const [];
      _clearSubtitle();
      AppLogger.debug(_tag, 'setSubtitleTrack: disabled');
    } else if (track.index != null) {
      final rel = _relativeSubtitleIndex(track.index!);
      if (rel >= 0) {
        _desiredSubtitleRel = rel;
        _player.activeSubtitleTracks = [rel];
        AppLogger.info(
          _tag,
          'setSubtitleTrack: activated globalIdx=${track.index} '
          'relIdx=$rel title=${track.title} lang=${track.language}',
        );
      } else {
        AppLogger.warn(
          _tag,
          'setSubtitleTrack: no relative index for global idx=${track.index} '
          '(engine has ${state.tracks.subtitle.length} subtitle tracks)',
        );
      }
    } else {
      AppLogger.warn(
        _tag,
        'setSubtitleTrack: track has no index and is not "no" — '
        'id=${track.id} title=${track.title} (no-op)',
      );
    }
    _reassertTracksPending = true;
    state.track = state.track.copyWith(subtitle: track);
    stream.addTrack(state.track);
  }

  @override
  void setNativeSubtitleRendering(bool enabled) {
    if (_disposed) return;
    _player.setProperty('subtitle', enabled ? '1' : '0');
    AppLogger.debug(
      _tag,
      'setNativeSubtitleRendering: ${enabled ? "ON (bitmap/PGS)" : "OFF (text overlay)"}',
    );
  }


  void _reapplyDesiredTracks() {
    final sub = _desiredSubtitleRel;
    final aud = _desiredAudioRel;
    if (sub != null) {
      _player.activeSubtitleTracks = sub >= 0 ? [sub] : const [];
    }
    if (aud != null) {
      _player.activeAudioTracks = aud >= 0 ? [aud] : const [];
    }
  }

  @override
  void setSubtitleStyle({
    required double fontSize,
    required double bgOpacity,
    required int position,
  }) {
    if (_disposed) return;
    _player
      ..setProperty('subtitle.font.size', fontSize.toStringAsFixed(0))
      ..setProperty('subtitle.size', 'video')
      ..setProperty('subtitle.alignment.x', '0')
      ..setProperty('subtitle.alignment.y', _subtitleAlignY(position))
      ..setProperty('subtitle.margin.y', _subtitleMarginY(position));
    final alpha = (bgOpacity.clamp(0.0, 1.0) * 255).round();
    _player.setProperty(
      'subtitle.color.background',
      '0x000000${alpha.toRadixString(16).padLeft(2, '0')}',
    );
  }

  @override
  Widget buildVideoView(BuildContext context, ValueListenable<BoxFit> fit) {
    return TextureVideoView(
      textureId: textureId,
      textureVersion: textureVersion,
      videoSize: videoSize,
      fit: fit,
    );
  }

  @override
  Future<void> dispose() async {
    if (_disposed) return;
    _disposed = true;
    _stopPositionTimer();
    _player.textureId.removeListener(_bumpTextureVersion);
    await _stateSub?.cancel();
    await _statusSub?.cancel();
    await _eventSub?.cancel();
    _player.onSubtitleText(null);
    _player.dispose();
    videoSize.dispose();
    textureVersion.dispose();
    _debugStats.dispose();
    stream.dispose();
  }

  void _configureBaseProperties() {
    _player.setProperty('subtitle', '0');
    _player
      ..setProperty('video.decoder', 'shader_resource=0')
      ..setProperty('avformat.strict', 'experimental')
      ..setProperty('avformat.safe', '0')
      ..setProperty('avio.reconnect', '1')
      ..setProperty('avio.reconnect_delay_max', '7')
      ..setProperty('avformat.rtsp_transport', 'tcp')
      ..setProperty('avformat.extension_picky', '0')
      ..setProperty('avformat.allowed_segment_extensions', 'ALL');
    final base = bufferRangeForRate(1.0);
    _player.setBufferRange(min: base.min, max: base.max);
  }

  void _applyPlaybackBufferRange() {
    final range = playbackBufferRangeForRate(
      state.rate,
      isDolbyVision: _isDolbyVision,
    );
    _player.setBufferRange(min: range.min, max: range.max);
  }

  void _setHttpHeaders(Map<String, String>? headers) {
    final values = headers?.entries
        .where((entry) => entry.key.isNotEmpty && entry.value.isNotEmpty)
        .map((entry) {
          final value = entry.value.replaceAll(RegExp(r'[\r\n]+'), ' ');
          return '${entry.key}: $value';
        })
        .join('\r\n');
    _player.setProperty(
      'avio.headers',
      values == null || values.isEmpty ? '' : '$values\r\n',
    );
  }


  void _logSchemeIfDirectConnect(String url) {
    final uri = Uri.tryParse(url);
    if (uri == null || uri.host == '127.0.0.1' || uri.host == 'localhost') {
      return;
    }
    AppLogger.info(
      _tag,
      'direct-connect open: scheme=${uri.scheme} host=${uri.host} '
      'port=${uri.hasPort ? uri.port : "(default)"}',
    );
  }


  Future<void> _configureAndroidTls(String url) async {
    if (!Platform.isAndroid) return;
    final uri = Uri.tryParse(url);
    if (uri == null || uri.scheme != 'https') return;
    final trustedCaBundlePath = await _loadAndroidTrustedCaBundlePath();
    _player.setProperty('avio.ca_file', trustedCaBundlePath);
    _player.setProperty('avio.tls_verify', '1');
  }

  void _handleStateChanged(
    ({mdk.PlaybackState oldValue, mdk.PlaybackState newValue}) event,
  ) {
    if (_disposed) return;
    final next = event.newValue;
    _setPlaying(next == mdk.PlaybackState.playing);
    if (next == mdk.PlaybackState.playing) {
      _startPositionTimer();
    }
    if (next == mdk.PlaybackState.stopped &&
        !_manualStop &&
        _isAtNaturalEnd()) {
      _emitCompleted();
    }
  }

  void _handleMediaStatusChanged(
    ({mdk.MediaStatus oldValue, mdk.MediaStatus newValue}) event,
  ) {
    if (_disposed) return;
    _refreshMediaInfo();
    if (_reassertTracksPending && event.newValue.test(mdk.MediaStatus.loaded)) {
      _reassertTracksPending = false;
      _reapplyDesiredTracks();
    }
    if (event.newValue.test(mdk.MediaStatus.end) &&
        !_manualStop &&
        _isAtNaturalEnd()) {
      _emitCompleted();
    }
  }

  void _handleEvent(mdk.MediaEvent event) {
    if (_disposed) return;
    if (event.category != 'reader.buffering') {
      _recentEvents.add('${event.category}|err=${event.error}|${event.detail}');
      if (_recentEvents.length > 16) _recentEvents.removeAt(0);
    }
    if (event.category == 'reader.buffering') {
      _pollPosition();
    } else if (event.category == 'decoder.video') {
      _activeVideoDecoder = event.detail;
      AppLogger.info(_tag, 'active video decoder: ${event.detail}');
    }
    _maybeEmitNetworkError(event);
  }


  void _maybeEmitNetworkError(mdk.MediaEvent event) {
    final payload = fvpNetworkErrorPayload(
      category: event.category,
      errorCode: event.error,
      detail: event.detail,
    );
    if (payload == null) return;
    AppLogger.warn(_tag, 'network error via mdk event: $payload');
    stream.addError(payload);
  }


  void _logDvDiagnostics() {
    try {
      final video = _player.mediaInfo.video;
      if (video == null || video.isEmpty) return;
      final codec = video.first.codec;
      AppLogger.info(
        _tag,
        'DV diag: doviProfile=${codec.doviProfile} '
        'colorSpace=${codec.colorSpace} codec=${codec.codec} '
        'isDolbyVision=$_isDolbyVision forceSW=$kDvForceSoftwareDecode '
        'hwdec=$_hwdecMode',
      );
    } catch (e) {
      AppLogger.debug(_tag, 'logDvDiagnostics failed', e);
    }
  }


  String? _currentColorSignature() {
    try {
      final video = _player.mediaInfo.video;
      if (video == null || video.isEmpty) return null;
      final codec = video.first.codec;
      return '${codec.doviProfile}/${codec.colorSpace}';
    } catch (e) {
      AppLogger.debug(_tag, 'currentColorSignature failed', e);
      return null;
    }
  }


  Future<void> _primeResumeSeek(
    Duration start,
    CancellationToken? cancel,
  ) async {
    for (var attempt = 1; attempt <= 2; attempt++) {
      final pos = await _runCancellable(
        _player.seek(position: start.inMilliseconds),
        cancel,
      );
      if (_disposed) return;
      cancel?.throwIfCancelled();
      if (pos >= 0) {
        AppLogger.info(
          _tag,
          'resume seek to ${start.inMilliseconds}ms (attempt $attempt)',
        );
        return;
      }
      AppLogger.warn(
        _tag,
        'resume seek to ${start.inMilliseconds}ms skipped ret=$pos '
        '(attempt $attempt)',
      );
      await Future<void>.delayed(const Duration(milliseconds: 200));
      if (_disposed) return;
      cancel?.throwIfCancelled();
    }
    AppLogger.warn(
      _tag,
      'resume seek failed after retries; continuing from file head',
    );
    _seekGuard.disarm();
  }


  void _logOpenFailureDiagnostics(String url, Duration start, int ret) {
    try {
      final status = _player.mediaStatus;
      final decoders = _player.videoDecoders;
      final host = Uri.tryParse(url)?.host ?? '?';
      final events = _recentEvents.isEmpty
          ? '(none)'
          : _recentEvents.join('  ▸ ');
      AppLogger.warn(
        _tag,
        'open FAILED ret=$ret startMs=${start.inMilliseconds} host=$host '
        'mediaStatus=$status decoders=$decoders isDV=$_isDolbyVision '
        'hwdec=$_hwdecMode\n  recentEvents: $events',
      );
    } catch (e) {
      AppLogger.warn(_tag, 'logOpenFailureDiagnostics failed', e);
    }
  }


  bool _isAtNaturalEnd() {
    final pos = state.position.inMilliseconds;
    final dur = state.duration.inMilliseconds;
    if (dur <= 0) return true;
    return pos >= dur - 2000;
  }

  void _handleSubtitleText(double start, double end, List<String> text) {
    if (_disposed) return;
    final cleaned = text
        .map(_stripAssMetadata)
        .where((s) => s.isNotEmpty)
        .toList();

    if (cleaned.isEmpty) return;
    if (!_subtitleCueReceived) {
      _subtitleCueReceived = true;
      final preview = cleaned.first.length > 40
          ? cleaned.first.substring(0, 40)
          : cleaned.first;
      AppLogger.debug(
        _tag,
        'subtitle: first cue received from engine '
        '(start=${start.toStringAsFixed(1)}s text="$preview")',
      );
    }
    _primaryCues.add(start, end, cleaned);
  }


  void _refreshPrimarySubtitle(Duration position) {
    if (_disposed || _primaryCues.isEmpty) return;
    final lines = _primaryCues.textAt(position);
    if (listEquals(lines, _lastEmittedSubtitle)) return;
    _lastEmittedSubtitle = List.unmodifiable(lines);
    state.subtitle = _lastEmittedSubtitle;
    stream.addSubtitle(state.subtitle);
  }

  void _startPositionTimer() {
    _positionTimer ??= Timer.periodic(
      const Duration(milliseconds: 250),
      (_) => _pollPosition(),
    );
    _startStatsTimer();
  }

  void _stopPositionTimer() {
    _positionTimer?.cancel();
    _positionTimer = null;
    _stopStatsTimer();
  }

  void _startStatsTimer() {
    _statsTimer ??= Timer.periodic(
      const Duration(seconds: 1),
      (_) => _sampleStats(),
    );
  }

  void _stopStatsTimer() {
    _statsTimer?.cancel();
    _statsTimer = null;
  }


  void _sampleStats() {
    if (_disposed || !state.playing) return;
    try {
      final bufferLeadMs = _player.buffered();
      final bitrateMbps = _player.mediaInfo.bitRate / 1e6;
      _debugStats.value = PlaybackStats(
        position: state.position,
        bufferLeadMs: bufferLeadMs,
        bitrateMbps: bitrateMbps,
        rate: state.rate,
        decoder: _activeVideoDecoder,
        isDolbyVision: _isDolbyVision,
        hwdec: _hwdecMode,
      );
    } catch (e) {
      AppLogger.debug(_tag, 'sampleStats failed', e);
    }
  }

  void _pollPosition() {
    if (_disposed) return;
    final position = Duration(milliseconds: _player.position);
    if (_seekGuard.shouldSuppress(position)) return;
    final bufferedEnd = Duration(
      milliseconds: _player.position + _player.buffered(),
    );
    state.position = position;
    if (state.duration > Duration.zero && bufferedEnd > state.duration) {
      state.buffer = state.duration;
    } else {
      state.buffer = bufferedEnd;
    }
    stream.addPosition(state.position);
    stream.addBuffer(state.buffer);
    _refreshPrimarySubtitle(position);
  }

  void _refreshMediaInfo() {
    if (_disposed) return;
    try {
      final info = _player.mediaInfo;
      if (info.duration > 0) {
        final duration = Duration(milliseconds: info.duration);
        if (duration != state.duration) {
          state.duration = duration;
          stream.addDuration(duration);
        }
      }
      final tracks = PlayerTracks(
        audio: (info.audio ?? const <mdk.AudioStreamInfo>[])
            .map(_audioTrackFromStream)
            .toList(growable: false),
        subtitle: (info.subtitle ?? const <mdk.SubtitleStreamInfo>[])
            .map(_subtitleTrackFromStream)
            .toList(growable: false),
      );
      if (tracks != state.tracks) {
        state.tracks = tracks;
        stream.addTracks(tracks);
      }
      _syncSelectedTracks();
    } catch (e) {
      AppLogger.debug(_tag, 'refreshMediaInfo failed', e);
    }
  }


  int _relativeAudioIndex(int globalIndex) =>
      state.tracks.audio.indexWhere((t) => t.index == globalIndex);

  int _relativeSubtitleIndex(int globalIndex) =>
      state.tracks.subtitle.indexWhere((t) => t.index == globalIndex);

  void _syncSelectedTracks() {
    final activeAudioRel = _player.activeAudioTracks;
    final audio =
        (activeAudioRel.isNotEmpty &&
            activeAudioRel.first >= 0 &&
            activeAudioRel.first < state.tracks.audio.length)
        ? state.tracks.audio[activeAudioRel.first]
        : const AudioTrack.auto();
    final activeSubRel = _player.activeSubtitleTracks;
    final subtitle =
        (activeSubRel.isNotEmpty &&
            activeSubRel.first >= 0 &&
            activeSubRel.first < state.tracks.subtitle.length)
        ? state.tracks.subtitle[activeSubRel.first]
        : const SubtitleTrack.no();
    final next = PlayerTrack(audio: audio, subtitle: subtitle);
    if (next != state.track) {
      state.track = next;
      stream.addTrack(next);
    }
  }


  Future<void> _refreshTexture() async {
    if (_disposed) return;
    final existing = _player.textureId.value;
    final hasTexture = existing != null && existing >= 0;

    ui.Size? newSize;
    try {
      newSize = await _player.textureSize;
    } catch (e) {
      AppLogger.debug(_tag, 'textureSize unavailable', e);
      newSize = null;
    }
    if (_disposed) return;

    final sizeChanged =
        hasTexture &&
        newSize != null &&
        _textureNaturalSize != null &&
        (newSize.width != _textureNaturalSize!.width ||
            newSize.height != _textureNaturalSize!.height);

    final newSignature = _currentColorSignature();
    final colorChanged =
        hasTexture &&
        _lastColorSignature != null &&
        newSignature != null &&
        newSignature != _lastColorSignature;

    if (!hasTexture || sizeChanged || colorChanged) {
      if (sizeChanged) {
        AppLogger.info(
          _tag,
          'texture size changed $_textureNaturalSize -> $newSize; recreating',
        );
      } else if (colorChanged) {
        AppLogger.info(
          _tag,
          'color signature changed $_lastColorSignature -> $newSignature; '
          'recreating texture to reset DV color pipeline',
        );
      }
      final int tex;
      try {
        tex = await _player.updateTexture();
      } catch (e) {
        if (_disposed) return;
        rethrow;
      }
      if (_disposed) return;
      if (tex < 0) {
        throw StateError('MDK 创建视频纹理失败');
      }
      _textureNaturalSize = newSize;
    }

    if (newSignature != null) {
      _lastColorSignature = newSignature;
    }

    if (newSize != null) {
      videoSize.value = newSize;
    } else if (!hasTexture) {
      videoSize.value = null;
    }
    if (_disposed) return;
    textureVersion.value++;
  }

  Future<void> _stopForMediaChange() async {
    _manualStop = true;
    _setPlaying(false);
    _stopPositionTimer();
    _clearSubtitle();
    if (_player.state != mdk.PlaybackState.stopped) {
      _player.state = mdk.PlaybackState.stopped;
      try {
        final stopped = _player.waitFor(
          mdk.PlaybackState.stopped,
          timeout: 1500,
        );
        if (!stopped) {
          AppLogger.warn(_tag, 'Timed out waiting for MDK to stop');
        }
      } catch (error) {
        AppLogger.warn(_tag, 'Failed to wait for MDK stop', error);
      }
    }
    await Future<void>.delayed(Duration.zero);
  }

  void _setPlaying(bool playing) {
    if (state.playing == playing) return;
    state.playing = playing;
    stream.addPlaying(playing);
  }

  void _emitCompleted() {
    if (_completedEmitted) return;
    _completedEmitted = true;
    state.completed = true;
    stream.addCompleted(true);
    _stopPositionTimer();
  }

  void _emitStateSnapshot() {
    stream.addPosition(state.position);
    stream.addDuration(state.duration);
    stream.addBuffer(state.buffer);
    stream.addPlaying(state.playing);
    stream.addCompleted(state.completed);
    stream.addTracks(state.tracks);
    stream.addTrack(state.track);
    stream.addSubtitle(state.subtitle);
    stream.addVolume(state.volume);
  }

  void _clearSubtitle() {
    _primaryCues.clear();
    _lastEmittedSubtitle = const [];
    state.subtitle = const [];
    stream.addSubtitle(const []);
  }

  void _bumpTextureVersion() {
    if (!_disposed) textureVersion.value++;
  }

  void _ensureNotDisposed() {
    if (_disposed) {
      throw StateError('播放器已释放');
    }
  }

  static AudioTrack _audioTrackFromStream(mdk.AudioStreamInfo stream) {
    final title = stream.metadata['title'];
    final language = stream.metadata['language'];
    return AudioTrack(
      id: stream.index.toString(),
      index: stream.index,
      title: title?.isNotEmpty == true ? title : null,
      language: language?.isNotEmpty == true ? language : null,
    );
  }

  static SubtitleTrack _subtitleTrackFromStream(mdk.SubtitleStreamInfo stream) {
    final title = stream.metadata['title'];
    final language = stream.metadata['language'];
    return SubtitleTrack(
      id: stream.index.toString(),
      index: stream.index,
      title: title?.isNotEmpty == true ? title : null,
      language: language?.isNotEmpty == true ? language : null,
    );
  }

  static String _subtitleAlignY(int position) {
    if (position <= 35) return '1';
    if (position >= 85) return '-1';
    return '0';
  }

  static String _subtitleMarginY(int position) {
    final p = position.clamp(0, 150);
    if (p >= 85) {
      return ((150 - p) * 4).clamp(10, 260).round().toString();
    }
    if (p <= 35) {
      return (p * 4).clamp(10, 180).round().toString();
    }
    return '10';
  }


  static String _stripAssMetadata(String text) {
    if (text.isEmpty) return text;
    final match = RegExp(
      r'^\d+,\d+,[^,]*,[^,]*,\d*,\d*,\d*,[^,]*,(.*)',
      dotAll: true,
    ).firstMatch(text);
    if (match != null) {
      text = match.group(1) ?? text;
    }
    text = text.replaceAll(RegExp(r'\{\\[^}]*\}'), '');
    return text.trim();
  }
}
