import 'dart:async';

import 'package:flutter/foundation.dart';

import '../../../core/contracts/cancellation.dart';
import '../../../core/contracts/playback.dart';
import '../../../core/infra/emby_api/playback_resolver.dart';
import '../../../core/infra/player_engine/player_engine.dart';
import '../../../core/media/external_subtitle_loader.dart';
import '../../../providers/audio_settings_provider.dart';
import '../../../providers/player_settings_provider.dart';
import '../../../providers/subtitle_settings_provider.dart';
import '../../../shared/utils/app_logger.dart';
import '../../../shared/utils/emby_ticks.dart';
import '../services/danmaku/danmaku_feeder.dart';
import '../services/emby_playback_reporter.dart';
import '../services/network_speed_monitor.dart';
import '../services/trakt_scrobble_reporter.dart';
import 'initial_subtitle_resolver.dart';
import 'playback_phase.dart';
import 'playback_request.dart';
import 'stall_detector.dart';

const _tag = 'PlaybackSession';
const _openTimeout = Duration(seconds: 60);
const _engineErrorGrace = Duration(seconds: 5);
const _stallPollInterval = Duration(seconds: 2);


const embeddedSubtitleFetchTimeout = Duration(seconds: 8);


class PlaybackSession {
  PlaybackSession({
    required this.engine,
    required this._reporter,
    required this.speedMonitor,
    required this.feeder,
    required this.context,
    required this.request,
    required this._prepareResult,
    required this._streamHeaders,
    required this._playerSettingsNotifier,
    this.pendingEngineOpenFuture,
    TraktScrobbleReporter Function(double Function() readProgress)?
    traktReporterFactory,
    this.rememberedAudioTrack,
    this.rememberedPrimarySubtitle,
    ExternalSubtitleLoader? externalSubtitleLoader,
  }) : _externalSubtitleLoader =
           externalSubtitleLoader ?? ExternalSubtitleLoader() {
    _subtitleDelaySeconds = request.subtitleSettings.delay;
    _traktReporter = traktReporterFactory?.call(_progressPercent);
    _subscriptions = <StreamSubscription<dynamic>>[
      engine.stream.position.listen(_onPosition),
      engine.stream.buffer.listen(_onBufferChanged),
      engine.stream.playing.listen(_onPlayingChanged),
      engine.stream.completed.listen(_onCompleted),
      engine.stream.volume.listen(_onVolume),
      engine.stream.tracks.listen(_onTracksChanged),
      engine.stream.error.listen(_onEngineError),
    ];
  }


  TraktScrobbleReporter? _traktReporter;

  final PlayerEngine engine;
  final EmbyPlaybackReporter _reporter;
  final NetworkSpeedMonitor speedMonitor;
  final DanmakuFeeder feeder;
  final PlaybackContext context;
  final PlaybackRequest request;

  final PlaybackRequestResult _prepareResult;
  final Map<String, String>? _streamHeaders;


  final Future<void>? pendingEngineOpenFuture;
  final PlayerSettingsNotifier _playerSettingsNotifier;
  final RememberedAudioTrack? rememberedAudioTrack;
  final RememberedSubtitleTrack? rememberedPrimarySubtitle;

  final ValueNotifier<PlaybackPhase> _phase = ValueNotifier<PlaybackPhase>(
    PlaybackPhase.preparing,
  );
  final CancellationToken _cancel = CancellationToken();
  late final List<StreamSubscription<dynamic>> _subscriptions;

  bool _disposed = false;
  bool _started = false;
  bool _isPaused = false;
  bool _lastFailureTerminal = false;
  bool _defaultsApplied = false;
  bool _engineOpenCompleted = false;
  Object? _lastError;
  Timer? _volumeSaveTimer;
  Timer? _engineErrorTimer;
  Timer? _stallPollTimer;
  Duration? _engineErrorAnchorPosition;
  String? _pendingEngineError;

  ValueListenable<PlaybackPhase> get phase => _phase;
  CancellationToken get cancelToken => _cancel;
  Object? get lastError => _lastError;


  bool get lastFailureTerminal => _lastFailureTerminal;
  bool get isPaused => _isPaused;
  bool get isDanmakuActive => _danmakuActive;


  bool Function()? onStreamFailure;

  final ExternalSubtitleLoader _externalSubtitleLoader;


