import 'dart:async';
import 'dart:io' show Platform;

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:forui/forui.dart';
import 'package:go_router/go_router.dart';

import '../../core/contracts/library.dart';
import '../../core/contracts/tmdb.dart';
import '../../providers/di_providers.dart';
import '../../providers/last_played_version_store.dart';
import '../../providers/session_provider.dart';
import '../../providers/tmdb_settings_provider.dart';
import '../../providers/version_priority_provider.dart';
import '../../shared/constants/breakpoints.dart';
import '../../shared/mappers/tmdb_image_url.dart';
import '../../shared/theme/app_theme.dart';
import '../../shared/utils/cross_server_search_key.dart';
import '../../shared/utils/format_utils.dart';
import '../../shared/utils/media_nav.dart';
import '../player/player_launcher.dart';
import '../../shared/utils/tmdb_cta_model.dart';
import '../../shared/utils/tmdb_primary_hit.dart';
import '../../shared/widgets/app_error_state.dart';
import '../../shared/widgets/app_skeleton.dart';
import '../../shared/widgets/empty_state.dart';
import 'widgets/additional_parts_section.dart';
import 'widgets/episode_list_section.dart';
import 'adapter/tmdb_detail_view_builder.dart';
import 'android/android_detail_geometry.dart';
import 'detail_entry.dart';
import 'model/detail_section_model.dart';
import 'model/episode_auto_select_args.dart';
import 'model/stream_selection.dart';
import 'tmdb_detail_body_android.dart';
import 'widgets/detail_immersive_scaffold.dart';
import 'widgets/detail_nav_bar.dart';
import 'widgets/detail_scroll_progress.dart';
import 'widgets/detail_view_renderer.dart';
import 'widgets/hero_action_buttons.dart';
import 'widgets/source_selection_section.dart';
import 'widgets/stream_selector_actions.dart';

const Color _kBg = Color(0xFF08090c);


const double _kHPad = 32.0;


class TmdbDetailView extends ConsumerStatefulWidget {
  final TmdbDetailEntry entry;

  const TmdbDetailView({super.key, required this.entry});

  @override
  ConsumerState<TmdbDetailView> createState() => _TmdbDetailViewState();
}

class _TmdbDetailViewState extends ConsumerState<TmdbDetailView> {
  static const _sectionDivider = FDivider(
    style: .delta(
      color: Color(0x1AFFFFFF),
      padding: .value(EdgeInsets.only(top: 24)),
    ),
  );

  final _scroll = ScrollController();
  final _scrollProgress = ValueNotifier<double>(0);

  bool? _favoriteOverride;
  bool? _playedOverride;


  String? _overrideHitKey;

  int _selectedSourceIdx = 0;
  CrossServerSourceCard? _selectedCrossServerCard;
  EmbyRawItem? _selectedTvEpisode;
  int? _selectedTvEpisodeSeasonNumber;
  String? _selectedTvEpisodeHitKey;
  int? _selectedSubtitleIdx;
  int _selectedAudioIdx = 0;


  String? _pendingMediaSourceId;


  String? _versionMemoryQueriedKey;


  String? _playbackOriginServerId;


  int? _positionTicksOverride;
  String? _positionOverrideItemKey;

  int? _positionOverrideFor(String? serverId, String? itemId) {
    if (_positionTicksOverride == null) return null;
    if (_positionOverrideItemKey != '$serverId|$itemId') return null;
    return _positionTicksOverride;
  }

  @override
  void initState() {
    super.initState();
    _scroll.addListener(_onScroll);
  }

  void _onScroll() {
    _scrollProgress.value = computeDetailScrollProgress(
      context,
      _scroll.offset,
      collapseExtent: Platform.isAndroid
          ? computeAndroidDetailCollapseExtent(context)
          : null,
    );
  }

