import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import '../focus/input_mode_tracker.dart';
import '../media/media_item.dart';
import '../media/media_item_types.dart';
import '../media/media_kind.dart';
import '../media/media_playlist.dart';
import '../mixins/context_menu_tap_mixin.dart';
import '../providers/download_provider.dart';
import '../providers/watch_state_overlay_provider.dart';
import '../services/download_storage_service.dart';
import '../services/settings_service.dart';
import 'settings_builder.dart';
import '../screens/media_detail_screen.dart';
import '../utils/content_utils.dart';
import '../utils/provider_extensions.dart';
import '../utils/formatters.dart';
import '../utils/media_navigation_helper.dart';
import '../utils/snackbar_helper.dart';
import '../theme/mono_tokens.dart';
import '../i18n/strings.g.dart';
import 'media_context_menu.dart';
import 'media_progress_bar.dart';
import 'media_card_list_layout.dart';
import 'backend_badge.dart';
import 'optimized_media_image.dart';

const _failedPosterUrlCacheLimit = 512;
final _failedPosterUrls = <String>{};

bool _hasFailedPosterUrl(String? url) => url != null && _failedPosterUrls.contains(url);

void _rememberFailedPosterUrl(String? url) {
  if (url == null || url.isEmpty) return;
  _failedPosterUrls.remove(url);
  _failedPosterUrls.add(url);
  if (_failedPosterUrls.length > _failedPosterUrlCacheLimit) {
    _failedPosterUrls.remove(_failedPosterUrls.first);
  }
}

class MediaCard extends StatefulWidget {
  /// Either a [MediaItem] or a [MediaPlaylist]. Typed as [Object] because Dart
  /// has no nominal union type — runtime `is` checks select the variant.
  final Object item;
  final double? width;
  final double? height;
  final void Function(String itemId)? onRefresh;
  final VoidCallback? onRemoveFromContinueWatching;
  final VoidCallback? onListRefresh; // Callback to refresh the entire parent list
  final bool forceGridMode;
  final bool forceListMode;
  final bool isInContinueWatching;
  final String? collectionId; // The collection ID if displaying within a collection
  final bool isOffline; // True for downloaded content without server access
  final bool mixedHubContext; // True when in a hub with mixed content (movies + episodes)
  final bool showServerName; // Show server name in list view (multi-server)

  const MediaCard({
    super.key,
    required this.item,
    this.width,
    this.height,
    this.onRefresh,
    this.onRemoveFromContinueWatching,
    this.onListRefresh,
    this.forceGridMode = false,
    this.forceListMode = false,
    this.isInContinueWatching = false,
    this.collectionId,
    this.isOffline = false,
    this.mixedHubContext = false,
    this.showServerName = false,
  });

  @override
  State<MediaCard> createState() => MediaCardState();
}

class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard> {
  /// Public method to trigger tap action (for keyboard/gamepad SELECT)
  void handleTap() {
    _handleTap(context, _effectiveItemForAction(context));
  }

  Object _effectiveItem(BuildContext context) {
    final item = widget.item;
    if (item is! MediaItem) return item;
    try {
      final patch = context.select<WatchStateOverlayProvider, WatchStateOverlayPatch?>(
        (provider) => provider.patchForGlobalKey(item.globalKey),
      );
      return WatchStateOverlayProvider.applyPatch(item, patch);
    } on ProviderNotFoundException {
      return item;
    }
  }

  Object _effectiveItemForAction(BuildContext context) {
    final item = widget.item;
    if (item is! MediaItem) return item;
    try {
      return context.read<WatchStateOverlayProvider>().apply(item);
    } on ProviderNotFoundException {
      return item;
    }
  }