  final ValueNotifier<List<String>> externalSubtitleLines =
      ValueNotifier<List<String>>(const []);


  final ValueNotifier<EmbySubtitleTrack?> activeExternalSubtitle =
      ValueNotifier<EmbySubtitleTrack?>(null);


  final ValueNotifier<int?> activeSubtitleStreamIndex = ValueNotifier<int?>(
    null,
  );


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


  void Function(String message)? onExternalSubtitleError;

  SubtitleCueSource? _externalCueSource;
  double _subtitleDelaySeconds = 0;


  int _externalLoadGeneration = 0;


  final Set<String> _prefetchedExternalSubtitleUrls = <String>{};


  bool _embeddedExtractionUnavailable = false;


  final StallDetector _stallDetector = StallDetector();
  final Stopwatch _clock = Stopwatch()..start();


  bool _failureSignalled = false;

  Timer? _danmakuInterp;
  Duration _danmakuAnchorPos = Duration.zero;
  Duration _danmakuAnchorClock = Duration.zero;
  Duration? _lastDanmakuPos;
  bool _danmakuAwaitingMovementBaseline = true;
  bool _danmakuMoving = false;
  bool _danmakuActive = false;
  static const Duration _danmakuInterpInterval = Duration(milliseconds: 32);


  static const Duration _danmakuStallGrace = Duration(milliseconds: 600);


  Duration _lastPositionAdvanceClock = Duration.zero;


  Future<void> start() async {
    if (_disposed) {
      throw StateError('PlaybackSession already disposed');
    }
    if (_started) {
      AppLogger.warn(_tag, 'start() called twice; ignoring');
      return;
    }
    _started = true;

    try {
      _cancel.throwIfCancelled();

      speedMonitor.stop();

      final startSeconds = _prepareResult.startPositionSeconds;
      if (startSeconds > 0) {
        feeder.seekTo(startSeconds, clear: true);
      }


      if (pendingEngineOpenFuture != null) {
        speedMonitor.start();
        AppLogger.debug(
          _tag,
          'awaiting pending engine.open (started in build)',
        );
        try {
          await _runWithTimeout(pendingEngineOpenFuture!, _openTimeout);
        } catch (_) {
          speedMonitor.stop();
          try {
            await engine.stop();
          } catch (e) {
            AppLogger.warn(_tag, 'engine.stop after open failure failed', e);
          }
          rethrow;
        }
        _cancel.throwIfCancelled();
      } else {
        speedMonitor.start();
        AppLogger.debug(_tag, 'engine.open already completed before start');
      }
      _engineOpenCompleted = true;
      AppLogger.debug(_tag, 'engine.open done');

      _applySubtitleStyleFromRequest();

      _tryApplyDefaultTracks(engine.state.tracks);

      _cancel.throwIfCancelled();
      await engine.play();
      _isPaused = false;
      _setPhase(PlaybackPhase.playing);
      _startDanmakuInterpolator();
      _startStallPolling();

      await _reporter.reportStarted(_buildReportPayload(isPaused: false));
      _reporter.startProgressTimer(_buildReportPayload);

      unawaited(_traktReporter?.onPlay() ?? Future<void>.value());
    } catch (e, s) {
      _lastError = e;
      AppLogger.error(_tag, 'session start failed', e, s);
      _setPhase(PlaybackPhase.failed);
      rethrow;
    }
  }