  @override
  void didUpdateWidget(covariant TmdbDetailView oldWidget) {
    super.didUpdateWidget(oldWidget);
    final old = oldWidget.entry;
    final next = widget.entry;
    if (old.mediaType != next.mediaType || old.tmdbId != next.tmdbId) {
      setState(() {
        _favoriteOverride = null;
        _playedOverride = null;
        _overrideHitKey = null;
        _selectedSourceIdx = 0;
        _selectedCrossServerCard = null;
        _selectedTvEpisode = null;
        _selectedTvEpisodeSeasonNumber = null;
        _selectedTvEpisodeHitKey = null;
        _selectedSubtitleIdx = null;
        _selectedAudioIdx = 0;
        _pendingMediaSourceId = null;
        _versionMemoryQueriedKey = null;
        _positionTicksOverride = null;
        _positionOverrideItemKey = null;
      });
      _scrollProgress.value = 0;
      if (_scroll.hasClients) _scroll.jumpTo(0);
    }
  }

  @override
  void dispose() {
    _scroll.removeListener(_onScroll);
    _scroll.dispose();
    _scrollProgress.dispose();
    super.dispose();
  }


  void _selectLocalSource(int index) {
    setState(() {
      _selectedSourceIdx = index;
      _selectedCrossServerCard = null;
      _selectedSubtitleIdx = null;
      _selectedAudioIdx = 0;
      _pendingMediaSourceId = null;
    });
  }


  void _selectCrossServerSource(CrossServerSourceCard card) {
    setState(() {
      _selectedCrossServerCard = card;
      _selectedSubtitleIdx = null;
      _selectedAudioIdx = 0;
      _pendingMediaSourceId = null;
    });
  }


  void _selectTvEpisode(EmbyRawItem episode, int? seasonNumber, String hitKey) {
    setState(() {
      _selectedTvEpisode = episode;
      _selectedTvEpisodeSeasonNumber = seasonNumber;
      _selectedTvEpisodeHitKey = hitKey;
      _selectedSourceIdx = 0;
      _selectedCrossServerCard = null;
      _selectedSubtitleIdx = null;
      _selectedAudioIdx = 0;
    });
  }


  Future<void> _playRestoringActiveServer({
    required String serverId,
    required String itemId,
    String? mediaSourceId,
    int preferredSubtitleStreamIndex = -1,
    int? preferredAudioStreamIndex,
    bool fromStart = false,
    int? startPositionTicks,
    String? backdropUrl,
  }) async {
    final originServerId = ref.read(activeSessionProvider)?.serverId;
    setState(() => _playbackOriginServerId = originServerId);
    PlayerExitResult? exitResult;
    try {
      exitResult = await playFromTmdbOnServer(
        context,
        ref,
        serverId: serverId,
        itemId: itemId,
        mediaSourceId: mediaSourceId,
        preferredSubtitleStreamIndex: preferredSubtitleStreamIndex,
        preferredAudioStreamIndex: preferredAudioStreamIndex,
        startPositionTicks: startPositionTicks,
        fromStart: fromStart,
        backdropUrl: backdropUrl,
      );
    } finally {
      if (mounted) {
        if (originServerId != null &&
            originServerId.isNotEmpty &&
            ref.read(activeSessionProvider)?.serverId != originServerId) {
          try {
            await ref
                .read(sessionProvider.notifier)
                .switchSession(originServerId);
          } catch (_) {}
        }
        if (mounted) setState(() => _playbackOriginServerId = null);
      }
    }
    final positionTicks = exitResult?.positionTicks;
    if (!mounted || positionTicks == null || positionTicks <= 0) return;
    final exitServerId = exitResult!.serverId;
    final exitItemId = exitResult.itemId;
    setState(() {
      _positionTicksOverride = positionTicks;
      _positionOverrideItemKey = '$exitServerId|$exitItemId';
      final ep = _selectedTvEpisode;
      if (ep != null && ep.Id == exitItemId) {
        _selectedTvEpisode = ep.copyWith(
          UserData: (ep.UserData ?? const EmbyRawUserData()).copyWith(
            PlaybackPositionTicks: positionTicks,
          ),
        );
      }
    });
  }