  String _buildSemanticLabel(Object item) {
    // Playlists don't expose kind, so build a simple localized label and exit early
    if (item is MediaPlaylist) {
      final count = item.leafCount;
      final countText = count != null ? ', ${t.playlists.itemCount(count: count)}' : '';
      return '${item.displayTitle}, ${t.playlists.playlist}$countText';
    }

    if (item is! MediaItem) {
      return '$item';
    }

    String baseLabel;
    switch (item.kind) {
      case MediaKind.episode:
        final episodeInfo = item.parentIndex != null && item.index != null ? 'S${item.parentIndex} E${item.index}' : '';
        baseLabel = t.accessibility.mediaCardEpisode(title: item.displayTitle, episodeInfo: episodeInfo);
      case MediaKind.season:
        final seasonInfo = item.parentIndex != null ? 'Season ${item.parentIndex}' : '';
        baseLabel = t.accessibility.mediaCardSeason(title: item.displayTitle, seasonInfo: seasonInfo);
      case MediaKind.movie:
        baseLabel = t.accessibility.mediaCardMovie(title: item.displayTitle);
      default:
        baseLabel = t.accessibility.mediaCardShow(title: item.displayTitle);
    }

    // Add watched status
    final hasActiveProgress =
        item.viewOffsetMs != null &&
        item.durationMs != null &&
        item.viewOffsetMs! > 0 &&
        item.viewOffsetMs! < item.durationMs!;

    if (hasActiveProgress) {
      final percent = ((item.viewOffsetMs! / item.durationMs!) * 100).round();
      baseLabel = '$baseLabel, ${t.accessibility.mediaCardPartiallyWatched(percent: percent)}';
    } else if (item.isWatched) {
      baseLabel = '$baseLabel, ${t.accessibility.mediaCardWatched}';
    } else {
      baseLabel = '$baseLabel, ${t.accessibility.mediaCardUnwatched}';
    }

    return baseLabel;
  }

  void _handleTap(BuildContext context, Object item) async {
    // Ignore taps while context menu is open to avoid double-activating
    if (contextMenuKey.currentState?.isContextMenuOpen == true) {
      return;
    }

    final result = await navigateToMediaItem(
      context,
      item,
      onRefresh: widget.onRefresh,
      isOffline: widget.isOffline,
      playDirectly: widget.isInContinueWatching,
    );

    if (!context.mounted) return;

    switch (result) {
      case MediaNavigationResult.unsupported:
        showAppSnackBar(context, t.messages.musicNotSupported);
      case MediaNavigationResult.listRefreshNeeded:
        widget.onListRefresh?.call();
      case MediaNavigationResult.navigated:
      case MediaNavigationResult.librarySelected:
        // Item refresh already handled by onRefresh callback in helper
        break;
    }
  }

  /// Get the local poster path for offline mode
  String? _getLocalPosterPath(BuildContext context, Object item) {
    if (!widget.isOffline) return null;
    if (item is! MediaItem) return null;

    if (item.serverId == null) return null;

    final downloadProvider = context.read<DownloadProvider>();
    final globalKey = item.globalKey;

    // Get artwork reference and resolve to local path using hash (includes serverId)
    final artwork = downloadProvider.getArtworkPaths(globalKey);
    return artwork?.getLocalPath(DownloadStorageService.instance, item.serverId!);
  }

  @override
  Widget build(BuildContext context) {
    return SettingsBuilder(
      prefs: const [
        SettingsService.viewMode,
        SettingsService.libraryDensity,
        SettingsService.episodePosterMode,
        SettingsService.showEpisodeNumberOnCards,
        SettingsService.hideSpoilers,
        SettingsService.showUnwatchedCount,
      ],
      builder: _buildContent,
    );
  }

  Widget _buildContent(BuildContext context) {
    final item = _effectiveItem(context);
    final ViewMode viewMode;
    if (widget.forceListMode) {
      viewMode = ViewMode.list;
    } else if (widget.forceGridMode) {
      viewMode = ViewMode.grid;
    } else {
      viewMode = SettingsService.instanceOrNull!.read(SettingsService.viewMode);
    }

    final semanticLabel = _buildSemanticLabel(item);
    final localPosterPath = _getLocalPosterPath(context, item);

    final cardWidget = viewMode == ViewMode.grid
        ? _buildGridCard(context, item, semanticLabel, localPosterPath)
        : _MediaCardList(
            item: item,
            semanticLabel: semanticLabel,
            onTap: () => _handleTap(context, item),
            onTapDown: storeTapPosition,
            onLongPress: showContextMenuFromTap,
            onSecondaryTapDown: storeTapPosition,
            onSecondaryTap: showContextMenuFromTap,
            density: SettingsService.instanceOrNull!.read(SettingsService.libraryDensity),
            isOffline: widget.isOffline,
            localPosterPath: localPosterPath,
            showServerName: widget.showServerName,
          );

    // MediaContextMenu as a non-widget helper — only wrap with its key for
    // programmatic context menu access; gesture callbacks are on InkWell directly.
    return MediaContextMenu(
      key: contextMenuKey,
      item: item,
      onRefresh: widget.onRefresh,
      onRemoveFromContinueWatching: widget.onRemoveFromContinueWatching,
      onListRefresh: widget.onListRefresh,
      onTap: () => _handleTap(context, item),
      isInContinueWatching: widget.isInContinueWatching,
      collectionId: widget.collectionId,
      child: cardWidget,
    );
  }