  Future<void> dispose() async {
    if (_disposed) return;
    _disposed = true;
    _cancel.cancel('session disposed');
    final sw = Stopwatch()..start();
    AppLogger.debug(_tag, 'dispose start');

    _volumeSaveTimer?.cancel();
    _volumeSaveTimer = null;
    _engineErrorTimer?.cancel();
    _engineErrorTimer = null;
    _stallPollTimer?.cancel();
    _stallPollTimer = null;
    _danmakuInterp?.cancel();
    _danmakuInterp = null;
    speedMonitor.stop();

    final stopReport = _reporter.reportStopped(
      _buildReportPayload(isPaused: _isPaused),
    );

    unawaited(_traktReporter?.onStop() ?? Future<void>.value());

    for (final sub in _subscriptions) {
      try {
        await sub.cancel();
      } catch (e) {
        AppLogger.warn(_tag, 'subscription cancel failed', e);
      }
    }
    AppLogger.debug(_tag, 'subs cancelled @${sw.elapsedMilliseconds}ms');

    final stopSw = Stopwatch()..start();
    try {
      await engine.stop();
    } catch (e) {
      AppLogger.warn(_tag, 'engine.stop failed', e);
    }
    AppLogger.debug(
      _tag,
      'engine.stop done in ${stopSw.elapsedMilliseconds}ms @${sw.elapsedMilliseconds}ms',
    );

    feeder.detach();
    _reporter.dispose();
    speedMonitor.dispose();


    AppLogger.debug(_tag, 'children disposed @${sw.elapsedMilliseconds}ms');

    final awaitSw = Stopwatch()..start();
    try {
      await stopReport.timeout(const Duration(seconds: 5));
    } catch (e) {
      AppLogger.debug(
        _tag,
        'stop report timed out / failed after ${awaitSw.elapsedMilliseconds}ms '
        '(dispose total ${sw.elapsedMilliseconds}ms)',
        e,
      );
    }

    _setPhase(PlaybackPhase.disposed);
    _phase.dispose();
    _externalSubtitleLoader.clear();
    externalSubtitleLines.dispose();
    activeExternalSubtitle.dispose();
    activeSubtitleStreamIndex.dispose();
    subtitleLoading.dispose();
    AppLogger.debug(_tag, 'dispose done in ${sw.elapsedMilliseconds}ms');
  }

  Future<void> play() async {
    if (_disposed) return;
    await engine.play();
  }

  Future<void> pause() async {
    if (_disposed) return;
    await engine.pause();
    _danmakuAwaitingMovementBaseline = true;
    _danmakuMoving = false;
    _syncDanmakuActive();
  }

  Future<void> seek(Duration position) async {
    if (_disposed) return;
    _danmakuAwaitingMovementBaseline = true;
    _danmakuMoving = false;
    _danmakuActive = false;
    feeder.seekTo(position.inMilliseconds / 1000.0, clear: true);
    await engine.seek(position);
  }

  Future<void> setRate(double rate) async {
    if (_disposed) return;
    await engine.setRate(rate);
  }

  Future<void> setVolume(double volume) async {
    if (_disposed) return;
    await engine.setVolume(volume);
  }

  Future<void> setAudioTrack(AudioTrack track) async {
    if (_disposed) return;
    await engine.setAudioTrack(track);
  }


  void prefetchExternalSubtitles() {
    if (_disposed) return;
    for (final track in _prepareResult.subtitles) {
      final url = track.deliveryUrl;
      if (!track.isExternal ||
          url == null ||
          url.isEmpty ||
          !PlaybackResolver.isTextSubtitleCodec(track.codec)) {
        continue;
      }
      if (!_prefetchedExternalSubtitleUrls.add(url)) continue;
      unawaited(_prefetchExternalSubtitle(url, track.index));
    }
  }

  Future<void> _prefetchExternalSubtitle(String url, int streamIndex) async {
    try {
      await _externalSubtitleLoader.load(url, headers: _streamHeaders);
      if (!_disposed) {
        AppLogger.debug(
          _tag,
          'external subtitle prefetched index=$streamIndex',
        );
      }
    } catch (error) {
      AppLogger.debug(
        _tag,
        'external subtitle prefetch failed index=$streamIndex',
        error,
      );
    }
  }


  Future<void> selectSubtitleByStreamIndex(int? streamIndex) async {
    if (_disposed) return;
    if (streamIndex == null || streamIndex < 0) {
      AppLogger.info(
        _tag,
        'selectSubtitle: user requested disable (streamIdx=$streamIndex)',
      );
      await _disableSubtitle();
      return;
    }
    final embyTrack = _prepareResult.subtitles
        .where((track) => track.index == streamIndex)
        .firstOrNull;
    if (embyTrack == null) {
      final availableIndices = _prepareResult.subtitles
          .map((track) => track.index)
          .toList(growable: false);
      AppLogger.warn(
        _tag,
        'selectSubtitle: streamIdx=$streamIndex not found in emby subtitles '
        '(available=$availableIndices)',
      );
      return;
    }
    AppLogger.info(
      _tag,
      'selectSubtitle: user requested streamIdx=$streamIndex '
      'external=${embyTrack.isExternal} codec=${embyTrack.codec} '
      'lang=${embyTrack.language} title="${embyTrack.title}"',
    );
    await _applyEmbySubtitleTrack(embyTrack);
  }