  void _selectSubtitleIndex(int? index) {
    setState(() => _selectedSubtitleIdx = index);
  }

  void _selectAudioIndex(int index) {
    setState(() => _selectedAudioIdx = index);
  }

  static String _hitKeyOf(TmdbLibraryHit hit) =>
      '${hit.serverId}|${hit.itemId}';


  Future<void> _toggleTmdbFavorite(TmdbLibraryHit hit, bool current) async {
    final next = !current;
    setState(() {
      _favoriteOverride = next;
      _overrideHitKey = _hitKeyOf(hit);
    });
    try {
      final uc = await ref.read(setFavoriteUseCaseProvider.future);
      final res = await uc.executeForServer(
        serverId: hit.serverId,
        input: FavoriteSetReq(itemId: hit.itemId, isFavorite: next),
      );
      if (!mounted) return;
      setState(() => _favoriteOverride = res.isFavorite);
      ref.invalidate(
        tmdbServerScopedDetailProvider((
          serverId: hit.serverId,
          itemId: hit.itemId,
        )),
      );
    } catch (_) {
      if (!mounted) return;
      setState(() => _favoriteOverride = current);
    }
  }


  Future<void> _toggleTmdbPlayed(TmdbLibraryHit hit, bool current) async {
    final next = !current;
    setState(() {
      _playedOverride = next;
      _overrideHitKey = _hitKeyOf(hit);
    });
    try {
      final uc = await ref.read(setPlayedUseCaseProvider.future);
      final res = await uc.executeForServer(
        serverId: hit.serverId,
        input: PlayedSetReq(itemId: hit.itemId, played: next),
      );
      if (!mounted) return;
      setState(() => _playedOverride = res.played);
      ref.invalidate(
        tmdbServerScopedDetailProvider((
          serverId: hit.serverId,
          itemId: hit.itemId,
        )),
      );
    } catch (_) {
      if (!mounted) return;
      setState(() => _playedOverride = current);
    }
  }

  @override
  Widget build(BuildContext context) {
    return _buildTmdbDetail(widget.entry);
  }