  /// Grid layout — inlined from former _MediaCardGrid, _PosterOverlay, and
  /// flattened Column. Semantics removed (InkWell provides button semantics).
  Widget _buildGridCard(BuildContext context, Object item, String semanticLabel, String? localPosterPath) {
    // Compute actual poster dimensions from card dimensions
    final posterWidth = widget.width != null ? widget.width! - 6 : null; // 3px padding each side
    final posterHeight = widget.height;

    return SizedBox(
      width: widget.width,
      child: InkWell(
        canRequestFocus: false,
        onTap: () => _handleTap(context, item),
        onTapDown: storeTapPosition,
        onLongPress: showContextMenuFromTap,
        onSecondaryTapDown: storeTapPosition,
        onSecondaryTap: showContextMenuFromTap,
        borderRadius: BorderRadius.circular(tokens(context).radiusSm),
        child: Padding(
          padding: const EdgeInsets.fromLTRB(3, 3, 3, 1),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // Poster with overlay
              if (posterHeight != null)
                SizedBox(
                  width: double.infinity,
                  height: posterHeight,
                  child: Stack(
                    children: [
                      ClipRRect(
                        borderRadius: BorderRadius.circular(tokens(context).radiusSm),
                        child: _buildPosterImage(
                          context,
                          item,
                          isOffline: widget.isOffline,
                          localPosterPath: localPosterPath,
                          mixedHubContext: widget.mixedHubContext,
                          knownWidth: posterWidth,
                          knownHeight: posterHeight,
                        ),
                      ),
                      if (item is MediaItem) _MediaCardHelpers.buildWatchProgress(context, item),
                    ],
                  ),
                )
              else
                Expanded(
                  child: Stack(
                    children: [
                      ClipRRect(
                        borderRadius: BorderRadius.circular(tokens(context).radiusSm),
                        child: _buildPosterImage(
                          context,
                          item,
                          isOffline: widget.isOffline,
                          localPosterPath: localPosterPath,
                          mixedHubContext: widget.mixedHubContext,
                        ),
                      ),
                      if (item is MediaItem) _MediaCardHelpers.buildWatchProgress(context, item),
                    ],
                  ),
                ),
              const SizedBox(height: 2),
              // Title (flattened — no inner Column)
              if (item is MediaItem && _hasClickableTitle(item))
                _ClickableText(
                  text: item.displayTitle,
                  style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13, height: 1.1),
                  onTap: () => _navigateToDetail(context, item, isOffline: widget.isOffline),
                )
              else
                Text(
                  item is MediaPlaylist ? item.title : (item as MediaItem).displayTitle,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13, height: 1.1),
                ),
              // Subtitle
              if (item is MediaPlaylist)
                _MediaCardHelpers.buildPlaylistMeta(context, item)
              else if (item is MediaItem)
                _MediaCardHelpers.buildMetadataSubtitle(context, item, isOffline: widget.isOffline),
            ],
          ),
        ),
      ),
    );
  }
}

class _MediaCardList extends StatelessWidget {
  /// Either a [MediaItem] or a [MediaPlaylist].
  final Object item;
  final String semanticLabel;
  final VoidCallback onTap;
  final VoidCallback onLongPress;
  final void Function(TapDownDetails)? onTapDown;
  final VoidCallback? onSecondaryTap;
  final void Function(TapDownDetails)? onSecondaryTapDown;
  final int density;
  final bool isOffline;
  final String? localPosterPath;
  final bool showServerName;

  const _MediaCardList({
    required this.item,
    required this.semanticLabel,
    required this.onTap,
    required this.onLongPress,
    this.onTapDown,
    this.onSecondaryTap,
    this.onSecondaryTapDown,
    required this.density,
    this.isOffline = false,
    this.localPosterPath,
    this.showServerName = false,
  });

  bool _usesWideAspectRatio() {
    if (item is! MediaItem) return false;
    final mode = SettingsService.instanceOrNull!.read(SettingsService.episodePosterMode);
    return (item as MediaItem).usesWideAspectRatio(mode);
  }