  Future<void> _disableSubtitle() async {
    _clearDartSubtitleState();
    activeSubtitleStreamIndex.value = null;
    engine.setNativeSubtitleRendering(false);
    await engine.setSubtitleTrack(const SubtitleTrack.no());
    AppLogger.debug(_tag, 'subtitle disabled');
  }


  Future<bool> _applyEmbySubtitleTrack(EmbySubtitleTrack track) async {
    final url = track.deliveryUrl;
    final isText = PlaybackResolver.isTextSubtitleCodec(track.codec);
    final hasUrl = url != null && url.isNotEmpty;
    final skipExtraction = !track.isExternal && _embeddedExtractionUnavailable;
    if (hasUrl && isText && !skipExtraction) {
      AppLogger.info(
        _tag,
        'applySubtitle: route=dart streamIdx=${track.index} '
        'external=${track.isExternal} codec=${track.codec} '
        '(text codec with delivery url)',
      );
      return _loadDartSubtitle(track);
    }
    final routeReason = !hasUrl
        ? 'no delivery url'
        : !isText
        ? 'bitmap codec'
        : 'embedded extraction unavailable (this session)';
    AppLogger.info(
      _tag,
      'applySubtitle: route=engine streamIdx=${track.index} '
      'external=${track.isExternal} codec=${track.codec} '
      'reason="$routeReason"',
    );
    return _applySubtitleViaEngine(track);
  }


  Future<bool> _loadDartSubtitle(EmbySubtitleTrack track) async {
    final url = track.deliveryUrl;
    if (url == null || url.isEmpty) return false;
    final generation = ++_externalLoadGeneration;
    subtitleLoading.value = true;
    activeExternalSubtitle.value = track;
    activeSubtitleStreamIndex.value = track.index;
    _externalCueSource = null;
    _setExternalSubtitleLines(const []);
    engine.setNativeSubtitleRendering(false);
    await engine.setSubtitleTrack(const SubtitleTrack.no());
    if (_disposed || generation != _externalLoadGeneration) return true;

    try {


      final cues = await _externalSubtitleLoader.load(
        url,
        headers: _streamHeaders,
        receiveTimeout: track.isExternal ? null : embeddedSubtitleFetchTimeout,
      );
      if (_disposed || generation != _externalLoadGeneration) return true;
      _externalCueSource = SubtitleCueSource(cues);
      _updateExternalSubtitleLines(engine.state.position);
      subtitleLoading.value = false;
      AppLogger.debug(
        _tag,
        'dart subtitle active index=${track.index} '
        'external=${track.isExternal} cues=${cues.length}',
      );
      return true;
    } catch (e) {
      if (_disposed || generation != _externalLoadGeneration) return true;
      AppLogger.warn(_tag, 'dart subtitle load failed idx=${track.index}', e);
      _clearDartSubtitleState();
      if (!track.isExternal) {


        _embeddedExtractionUnavailable = true;
        if (_applySubtitleViaEngine(track)) {
          AppLogger.info(
            _tag,
            'embedded extraction unavailable; fell back to engine switch '
            'idx=${track.index} (subsequent embedded subs skip extraction)',
          );
          return true;
        }
      }
      activeSubtitleStreamIndex.value = null;
      engine.setNativeSubtitleRendering(false);
      onExternalSubtitleError?.call('字幕加载失败，已回退为无字幕');
      return false;
    }
  }


  bool _applySubtitleViaEngine(EmbySubtitleTrack track) {
    final engineSubs = engine.state.tracks.embeddedSubtitles;
    final embeddedIndices = _embeddedEmbySubtitles
        .map((embyTrack) => embyTrack.index)
        .toList(growable: false);
    final pos = embeddedIndices.indexOf(track.index);
    if (pos < 0 || pos >= engineSubs.length) {
      AppLogger.warn(
        _tag,
        'applySubtitleViaEngine: streamIdx=${track.index} not found in engine '
        'tracks (embeddedIndices=$embeddedIndices, '
        'engineSubCount=${engineSubs.length})',
      );
      return false;
    }
    _clearDartSubtitleState();
    activeSubtitleStreamIndex.value = track.index;
    _configureSubtitleRenderingModeForCodec(track.codec);
    engine.setSubtitleTrack(engineSubs[pos]);
    AppLogger.debug(
      _tag,
      'engine subtitle activated streamIdx=${track.index} pos=$pos '
      'engineTrack=${engineSubs[pos].id} codec=${track.codec}',
    );
    return true;
  }