  Widget _buildTmdbDetail(TmdbDetailEntry entry) {
    final key = (tmdbId: entry.tmdbId, mediaType: entry.mediaType);
    final settings = ref.watch(tmdbSettingsProvider).value;
    final imageBaseUrl = settings?.effectiveImageBaseUrl ?? '';
    final language = settings?.language ?? 'zh-CN';
    final asyncDetail = ref.watch(tmdbEnrichedDetailProvider(key));

    if (Platform.isAndroid) {
      return Theme(
        data: AppTheme.dark(),
        child: asyncDetail.when(
          loading: () => _buildTmdbContent(
            entry: entry,
            enriched: null,
            imageBaseUrl: imageBaseUrl,
            language: language,
            showSpinner: true,
          ),
          error: (_, _) => Scaffold(
            appBar: AppBar(
              leading: IconButton(
                icon: const Icon(Icons.arrow_back),
                onPressed: () => context.pop(),
              ),
            ),
            body: AppErrorState(
              title: '加载失败',
              message: '无法获取该内容的 TMDB 详情',
              onRetry: () => ref.invalidate(tmdbEnrichedDetailProvider(key)),
            ),
          ),
          data: (enriched) {
            if (enriched == null) {
              return Scaffold(
                appBar: AppBar(
                  leading: IconButton(
                    icon: const Icon(Icons.arrow_back),
                    onPressed: () => context.pop(),
                  ),
                ),
                body: const Center(
                  child: EmptyState(
                    icon: Icons.movie_filter_outlined,
                    title: '暂无详情',
                    message: '请检查 TMDB 配置后重试',
                  ),
                ),
              );
            }
            return _buildTmdbContent(
              entry: entry,
              enriched: enriched,
              imageBaseUrl: imageBaseUrl,
              language: language,
              showSpinner: false,
            );
          },
        ),
      );
    }

    final detail = asyncDetail.value?.detail;
    final title = (detail?.title.isNotEmpty ?? false)
        ? detail!.title
        : (entry.seed?.title ?? '');

    return Theme(
      data: AppTheme.dark(),
      child: Scaffold(
        backgroundColor: _kBg,
        body: Stack(
          children: [
            Positioned.fill(
              child: asyncDetail.when(
                loading: () => _buildTmdbContent(
                  entry: entry,
                  enriched: null,
                  imageBaseUrl: imageBaseUrl,
                  language: language,
                  showSpinner: true,
                ),
                error: (_, _) => AppErrorState(
                  title: '加载失败',
                  message: '无法获取该内容的 TMDB 详情',
                  onRetry: () =>
                      ref.invalidate(tmdbEnrichedDetailProvider(key)),
                ),
                data: (enriched) {
                  if (enriched == null) {
                    return const Center(
                      child: EmptyState(
                        icon: Icons.movie_filter_outlined,
                        title: '暂无详情',
                        message: '请检查 TMDB 配置后重试',
                      ),
                    );
                  }
                  return _buildTmdbContent(
                    entry: entry,
                    enriched: enriched,
                    imageBaseUrl: imageBaseUrl,
                    language: language,
                    showSpinner: false,
                  );
                },
              ),
            ),
            Positioned(
              top: 0,
              left: 0,
              right: 0,
              child: DetailNavBar(
                scrollProgress: _scrollProgress,
                title: title,
                onBack: () => context.pop(),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildTmdbContent({
    required TmdbDetailEntry entry,
    required TmdbEnrichedDetail? enriched,
    required String imageBaseUrl,
    required String language,
    required bool showSpinner,
  }) {
    final mediaType = entry.mediaType;
    final tmdbId = entry.tmdbId;
    final detail = enriched?.detail;
    final title = (detail?.title.isNotEmpty ?? false)
        ? detail!.title
        : (entry.seed?.title ?? '');

    final imdbIdForLookup = mediaType == 'movie' ? enriched?.imdbId : null;
    final detailBackdropUrl = TmdbImageUrl.backdrop(
      imageBaseUrl,
      detail?.backdropPath ?? entry.seed?.backdropPath,
    );

    final availabilityTmdbAsync = ref.watch(
      tmdbLibraryAvailabilityProvider((
        mediaType: mediaType,
        tmdbId: tmdbId,
        imdbId: '',
      )),
    );
    final tmdbResultEmpty = availabilityTmdbAsync.value?.isEmpty == true;
    final needsImdbFallback = imdbIdForLookup != null && tmdbResultEmpty;
    final availabilityImdbAsync = needsImdbFallback
        ? ref.watch(
            tmdbLibraryAvailabilityProvider((
              mediaType: mediaType,
              tmdbId: tmdbId,
              imdbId: imdbIdForLookup,
            )),
          )
        : null;
    final availabilityAsync = availabilityImdbAsync ?? availabilityTmdbAsync;
    final hits = availabilityAsync.value ?? const <TmdbLibraryHit>[];
    final availabilityResolving =
        (availabilityTmdbAsync.isLoading &&
            availabilityTmdbAsync.value == null) ||
        (availabilityImdbAsync?.isLoading == true &&
            availabilityImdbAsync?.value == null) ||
        (mediaType == 'movie' && enriched == null && tmdbResultEmpty);
    final liveActiveServerId = ref.watch(activeSessionProvider)?.serverId;
    final activeServerId = _playbackOriginServerId ?? liveActiveServerId;
    final rawPrimaryHit = selectPrimaryTmdbHit(
      hits,
      activeServerId: activeServerId,
    );
    final tvRecentHit = (rawPrimaryHit != null && mediaType == 'tv')
        ? ref
              .watch(
                tmdbTvRecentHitProvider((
                  mediaType: mediaType,
                  tmdbId: tmdbId,
                  imdbId: imdbIdForLookup ?? '',
                )),
              )
              .value
        : null;
    final primaryHit = tvRecentHit ?? rawPrimaryHit;

    AsyncValue<MediaDetailRes>? scopedDetailAsync;
    EmbyRawItem? serverScopedItem;
    if (primaryHit != null) {
      final scopedKey = (
        serverId: primaryHit.serverId,
        itemId: primaryHit.itemId,
      );
      scopedDetailAsync = ref.watch(tmdbServerScopedDetailProvider(scopedKey));
      serverScopedItem = scopedDetailAsync?.value?.base;
    }
    if (_overrideHitKey != null &&
        (primaryHit == null || _overrideHitKey != _hitKeyOf(primaryHit))) {
      _favoriteOverride = null;
      _playedOverride = null;
      _overrideHitKey = null;
    }
    final isFavorite =
        _favoriteOverride ?? (serverScopedItem?.UserData?.IsFavorite ?? false);
    final isPlayed =
        _playedOverride ?? (serverScopedItem?.UserData?.Played ?? false);

    final isMovieHit = primaryHit != null && mediaType == 'movie';
    final isTvHit = primaryHit != null && mediaType == 'tv';
    final primaryHitKey = primaryHit == null ? null : _hitKeyOf(primaryHit);
    final selectedTvEpisode =
        isTvHit && _selectedTvEpisodeHitKey == primaryHitKey
        ? _selectedTvEpisode
        : null;
    final selectedTvEpisodeSeasonNumber = selectedTvEpisode == null
        ? null
        : _selectedTvEpisodeSeasonNumber;

    final tvAnchorForResume = isTvHit ? primaryHit : null;
    final tvResumeAsync = tvAnchorForResume == null
        ? null
        : ref.watch(
            seriesResumeTargetProvider((
              serverId: tvAnchorForResume.serverId,
              seriesId: tvAnchorForResume.itemId,
            )),
          );
    final tvAutoSelect = EpisodeAutoSelectArgs.fromResume(tvResumeAsync);

    final priorities =
        ref.watch(versionPriorityProvider).value?.activePriorities ??
        const <VersionPriority>[];
    final localSourceItem = isMovieHit ? serverScopedItem : selectedTvEpisode;
    final sortedSources = localSourceItem != null
        ? sortMediaSources(mediaSourcesOfItem(localSourceItem), priorities)
        : const <EmbyRawMediaSource>[];

    final memServerId = primaryHit?.serverId;
    final memItemId = localSourceItem?.Id;
    if (memServerId != null &&
        memServerId.isNotEmpty &&
        memItemId != null &&
        memItemId.isNotEmpty) {
      final memKey = '$memServerId:$memItemId';
      if (memKey != _versionMemoryQueriedKey) {
        _versionMemoryQueriedKey = memKey;
        final store = ref.read(lastPlayedVersionStoreProvider);
        unawaited(
          store
              .get(serverId: memServerId, itemId: memItemId)
              .then((id) {
                if (!mounted || id == null) return;
                if (_versionMemoryQueriedKey != memKey) return;
                if (_selectedSourceIdx != 0) return;
                setState(() => _pendingMediaSourceId = id);
              })
              .onError((e, s) {}),
        );
      }
    }
    final pendingId = _pendingMediaSourceId;
    if (pendingId != null && sortedSources.isNotEmpty) {
      final idx = resolveDefaultSourceIndex(
        sortedSources,
        pendingId,
        fallback: -1,
      );
      if (idx >= 0 && idx != _selectedSourceIdx) {
        WidgetsBinding.instance.addPostFrameCallback((_) {
          if (!mounted) return;
          setState(() {
            _selectedSourceIdx = idx;
            _pendingMediaSourceId = null;
          });
        });
      } else {
        _pendingMediaSourceId = null;
      }
    }

    final effectiveCrossServerCard = isTvHit && selectedTvEpisode == null
        ? null
        : _selectedCrossServerCard;

    final localSourceId =
        effectiveCrossServerCard == null &&
            _selectedSourceIdx < sortedSources.length
        ? sortedSources[_selectedSourceIdx].Id
        : null;
    final selectedMediaSource = resolveActiveMediaSource(
      effectiveCrossServerCard,
      sortedSources,
      _selectedSourceIdx,
    );
    final streamSelection = resolveStreamSelection(
      selectedMediaSource,
      _selectedSubtitleIdx,
      _selectedAudioIdx,
    );
    final subtitles = streamSelection.subtitles;
    final audios = streamSelection.audios;
    final preferredSubtitleStreamIndex =
        streamSelection.preferredSubtitleStreamIndex;
    final preferredAudioStreamIndex = streamSelection.preferredAudioStreamIndex;

    void play(bool fromStart) {
      final card = effectiveCrossServerCard;
      if (card != null) {
        final pos =
            selectedTvEpisode?.UserData?.PlaybackPositionTicks ??
            _positionOverrideFor(card.serverId, card.landingItemId) ??
            _positionOverrideFor(primaryHit?.serverId, primaryHit?.itemId) ??
            primaryHit?.playbackPositionTicks;
        final resumeTicks = (!fromStart && pos != null && pos > 0) ? pos : null;
        unawaited(
          _playRestoringActiveServer(
            serverId: card.serverId,
            itemId: card.landingItemId,
            mediaSourceId: card.mediaSourceId,
            preferredSubtitleStreamIndex: preferredSubtitleStreamIndex,
            preferredAudioStreamIndex: preferredAudioStreamIndex,
            startPositionTicks: resumeTicks,
            fromStart: resumeTicks == null,
            backdropUrl: detailBackdropUrl,
          ),
        );
        return;
      }
      if (primaryHit == null) return;
      final localItemId = selectedTvEpisode?.Id ?? primaryHit.itemId;
      final localResume = fromStart
          ? null
          : _positionOverrideFor(primaryHit.serverId, localItemId);
      unawaited(
        _playRestoringActiveServer(
          serverId: primaryHit.serverId,
          itemId: localItemId,
          mediaSourceId: localSourceId,
          preferredSubtitleStreamIndex: preferredSubtitleStreamIndex,
          preferredAudioStreamIndex: preferredAudioStreamIndex,
          startPositionTicks: localResume,
          fromStart: fromStart,
          backdropUrl: detailBackdropUrl,
        ),
      );
    }

    final imdb = imdbIdForLookup?.trim() ?? '';
    final crossServerReq = CrossServerSearchReq(
      anchorType: 'Movie',
      itemName: title,
      providerIds: {'Tmdb': tmdbId, if (imdb.isNotEmpty) 'Imdb': imdb},
    );
    final tvEpisodeReq = selectedTvEpisode == null || serverScopedItem == null
        ? null
        : buildEpisodeCrossServerReq(
            episode: selectedTvEpisode,
            seriesItem: serverScopedItem,
            seasonNumberOverride: selectedTvEpisodeSeasonNumber,
          );

    final hitStateSections = <DetailSection>[
      if (isMovieHit) ...[
        DetailSection(
          id: 'tmdb_movie_source',
          child: Padding(
            padding: const EdgeInsets.fromLTRB(0, 24, 0, 0),
            child: SourceSelectionSection(
              req: crossServerReq,
              localSources: sortedSources,
              selectedSourceIdx: _selectedSourceIdx,
              selectedCrossServerCard: effectiveCrossServerCard,
              currentServerName: primaryHit.serverName,
              excludedServerIdOverride: primaryHit.serverId,
              localMediaInfoItem: localSourceItem,
              showLoadingWhenEmpty: true,
              externalLoading: scopedDetailAsync?.isLoading ?? false,
              loadingText: '正在查找可用版本…',
              onLocalSourceTap: (src, index) => _selectLocalSource(index),
              onCrossServerSourceTap: _selectCrossServerSource,
            ),
          ),
        ),
        DetailSection(
          id: 'tmdb_movie_additional_parts',
          child: Padding(
            padding: const EdgeInsets.fromLTRB(0, 24, 0, 0),
            child: AdditionalPartsSection(
              itemId: primaryHit.itemId,
              embedded: true,
              serverId: primaryHit.serverId,
              onItemTap: (item) => unawaited(
                _playRestoringActiveServer(
                  serverId: primaryHit.serverId,
                  itemId: item.Id,
                  backdropUrl: detailBackdropUrl,
                ),
              ),
            ),
          ),
        ),
        const DetailSection(id: 'tmdb_movie_divider', child: _sectionDivider),
      ],
      if (isTvHit) ...[
        DetailSection(
          id: 'tmdb_tv_episodes',
          child: Padding(
            padding: const EdgeInsets.fromLTRB(0, 24, 0, 0),
            child: EpisodeListSection(
              seriesId: primaryHit.itemId,
              tmdbSeriesId: tmdbId,
              serverId: primaryHit.serverId,
              embedded: true,
              autoSelectFirstEpisode: tvAutoSelect.autoSelectFirstEpisode,
              autoSelectEpisodeId: tvAutoSelect.autoSelectEpisodeId,
              autoSelectSeasonId: tvAutoSelect.autoSelectSeasonId,
              currentEpisodeId: selectedTvEpisode?.Id,
              onEpisodeTap: (episode) =>
                  _selectTvEpisode(episode, null, primaryHitKey!),
              onEpisodeTapWithSeason: (episode, seasonNumber) =>
                  _selectTvEpisode(episode, seasonNumber, primaryHitKey!),
            ),
          ),
        ),
        if (tvEpisodeReq != null)
          DetailSection(
            id: 'tmdb_tv_source',
            child: Padding(
              padding: const EdgeInsets.fromLTRB(0, 24, 0, 0),
              child: SourceSelectionSection(
                req: tvEpisodeReq,
                localSources: sortedSources,
                selectedSourceIdx: _selectedSourceIdx,
                selectedCrossServerCard: effectiveCrossServerCard,
                currentServerName: primaryHit.serverName,
                excludedServerIdOverride: primaryHit.serverId,
                localMediaInfoItem: localSourceItem,
                showLoadingWhenEmpty: true,
                loadingText: '正在查找该集的可用版本…',
                onLocalSourceTap: (src, index) => _selectLocalSource(index),
                onCrossServerSourceTap: _selectCrossServerSource,
              ),
            ),
          ),
        const DetailSection(id: 'tmdb_tv_divider', child: _sectionDivider),
      ],
    ];

    final heroActions = _buildHeroActions(
      mediaType: mediaType,
      primaryHit: primaryHit,
      hitCount: hits.length,
      availabilityResolving: availabilityResolving,
      isFavorite: isFavorite,
      isPlayed: isPlayed,
      subtitles: subtitles,
      audios: audios,
      onPlay: () => play(false),
      onReplay: () => play(true),
    );

    final viewState = buildTmdbDetailViewState(
      mediaType: mediaType,
      enriched: enriched,
      seed: entry.seed,
      imageBaseUrl: imageBaseUrl,
      language: language,
      heroActions: heroActions,
      hitStateSections: hitStateSections,
      showSpinner: showSpinner,
    );

    if (Platform.isAndroid) {
      return TmdbDetailBodyAndroid(
        entry: entry,
        enriched: enriched,
        imageBaseUrl: imageBaseUrl,
        language: language,
        showSpinner: showSpinner,
        scrollController: _scroll,
        scrollProgress: _scrollProgress,
        primaryHit: primaryHit,
        availabilityResolving: availabilityResolving,
        isFavorite: isFavorite,
        isPlayed: isPlayed,
        subtitles: subtitles,
        audios: audios,
        selectedSubtitleIdx: _selectedSubtitleIdx,
        selectedAudioIdx: _selectedAudioIdx,
        onPlay: () => play(false),
        onReplay: () => play(true),
        onToggleFavorite: primaryHit != null
            ? () => unawaited(_toggleTmdbFavorite(primaryHit, isFavorite))
            : () {},
        onTogglePlayed: primaryHit != null
            ? () => unawaited(_toggleTmdbPlayed(primaryHit, isPlayed))
            : () {},
        onSelectSubtitleIndex: _selectSubtitleIndex,
        onSelectAudioIndex: _selectAudioIndex,
        hitCount: hits.length,
        hitStateSections: hitStateSections,
      );
    }

    final isCompact = MediaQuery.sizeOf(context).width < Breakpoints.detail;
    return DetailImmersiveScaffold(
      scrollController: _scroll,
      scrollProgress: _scrollProgress,
      backdropUrl: viewState.image.backdropUrl,
      fallbackUrls: viewState.image.fallbackUrls,
      embyBaseUrl: viewState.image.embyBaseUrl,
      imageHeaders: viewState.image.imageHeaders,
      compact: isCompact,
      horizontalPadding: isCompact ? 16 : _kHPad,
      child: DetailViewRenderer(state: viewState),
    );
  }


  Widget? _buildHeroActions({
    required String mediaType,
    required TmdbLibraryHit? primaryHit,
    required int hitCount,
    required bool availabilityResolving,
    required bool isFavorite,
    required bool isPlayed,
    required List<EmbyRawMediaStream> subtitles,
    required List<EmbyRawMediaStream> audios,
    required VoidCallback onPlay,
    required VoidCallback onReplay,
  }) {
    final compact = MediaQuery.sizeOf(context).width < Breakpoints.detail;
    final btnH = compact ? 44.0 : 52.0;

    if (availabilityResolving) {
      return ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 360),
        child: AppSkeleton(height: btnH, radius: 12),
      );
    }

    final ctaLabel = buildTmdbCtaLabel(
      mediaType: mediaType,
      primaryHit: primaryHit,
      hitCount: hitCount,
    );
    final hit = primaryHit;
    if (ctaLabel == null || hit == null) return null;

    final pos =
        _positionOverrideFor(hit.serverId, hit.itemId) ??
        hit.playbackPositionTicks ??
        0;
    final runtime = hit.runTimeTicks ?? 0;
    final hasProgress = pos > 0;
    final progressPercent = (hasProgress && runtime > 0)
        ? (pos / runtime * 100).clamp(0.0, 100.0)
        : null;
    final positionClock = hasProgress ? formatTicksAsClock(pos) : null;
    final extraActions = buildStreamSelectorPills(
      subtitles: subtitles,
      audios: audios,
      selectedSubtitleIdx: _selectedSubtitleIdx,
      selectedAudioIdx: _selectedAudioIdx,
      onSelectSubtitle: _selectSubtitleIndex,
      onSelectAudio: _selectAudioIndex,
      height: btnH,
    );

    return DetailHeroActions(
      showReplay: hasProgress,
      onReplay: onReplay,
      isPlayed: isPlayed,
      onTogglePlayed: () => _toggleTmdbPlayed(hit, isPlayed),
      isFavorite: isFavorite,
      onToggleFavorite: () => _toggleTmdbFavorite(hit, isFavorite),
      extraIconActions: extraActions,
      onPlay: onPlay,
      playLabel: ctaLabel,
      progressPercent: progressPercent,
      trailingLabel: positionClock,
      compact: compact,
    );
  }
}