  double _posterWidth(BuildContext context) =>
      MediaCardListLayout.posterWidth(density: density, usesWideAspectRatio: _usesWideAspectRatio());

  double _posterHeight(BuildContext context) =>
      MediaCardListLayout.posterHeight(density: density, usesWideAspectRatio: _usesWideAspectRatio());

  double get _titleFontSize => 13 + LibraryDensity.factor(density) * 3; // 13–16

  double get _metadataFontSize => 10 + LibraryDensity.factor(density) * 3; // 10–13

  double get _subtitleFontSize => 11 + LibraryDensity.factor(density) * 3; // 11–14

  double get _summaryFontSize {
    // Summary uses the same sizing as metadata text
    return _metadataFontSize;
  }

  int get _summaryMaxLines => density <= 2 ? 2 : density; // 2, 2, 3, 4, 5

  String _buildMetadataLine() {
    final parts = <String>[];

    if (item is MediaPlaylist) {
      final playlist = item as MediaPlaylist;
      if (playlist.leafCount != null && playlist.leafCount! > 0) {
        parts.add(t.playlists.itemCount(count: playlist.leafCount!));
      }

      if (playlist.durationMs != null) {
        parts.add(formatDurationTextual(playlist.durationMs!));
      }

      if (playlist.smart) {
        parts.add(t.playlists.smartPlaylist);
      }
    } else if (item is MediaItem) {
      final mi = item as MediaItem;

      if (mi.kind == MediaKind.collection) {
        final count = mi.childCount ?? mi.leafCount;
        if (count != null && count > 0) {
          parts.add(t.playlists.itemCount(count: count));
        }
      } else {
        if (mi.contentRating != null && mi.contentRating!.isNotEmpty) {
          final rating = formatContentRating(mi.contentRating);
          if (rating.isNotEmpty) {
            parts.add(rating);
          }
        }

        if (mi.year != null) {
          parts.add('${mi.year}');
        }

        if (mi.editionTitle case final editionTitle?) {
          parts.add(editionTitle);
        }

        if (mi.durationMs != null) {
          parts.add(formatDurationTextual(mi.durationMs!));
        }

        if (mi.rating != null) {
          parts.add('${formatRating(mi.rating!)}★');
        }

        if (mi.studio != null && mi.studio!.isNotEmpty) {
          parts.add(mi.studio!);
        }
      }
    }

    return parts.join(' • ');
  }

  String? _buildSubtitleText(BuildContext context) {
    if (item is MediaPlaylist) {
      return null;
    } else if (item is MediaItem) {
      final mi = item as MediaItem;

      if (mi.parentIndex != null && mi.index != null) {
        final showEp = SettingsService.instanceOrNull!.read(SettingsService.showEpisodeNumberOnCards);
        return showEp ? 'S${mi.parentIndex} E${mi.index}' : 'S${mi.parentIndex}';
      }

      if (mi.displaySubtitle != null) {
        return mi.displaySubtitle;
      } else if (mi.parentTitle != null) {
        return mi.parentTitle;
      }
    }

    // Year is now shown in metadata line, so don't show it here
    return null;
  }

  String? _summary() {
    final it = item;
    if (it is MediaItem) return it.summary;
    if (it is MediaPlaylist) return it.summary;
    return null;
  }

  String _displayTitle() {
    final it = item;
    if (it is MediaItem) return it.displayTitle;
    if (it is MediaPlaylist) return it.displayTitle;
    return '';
  }