  void _clearDartSubtitleState() {
    _externalLoadGeneration++;
    subtitleLoading.value = false;
    activeExternalSubtitle.value = null;
    _externalCueSource = null;
    _setExternalSubtitleLines(const []);
  }


  void _updateExternalSubtitleLines(Duration position) {
    final source = _externalCueSource;
    if (source == null) return;
    final effectivePosition =
        position -
        Duration(milliseconds: (_subtitleDelaySeconds * 1000).round());
    final lines = effectivePosition.isNegative
        ? const <String>[]
        : source.textAt(effectivePosition);
    _setExternalSubtitleLines(lines);
  }

  void _setExternalSubtitleLines(List<String> lines) {
    if (listEquals(externalSubtitleLines.value, lines)) return;
    externalSubtitleLines.value = lines;
  }

  void applySubtitleStyle(SubtitleStyleSettings settings) {
    if (_disposed) return;
    _subtitleDelaySeconds = settings.delay;
    _updateExternalSubtitleLines(engine.state.position);
    engine.setSubtitleStyle(
      fontSize: settings.fontSize,
      bgOpacity: settings.bgOpacity,
      position: settings.position,
    );
  }

  void _onPosition(Duration position) {
    if (_disposed) return;
    _updateExternalSubtitleLines(position);
    if (_engineErrorAnchorPosition != null &&
        position != _engineErrorAnchorPosition) {
      _engineErrorTimer?.cancel();
      _engineErrorTimer = null;
      _engineErrorAnchorPosition = null;
      _pendingEngineError = null;
    }
    final last = _lastDanmakuPos;
    _lastDanmakuPos = position;
    final positionAdvanced = last != null && position != last;
    if (positionAdvanced) {


      _lastPositionAdvanceClock = _clock.elapsed;
      _danmakuAnchorPos = position;
      _danmakuAnchorClock = _clock.elapsed;
    }
    _checkStall(position);
    if (_danmakuAwaitingMovementBaseline) {
      _danmakuAwaitingMovementBaseline = false;
      _danmakuMoving = false;
      _lastPositionAdvanceClock = _clock.elapsed;
      _syncDanmakuActive();
      return;
    }
    final withinStallGrace =
        _clock.elapsed - _lastPositionAdvanceClock <= _danmakuStallGrace;
    _danmakuMoving = positionAdvanced || (_danmakuMoving && withinStallGrace);
    _syncDanmakuActive();
    if (_danmakuActive) {
      feeder.onPosition(position.inMilliseconds / 1000.0);
    }
  }

  void _onBufferChanged(Duration buffer) {
    if (_disposed) return;
    if (!engine.state.playing || buffer > engine.state.position) return;


    final positionStalled =
        _clock.elapsed - _lastPositionAdvanceClock > _danmakuStallGrace;
    if (!positionStalled) return;
    _danmakuAwaitingMovementBaseline = true;
    _danmakuMoving = false;
    _syncDanmakuActive();
  }


  void _syncDanmakuActive() {
    final active = engine.state.playing && _danmakuMoving;
    final posSec = engine.state.position.inMilliseconds / 1000.0;
    if (active && !_danmakuActive) {
      _danmakuAnchorPos = engine.state.position;
      _danmakuAnchorClock = _clock.elapsed;
      feeder.playFrom(posSec);
    } else if (!active) {
      feeder.freezeAt(posSec);
    }
    _danmakuActive = active;
  }


  void _startDanmakuInterpolator() {
    _danmakuAnchorPos = engine.state.position;
    _danmakuAnchorClock = _clock.elapsed;
    _danmakuInterp ??= Timer.periodic(_danmakuInterpInterval, (_) {
      if (_disposed) return;
      if (!_danmakuActive) return;
      final elapsed = _clock.elapsed - _danmakuAnchorClock;
      if (elapsed.isNegative) return;
      final rate = engine.state.rate;
      final est =
          _danmakuAnchorPos +
          Duration(microseconds: (elapsed.inMicroseconds * rate).round());
      feeder.onPosition(est.inMilliseconds / 1000.0);
    });
  }

  void _startStallPolling() {
    _stallPollTimer ??= Timer.periodic(_stallPollInterval, (_) {
      if (_disposed) return;
      _checkStall(engine.state.position);
    });
  }