  Widget _buildEpisodeSubtitle(BuildContext context, MediaItem mi) {
    final style = Theme.of(context).textTheme.bodySmall?.copyWith(
      color: tokens(context).textMuted.withValues(alpha: 0.85),
      fontSize: _subtitleFontSize,
    );
    final episodeTitle = mi.displaySubtitle ?? mi.displayTitle;
    final showEp = SettingsService.instanceOrNull!.read(SettingsService.showEpisodeNumberOnCards);
    final episodeNum = (showEp && mi.index != null) ? ' E${mi.index}' : '';
    return Row(
      children: [
        _ClickableText(
          text: 'S${mi.parentIndex}',
          style: style,
          onTap: () => _navigateToSeason(context, mi, isOffline: isOffline),
        ),
        Text('$episodeNum · ', style: style),
        Expanded(
          child: Text(episodeTitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: style),
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    final metadataLine = _buildMetadataLine();
    final subtitle = _buildSubtitleText(context);

    return InkWell(
      canRequestFocus: false, // Keyboard handled by FocusableMediaCard
      onTap: onTap,
      onTapDown: onTapDown,
      onLongPress: onLongPress,
      onSecondaryTapDown: onSecondaryTapDown,
      onSecondaryTap: onSecondaryTap,
      borderRadius: BorderRadius.circular(tokens(context).radiusSm),
      child: Padding(
        padding: const EdgeInsets.all(8),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            SizedBox(
              width: _posterWidth(context),
              height: _posterHeight(context),
              child: Stack(
                children: [
                  ClipRRect(
                    borderRadius: BorderRadius.circular(tokens(context).radiusSm),
                    child: _buildPosterImage(context, item, isOffline: isOffline, localPosterPath: localPosterPath),
                  ),
                  if (item is MediaItem) _MediaCardHelpers.buildWatchProgress(context, item as MediaItem),
                ],
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.start,
                children: [
                  if (item is MediaItem && _hasClickableTitle(item as MediaItem))
                    _ClickableText(
                      text: (item as MediaItem).displayTitle,
                      style: TextStyle(fontWeight: FontWeight.w600, fontSize: _titleFontSize, height: 1.2),
                      onTap: () => _navigateToDetail(context, item as MediaItem, isOffline: isOffline),
                    )
                  else
                    Text(
                      _displayTitle(),
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                      style: TextStyle(fontWeight: FontWeight.w600, fontSize: _titleFontSize, height: 1.2),
                    ),
                  const SizedBox(height: 4),
                  if (metadataLine.isNotEmpty) ...[
                    Text(
                      metadataLine,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
                        color: tokens(context).textMuted.withValues(alpha: 0.9),
                        fontSize: _metadataFontSize,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                    const SizedBox(height: 2),
                  ],
                  if (item is MediaItem &&
                      (item as MediaItem).isEpisode &&
                      (item as MediaItem).parentIndex != null &&
                      (item as MediaItem).parentId != null) ...[
                    _buildEpisodeSubtitle(context, item as MediaItem),
                    const SizedBox(height: 4),
                  ] else if (subtitle != null) ...[
                    Text(
                      subtitle,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
                        color: tokens(context).textMuted.withValues(alpha: 0.85),
                        fontSize: _subtitleFontSize,
                      ),
                    ),
                    const SizedBox(height: 4),
                  ],
                  if (!(item is MediaItem &&
                          SettingsService.instanceOrNull!.read(SettingsService.hideSpoilers) &&
                          (item as MediaItem).shouldHideSpoiler) &&
                      _summary() != null) ...[
                    Text(
                      _summary()!,
                      maxLines: _summaryMaxLines,
                      overflow: TextOverflow.ellipsis,
                      style: Theme.of(context).textTheme.bodySmall?.copyWith(
                        color: tokens(context).textMuted.withValues(alpha: 0.7),
                        fontSize: _summaryFontSize,
                        height: 1.3,
                      ),
                    ),
                  ],
                  if (showServerName && item is MediaItem && (item as MediaItem).serverName != null) ...[
                    const SizedBox(height: 4),
                    Row(
                      children: [
                        BackendBadge(
                          backend: (item as MediaItem).backend,
                          size: _metadataFontSize + 2,
                          color: tokens(context).textMuted.withValues(alpha: 0.6),
                        ),
                        const SizedBox(width: 4),
                        Flexible(
                          child: Text(
                            (item as MediaItem).serverName!,
                            maxLines: 1,
                            overflow: TextOverflow.ellipsis,
                            style: Theme.of(context).textTheme.bodySmall?.copyWith(
                              color: tokens(context).textMuted.withValues(alpha: 0.6),
                              fontSize: _metadataFontSize,
                            ),
                          ),
                        ),
                      ],
                    ),
                  ],
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Widget _buildPosterImage(
  BuildContext context,
  Object item, {
  bool isOffline = false,
  String? localPosterPath,
  bool mixedHubContext = false,
  double? knownWidth,
  double? knownHeight,
}) {
  String? posterUrl;
  IconData fallbackIcon = Symbols.movie_rounded;

  if (item is MediaPlaylist) {
    posterUrl = item.displayImagePath;
    fallbackIcon = Symbols.playlist_play_rounded;

    return OptimizedMediaImage.playlist(
      client: isOffline ? null : context.tryGetMediaClientWithFallback(item.serverId),
      imagePath: posterUrl,
      width: knownWidth ?? double.infinity,
      height: knownHeight ?? double.infinity,
      fit: BoxFit.cover,
      localFilePath: localPosterPath,
    );
  } else if (item is MediaItem) {
    final episodePosterMode = SettingsService.instanceOrNull!.read(SettingsService.episodePosterMode);
    final hideSpoilers = SettingsService.instanceOrNull!.read(SettingsService.hideSpoilers);
    final shouldBlur =
        hideSpoilers && item.shouldHideSpoiler && episodePosterMode == EpisodePosterMode.episodeThumbnail;
    final primaryPosterUrl = item.posterThumb(mode: episodePosterMode, mixedHubContext: mixedHubContext);
    final posterFallbackUrl = item.posterThumbFallback(mode: episodePosterMode, mixedHubContext: mixedHubContext);
    final useRememberedFallback = posterFallbackUrl != null && _hasFailedPosterUrl(primaryPosterUrl);
    posterUrl = useRememberedFallback ? posterFallbackUrl : primaryPosterUrl;
    final mediaClient = isOffline ? null : context.tryGetMediaClientWithFallback(item.serverId);

    Widget image;

    // Use thumb image type for 16:9 content (episodes, or movies in mixed hubs)
    if (item.usesWideAspectRatio(episodePosterMode, mixedHubContext: mixedHubContext)) {
      image = OptimizedMediaImage.thumb(
        client: mediaClient,
        imagePath: posterUrl,
        width: knownWidth ?? double.infinity,
        height: knownHeight ?? double.infinity,
        fit: BoxFit.cover,
        localFilePath: localPosterPath,
      );
    } else {
      image = OptimizedMediaImage.poster(
        client: mediaClient,
        imagePath: posterUrl,
        width: knownWidth ?? double.infinity,
        height: knownHeight ?? double.infinity,
        fit: BoxFit.cover,
        errorWidget: posterFallbackUrl == null || useRememberedFallback
            ? null
            : (_, _, _) {
                _rememberFailedPosterUrl(primaryPosterUrl);
                return OptimizedMediaImage.poster(
                  client: mediaClient,
                  imagePath: posterFallbackUrl,
                  width: knownWidth ?? double.infinity,
                  height: knownHeight ?? double.infinity,
                  fit: BoxFit.cover,
                );
              },
        localFilePath: localPosterPath,
      );
    }

    if (shouldBlur) {
      return ClipRect(
        child: ImageFiltered(imageFilter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), child: image),
      );
    }
    return image;
  }

  return SkeletonLoader(
    child: Center(child: AppIcon(fallbackIcon, fill: 1, size: 40, color: Colors.white54)),
  );
}

class _MediaCardHelpers {
  static Widget buildPlaylistMeta(BuildContext context, MediaPlaylist playlist) {
    if (playlist.leafCount != null && playlist.leafCount! > 0) {
      return Text(
        t.playlists.itemCount(count: playlist.leafCount!),
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
        style: Theme.of(
          context,
        ).textTheme.bodySmall?.copyWith(color: tokens(context).textMuted, fontSize: 11, height: 1.1),
      );
    }
    return const SizedBox.shrink();
  }

  /// Builds metadata subtitle (for collections, episodes, movies, shows)
  static Widget buildMetadataSubtitle(BuildContext context, MediaItem mi, {bool isOffline = false}) {
    final subtitleStyle = Theme.of(
      context,
    ).textTheme.bodySmall?.copyWith(color: tokens(context).textMuted, fontSize: 11, height: 1.1);

    // For collections, show item count
    if (mi.kind == MediaKind.collection) {
      final count = mi.childCount ?? mi.leafCount;
      if (count != null && count > 0) {
        return Text(
          t.playlists.itemCount(count: count),
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
          style: subtitleStyle,
        );
      }
    }

    // For episodes, show "S# · Episode Title" with clickable season link
    if (mi.isEpisode && mi.parentIndex != null) {
      final episodeTitle = mi.displaySubtitle ?? mi.displayTitle;
      final showEp = SettingsService.instanceOrNull!.read(SettingsService.showEpisodeNumberOnCards);
      final episodeSuffix = (showEp && mi.index != null) ? ' E${mi.index}' : '';
      if (mi.parentId != null) {
        return Row(
          children: [
            _ClickableText(
              text: 'S${mi.parentIndex}',
              style: subtitleStyle,
              onTap: () => _navigateToSeason(context, mi, isOffline: isOffline),
            ),
            Text('$episodeSuffix · ', style: subtitleStyle),
            Expanded(
              child: Text(episodeTitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: subtitleStyle),
            ),
          ],
        );
      }
      return Text(
        'S${mi.parentIndex}$episodeSuffix · $episodeTitle',
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
        style: subtitleStyle,
      );
    }

    // For other media types, show subtitle/parent/year
    if (mi.displaySubtitle != null) {
      return Text(mi.displaySubtitle!, maxLines: 1, overflow: TextOverflow.ellipsis, style: subtitleStyle);
    } else if (mi.parentTitle != null) {
      return Text(mi.parentTitle!, maxLines: 1, overflow: TextOverflow.ellipsis, style: subtitleStyle);
    } else if (mi.year != null) {
      final edition = mi.editionTitle;
      return Text(
        edition != null ? '${mi.year} · $edition' : '${mi.year}',
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
        style: subtitleStyle,
      );
    }

    return const SizedBox.shrink();
  }

  /// Builds watch progress overlay (checkmark for watched, progress bar for in-progress)
  static Widget buildWatchProgress(BuildContext context, MediaItem mi) {
    final showUnwatchedCount = SettingsService.instanceOrNull!.read(SettingsService.showUnwatchedCount);

    final hasActiveProgress =
        mi.viewOffsetMs != null && mi.durationMs != null && mi.viewOffsetMs! > 0 && mi.viewOffsetMs! < mi.durationMs!;

    return Stack(
      children: [
        // Watched indicator (checkmark)
        if (mi.isWatched && !hasActiveProgress)
          Positioned(
            top: 4,
            right: 4,
            child: Container(
              padding: const EdgeInsets.all(4),
              decoration: BoxDecoration(
                color: tokens(context).text,
                shape: BoxShape.circle,
                boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 4)],
              ),
              child: AppIcon(Symbols.check_rounded, fill: 1, color: tokens(context).bg, size: 16),
            ),
          ),
        if (showUnwatchedCount &&
            !mi.isWatched &&
            (mi.kind == MediaKind.show || mi.kind == MediaKind.season) &&
            (mi.leafCount != null && mi.leafCount! > 0 && mi.viewedLeafCount != null))
          Positioned(
            top: 4,
            right: 4,
            child: Container(
              width: 24,
              height: 24,
              decoration: BoxDecoration(
                color: tokens(context).text,
                shape: BoxShape.circle,
                boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 4)],
              ),
              alignment: Alignment.center,
              child: Text(
                '${mi.leafCount! - mi.viewedLeafCount!}',
                style: TextStyle(color: tokens(context).bg, fontSize: 12, fontWeight: FontWeight.bold),
              ),
            ),
          ),
        // Progress bar for partially watched content (episodes/movies)
        if (hasActiveProgress)
          Positioned(
            bottom: 0,
            left: 0,
            right: 0,
            child: ClipRRect(
              borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8)),
              child: MediaProgressBar(viewOffset: mi.viewOffsetMs!, duration: mi.durationMs!),
            ),
          ),
        // Progress bar for seasons (viewedLeafCount / leafCount)
        if (mi.isSeason && mi.isPartiallyWatched)
          Positioned(
            bottom: 0,
            left: 0,
            right: 0,
            child: ClipRRect(
              borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8)),
              child: LinearProgressIndicator(
                value: mi.viewedLeafCount! / mi.leafCount!,
                backgroundColor: tokens(context).outline,
                valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).colorScheme.primary),
                minHeight: 4,
              ),
            ),
          ),
      ],
    );
  }
}

/// Whether this media item has a clickable title that navigates somewhere.
/// Episodes/seasons navigate to their parent show; movies navigate to their detail page.
bool _hasClickableTitle(MediaItem mi) {
  if (mi.isEpisode) return mi.grandparentId != null;
  if (mi.isSeason) return mi.parentId != null;
  if (mi.isMovie) return true;
  return false;
}

void _navigateToSeason(BuildContext context, MediaItem episode, {bool isOffline = false}) {
  if (episode.grandparentId != null) {
    final showStub = MediaItem(
      id: episode.grandparentId!,
      backend: episode.backend,
      kind: MediaKind.show,
      title: episode.grandparentTitle ?? episode.displayTitle,
      thumbPath: episode.grandparentThumbPath,
      artPath: episode.grandparentArtPath,
      libraryId: episode.libraryId,
      libraryTitle: episode.libraryTitle,
      serverId: episode.serverId,
      serverName: episode.serverName,
    );
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (_) =>
            MediaDetailScreen(metadata: showStub, isOffline: isOffline, initialSeasonIndex: episode.parentIndex),
      ),
    );
  } else if (episode.parentId != null) {
    // Fallback: navigate to season directly if no grandparent
    final seasonStub = MediaItem(
      id: episode.parentId!,
      backend: episode.backend,
      kind: MediaKind.season,
      title: episode.parentTitle ?? 'Season ${episode.parentIndex ?? ''}',
      index: episode.parentIndex,
      parentId: episode.grandparentId,
      thumbPath: episode.parentThumbPath,
      libraryId: episode.libraryId,
      libraryTitle: episode.libraryTitle,
      serverId: episode.serverId,
      serverName: episode.serverName,
    );
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (_) => MediaDetailScreen(metadata: seasonStub, isOffline: isOffline),
      ),
    );
  }
}