  void _checkStall(Duration position) {
    if (_failureSignalled) return;
    final activelyPlaying =
        _phase.value == PlaybackPhase.playing && engine.state.playing;
    final fired = _stallDetector.update(
      position: position,
      bufferedPosition: engine.state.buffer,
      bytesDownloaded: speedMonitor.latestTotalBytes,
      isActivelyPlaying: activelyPlaying,
      now: _clock.elapsed,
    );
    if (fired) {
      AppLogger.warn(
        _tag,
        'stream failure: stall detector (${_stallDetector.threshold.inSeconds}s) → relink',
      );
      _signalFailure();
    }
  }


  void _signalFailure() {
    if (_disposed || _failureSignalled) return;
    final handled = onStreamFailure?.call() ?? false;
    if (handled) _failureSignalled = true;
  }

  void _onEngineError(String error) {
    if (_disposed || _phase.value == PlaybackPhase.failed) return;
    AppLogger.warn(_tag, 'engine error: $error');
    if (engine.isNetworkStreamError(error)) {
      _lastFailureTerminal = isTerminalStreamOpenError(error);
      AppLogger.warn(
        _tag,
        'stream failure: engine network error → '
        '${_lastFailureTerminal ? 'terminal (skip relink)' : 'relink'}',
      );
      _signalFailure();
      return;
    }
    _pendingEngineError = error;
    _engineErrorAnchorPosition ??= engine.state.position;
    _engineErrorTimer?.cancel();
    _engineErrorTimer = Timer(_engineErrorGrace, () {
      if (_disposed || _phase.value == PlaybackPhase.failed) return;
      final anchor = _engineErrorAnchorPosition;
      if (anchor != null && engine.state.position == anchor) {
        final message = _pendingEngineError ?? error;
        _lastError = Exception('播放失败: $message');
        _setPhase(PlaybackPhase.failed);
      }
      _engineErrorAnchorPosition = null;
      _pendingEngineError = null;
      _engineErrorTimer = null;
    });
  }

  void _onPlayingChanged(bool playing) {
    if (_disposed) return;
    if (playing) {
      _danmakuAwaitingMovementBaseline = true;
      _danmakuMoving = false;
    } else {
      _danmakuAwaitingMovementBaseline = true;
      _danmakuMoving = false;
    }
    _syncDanmakuActive();
    final wasPaused = _isPaused;
    _isPaused = !playing;
    if (_reporter.startedReported &&
        !_reporter.stoppedReported &&
        _isPaused != wasPaused) {
      unawaited(
        _reporter.reportProgress(_buildReportPayload(isPaused: _isPaused)),
      );
      if (_isPaused) {
        unawaited(_traktReporter?.onPause() ?? Future<void>.value());
      } else {
        unawaited(_traktReporter?.onPlay() ?? Future<void>.value());
      }
    }
  }

  void _onCompleted(bool completed) {
    if (_disposed) return;
    if (!completed) return;
    unawaited(
      _reporter.reportStopped(_buildReportPayload(isPaused: _isPaused)),
    );
    unawaited(_traktReporter?.onCompleted() ?? Future<void>.value());
    _setPhase(PlaybackPhase.ended);
  }

  void _onVolume(double volume) {
    if (_disposed) return;
    _volumeSaveTimer?.cancel();
    _volumeSaveTimer = Timer(const Duration(milliseconds: 500), () {
      if (!_disposed) _playerSettingsNotifier.setVolume(volume);
    });
  }

  void _onTracksChanged(PlayerTracks tracks) {
    if (_disposed || _defaultsApplied || !_engineOpenCompleted) return;
    _tryApplyDefaultTracks(tracks);
  }

  void _tryApplyDefaultTracks(PlayerTracks tracks) {
    if (_defaultsApplied || _disposed) return;
    final audio = tracks.audio
        .where((t) => t.id != 'auto' && t.id != 'no')
        .toList(growable: false);
    final subs = tracks.subtitle
        .where((t) => t.id != 'auto' && t.id != 'no')
        .toList(growable: false);
    if (audio.isEmpty && subs.isEmpty) return;
    _defaultsApplied = true;
    _applyDefaultTracks(audio);
  }