/// Navigate to the detail screen for a media item.
/// For episodes/seasons: navigates to the parent show with season pre-selected.
/// For movies and other types: navigates to the item's own detail page.
void _navigateToDetail(BuildContext context, MediaItem mi, {bool isOffline = false}) {
  MediaItem target = mi;
  int? initialSeasonIndex;

  if (mi.isEpisode && mi.grandparentId != null) {
    target = MediaItem(
      id: mi.grandparentId!,
      backend: mi.backend,
      kind: MediaKind.show,
      title: mi.grandparentTitle ?? mi.displayTitle,
      thumbPath: mi.grandparentThumbPath,
      artPath: mi.grandparentArtPath,
      libraryId: mi.libraryId,
      libraryTitle: mi.libraryTitle,
      serverId: mi.serverId,
      serverName: mi.serverName,
    );
  } else if (mi.isSeason && mi.parentId != null) {
    initialSeasonIndex = mi.index;
    target = MediaItem(
      id: mi.parentId!,
      backend: mi.backend,
      kind: MediaKind.show,
      title: mi.grandparentTitle ?? mi.parentTitle ?? mi.displayTitle,
      thumbPath: mi.grandparentThumbPath ?? mi.parentThumbPath,
      artPath: mi.grandparentArtPath,
      libraryId: mi.libraryId,
      libraryTitle: mi.libraryTitle,
      serverId: mi.serverId,
      serverName: mi.serverName,
    );
  }

  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (_) => MediaDetailScreen(metadata: target, isOffline: isOffline, initialSeasonIndex: initialSeasonIndex),
    ),
  );
}

/// Text widget that shows hover underline + pointer cursor only in pointer mode.
/// In keyboard/dpad mode, renders as plain text with no interaction.
class _ClickableText extends StatefulWidget {
  final String text;
  final TextStyle? style;
  final VoidCallback onTap;

  const _ClickableText({required this.text, this.style, required this.onTap});

  @override
  State<_ClickableText> createState() => _ClickableTextState();
}

class _ClickableTextState extends State<_ClickableText> {
  bool _isHovered = false;

  @override
  Widget build(BuildContext context) {
    final isKeyboard = InputModeTracker.isKeyboardMode(context);
    final baseStyle = widget.style ?? const TextStyle();

    if (isKeyboard) {
      return Text(widget.text, maxLines: 1, overflow: TextOverflow.ellipsis, style: baseStyle);
    }

    return MouseRegion(
      cursor: SystemMouseCursors.click,
      onEnter: (_) => setState(() => _isHovered = true),
      onExit: (_) => setState(() => _isHovered = false),
      child: GestureDetector(
        onTap: widget.onTap,
        child: Text(
          widget.text,
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
          style: baseStyle.copyWith(
            decoration: _isHovered ? TextDecoration.underline : null,
            decorationColor: baseStyle.color,
          ),
        ),
      ),
    );
  }
}

/// Static skeleton placeholder with a fixed semi-transparent fill.
class SkeletonLoader extends StatelessWidget {
  final Widget? child;
  final BorderRadius? borderRadius;

  const SkeletonLoader({super.key, this.child, this.borderRadius});

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.075),
        borderRadius: borderRadius ?? BorderRadius.circular(tokens(context).radiusSm),
      ),
      child: child,
    );
  }
}