  void _applyDefaultTracks(List<AudioTrack> audioTracks) {
    if (audioTracks.isNotEmpty) {
      final audioIndices = _prepareResult.audioTracks
          .map((e) => e.index)
          .toList(growable: false);
      final selectedIndex = _resolveAudioTrackIndex(audioIndices);
      if (selectedIndex != null) {
        var target = audioTracks
            .where((t) => t.index == selectedIndex)
            .firstOrNull;
        if (target == null) {
          final pos = audioIndices.indexOf(selectedIndex);
          if (pos >= 0 && pos < audioTracks.length) {
            target = audioTracks[pos];
          }
        }
        if (target != null) {
          engine.setAudioTrack(target);
        }
      }
    }

    final subtitleSelection = resolveInitialSubtitleSelection(
      subtitles: _prepareResult.subtitles,
      preferredSubtitleStreamIndex: request.preferredSubtitleStreamIndex,
      defaultSubtitleStreamIndex: _prepareResult.defaultSubtitleIndex,
      rememberedSubtitle: rememberedPrimarySubtitle,
    );
    final remembered = rememberedPrimarySubtitle;
    final rememberedSummary = remembered == null
        ? 'null'
        : remembered.isNone
        ? 'none-latch'
        : 'idx=${remembered.streamIndex} lang=${remembered.language} '
              'title="${remembered.title}"';
    AppLogger.info(
      _tag,
      'initialSubtitle: type=${subtitleSelection.type.name} '
      'preferred=${request.preferredSubtitleStreamIndex} '
      'default=${_prepareResult.defaultSubtitleIndex} '
      'remembered=$rememberedSummary '
      'availableCount=${_prepareResult.subtitles.length}',
    );
    switch (subtitleSelection.type) {
      case InitialSubtitleSelectionType.unchanged:
        break;
      case InitialSubtitleSelectionType.disabled:
        unawaited(_disableSubtitle());
        break;
      case InitialSubtitleSelectionType.track:
        final selectedTrack = subtitleSelection.track;
        if (selectedTrack != null) {
          AppLogger.info(
            _tag,
            'initialSubtitle: applying streamIdx=${selectedTrack.index} '
            'external=${selectedTrack.isExternal} codec=${selectedTrack.codec}',
          );
          unawaited(_applyEmbySubtitleTrack(selectedTrack));
        }
        break;
    }
  }


  List<EmbySubtitleTrack> get _embeddedEmbySubtitles => _prepareResult.subtitles
      .where((track) => !track.isExternal)
      .toList(growable: false);

  void _configureSubtitleRenderingModeForCodec(String? codec) {
    final isBitmap =
        codec != null && !PlaybackResolver.isTextSubtitleCodec(codec);
    engine.setNativeSubtitleRendering(isBitmap);
  }

  int? _resolveAudioTrackIndex(List<int> audioIndices) {
    final embyTracks = _prepareResult.audioTracks;

    final routeIndex = request.preferredAudioStreamIndex;
    if (routeIndex != null && audioIndices.contains(routeIndex)) {
      return routeIndex;
    }

    final remembered = rememberedAudioTrack;
    if (remembered != null) {
      final rememberedIsCommentary = _isCommentary(remembered.displayTitle);

      final exact = embyTracks
          .where(
            (t) =>
                t.index == remembered.streamIndex &&
                _langMatch(t.language, remembered.language) &&
                t.codec == remembered.codec,
          )
          .firstOrNull;
      if (exact != null) return exact.index;

      final candidates = rememberedIsCommentary
          ? embyTracks
          : embyTracks.where((t) => !_isCommentary(t.displayTitle)).toList();

      if (remembered.language != null) {
        final byLangCodecCh = candidates
            .where(
              (t) =>
                  _langMatch(t.language, remembered.language) &&
                  t.codec == remembered.codec &&
                  t.channels == remembered.channels,
            )
            .firstOrNull;
        if (byLangCodecCh != null) return byLangCodecCh.index;

        if (remembered.channels != null) {
          final byLangCh = candidates
              .where(
                (t) =>
                    _langMatch(t.language, remembered.language) &&
                    t.channels == remembered.channels,
              )
              .firstOrNull;
          if (byLangCh != null) return byLangCh.index;
        }

        final byLang = candidates
            .where((t) => _langMatch(t.language, remembered.language))
            .firstOrNull;
        if (byLang != null) return byLang.index;
      }
    }

    final prefLang = request.audioSettings.preferredAudioLanguage;
    if (prefLang.isNotEmpty) {
      var langMatches = embyTracks
          .where(
            (t) =>
                _langMatch(t.language, prefLang) &&
                !_isCommentary(t.displayTitle),
          )
          .toList();
      if (langMatches.isEmpty) {
        langMatches = embyTracks
            .where((t) => _langMatch(t.language, prefLang))
            .toList();
      }
      if (langMatches.isNotEmpty) {
        langMatches.sort((a, b) {
          final defaultCmp = (b.isDefault ? 1 : 0) - (a.isDefault ? 1 : 0);
          if (defaultCmp != 0) return defaultCmp;
          return (b.channels ?? 0).compareTo(a.channels ?? 0);
        });
        return langMatches.first.index;
      }
    }

    return _prepareResult.mediaSource?.DefaultAudioStreamIndex;
  }

  static bool _isCommentary(String? title) {
    if (title == null) return false;
    final lower = title.toLowerCase();
    return lower.contains('commentary') || lower.contains('comment');
  }

  static bool _langMatch(String? trackLang, String? targetLang) {
    if (trackLang == null || targetLang == null) return false;
    final a = trackLang.toLowerCase();
    final b = targetLang.toLowerCase();
    if (a == b) return true;

    const subGroups = <String, Set<String>>{
      'cmn': {'mandarin', '国语', '普通话'},
      'yue': {'cantonese', '粤语'},
    };
    for (final entry in subGroups.entries) {
      final all = {entry.key, ...entry.value};
      if (all.contains(a) && all.contains(b)) return true;
    }

    const macroGroups = <String, Set<String>>{
      'chi': {
        'zho',
        'chinese',
        'zh',
        '中文',
        'cmn',
        'mandarin',
        '国语',
        '普通话',
        'yue',
        'cantonese',
        '粤语',
      },
      'eng': {'english', 'en'},
      'jpn': {'japanese', 'ja', 'jp'},
      'kor': {'korean', 'ko'},
      'fra': {'french', 'fr', 'fre'},
      'deu': {'german', 'de', 'ger'},
      'spa': {'spanish', 'es'},
      'ita': {'italian', 'it'},
      'por': {'portuguese', 'pt'},
      'rus': {'russian', 'ru'},
      'tha': {'thai', 'th'},
      'vie': {'vietnamese', 'vi'},
    };
    for (final entry in macroGroups.entries) {
      final all = {entry.key, ...entry.value};
      if (all.contains(a) && all.contains(b)) return true;
    }
    return false;
  }

  void _applySubtitleStyleFromRequest() {
    final s = request.subtitleSettings;
    engine.setSubtitleStyle(
      fontSize: s.fontSize,
      bgOpacity: s.bgOpacity,
      position: s.position,
    );
  }


  double _progressPercent() {
    final pos = engine.state.position;
    final ticks = context.runTimeTicks;
    final dur = (ticks != null && ticks > 0)
        ? durationFromTicks(ticks)
        : engine.state.duration;
    if (dur <= Duration.zero || pos <= Duration.zero) return 0;
    final pct = pos.inMilliseconds / dur.inMilliseconds * 100;
    return pct.clamp(0.0, 100.0);
  }

  PlaybackReportPayload _buildReportPayload({
    required bool isPaused,
    String? eventName,
  }) {
    final pos = engine.state.position;
    final dur = engine.state.duration;
    return PlaybackReportPayload(
      itemId: context.itemId,
      mediaSourceId: context.mediaSourceId,
      playSessionId: context.playSessionId,
      positionTicks: ticksFromDuration(pos),
      runTimeTicks:
          context.runTimeTicks ??
          (dur > Duration.zero ? ticksFromDuration(dur) : null),
      playMethod: context.playMethod,
      isPaused: isPaused,
      isMuted: engine.state.volume <= 0,
      playbackRate: engine.state.rate,
      eventName: eventName,
    );
  }

  void _setPhase(PlaybackPhase next) {
    if (_phase.value == next) return;
    if (_phase.value == PlaybackPhase.disposed) return;
    _phase.value = next;
  }

  Future<void> _runWithTimeout(Future<void> future, Duration timeout) {
    return future.timeout(
      timeout,
      onTimeout: () {
        throw TimeoutException('engine.open exceeded $timeout', timeout);
      },
    );
  }
}
