#include "detailview.h"
#include "../../components/detailactionwidget.h"
#include "../../components/detailbottominfowidget.h"
#include "../../components/detailcontentwidget.h"
#include "../../components/detailtagbutton.h"
#include "../../components/flowlayout.h"
#include "../../components/horizontallistviewgallery.h"
#include "../../components/mediaactionmenu.h"
#include "../../components/mediasectionwidget.h"
#include "../../components/modernmenubutton.h"
#include "../../components/moderntoast.h"
#include "../../managers/playbackmanager.h"
#include "../../managers/thememanager.h"
#include "../../utils/mediaitemutils.h"
#include "../../utils/detailcacheutils.h"
#include "../../utils/mediasourcepreferenceutils.h"
#include "../../utils/numberextractor.h"
#include "../../utils/playerpreferenceutils.h"
#include "../../utils/smoothscrollcontroller.h"
#include "overviewdialog.h"
#include <config/config_keys.h>
#include <config/configstore.h>
#include <qembycore.h>
#include <services/manager/servermanager.h>
#include <services/media/mediaservice.h>

#include <QClipboard>
#include <QDate>
#include <QDebug>
#include <QEvent>
#include <QFuture>
#include <QFutureWatcher>
#include <QGraphicsDropShadowEffect>
#include <QGridLayout>
#include <QGuiApplication>
#include <QHash>
#include <QHBoxLayout>
#include <QIcon>
#include <QImage>
#include <QIntValidator>
#include <QLabel>
#include <QLineEdit>
#include <QListView>
#include <QPainter>
#include <QPainterPath>
#include <QPointer>
#include <QPushButton>
#include <QRegularExpression>
#include <QScrollArea>
#include <QScrollBar>
#include <QSignalBlocker>
#include <QTextDocument>
#include <QTimer>
#include <QVBoxLayout>
#include <QWheelEvent>
#include <QtConcurrent/QtConcurrentRun>

#include <qcorofuture.h>

#include <optional>

namespace {

QWidget *createTransparentReserveWidget(QWidget *parent, const char *objectName,
                                        int height) {
  auto *widget = new QWidget(parent);
  widget->setObjectName(QString::fromLatin1(objectName));
  widget->setFixedHeight(height);
  widget->hide();
  return widget;
}

int reserveHeightFor(QWidget *widget, int fallbackHeight) {
  if (!widget)
    return fallbackHeight;

  const int hintHeight = widget->sizeHint().height();
  return qMax(fallbackHeight, hintHeight);
}

void showReserveWidget(QWidget *reserve, int height = -1) {
  if (!reserve)
    return;
  if (height > 0 && reserve->height() != height)
    reserve->setFixedHeight(height);
  reserve->show();
}

void hideReserveWidget(QWidget *reserve) {
  if (reserve)
    reserve->hide();
}

void prepareReservedSection(QWidget *reserve, MediaSectionWidget *section,
                            int height = -1) {
  showReserveWidget(reserve, height);
  if (section)
    section->clear();
}

void applyReservedSectionItems(QWidget *reserve, MediaSectionWidget *section,
                               const QList<MediaItem> &items) {
  if (!section) {
    hideReserveWidget(reserve);
    return;
  }

  if (items.isEmpty()) {
    section->clear();
    hideReserveWidget(reserve);
    return;
  }

  hideReserveWidget(reserve);
  section->setItems(items);
}







QString computeSourcesFingerprint(const QList<MediaSourceInfo> &sources) {
  if (sources.isEmpty())
    return QStringLiteral("<empty>");
  QStringList parts;
  parts.reserve(sources.size());
  for (const auto &src : sources) {
    QStringList streamParts;
    streamParts.reserve(src.mediaStreams.size());
    for (const auto &s : src.mediaStreams) {
      streamParts << QStringLiteral("%1:%2:%3:%4")
                         .arg(s.type, QString::number(s.index), s.codec,
                              s.language);
    }
    parts << QStringLiteral("%1#%2#%3")
                 .arg(src.id, QString::number(src.mediaStreams.size()),
                      streamParts.join(QLatin1Char('|')));
  }
  return parts.join(QLatin1Char(';'));
}




QString computeBottomInfoFingerprint(const MediaItem &item,
                                     const QList<MediaSourceInfo> &sources) {
  QStringList studioParts;
  studioParts.reserve(item.studios.size());
  for (const auto &s : item.studios)
    studioParts << s.name;

  QStringList urlParts;
  urlParts.reserve(item.externalUrls.size());
  for (const auto &u : item.externalUrls)
    urlParts << (u.name + QLatin1Char('=') + u.url);

  return QStringLiteral("%1|tags=%2|studios=%3|urls=%4|src=%5")
      .arg(item.id, item.tags.join(QLatin1Char(',')),
           studioParts.join(QLatin1Char(',')),
           urlParts.join(QLatin1Char(',')), computeSourcesFingerprint(sources));
}

QString formatDetailReleaseDate(const MediaItem &item) {
  QString dateText = item.premiereDate.trimmed();
  if (!dateText.isEmpty()) {
    const int timeSeparator = dateText.indexOf(QLatin1Char('T'));
    if (timeSeparator > 0)
      dateText = dateText.left(timeSeparator);

    
    const int dateLen = dateText.length();
    if (dateLen > 0) {
      const QString dateOnly = dateText.left(qMin(dateLen, 10));
      const QDate date = QDate::fromString(dateOnly, Qt::ISODate);
      if (date.isValid())
        return date.toString(QStringLiteral("yyyy-MM-dd"));
      return dateOnly;
    }
  }

  if (item.productionYear > 0)
    return QString::number(item.productionYear);

  return QString();
}

QString formatEpisodeTag(const MediaItem &item) {
  QString tag;
  if (item.parentIndexNumber >= 0) {
    tag += QStringLiteral("S%1").arg(item.parentIndexNumber, 2, 10,
                                     QLatin1Char('0'));
  }
  if (item.indexNumber >= 0) {
    tag += QStringLiteral("E%1").arg(item.indexNumber, 2, 10,
                                     QLatin1Char('0'));
  }
  return tag;
}

MediaItem resolveSeasonPlaybackEpisode(const QList<MediaItem> &episodes) {
  for (const MediaItem &episode : episodes) {
    if (episode.userData.playbackPositionTicks > 0 &&
        !episode.userData.played && episode.userData.playedPercentage < 99.0) {
      return episode;
    }
  }

  for (const MediaItem &episode : episodes) {
    if (!episode.userData.played)
      return episode;
  }

  return episodes.isEmpty() ? MediaItem{} : episodes.first();
}

MediaItem withSeedContext(MediaItem cachedItem, const MediaItem &seedItem) {
  if (seedItem.id.isEmpty() || seedItem.id != cachedItem.id)
    return cachedItem;

  if (cachedItem.name.trimmed().isEmpty())
    cachedItem.name = seedItem.name;
  if (cachedItem.type.trimmed().isEmpty())
    cachedItem.type = seedItem.type;
  if (cachedItem.mediaType.trimmed().isEmpty())
    cachedItem.mediaType = seedItem.mediaType;
  if (cachedItem.overview.trimmed().isEmpty())
    cachedItem.overview = seedItem.overview;
  if (cachedItem.genres.isEmpty())
    cachedItem.genres = seedItem.genres;
  if (cachedItem.taglines.isEmpty())
    cachedItem.taglines = seedItem.taglines;
  if (cachedItem.mediaSources.isEmpty())
    cachedItem.mediaSources = seedItem.mediaSources;
  if (cachedItem.images.primaryTag.isEmpty() &&
      cachedItem.images.thumbTag.isEmpty() &&
      cachedItem.images.backdropTag.isEmpty() &&
      cachedItem.images.logoTag.isEmpty()) {
    cachedItem.images = seedItem.images;
  }

  cachedItem.playlistId = seedItem.playlistId;
  cachedItem.playlistItemId = seedItem.playlistItemId;
  cachedItem.hasResumeContext = seedItem.hasResumeContext;
  cachedItem.resumeItemId = seedItem.resumeItemId;
  cachedItem.resumeUserData = seedItem.resumeUserData;
  return cachedItem;
}

bool mediaItemListsEqual(const QList<MediaItem> &left,
                         const QList<MediaItem> &right) {
  if (left.size() != right.size())
    return false;

  for (int i = 0; i < left.size(); ++i) {
    if (DetailCacheUtils::fingerprint(left[i]) !=
        DetailCacheUtils::fingerprint(right[i])) {
      return false;
    }
  }
  return true;
}

QCoro::Task<bool> mediaItemListsEqualAsync(QList<MediaItem> left,
                                           QList<MediaItem> right) {
  co_return co_await QtConcurrent::run(
      [left = std::move(left), right = std::move(right)]() mutable {
        return mediaItemListsEqual(left, right);
      });
}

QString castFingerprint(const MediaItem &item) {
  QStringList parts;
  parts.reserve(item.people.size());
  for (const auto &person : item.people) {
    parts << QStringLiteral("%1:%2:%3:%4")
                 .arg(person.id, person.name, person.type, person.role);
  }
  return parts.join(QLatin1Char('|'));
}

bool episodesBelongToSeason(const QList<MediaItem> &episodes,
                            const MediaItem &season) {
  if (episodes.isEmpty() || season.indexNumber < 0)
    return true;

  for (const MediaItem &episode : episodes) {
    if (episode.parentIndexNumber < 0)
      continue;
    if (episode.parentIndexNumber != season.indexNumber)
      return false;
  }
  return true;
}

bool cacheEntryHasAnySecondaryData(
    const DetailCacheUtils::DetailCacheEntry &entry) {
  return entry.hasPlayableItem || entry.hasSeasons || entry.hasSeasonEpisodes ||
         entry.hasSimilarItems || entry.hasCollections ||
         entry.hasAdditionalParts || !entry.item.people.isEmpty();
}

struct CachedDetailImages {
  std::optional<QImage> poster;
  std::optional<QImage> backdrop;
  std::optional<QImage> logo;
};

void saveDetailCacheAsync(QString serverId, QString userId, MediaItem item,
                          QString fingerprint) {
  if (serverId.isEmpty() || userId.isEmpty() || item.id.isEmpty())
    return;

  (void)QtConcurrent::run([serverId = std::move(serverId),
                           userId = std::move(userId), item = std::move(item),
                           fingerprint = std::move(fingerprint)]() mutable {
    QString errorString;
    const QString itemId = item.id;
    DetailCacheUtils::DetailCacheEntry entry;
    const auto existing = DetailCacheUtils::load(serverId, userId, itemId);
    if (existing.has_value())
      entry = *existing;
    entry.item = item;
    if (entry.hasPlayableItem && !entry.playableItemFromServer) {
      entry.hasPlayableItem = false;
      entry.playableItem = MediaItem{};
    }
    const bool saved =
        DetailCacheUtils::save(serverId, userId, entry, &errorString);
    if (saved) {
      qDebug() << "[DetailView] Detail cache saved"
               << "itemId=" << itemId
               << "fingerprint=" << fingerprint.left(12);
    } else {
      qWarning() << "[DetailView] Detail cache save failed"
                 << "itemId=" << itemId << "error=" << errorString;
    }
  });
}

void saveDetailCacheEntryAsync(QString serverId, QString userId,
                               DetailCacheUtils::DetailCacheEntry entry,
                               QString reason) {
  if (serverId.isEmpty() || userId.isEmpty() || entry.item.id.isEmpty())
    return;

  (void)QtConcurrent::run([serverId = std::move(serverId),
                           userId = std::move(userId), entry = std::move(entry),
                           reason = std::move(reason)]() mutable {
    QString errorString;
    const QString itemId = entry.item.id;
    const auto existing = DetailCacheUtils::load(serverId, userId, itemId);
    if (existing.has_value()) {
      if (!entry.hasPlayableItem && existing->hasPlayableItem &&
          existing->playableItemFromServer) {
        entry.hasPlayableItem = true;
        entry.playableItem = existing->playableItem;
        entry.playableItemFromServer = true;
      }
      if (!entry.hasSeasons && existing->hasSeasons) {
        entry.hasSeasons = true;
        entry.seasons = existing->seasons;
      }
      if (!entry.hasSeasonEpisodes && existing->hasSeasonEpisodes) {
        entry.hasSeasonEpisodes = true;
        entry.seasonEpisodes = existing->seasonEpisodes;
        entry.seasonIndex = existing->seasonIndex;
        entry.seasonId = existing->seasonId;
      }
      if (!entry.hasSimilarItems && existing->hasSimilarItems) {
        entry.hasSimilarItems = true;
        entry.similarItems = existing->similarItems;
      }
      if (!entry.hasCollections && existing->hasCollections) {
        entry.hasCollections = true;
        entry.collections = existing->collections;
      }
      if (!entry.hasAdditionalParts && existing->hasAdditionalParts) {
        entry.hasAdditionalParts = true;
        entry.additionalParts = existing->additionalParts;
      }
    }
    if (entry.hasPlayableItem && !entry.playableItemFromServer) {
      entry.hasPlayableItem = false;
      entry.playableItem = MediaItem{};
    }
    const QString itemFp = DetailCacheUtils::fingerprint(entry.item);
    const QString sectionsFp = DetailCacheUtils::sectionsFingerprint(entry);
    const bool saved =
        DetailCacheUtils::save(serverId, userId, entry, &errorString);
    if (saved) {
      qDebug() << "[DetailView] Detail cache snapshot saved"
               << "itemId=" << itemId << "reason=" << reason
               << "fingerprint=" << itemFp.left(12)
               << "sections=" << sectionsFp.left(12);
    } else {
      qWarning() << "[DetailView] Detail cache snapshot save failed"
                 << "itemId=" << itemId << "reason=" << reason
                 << "error=" << errorString;
    }
  });
}

void saveDetailImageAsync(QString serverId, QString userId,
                          QString ownerItemId, QString role,
                          QString imageItemId, QString imageType,
                          QString imageTag, int maxWidth, QImage image) {
  if (serverId.isEmpty() || userId.isEmpty() || ownerItemId.isEmpty() ||
      imageTag.isEmpty() || image.isNull())
    return;

  (void)QtConcurrent::run([serverId = std::move(serverId),
                           userId = std::move(userId),
                           ownerItemId = std::move(ownerItemId),
                           role = std::move(role),
                           imageItemId = std::move(imageItemId),
                           imageType = std::move(imageType),
                           imageTag = std::move(imageTag), maxWidth,
                           image = std::move(image)]() mutable {
    QString errorString;
    const bool saved = DetailCacheUtils::saveImage(
        serverId, userId, ownerItemId, role, imageItemId, imageType, imageTag,
        maxWidth, image, &errorString);
    if (!saved) {
      qWarning() << "[DetailView] Detail image cache save failed"
                 << "itemId=" << ownerItemId << "role=" << role
                 << "error=" << errorString;
    }
  });
}

} 

DetailView::DetailView(QEmbyCore *core, QWidget *parent)
    : BaseView(core, parent) {
  setAttribute(Qt::WA_StyledBackground, true);
  setObjectName("detail-view");
  setProperty("showGlobalSearch", true);
  setupUi();
}

void DetailView::scrollToTop() {
  if (m_mainScrollArea && m_mainScrollArea->verticalScrollBar()) {
    if (m_vScrollController) {
      m_vScrollController->scrollTo(0, false);
    } else {
      m_mainScrollArea->verticalScrollBar()->setValue(0);
    }
  }
}

void DetailView::setupUi() {
  auto *mainLayout = new QVBoxLayout(this);
  mainLayout->setContentsMargins(0, 0, 0, 0);

  m_mainScrollArea = new QScrollArea(this);
  m_mainScrollArea->setWidgetResizable(true);
  m_mainScrollArea->setFrameShape(QFrame::NoFrame);
  m_mainScrollArea->setObjectName("detail-scrollarea");
  m_mainScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  m_mainScrollArea->setStyleSheet("QScrollArea { background: transparent; }");

  m_vScrollController =
      new SmoothScrollController(m_mainScrollArea->verticalScrollBar(), this);
  m_vScrollController->setDuration(170);

  m_contentWidget = new DetailContentWidget(m_mainScrollArea);
  m_contentWidget->setObjectName("detail-content-container");

  auto *contentLayout = new QVBoxLayout(m_contentWidget);
  contentLayout->setContentsMargins(0, 0, 0, 10);
  contentLayout->setSpacing(0);

  QWidget *infoContainer = new QWidget(m_contentWidget);
  infoContainer->setObjectName("detail-info-container");
  infoContainer->setStyleSheet(
      "QWidget#detail-info-container { background: transparent; }");

  m_infoLayout = new QGridLayout(infoContainer);
  m_infoLayout->setContentsMargins(40, 40, 40, 0);
  m_infoLayout->setHorizontalSpacing(40);
  m_infoLayout->setVerticalSpacing(0);
  m_infoLayout->setRowMinimumHeight(0, 110);

  m_logoLabel = new QLabel(infoContainer);
  m_logoLabel->setObjectName("detail-logo-label");
  m_logoLabel->setAlignment(Qt::AlignRight | Qt::AlignTop);
  m_infoLayout->addWidget(m_logoLabel, 0, 1, 1, 2,
                          Qt::AlignRight | Qt::AlignTop);

  m_posterLabel = new QLabel(infoContainer);
  m_posterLabel->setFixedSize(250, 375);
  m_posterLabel->setObjectName("detail-poster-label");
  m_posterLabel->setAlignment(Qt::AlignCenter);

  
  
  
  m_posterShadow = new QGraphicsDropShadowEffect(this);
  m_posterShadow->setBlurRadius(20);
  m_posterShadow->setColor(QColor(0, 0, 0, 80));
  m_posterShadow->setOffset(0, 8);
  m_posterShadow->setEnabled(false);
  m_posterLabel->setGraphicsEffect(m_posterShadow);
  m_infoLayout->addWidget(m_posterLabel, 1, 0, 1, 1, Qt::AlignTop);

  m_textContainer = new QWidget(infoContainer);
  m_textContainer->setMaximumWidth(1200);
  m_textContainer->setSizePolicy(QSizePolicy::Expanding,
                                 QSizePolicy::Preferred);
  auto *textLayout = new QVBoxLayout(m_textContainer);
  textLayout->setContentsMargins(0, 0, 0, 0);
  textLayout->setSpacing(10);
  textLayout->setAlignment(Qt::AlignTop);

  m_titleLabel = new QLabel(m_textContainer);
  m_titleLabel->setObjectName("detail-title");
  m_titleLabel->setWordWrap(true);

  m_metaRowWidget = new QWidget(m_textContainer);
  m_metaRowWidget->setObjectName("detail-meta-row");
  auto *metaLayout = new QHBoxLayout(m_metaRowWidget);
  metaLayout->setContentsMargins(0, 0, 0, 0);
  metaLayout->setSpacing(6);

  m_ratingStarLabel = new QLabel(m_metaRowWidget);
  m_ratingStarLabel->setFixedWidth(16);
  m_ratingStarLabel->setAlignment(Qt::AlignCenter);
  m_ratingStarLabel->setPixmap(
      QIcon(":/svg/dark/star-filled.svg").pixmap(16, 16));
  m_ratingStarLabel->setContentsMargins(0, 2, 0, 0);
  m_ratingStarLabel->hide();

  m_metaLabel = new QLabel(m_metaRowWidget);
  m_metaLabel->setObjectName("detail-meta");
  m_metaLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);

  m_numberButton = new QPushButton(m_metaRowWidget);
  m_numberButton->setObjectName("detail-number-chip");
  m_numberButton->setCursor(Qt::PointingHandCursor);
  m_numberButton->setFocusPolicy(Qt::NoFocus);
  m_numberButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
  m_numberButton->hide();
  connect(m_numberButton, &QPushButton::clicked, this,
          &DetailView::copyDisplayedNumber);

  metaLayout->addWidget(m_ratingStarLabel);
  metaLayout->addWidget(m_metaLabel, 0, Qt::AlignLeft);
  metaLayout->addWidget(m_numberButton, 0, Qt::AlignLeft);
  metaLayout->addStretch(1);
  m_metaRowWidget->hide();

  m_tagsWidget = new QWidget(m_textContainer);
  m_tagsWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
  m_tagsLayout = new FlowLayout(m_tagsWidget, 0, 8, 8);

  m_actionWidget = new DetailActionWidget(m_textContainer);
  connect(m_actionWidget, &DetailActionWidget::playRequested, this,
          [this]() -> QCoro::Task<void> {
            co_await executePlay(m_currentPlayableItem, 0);
          });
  connect(m_actionWidget, &DetailActionWidget::resumeRequested, this,
          [this]() -> QCoro::Task<void> {
            co_await executePlay(
                m_currentPlayableItem,
                m_currentPlayableItem.userData.playbackPositionTicks);
          });
  connect(m_actionWidget, &DetailActionWidget::favoriteRequested, this,
          [this]() {
            if (!m_currentMediaItem.id.isEmpty())
              handleFavoriteRequested(m_currentMediaItem);
          });
  connect(m_actionWidget, &DetailActionWidget::playedToggleRequested, this,
          [this]() {
            const MediaItem &target =
                m_currentPlayableItem.type == "Episode" ? m_currentPlayableItem
                                                        : m_currentMediaItem;
            if (target.id.isEmpty())
              return;
            if (target.userData.played)
              handleMarkUnplayedRequested(target);
            else
              handleMarkPlayedRequested(target);
          });
  connect(ThemeManager::instance(), &ThemeManager::themeChanged, this,
          [this]() {
            const MediaItem &target =
                m_currentPlayableItem.type == "Episode" ? m_currentPlayableItem
                                                        : m_currentMediaItem;
            if (!target.id.isEmpty())
              m_actionWidget->setPlayedState(target.userData.played);
          });
  connect(m_actionWidget, &DetailActionWidget::sourceVersionChanged, this,
          &DetailView::onVersionChanged);
  connect(m_actionWidget, &DetailActionWidget::externalPlayRequested, this,
          [this](const QString &playerPath) -> QCoro::Task<void> {
            co_await executeExternalPlay(m_currentPlayableItem, playerPath);
          });

  m_taglineLabel = new QLabel(m_textContainer);
  m_taglineLabel->setObjectName("detail-tagline");

  m_overviewLabel = new QLabel(m_textContainer);
  m_overviewLabel->setObjectName("detail-overview");
  m_overviewLabel->setWordWrap(true);
  m_overviewLabel->setSizePolicy(QSizePolicy::Preferred,
                                 QSizePolicy::Preferred);
  m_overviewLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);
  m_overviewLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
  connect(m_overviewLabel, &QLabel::linkActivated, this,
          &DetailView::onOverviewMoreClicked);

  textLayout->addWidget(m_titleLabel);
  textLayout->addWidget(m_metaRowWidget);
  textLayout->addWidget(m_tagsWidget);
  textLayout->addWidget(m_actionWidget);
  textLayout->addSpacing(4);
  textLayout->addWidget(m_taglineLabel);
  textLayout->addWidget(m_overviewLabel);
  textLayout->addSpacing(10);

  m_infoLayout->addWidget(m_textContainer, 1, 1, 1, 1, Qt::AlignTop);
  m_infoLayout->setColumnStretch(0, 0);
  m_infoLayout->setColumnStretch(1, 10);
  m_infoLayout->setColumnStretch(2, 1);
  contentLayout->addWidget(infoContainer);

  
  
  
  m_seasonWidget =
      new MediaSectionWidget(tr("Seasons"), m_core, m_contentWidget);
  m_seasonWidget->setCardStyle(MediaCardDelegate::Poster);
  m_seasonWidget->setGalleryHeight(300);

  
  m_episodeWidget =
      new MediaSectionWidget(tr("Episodes"), m_core, m_contentWidget);
  m_episodeWidget->setCardStyle(MediaCardDelegate::LibraryTile);
  m_episodeWidget->gallery()->setHoverControls(
      MediaCardDelegate::HoverControlPlay |
      MediaCardDelegate::HoverControlMore);
  
  
  {
    int imgH = 100;
    int imgW = qRound(imgH * 16.0 / 9.0); 
    const QSize episodeTileSize(imgW + 16, 8 + imgH + 6 + 20); 
    m_episodeTileWidth = episodeTileSize.width();
    m_episodeWidget->setTileSize(episodeTileSize);
  }
  m_episodeWidget->setGalleryHeight(145);

  
  m_episodeHeaderControls = new QWidget(m_episodeWidget);
  m_episodeHeaderControls->setObjectName("detail-episode-header-controls");
  m_episodeHeaderControls->setSizePolicy(QSizePolicy::Fixed,
                                         QSizePolicy::Fixed);
  auto *episodeHeaderLayout = new QHBoxLayout(m_episodeHeaderControls);
  episodeHeaderLayout->setContentsMargins(0, 0, 0, 0);
  episodeHeaderLayout->setSpacing(6);
  episodeHeaderLayout->setSizeConstraint(QLayout::SetFixedSize);

  m_seasonSwitcher = new ModernMenuButton(m_episodeHeaderControls);
  m_seasonSwitcher->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
  m_seasonSwitcher->hide(); 

  m_episodeJumpEdit = new QLineEdit(m_episodeHeaderControls);
  m_episodeJumpEdit->setObjectName("detail-episode-jump-edit");
  m_episodeJumpEdit->setPlaceholderText(tr("Ep #"));
  m_episodeJumpEdit->setToolTip(tr("Jump to episode number"));
  m_episodeJumpEdit->setAlignment(Qt::AlignCenter);
  m_episodeJumpEdit->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
  m_episodeJumpEdit->setFixedWidth(64);
  m_episodeJumpEdit->setFixedHeight(m_seasonSwitcher->sizeHint().height());
  m_episodeJumpEdit->setMaxLength(4);
  m_episodeJumpValidator = new QIntValidator(1, 9999, m_episodeJumpEdit);
  m_episodeJumpEdit->setValidator(m_episodeJumpValidator);
  m_episodeJumpEdit->hide(); 

  episodeHeaderLayout->addWidget(m_seasonSwitcher, 0, Qt::AlignVCenter);
  episodeHeaderLayout->addWidget(m_episodeJumpEdit, 0, Qt::AlignVCenter);
  m_episodeHeaderControls->hide();
  m_episodeWidget->setHeaderWidget(m_episodeHeaderControls);
  m_episodeHeaderControls->hide();

  m_episodeSectionReserveWidget = createTransparentReserveWidget(
      m_contentWidget, "detail-episode-section-reserve",
      reserveHeightFor(m_episodeWidget, 190));
  m_seasonSectionReserveWidget = createTransparentReserveWidget(
      m_contentWidget, "detail-season-section-reserve",
      reserveHeightFor(m_seasonWidget, 320));

  connect(m_seasonSwitcher, &ModernMenuButton::currentIndexChanged, this,
          [this](int idx) {
            if (idx >= 0 && idx != m_currentSeasonIndex) {
              markManualSeriesSelection(QStringLiteral("season-switcher"));
              m_currentSeasonIndex = idx;
              QCoro::connect(switchToSeason(idx, true), this, []() {});
            }
          });
  connect(m_episodeJumpEdit, &QLineEdit::editingFinished, this,
          &DetailView::submitEpisodeJump);

  m_castWidget =
      new MediaSectionWidget(tr("Cast & Crew"), m_core, m_contentWidget);
  m_castWidget->setCardStyle(MediaCardDelegate::Cast);
  m_castWidget->setGalleryHeight(280);

  m_castSectionReserveWidget = new QWidget(m_contentWidget);
  m_castSectionReserveWidget->setObjectName("detail-cast-section-reserve");
  m_castSectionReserveWidget->setFixedHeight(
      qMax(320, m_castWidget->sizeHint().height()));
  m_castSectionReserveWidget->hide();

  m_additionalPartsWidget =
      new MediaSectionWidget(tr("Additional Parts"), m_core, m_contentWidget);
  m_additionalPartsWidget->setCardStyle(MediaCardDelegate::Poster);
  m_additionalPartsWidget->setGalleryHeight(300);
  m_additionalPartsSectionReserveWidget = createTransparentReserveWidget(
      m_contentWidget, "detail-additional-section-reserve",
      reserveHeightFor(m_additionalPartsWidget, 350));

  m_collectionWidget =
      new MediaSectionWidget(tr("Collection"), m_core, m_contentWidget);
  m_collectionWidget->setCardStyle(MediaCardDelegate::Poster);
  m_collectionWidget->setGalleryHeight(300);
  m_collectionSectionReserveWidget = createTransparentReserveWidget(
      m_contentWidget, "detail-collection-section-reserve",
      reserveHeightFor(m_collectionWidget, 350));

  m_similarWidget =
      new MediaSectionWidget(tr("More Like This"), m_core, m_contentWidget);
  m_similarWidget->setCardStyle(MediaCardDelegate::Poster);
  m_similarWidget->setGalleryHeight(300);
  m_similarSectionReserveWidget = createTransparentReserveWidget(
      m_contentWidget, "detail-similar-section-reserve",
      reserveHeightFor(m_similarWidget, 350));

  m_seasonWidget->gallery()->listView()->viewport()->installEventFilter(this);
  m_episodeWidget->gallery()->listView()->viewport()->installEventFilter(this);
  m_castWidget->gallery()->listView()->viewport()->installEventFilter(this);
  m_additionalPartsWidget->gallery()
      ->listView()
      ->viewport()
      ->installEventFilter(this);
  m_collectionWidget->gallery()->listView()->viewport()->installEventFilter(
      this);
  m_similarWidget->gallery()->listView()->viewport()->installEventFilter(this);

  auto connectGallerySignals = [this](MediaSectionWidget *widget) {
    connect(widget, &MediaSectionWidget::playRequested, this,
            &BaseView::handlePlayRequested);
    connect(widget, &MediaSectionWidget::favoriteRequested, this,
            &BaseView::handleFavoriteRequested);
    connect(widget, &MediaSectionWidget::moreMenuRequested, this,
            &BaseView::handleMoreMenuRequested);
  };

  connectGallerySignals(m_castWidget);
  connectGallerySignals(m_additionalPartsWidget);
  connectGallerySignals(m_collectionWidget);
  connectGallerySignals(m_similarWidget);

  connect(m_seasonWidget, &MediaSectionWidget::playRequested, this,
          [this](MediaItem item) -> QCoro::Task<void> {
            if (item.type == "Season") {
              markManualSeriesSelection(QStringLiteral("season-play"));
              co_await executePlaySeason(item);
            } else {
              handlePlayRequested(item);
            }
          });
  connect(m_seasonWidget, &MediaSectionWidget::favoriteRequested, this,
          &BaseView::handleFavoriteRequested);
  connect(m_seasonWidget, &MediaSectionWidget::moreMenuRequested, this,
          &BaseView::handleMoreMenuRequested);

  connect(m_episodeWidget, &MediaSectionWidget::playRequested, this,
          [this](MediaItem item) -> QCoro::Task<void> {
            if (item.type == "Episode") {
              markManualSeriesSelection(QStringLiteral("episode-play"));
              co_await applySeriesPlayableItem(item, false);
              if (m_currentPlayableItem.id == item.id) {
                co_await executePlay(
                    m_currentPlayableItem,
                    m_currentPlayableItem.userData.playbackPositionTicks);
              }
            } else {
              handlePlayRequested(item);
            }
          });
  connect(m_episodeWidget, &MediaSectionWidget::favoriteRequested, this,
          &BaseView::handleFavoriteRequested);
  connect(m_episodeWidget, &MediaSectionWidget::moreMenuRequested, this,
          [this](const MediaItem &item, const QPoint &globalPos) {
            MediaActionMenu menu(item, m_core, this,
                                 {CardContextMenuAction::MarkPlayed,
                                  CardContextMenuAction::MarkUnplayed});
            CardContextMenuRequest request = menu.execRequest(globalPos);
            dispatchCardContextMenuRequest(item, request);
          });

  auto commonClicked = [this](const MediaItem &item) {
    if (item.type == "BoxSet" || item.type == "Playlist" ||
        item.type == "Folder")
      Q_EMIT navigateToFolder(item.id, item.name);
    else if (item.type == "Person")
      Q_EMIT navigateToPerson(item.id, item.name);
    else
      Q_EMIT navigateToDetail(item.id, item.name, item);
  };

  
  connect(m_seasonWidget, &MediaSectionWidget::itemClicked, this,
          [this](const MediaItem &item) {
            if (item.type == "Season")
              Q_EMIT navigateToSeason(m_currentItemId, item.id, item.name);
            else
              Q_EMIT navigateToDetail(item.id, item.name, item);
          });

  
  connect(m_episodeWidget, &MediaSectionWidget::itemClicked, this,
          [this, commonClicked](const MediaItem &item) {
            if (item.type == "Episode") {
              markManualSeriesSelection(QStringLiteral("episode-click"));
              QCoro::connect(applySeriesPlayableItem(item, true), this,
                             []() {});
            } else {
              commonClicked(item);
            }
          });

  connect(m_castWidget, &MediaSectionWidget::itemClicked, this,
          [this](const MediaItem &item) {
            Q_EMIT navigateToPerson(item.id, item.name);
          });
  connect(m_additionalPartsWidget, &MediaSectionWidget::itemClicked, this,
          commonClicked);
  connect(m_collectionWidget, &MediaSectionWidget::itemClicked, this,
          [this, commonClicked](const MediaItem &item) {
            if (!item.id.isEmpty()) {
              qDebug() << "[DetailView] Navigate collection membership"
                       << "itemId=" << item.id << "name=" << item.name
                       << "type=" << item.type
                       << "collectionType=" << item.collectionType;
              Q_EMIT navigateToFolder(item.id, item.name);
              return;
            }
            commonClicked(item);
          });
  connect(m_similarWidget, &MediaSectionWidget::itemClicked, this,
          commonClicked);

  contentLayout->addWidget(m_episodeSectionReserveWidget);
  contentLayout->addWidget(m_episodeWidget);
  contentLayout->addWidget(m_seasonSectionReserveWidget);
  contentLayout->addWidget(m_seasonWidget);
  contentLayout->addWidget(m_castSectionReserveWidget);
  contentLayout->addWidget(m_castWidget);
  contentLayout->addWidget(m_additionalPartsSectionReserveWidget);
  contentLayout->addWidget(m_additionalPartsWidget);
  contentLayout->addWidget(m_collectionSectionReserveWidget);
  contentLayout->addWidget(m_collectionWidget);
  contentLayout->addWidget(m_similarSectionReserveWidget);
  contentLayout->addWidget(m_similarWidget);

  m_bottomInfoWidget = new DetailBottomInfoWidget(m_contentWidget);
  connect(m_bottomInfoWidget, &DetailBottomInfoWidget::filterClicked, this,
          [this](const QString &type, const QString &value) {
            Q_EMIT navigateToFilteredView(type, value);
          });
  m_bottomInfoReserveWidget = createTransparentReserveWidget(
      m_contentWidget, "detail-bottom-info-reserve", 220);
  contentLayout->addWidget(m_bottomInfoReserveWidget);
  contentLayout->addWidget(m_bottomInfoWidget);

  contentLayout->addStretch();
  m_mainScrollArea->setWidget(m_contentWidget);
  mainLayout->addWidget(m_mainScrollArea);
  m_mainScrollArea->viewport()->installEventFilter(this);
}

QCoro::Task<void> DetailView::loadItem(const QString &itemId,
                                       const MediaItem &seedItem) {
  QPointer<DetailView> safeThis(this);
  setDeferredPresentationBatchActive(false);
  m_currentItemId = itemId;
  
  
  m_skipNextSilentRefresh = true;
  m_pendingDeferredFetchId = itemId;
  m_pendingFetchedItem = MediaItem{};
  m_pendingFetchReady = false;
  m_pendingAnimationGuardDone = false;
  m_pendingFetchedItemFromCache = false;
  m_deferBottomInfoReveal = false;
  m_currentPlayableItem = MediaItem{};
  m_currentSeasonEpisodes.clear();
  updateEpisodeJumpControl(0);
  
  m_appliedSourcesFingerprint.clear();
  m_appliedBottomInfoFingerprint.clear();
  m_detailCacheServerId.clear();
  m_detailCacheUserId.clear();
  m_cachedDetailFingerprint.clear();
  m_cachedSectionsFingerprint.clear();
  m_cachedSeriesPlayableItem = MediaItem{};
  m_cachedSeriesSeasons.clear();
  m_cachedSeasonEpisodes.clear();
  m_cachedSimilarItems.clear();
  m_cachedCollections.clear();
  m_cachedAdditionalParts.clear();
  m_cachedSeasonId.clear();
  m_manualSelectedSeasonId.clear();
  m_manualSelectedEpisodeId.clear();
  m_cachedSeasonIndex = -1;
  m_manualSelectedSeasonIndex = -1;
  m_hasManualSeriesSelection = false;
  m_hasCachedSeriesPlayableItem = false;
  m_hasCachedSeriesSeasons = false;
  m_hasCachedSeasonEpisodes = false;
  m_hasCachedSimilarItems = false;
  m_hasCachedCollections = false;
  m_hasCachedAdditionalParts = false;
  m_hasCachedCastData = false;
  m_appliedCachedCastToUi = false;
  m_appliedCachedSeriesSeasonsToUi = false;
  m_appliedCachedSeasonEpisodesToUi = false;
  m_appliedCachedSimilarItemsToUi = false;
  m_appliedCachedCollectionsToUi = false;
  m_appliedCachedAdditionalPartsToUi = false;
  m_appliedCastFingerprint.clear();
  m_appliedSeriesPlayableFingerprint.clear();

  if (m_core && m_core->serverManager()) {
    const ServerProfile profile = m_core->serverManager()->activeProfile();
    m_detailCacheServerId = profile.id;
    m_detailCacheUserId = profile.userId;
  }

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  const bool hasSeed = !seedItem.id.isEmpty();
  
  
  
  m_deferBottomInfoReveal = hasSeed;

  if (hasSeed) {
    m_currentPlayableItem = seedItem;
    applySeedToUi(seedItem);
    if (seedItem.type == "Series") {
      prepareReservedSection(m_episodeSectionReserveWidget, m_episodeWidget,
                             reserveHeightFor(m_episodeWidget, 190));
      prepareReservedSection(m_seasonSectionReserveWidget, m_seasonWidget,
                             reserveHeightFor(m_seasonWidget, 320));
    } else {
      hideReserveWidget(m_episodeSectionReserveWidget);
      hideReserveWidget(m_seasonSectionReserveWidget);
      m_seasonWidget->clear();
      m_episodeWidget->clear();
    }
  } else {
    
    m_titleLabel->setText(tr("Loading..."));
    m_logoLabel->clear();
    m_logoLabel->hide();
    m_metaLabel->clear();
    updateDisplayNumber(QString());
    m_metaRowWidget->hide();
    m_overviewLabel->clear();
    m_taglineLabel->hide();

    m_currentBackdropPix = QPixmap();
    m_currentPosterPix = QPixmap();
    updateBackdrop();
    m_posterLabel->setPixmap(QPixmap());

    m_actionWidget->clear();
    m_bottomInfoWidget->clear();
    clearLayout(m_tagsLayout);

    m_seasonWidget->clear();
    m_episodeWidget->clear();
    hideReserveWidget(m_episodeSectionReserveWidget);
    hideReserveWidget(m_seasonSectionReserveWidget);

    m_seriesSeasons.clear();
    m_currentSeasonIndex = 0;
    if (m_seasonSwitcher) {
      QSignalBlocker blocker(m_seasonSwitcher);
      m_seasonSwitcher->clear();
      m_seasonSwitcher->hide();
    }
    updateEpisodeHeaderControlsVisibility();
  }

  
  
  prepareReservedSection(m_castSectionReserveWidget, m_castWidget,
                         reserveHeightFor(m_castWidget, 320));
  prepareReservedSection(m_additionalPartsSectionReserveWidget,
                         m_additionalPartsWidget,
                         reserveHeightFor(m_additionalPartsWidget, 350));
  prepareReservedSection(m_collectionSectionReserveWidget, m_collectionWidget,
                         reserveHeightFor(m_collectionWidget, 350));
  prepareReservedSection(m_similarSectionReserveWidget, m_similarWidget,
                         reserveHeightFor(m_similarWidget, 350));

  scrollToTop();

  
  
  if (!itemId.isEmpty()) {
    QCoro::connect(loadDetailCacheAndStartPrefetch(
                       itemId, seedItem, m_detailCacheServerId,
                       m_detailCacheUserId),
                   this, []() {});
  }

  
  
  QTimer::singleShot(380, this, [safeThis, itemId]() {
    if (!safeThis || safeThis->m_pendingDeferredFetchId != itemId)
      return;
    if (safeThis->m_posterShadow)
      safeThis->m_posterShadow->setEnabled(true);
    safeThis->m_pendingAnimationGuardDone = true;
    safeThis->maybeFlushDeferredUpdate(itemId);
  });

  co_return;
}

QCoro::Task<void>
DetailView::loadDetailCacheAndStartPrefetch(QString itemId, MediaItem seedItem,
                                            QString cacheServerId,
                                            QString cacheUserId) {
  QPointer<DetailView> safeThis(this);
  QString targetId = std::move(itemId);
  MediaItem seed = std::move(seedItem);
  QString serverId = std::move(cacheServerId);
  QString userId = std::move(cacheUserId);
  QString cachedFingerprint;

  if (!serverId.isEmpty() && !userId.isEmpty() && !targetId.isEmpty()) {
    const auto cachedEntry = co_await QtConcurrent::run(
        [serverId, userId, targetId]() mutable {
          return DetailCacheUtils::load(serverId, userId, targetId);
        });
    if (!safeThis || safeThis->m_currentItemId != targetId)
      co_return;

    if (cachedEntry.has_value()) {
      MediaItem cachedItem = withSeedContext(cachedEntry->item, seed);
      cachedFingerprint = cachedEntry->fingerprint;

      safeThis->m_cachedDetailFingerprint = cachedEntry->fingerprint;
      safeThis->m_cachedSectionsFingerprint =
          cachedEntry->sectionsFingerprint;
      safeThis->m_cachedSeriesPlayableItem =
          withSeedContext(cachedEntry->playableItem, MediaItem{});
      safeThis->m_cachedSeriesSeasons = cachedEntry->seasons;
      safeThis->m_cachedSeasonEpisodes = cachedEntry->seasonEpisodes;
      safeThis->m_cachedSimilarItems = cachedEntry->similarItems;
      safeThis->m_cachedCollections = cachedEntry->collections;
      safeThis->m_cachedAdditionalParts = cachedEntry->additionalParts;
      safeThis->m_cachedSeasonId = cachedEntry->seasonId;
      safeThis->m_cachedSeasonIndex = cachedEntry->seasonIndex;
      safeThis->m_manualSelectedSeasonId.clear();
      safeThis->m_manualSelectedSeasonIndex = -1;
      safeThis->m_manualSelectedEpisodeId.clear();
      safeThis->m_hasCachedSeriesPlayableItem =
          cachedEntry->hasPlayableItem && cachedEntry->playableItemFromServer;
      if (!safeThis->m_hasCachedSeriesPlayableItem)
        safeThis->m_cachedSeriesPlayableItem = MediaItem{};
      safeThis->m_hasCachedSeriesSeasons = cachedEntry->hasSeasons;
      safeThis->m_hasCachedSeasonEpisodes =
          cachedEntry->hasSeasonEpisodes;
      safeThis->m_hasCachedSimilarItems = cachedEntry->hasSimilarItems;
      safeThis->m_hasCachedCollections = cachedEntry->hasCollections;
      safeThis->m_hasCachedAdditionalParts =
          cachedEntry->hasAdditionalParts;
      safeThis->m_hasCachedCastData = !cachedItem.people.isEmpty();
      const bool hasCachedSecondaryData =
          cacheEntryHasAnySecondaryData(*cachedEntry);

      if (!cachedItem.name.trimmed().isEmpty()) {
        safeThis->m_pendingFetchedItem = cachedItem;
        safeThis->m_pendingFetchReady = true;
        safeThis->m_pendingFetchedItemFromCache = true;

        
        
        const bool hasSeedUi =
            safeThis->m_currentMediaItem.id == targetId &&
            !safeThis->m_currentMediaItem.name.trimmed().isEmpty();
        if (!hasSeedUi) {
          safeThis->applySeedToUi(cachedItem);
        }
        safeThis->maybeFlushDeferredUpdate(targetId);

        qDebug() << "[DetailView] Detail cache hit"
                 << "itemId=" << targetId
                 << "fingerprint=" << cachedFingerprint.left(12)
                 << "hasSecondaryCache=" << hasCachedSecondaryData
                 << "deferredSeedApply=" << hasSeedUi;
      } else {
        cachedFingerprint.clear();
        safeThis->m_cachedDetailFingerprint.clear();
        safeThis->m_cachedSectionsFingerprint.clear();
        safeThis->m_cachedSeriesPlayableItem = MediaItem{};
        safeThis->m_cachedSeriesSeasons.clear();
        safeThis->m_cachedSeasonEpisodes.clear();
        safeThis->m_cachedSimilarItems.clear();
        safeThis->m_cachedCollections.clear();
        safeThis->m_cachedAdditionalParts.clear();
        safeThis->m_cachedSeasonId.clear();
        safeThis->m_manualSelectedSeasonId.clear();
        safeThis->m_manualSelectedEpisodeId.clear();
        safeThis->m_cachedSeasonIndex = -1;
        safeThis->m_manualSelectedSeasonIndex = -1;
        safeThis->m_hasCachedSeriesPlayableItem = false;
        safeThis->m_hasCachedSeriesSeasons = false;
        safeThis->m_hasCachedSeasonEpisodes = false;
        safeThis->m_hasCachedSimilarItems = false;
        safeThis->m_hasCachedCollections = false;
        safeThis->m_hasCachedAdditionalParts = false;
        safeThis->m_hasCachedCastData = false;
        qWarning() << "[DetailView] Ignore incomplete detail cache"
                   << "itemId=" << targetId
                   << "file=" << cachedEntry->filePath;
      }
    } else {
      qDebug() << "[DetailView] Detail cache miss"
               << "itemId=" << targetId;
    }
  }

  if (!safeThis || safeThis->m_currentItemId != targetId)
    co_return;

  if (safeThis->m_core && safeThis->m_core->mediaService()) {
    QCoro::connect(safeThis->prefetchItemDetail(targetId, serverId, userId,
                                                cachedFingerprint),
                   safeThis.data(), []() {});
  }
}

QCoro::Task<void> DetailView::prefetchItemDetail(QString itemId,
                                                 QString cacheServerId,
                                                 QString cacheUserId,
                                                 QString cachedFingerprint) {
  QPointer<DetailView> safeThis(this);
  QString targetId = std::move(itemId);
  QString serverId = std::move(cacheServerId);
  QString userId = std::move(cacheUserId);
  QString previousFingerprint = std::move(cachedFingerprint);

  try {
    MediaItem item =
        co_await m_core->mediaService()->getItemDetail(targetId);
    const QString fetchedFingerprint = co_await QtConcurrent::run(
        [item]() mutable { return DetailCacheUtils::fingerprint(item); });
    const bool differsFromCache =
        previousFingerprint.isEmpty() ||
        fetchedFingerprint != previousFingerprint;

    if (differsFromCache) {
      saveDetailCacheAsync(serverId, userId, item, fetchedFingerprint);
    } else {
      qDebug() << "[DetailView] Detail cache unchanged"
               << "itemId=" << targetId
               << "fingerprint=" << fetchedFingerprint.left(12);
    }

    if (!safeThis || (safeThis->m_pendingDeferredFetchId != targetId &&
                      safeThis->m_currentItemId != targetId)) {
      
      co_return;
    }

    safeThis->m_cachedDetailFingerprint = fetchedFingerprint;

    if (safeThis->m_pendingDeferredFetchId == targetId) {
      if (!differsFromCache && safeThis->m_pendingFetchReady &&
          safeThis->m_pendingFetchedItemFromCache) {
        safeThis->maybeFlushDeferredUpdate(targetId);
        co_return;
      }

      
      
      safeThis->m_pendingFetchedItem = std::move(item);
      safeThis->m_pendingFetchReady = true;
      safeThis->m_pendingFetchedItemFromCache = false;
      safeThis->maybeFlushDeferredUpdate(targetId);
      co_return;
    }

    
    if (safeThis->m_currentItemId == targetId && differsFromCache) {
      qDebug() << "[DetailView] Applying refreshed detail after cache"
               << "itemId=" << targetId;
      QCoro::connect(safeThis->executeDeferredUpdate(std::move(item), true),
                     safeThis.data(), []() {});
    }
  } catch (...) {
    if (safeThis && safeThis->m_pendingDeferredFetchId == targetId) {
      if (!safeThis->m_pendingFetchReady &&
          safeThis->m_currentMediaItem.id != targetId) {
        safeThis->m_pendingDeferredFetchId.clear();
        const QString errorText = tr("Error Loading Item");
        QMetaObject::invokeMethod(
            safeThis.data(),
            [safeThis, targetId, errorText]() {
              if (safeThis && safeThis->m_currentItemId == targetId)
                safeThis->m_titleLabel->setText(errorText);
            },
            Qt::QueuedConnection);
      } else {
        qDebug() << "[DetailView] Detail refresh failed; using existing data"
                 << "itemId=" << targetId;
      }
    }
  }
}

void DetailView::maybeFlushDeferredUpdate(const QString &itemId) {
  if (m_pendingDeferredFetchId != itemId)
    return;
  if (!m_pendingFetchReady || !m_pendingAnimationGuardDone)
    return;

  
  m_pendingDeferredFetchId.clear();

  MediaItem item = m_pendingFetchedItem;
  const bool fromCache = m_pendingFetchedItemFromCache;
  m_pendingFetchedItem = MediaItem{};
  m_pendingFetchReady = false;
  m_pendingAnimationGuardDone = false;
  m_pendingFetchedItemFromCache = false;

  if (item.type != "Series" || m_currentPlayableItem.type != "Episode" ||
      m_currentPlayableItem.id.isEmpty()) {
    m_currentPlayableItem = item;
  }
  qDebug() << "[DetailView] Deferred detail update"
           << "itemId=" << itemId << "fromCache=" << fromCache;
  QCoro::connect(executeDeferredUpdate(std::move(item)), this, []() {});
}

QCoro::Task<void> DetailView::executeDeferredUpdate(MediaItem item,
                                                    bool isSilentRefresh) {
  QPointer<DetailView> safeThis(this);
  const QString targetId = item.id;
  const QString detectedType = item.type;
  const bool batchPresentation = isVisible();

  if (batchPresentation) {
    
    
    
    setDeferredPresentationBatchActive(true);
    QTimer::singleShot(160, this, [safeThis]() {
      if (safeThis && safeThis->m_deferredPresentationBatchActive)
        safeThis->setDeferredPresentationBatchActive(false);
    });
  }

  
  
  
  co_await updateUi(item, isSilentRefresh);
  if (!safeThis)
    co_return;
  safeThis->persistDetailCacheSnapshot(QStringLiteral("detail-ui"));

  if (isSilentRefresh) {
    safeThis->setDeferredPresentationBatchActive(false);
    co_return;
  }

  if (batchPresentation) {
    QTimer::singleShot(0, safeThis.data(),
                       [safeThis, targetId, detectedType]() {
                         if (!safeThis)
                           return;
                         safeThis->finishDeferredPresentationBatch(targetId,
                                                                   detectedType);
                       });
  } else {
    safeThis->startSecondaryFetches(targetId, detectedType);
  }
}

void DetailView::setDeferredPresentationBatchActive(bool active) {
  m_deferredPresentationBatchActive = active;
}

void DetailView::finishDeferredPresentationBatch(
    const QString &targetId, const QString &detectedType) {
  if (m_currentItemId != targetId) {
    setDeferredPresentationBatchActive(false);
    return;
  }

  updateTagLayoutHeight();
  updateEpisodeJumpControl(m_currentSeasonEpisodes.size());

  if (m_contentWidget && m_contentWidget->layout())
    m_contentWidget->layout()->activate();
  if (layout())
    layout()->activate();

  setDeferredPresentationBatchActive(false);

  startSecondaryFetches(targetId, detectedType);
}

void DetailView::startSecondaryFetches(const QString &targetId,
                                       const QString &detectedType) {
  if (targetId.isEmpty() || !m_core || !m_core->mediaService())
    return;

  const auto startNetworkFetches = [this, targetId, detectedType]() {
    qDebug() << "[DetailView][network] Start secondary fetches"
             << "itemId=" << targetId << "type=" << detectedType;
    QCoro::connect(
        executeFetchSecondaries(QPointer<DetailView>(this), m_core, targetId,
                                detectedType),
        this, []() {});
  };

  if (detectedType == "Series") {
    const bool hasCachedSeriesSecondaryUi =
        m_hasCachedCastData || m_hasCachedSeriesPlayableItem ||
        m_hasCachedSeriesSeasons || m_hasCachedSeasonEpisodes ||
        m_hasCachedSimilarItems || m_hasCachedCollections ||
        m_hasCachedAdditionalParts;
    startNetworkFetches();
    if (hasCachedSeriesSecondaryUi) {
      applyCachedSecondaryData(targetId, detectedType);
    } else {
      qDebug() << "[DetailView][cache-ui] Skip cached secondary data"
               << "itemId=" << targetId << "type=" << detectedType
               << "reason=" << "no-cached-series-secondary-data";
    }
    return;
  }

  applyCachedSecondaryData(targetId, detectedType);
  startNetworkFetches();
}

void DetailView::applyCachedSecondaryData(const QString &targetId,
                                          const QString &detectedType,
                                          bool revealBottomInfo) {
  if (targetId != m_currentItemId)
    return;

  qDebug() << "[DetailView][cache-ui] Applying cached secondary data"
           << "itemId=" << targetId << "type=" << detectedType
           << "sections=" << m_cachedSectionsFingerprint.left(12);

  if (detectedType == "Series") {
    if (!m_hasManualSeriesSelection && m_hasCachedSeriesPlayableItem &&
        m_cachedSeriesPlayableItem.type == "Episode" &&
        !m_cachedSeriesPlayableItem.id.isEmpty()) {
      if (m_currentPlayableItem.id != m_cachedSeriesPlayableItem.id)
        applySeriesPlayableItemToUi(m_cachedSeriesPlayableItem);
    }

    if (m_hasCachedSeriesSeasons && !m_appliedCachedSeriesSeasonsToUi) {
      m_seriesSeasons = m_cachedSeriesSeasons;
      int targetSeasonIdx = 0;
      const int cachedEpisodeSeasonIndex = m_cachedSeasonIndex;
      const QString cachedEpisodeSeasonId = m_cachedSeasonId;
      bool selectedSeasonResolved = false;
      if (m_hasManualSeriesSelection && m_currentSeasonIndex >= 0 &&
          m_currentSeasonIndex < m_seriesSeasons.size()) {
        targetSeasonIdx = m_currentSeasonIndex;
        selectedSeasonResolved = true;
      }
      const int playableParentIdx = m_currentPlayableItem.parentIndexNumber;
      if (!selectedSeasonResolved && !m_hasManualSeriesSelection &&
          playableParentIdx >= 0) {
        for (int i = 0; i < m_seriesSeasons.size(); ++i) {
          if (m_seriesSeasons[i].indexNumber == playableParentIdx) {
            targetSeasonIdx = i;
            break;
          }
        }
      }
      if (!m_seriesSeasons.isEmpty()) {
        targetSeasonIdx = qBound(0, targetSeasonIdx,
                                 m_seriesSeasons.size() - 1);
        m_currentSeasonIndex = targetSeasonIdx;
        updateSeasonSwitcher(targetSeasonIdx);
      }

      if (m_hasCachedSeasonEpisodes &&
          cachedEpisodeSeasonIndex == targetSeasonIdx &&
          cachedEpisodeSeasonId ==
              (m_seriesSeasons.isEmpty()
                   ? QString()
                   : m_seriesSeasons[targetSeasonIdx].id) &&
          !m_seriesSeasons.isEmpty() &&
          episodesBelongToSeason(m_cachedSeasonEpisodes,
                                 m_seriesSeasons[targetSeasonIdx])) {
        m_currentSeasonEpisodes = m_cachedSeasonEpisodes;
        if (!m_appliedCachedSeasonEpisodesToUi && m_episodeWidget) {
          applyReservedSectionItems(m_episodeSectionReserveWidget,
                                    m_episodeWidget,
                                    m_currentSeasonEpisodes);
          updateEpisodeJumpControl(m_currentSeasonEpisodes.size());
          const QString highlightedEpisodeId = m_currentPlayableItem.id;
          if (!highlightedEpisodeId.isEmpty()) {
            m_episodeWidget->gallery()->setHighlightedItemId(
                highlightedEpisodeId);
            m_episodeWidget->gallery()->scrollToItemId(highlightedEpisodeId);
          }
        }
        m_appliedCachedSeasonEpisodesToUi = true;
      }

      if (m_seasonWidget) {
        m_seasonWidget->setTitle(tr("Seasons"));
        m_seasonWidget->setCardStyle(MediaCardDelegate::Poster);
        m_seasonWidget->setGalleryHeight(300);
        applyReservedSectionItems(m_seasonSectionReserveWidget, m_seasonWidget,
                                  m_seriesSeasons);
        m_appliedCachedSeriesSeasonsToUi = true;
      }
    }
  }

  
  if (m_hasCachedCastData && !m_appliedCachedCastToUi) {
    applyCastSection(m_currentMediaItem);
    m_appliedCachedCastToUi = true;
  }
  if (revealBottomInfo && m_deferBottomInfoReveal) {
    m_deferBottomInfoReveal = false;
    hideReserveWidget(m_bottomInfoReserveWidget);
    if (m_bottomInfoWidget)
      m_bottomInfoWidget->show();
  }

  
  
  
  if (m_hasCachedAdditionalParts && !m_appliedCachedAdditionalPartsToUi) {
    applyReservedSectionItems(m_additionalPartsSectionReserveWidget,
                              m_additionalPartsWidget,
                              m_cachedAdditionalParts);
    m_appliedCachedAdditionalPartsToUi = true;
  } else if (!m_hasCachedAdditionalParts &&
             (m_hasCachedCollections || m_hasCachedSimilarItems)) {
    applyReservedSectionItems(m_additionalPartsSectionReserveWidget,
                              m_additionalPartsWidget, {});
  }

  if (m_hasCachedCollections && !m_appliedCachedCollectionsToUi) {
    applyCollectionGalleryLayout(m_cachedCollections);
    applyReservedSectionItems(m_collectionSectionReserveWidget,
                              m_collectionWidget, m_cachedCollections);
    m_appliedCachedCollectionsToUi = true;
  } else if (!m_hasCachedCollections && m_hasCachedSimilarItems) {
    applyReservedSectionItems(m_collectionSectionReserveWidget,
                              m_collectionWidget, {});
  }

  if (m_hasCachedSimilarItems && !m_appliedCachedSimilarItemsToUi) {
    applyReservedSectionItems(m_similarSectionReserveWidget, m_similarWidget,
                              m_cachedSimilarItems);
    m_appliedCachedSimilarItemsToUi = true;
  }
}

void DetailView::persistDetailCacheSnapshot(const QString &reason) {
  if (m_detailCacheServerId.isEmpty() || m_detailCacheUserId.isEmpty() ||
      m_currentMediaItem.id.isEmpty())
    return;

  DetailCacheUtils::DetailCacheEntry entry;
  entry.item = m_currentMediaItem;
  const bool isSeries = m_currentMediaItem.type == "Series";
  entry.hasPlayableItem = isSeries && m_currentPlayableItem.type == "Episode" &&
                          !m_currentPlayableItem.id.isEmpty();
  if (entry.hasPlayableItem)
    entry.playableItem = m_currentPlayableItem;
  entry.hasSeasons = isSeries && m_hasCachedSeriesSeasons;
  entry.seasons = m_cachedSeriesSeasons;
  entry.hasSeasonEpisodes = isSeries && m_hasCachedSeasonEpisodes;
  entry.seasonEpisodes = m_cachedSeasonEpisodes;
  entry.seasonIndex = isSeries ? m_cachedSeasonIndex : -1;
  entry.seasonId = isSeries ? m_cachedSeasonId : QString();
  entry.playableItemFromServer = entry.hasPlayableItem;
  entry.hasSimilarItems = m_hasCachedSimilarItems;
  entry.similarItems = m_cachedSimilarItems;
  entry.hasCollections = m_hasCachedCollections;
  entry.collections = m_cachedCollections;
  entry.hasAdditionalParts = m_hasCachedAdditionalParts;
  entry.additionalParts = m_cachedAdditionalParts;

  saveDetailCacheEntryAsync(m_detailCacheServerId, m_detailCacheUserId, entry,
                            reason);
}

void DetailView::updateMetaRow(const MediaItem &item,
                               const QString &leadingMeta) {
  QStringList metas;
  if (item.communityRating > 0) {
    m_ratingStarLabel->show();
    metas << QString::number(item.communityRating, 'f', 1);
  } else {
    m_ratingStarLabel->hide();
  }
  if (!leadingMeta.isEmpty())
    metas << leadingMeta;
  const QString releaseDate = formatDetailReleaseDate(item);
  if (!releaseDate.isEmpty())
    metas << releaseDate;
  if (item.runTimeTicks > 0)
    metas << formatRunTime(item.runTimeTicks);
  if (!item.officialRating.isEmpty())
    metas << item.officialRating;

  m_metaLabel->setText(metas.join("  \u2022  "));
  m_metaRowWidget->show();
}






void DetailView::applySeedToUi(const MediaItem &seed) {
  m_currentMediaItem = seed;
  m_titleLabel->setText(seed.name);

  
  updateMetaRow(seed, formatEpisodeTag(seed));

  
  const bool shouldShowNumber = shouldShowDisplayNumber(seed);
  const QString displayNumber =
      shouldShowNumber ? extractDisplayNumber(seed) : QString();
  updateDisplayNumber(displayNumber);

  
  QString text = seed.overview;
  text.replace(QRegularExpression("<br\\s*/?>",
                                  QRegularExpression::CaseInsensitiveOption),
               "\n");
  text.replace("</p>", "\n\n");
  text.remove(QRegularExpression("<[^>]*>"));
  QTextDocument doc;
  doc.setHtml(text);
  QString cleanText = doc.toPlainText();
  cleanText.replace(QChar::LineSeparator, '\n');
  cleanText.remove(QRegularExpression("<[^>]*>"));
  m_cleanOverviewText = cleanText;
  m_lastOverviewWidth = -1;
  updateOverviewElidedText();

  
  if (!seed.taglines.isEmpty()) {
    m_taglineLabel->setText(seed.taglines.first());
    m_taglineLabel->show();
  } else {
    m_taglineLabel->hide();
  }

  
  m_isFavorite = seed.isFavorite();
  m_actionWidget->setFavoriteState(m_isFavorite);
  m_actionWidget->setPlayedState(seed.userData.played);

  
  
  
  
  if (seed.type == "Series") {
    m_actionWidget->setSeriesLoadingMode();
  } else {
    m_actionWidget->setupNormalMode(seed);
  }

  
  
  
  
  
  if (!seed.mediaSources.isEmpty()) {
    m_actionWidget->setSources(seed.mediaSources, 0);
    if (!seed.mediaSources.first().mediaStreams.isEmpty()) {
      m_actionWidget->setStreams(seed.mediaSources.first());
    }
    m_appliedSourcesFingerprint = computeSourcesFingerprint(seed.mediaSources);
  } else {
    m_actionWidget->setSources(QList<MediaSourceInfo>{}, 0);
    m_actionWidget->setStreams(MediaSourceInfo{});
    m_appliedSourcesFingerprint =
        computeSourcesFingerprint(QList<MediaSourceInfo>{});
  }
  m_actionWidget->refreshExtPlayerButton();

  
  
  
  buildTagButtons(seed.genres);

  
  
  
  
  
  
  QList<MediaSourceInfo> seedDefaultSource;
  if (!seed.mediaSources.isEmpty())
    seedDefaultSource.append(seed.mediaSources.first());
  m_bottomInfoWidget->setInfo(seed, seedDefaultSource);
  m_bottomInfoWidget->setVisible(!m_deferBottomInfoReveal);
  if (m_deferBottomInfoReveal) {
    showReserveWidget(m_bottomInfoReserveWidget,
                      reserveHeightFor(m_bottomInfoWidget, 220));
  } else {
    hideReserveWidget(m_bottomInfoReserveWidget);
  }
  m_appliedBottomInfoFingerprint =
      computeBottomInfoFingerprint(seed, seedDefaultSource);

  
  
  m_currentBackdropPix = QPixmap();
  updateBackdrop();

  
  
  
  
  
  QCoro::connect(executeLoadImages(QPointer<DetailView>(this), m_core, seed,
                                   false),
                 this, []() {});
}





void DetailView::buildTagButtons(const QStringList &genres) {
  if (!m_tagsWidget || !m_tagsLayout)
    return;

  
  if (m_tagsLayout->count() == genres.size()) {
    bool same = true;
    for (int i = 0; i < genres.size(); ++i) {
      QLayoutItem *layoutItem = m_tagsLayout->itemAt(i);
      QWidget *w = layoutItem ? layoutItem->widget() : nullptr;
      auto *btn = qobject_cast<QPushButton *>(w);
      if (!btn || btn->text() != genres[i]) {
        same = false;
        break;
      }
    }
    if (same)
      return;
  }

  m_tagsWidget->setUpdatesEnabled(false);
  clearLayout(m_tagsLayout);
  for (const QString &genre : genres) {
    auto *tagBtn = new DetailTagButton(genre, m_tagsWidget);
    connect(tagBtn, &QPushButton::clicked, this,
            [this, genre]() { Q_EMIT navigateToFilteredView("Genre", genre); });
    m_tagsLayout->addWidget(tagBtn);
  }
  m_tagsWidget->setUpdatesEnabled(true);
  updateTagLayoutHeight();
  QTimer::singleShot(0, this, [this]() { updateTagLayoutHeight(); });
}

void DetailView::applyCastSection(const MediaItem &item) {
  if (!m_castWidget)
    return;

  const QString fp = castFingerprint(item);
  if (!fp.isEmpty() && fp == m_appliedCastFingerprint)
    return;

  if (item.people.isEmpty()) {
    applyReservedSectionItems(m_castSectionReserveWidget, m_castWidget, {});
    m_appliedCastFingerprint = fp;
    return;
  }

  QList<MediaItem> castItems;
  castItems.reserve(item.people.size());
  for (const auto &person : item.people) {
    MediaItem fakeItem;
    fakeItem.id = person.id;
    fakeItem.name = person.name;
    fakeItem.images.primaryTag = person.primaryImageTag;
    
    QStringList personParts;
    if (!person.type.isEmpty()) {
      
      static const QHash<QString, const char *> typeMap = {
          {"Actor", QT_TR_NOOP("Actor")},
          {"Director", QT_TR_NOOP("Director")},
          {"Writer", QT_TR_NOOP("Writer")},
          {"Producer", QT_TR_NOOP("Producer")},
          {"Composer", QT_TR_NOOP("Composer")},
          {"Conductor", QT_TR_NOOP("Conductor")},
          {"GuestStar", QT_TR_NOOP("GuestStar")},
          {"Editor", QT_TR_NOOP("Editor")},
          {"Lyricist", QT_TR_NOOP("Lyricist")},
      };
      auto it = typeMap.constFind(person.type);
      personParts << (it != typeMap.constEnd() ? tr(it.value())
                                               : person.type);
    }
    if (!person.role.isEmpty())
      personParts << person.role;
    fakeItem.overview = personParts.join(" · ");
    fakeItem.type = "Person";
    castItems.append(fakeItem);
  }

  applyReservedSectionItems(m_castSectionReserveWidget, m_castWidget,
                            castItems);
  m_appliedCastFingerprint = fp;
}

void DetailView::applyCollectionGalleryLayout(
    const QList<MediaItem> &collections) {
  int landscapeSignals = 0;
  int portraitSignals = 0;

  const auto hasLandscapeArtwork = [](const MediaItem &item) {
    const double primaryAspectRatio = item.images.primaryImageAspectRatio;
    if (primaryAspectRatio > 1.05)
      return true;
    if (primaryAspectRatio > 0.0)
      return false;

    if (!item.images.primaryTag.isEmpty())
      return false;

    return !item.images.thumbTag.isEmpty() ||
           !item.images.backdropTag.isEmpty() ||
           !item.images.parentThumbTag.isEmpty() ||
           !item.images.parentBackdropTag.isEmpty();
  };

  for (const MediaItem &collection : collections) {
    if (hasLandscapeArtwork(collection)) {
      ++landscapeSignals;
    } else if (!collection.images.primaryTag.isEmpty() ||
               collection.images.primaryImageAspectRatio > 0.0) {
      ++portraitSignals;
    }
  }

  const bool useLandscapeCards =
      landscapeSignals > 0 && landscapeSignals > portraitSignals;

  if (useLandscapeCards) {
    m_collectionWidget->setCardStyle(MediaCardDelegate::LibraryTile);
    m_collectionWidget->setGalleryHeight(230);
  } else {
    m_collectionWidget->setCardStyle(MediaCardDelegate::Poster);
    m_collectionWidget->setGalleryHeight(300);
  }

  qDebug() << "[DetailView] Collection gallery layout"
           << "itemCount=" << collections.size()
           << "landscapeSignals=" << landscapeSignals
           << "portraitSignals=" << portraitSignals
           << "style="
           << (useLandscapeCards ? "LibraryTile" : "Poster");
}







QCoro::Task<void>
DetailView::executeFetchSecondaries(QPointer<DetailView> safeThis,
                                    QEmbyCore *core, QString targetId,
                                    QString itemType) {
  if (!safeThis)
    co_return;

  
  
  if (itemType == "Series") {
    if (!safeThis->m_hasManualSeriesSelection &&
        safeThis->m_hasCachedSeriesPlayableItem &&
        safeThis->m_cachedSeriesPlayableItem.type == "Episode" &&
        !safeThis->m_cachedSeriesPlayableItem.id.isEmpty()) {
      safeThis->applySeriesPlayableItemToUi(
          safeThis->m_cachedSeriesPlayableItem);
      qDebug() << "[DetailView][cache-ui] Applied cached series playable item"
               << "seriesId=" << targetId
               << "episodeId=" << safeThis->m_cachedSeriesPlayableItem.id;
    }
    qDebug() << "[DetailView][network] Fetch series next-up"
             << "seriesId=" << targetId
             << "applyToUi=" << !safeThis->m_hasManualSeriesSelection;
    co_await safeThis->fetchSeriesNextUp(
        targetId, !safeThis->m_hasManualSeriesSelection);
    if (!safeThis || safeThis->m_currentItemId != targetId)
      co_return;

    
    qDebug() << "[DetailView][network] Fetch series seasons"
             << "seriesId=" << targetId;
    auto seasons = co_await core->mediaService()->getSeasons(targetId);
    if (!safeThis || safeThis->m_currentItemId != targetId)
      co_return;

    bool seasonsUnchanged = false;
    if (safeThis->m_appliedCachedSeriesSeasonsToUi) {
      seasonsUnchanged = co_await mediaItemListsEqualAsync(
          safeThis->m_cachedSeriesSeasons, seasons);
      if (!safeThis || safeThis->m_currentItemId != targetId)
        co_return;
    }
    safeThis->m_seriesSeasons = seasons;
    safeThis->m_cachedSeriesSeasons = seasons;
    safeThis->m_hasCachedSeriesSeasons = true;

    
    
    int targetSeasonIdx = 0;
    bool selectedSeasonResolved = false;
    if (safeThis->m_hasManualSeriesSelection &&
        !safeThis->m_manualSelectedSeasonId.isEmpty()) {
      for (int i = 0; i < seasons.size(); ++i) {
        if (seasons[i].id == safeThis->m_manualSelectedSeasonId) {
          targetSeasonIdx = i;
          selectedSeasonResolved = true;
          break;
        }
      }
    }
    if (!selectedSeasonResolved &&
        safeThis->m_hasManualSeriesSelection &&
        safeThis->m_manualSelectedSeasonIndex >= 0 &&
        safeThis->m_manualSelectedSeasonIndex < seasons.size()) {
      targetSeasonIdx = safeThis->m_manualSelectedSeasonIndex;
      selectedSeasonResolved = true;
    }
    if (!selectedSeasonResolved &&
        safeThis->m_hasManualSeriesSelection &&
        safeThis->m_currentSeasonIndex >= 0 &&
        safeThis->m_currentSeasonIndex < seasons.size()) {
      targetSeasonIdx = safeThis->m_currentSeasonIndex;
      selectedSeasonResolved = true;
    }
    int playableParentIdx = safeThis->m_currentPlayableItem.parentIndexNumber;
    if (!selectedSeasonResolved &&
        !safeThis->m_hasManualSeriesSelection && playableParentIdx >= 0) {
      for (int i = 0; i < seasons.size(); ++i) {
        if (seasons[i].indexNumber == playableParentIdx) {
          targetSeasonIdx = i;
          break;
        }
      }
    }
    if (!seasons.isEmpty())
      targetSeasonIdx = qBound(0, targetSeasonIdx, seasons.size() - 1);
    safeThis->m_currentSeasonIndex = targetSeasonIdx;

    
    if (!seasons.isEmpty() && safeThis->m_episodeWidget) {
      safeThis->updateSeasonSwitcher(targetSeasonIdx);

      const QString playableItemId =
          safeThis->m_hasManualSeriesSelection &&
                  !safeThis->m_manualSelectedEpisodeId.isEmpty()
              ? safeThis->m_manualSelectedEpisodeId
              : safeThis->m_currentPlayableItem.id;
      co_await safeThis->loadEpisodesForSeason(
          targetSeasonIdx, playableItemId, !playableItemId.isEmpty(),
          safeThis->m_hasManualSeriesSelection);
    } else if (safeThis->m_episodeWidget) {
      applyReservedSectionItems(safeThis->m_episodeSectionReserveWidget,
                                safeThis->m_episodeWidget, {});
    }
    if (!safeThis || safeThis->m_currentItemId != targetId)
      co_return;

    
    if (safeThis->m_seasonWidget && !seasonsUnchanged) {
      safeThis->m_seasonWidget->setTitle(tr("Seasons"));
      safeThis->m_seasonWidget->setCardStyle(MediaCardDelegate::Poster);
      safeThis->m_seasonWidget->setGalleryHeight(300);
      applyReservedSectionItems(safeThis->m_seasonSectionReserveWidget,
                                safeThis->m_seasonWidget, seasons);
    }
  }

  
  if (safeThis->m_currentItemId != targetId)
    co_return;
  qDebug() << "[DetailView][ui] Apply cast section"
           << "itemId=" << targetId
           << "afterSeriesSections=" << (itemType == "Series");
  safeThis->applyCastSection(safeThis->m_currentMediaItem);
  if (safeThis->m_deferBottomInfoReveal) {
    safeThis->m_deferBottomInfoReveal = false;
    hideReserveWidget(safeThis->m_bottomInfoReserveWidget);
    if (safeThis->m_bottomInfoWidget)
      safeThis->m_bottomInfoWidget->show();
  }
  if (!safeThis)
    co_return;

  
  
  
  try {
    qDebug() << "[DetailView][network] Fetch similar items"
             << "itemId=" << targetId;
    QList<MediaItem> similar =
        co_await core->mediaService()->getSimilarItems(targetId, 15);
    if (!safeThis || safeThis->m_currentItemId != targetId)
      co_return;

    bool similarUnchanged = false;
    if (safeThis->m_appliedCachedSimilarItemsToUi) {
      similarUnchanged = co_await mediaItemListsEqualAsync(
          safeThis->m_cachedSimilarItems, similar);
      if (!safeThis || safeThis->m_currentItemId != targetId)
        co_return;
    }
    safeThis->m_cachedSimilarItems = similar;
    safeThis->m_hasCachedSimilarItems = true;
    if (!similarUnchanged) {
      applyReservedSectionItems(safeThis->m_similarSectionReserveWidget,
                                safeThis->m_similarWidget, similar);
    }
  } catch (...) {
    if (safeThis && safeThis->m_currentItemId == targetId &&
        !safeThis->m_appliedCachedSimilarItemsToUi) {
      applyReservedSectionItems(safeThis->m_similarSectionReserveWidget,
                                safeThis->m_similarWidget, {});
    }
  }
  if (!safeThis)
    co_return;

  
  
  try {
    qDebug() << "[DetailView][network] Fetch item collections"
             << "itemId=" << targetId;
    QList<MediaItem> collections =
        co_await core->mediaService()->getItemCollections(targetId);
    if (!safeThis || safeThis->m_currentItemId != targetId)
      co_return;

    bool collectionsUnchanged = false;
    if (safeThis->m_appliedCachedCollectionsToUi) {
      collectionsUnchanged = co_await mediaItemListsEqualAsync(
          safeThis->m_cachedCollections, collections);
      if (!safeThis || safeThis->m_currentItemId != targetId)
        co_return;
    }
    safeThis->m_cachedCollections = collections;
    safeThis->m_hasCachedCollections = true;
    if (!collectionsUnchanged) {
      safeThis->applyCollectionGalleryLayout(collections);
      applyReservedSectionItems(safeThis->m_collectionSectionReserveWidget,
                                safeThis->m_collectionWidget, collections);
    }
  } catch (...) {
    if (safeThis && safeThis->m_currentItemId == targetId &&
        !safeThis->m_appliedCachedCollectionsToUi) {
      applyReservedSectionItems(safeThis->m_collectionSectionReserveWidget,
                                safeThis->m_collectionWidget, {});
    }
  }
  if (!safeThis)
    co_return;

  
  try {
    qDebug() << "[DetailView][network] Fetch additional parts"
             << "itemId=" << targetId;
    QList<MediaItem> additionalParts =
        co_await core->mediaService()->getAdditionalParts(targetId);
    if (!safeThis || safeThis->m_currentItemId != targetId)
      co_return;

    bool additionalPartsUnchanged = false;
    if (safeThis->m_appliedCachedAdditionalPartsToUi) {
      additionalPartsUnchanged = co_await mediaItemListsEqualAsync(
          safeThis->m_cachedAdditionalParts, additionalParts);
      if (!safeThis || safeThis->m_currentItemId != targetId)
        co_return;
    }
    safeThis->m_cachedAdditionalParts = additionalParts;
    safeThis->m_hasCachedAdditionalParts = true;
    if (!additionalPartsUnchanged) {
      applyReservedSectionItems(safeThis->m_additionalPartsSectionReserveWidget,
                                safeThis->m_additionalPartsWidget,
                                additionalParts);
    }
  } catch (...) {
    if (safeThis && safeThis->m_currentItemId == targetId &&
        !safeThis->m_appliedCachedAdditionalPartsToUi) {
      applyReservedSectionItems(
          safeThis->m_additionalPartsSectionReserveWidget,
          safeThis->m_additionalPartsWidget, {});
    }
  }

  if (safeThis && safeThis->m_currentItemId == targetId)
    safeThis->persistDetailCacheSnapshot(QStringLiteral("secondaries"));
}

QCoro::Task<void> DetailView::fetchSeriesNextUp(const QString &targetId,
                                                bool applyToUi) {
  QPointer<DetailView> safeThis(this);
  try {
    MediaItem playableItem;
    qDebug() << "[DetailView][network] Fetch next-up"
             << "seriesId=" << targetId;
    auto nextUp = co_await m_core->mediaService()->getNextUp(targetId);

    if (!nextUp.isEmpty())
      playableItem = nextUp.first();
    else {
      qDebug() << "[DetailView][network] Fetch seasons for next-up fallback"
               << "seriesId=" << targetId;
      auto seasons = co_await m_core->mediaService()->getSeasons(targetId);
      if (!seasons.isEmpty()) {
        qDebug() << "[DetailView][network] Fetch episodes for next-up fallback"
                 << "seriesId=" << targetId
                 << "seasonId=" << seasons.first().id;
        auto episodes = co_await m_core->mediaService()->getEpisodes(
            targetId, seasons.first().id);
        if (!episodes.isEmpty())
          playableItem = episodes.first();
      }
    }

    if (safeThis && safeThis->m_currentItemId == targetId &&
        !playableItem.id.isEmpty()) {
      safeThis->m_cachedSeriesPlayableItem = playableItem;
      safeThis->m_hasCachedSeriesPlayableItem = true;

      if (!applyToUi || safeThis->m_hasManualSeriesSelection) {
        qDebug() << "[DetailView][network] Updated server next-up cache without "
                    "changing manual selection"
                 << "seriesId=" << targetId
                 << "episodeId=" << playableItem.id
                 << "applyToUi=" << applyToUi
                 << "manualSelection="
                 << safeThis->m_hasManualSeriesSelection;
        safeThis->persistDetailCacheSnapshot(
            QStringLiteral("series-server-next-up"));
        co_return;
      }

      co_await safeThis->applySeriesPlayableItem(playableItem);
    }
  } catch (...) {
  }
}

void DetailView::applySeriesPlayableItemToUi(const MediaItem &playableItem,
                                             bool scrollToEpisode) {
  if (playableItem.id.isEmpty() || m_currentMediaItem.type != "Series")
    return;

  const QString playableFingerprint =
      DetailCacheUtils::fingerprint(playableItem);
  const bool playableUnchanged =
      !m_appliedSeriesPlayableFingerprint.isEmpty() &&
      playableFingerprint == m_appliedSeriesPlayableFingerprint;
  m_currentPlayableItem = playableItem;
  const QString episodeTag = formatEpisodeTag(playableItem);

  if (!playableUnchanged) {
    const QString actionTag =
        episodeTag.isEmpty() ? playableItem.name : episodeTag;
    m_actionWidget->setupSeriesMode(playableItem, actionTag);
    updateMetaRow(m_currentMediaItem, episodeTag);

    m_actionWidget->setSources(playableItem.mediaSources, 0);
    m_appliedSourcesFingerprint =
        computeSourcesFingerprint(playableItem.mediaSources);

    QList<MediaSourceInfo> selectedSources;
    if (!playableItem.mediaSources.isEmpty()) {
      int sourceIndex = m_actionWidget->currentSourceIndex();
      if (sourceIndex >= playableItem.mediaSources.size())
        sourceIndex = 0;

      const MediaSourceInfo &selectedSource =
          playableItem.mediaSources[sourceIndex];
      m_actionWidget->setStreams(selectedSource);
      selectedSources.append(selectedSource);
    } else {
      m_actionWidget->setStreams(MediaSourceInfo{});
    }

    if (!selectedSources.isEmpty()) {
      const QString bottomInfoFingerprint =
          computeBottomInfoFingerprint(playableItem, selectedSources);
      if (bottomInfoFingerprint != m_appliedBottomInfoFingerprint) {
        m_bottomInfoWidget->setInfo(playableItem, selectedSources);
        m_appliedBottomInfoFingerprint = bottomInfoFingerprint;
        m_bottomInfoWidget->setVisible(!m_deferBottomInfoReveal);
        if (m_deferBottomInfoReveal) {
          showReserveWidget(m_bottomInfoReserveWidget,
                            reserveHeightFor(m_bottomInfoWidget, 220));
        } else {
          hideReserveWidget(m_bottomInfoReserveWidget);
        }
      } else {
        m_bottomInfoWidget->setVisible(!m_deferBottomInfoReveal);
      }
    }

    m_appliedSeriesPlayableFingerprint = playableFingerprint;
  }

  m_actionWidget->refreshExtPlayerButton();
  m_actionWidget->setPlayedState(playableItem.userData.played);
  if (m_episodeWidget) {
    m_episodeWidget->gallery()->setHighlightedItemId(playableItem.id);
    if (scrollToEpisode)
      m_episodeWidget->gallery()->scrollToItemId(playableItem.id);
  }

  qDebug() << "[DetailView] Apply series playable item"
           << "seriesId=" << m_currentItemId << "episodeId=" << playableItem.id
           << "episodeTag=" << episodeTag
           << "sourceCount=" << playableItem.mediaSources.size();
}

QCoro::Task<void> DetailView::applySeriesPlayableItem(MediaItem playableItem,
                                                      bool scrollToEpisode) {
  if (playableItem.id.isEmpty() || m_currentMediaItem.type != "Series")
    co_return;

  QPointer<DetailView> safeThis(this);
  const QString seriesId = m_currentItemId;
  const QString playableItemId = playableItem.id;

  applySeriesPlayableItemToUi(playableItem, scrollToEpisode);
  rememberSeriesSelection(playableItem, QStringLiteral("series-selection"),
                          true);

  const bool needsPlaybackInfo =
      playableItem.mediaSources.isEmpty() ||
      playableItem.mediaSources.first().mediaStreams.isEmpty();
  if (!needsPlaybackInfo)
    co_return;

  try {
    PlaybackInfo info =
        co_await m_core->mediaService()->getPlaybackInfo(playableItemId);
    if (!safeThis || safeThis->m_currentItemId != seriesId ||
        safeThis->m_currentMediaItem.type != "Series" ||
        safeThis->m_currentPlayableItem.id != playableItemId)
      co_return;
    if (!info.mediaSources.isEmpty()) {
      playableItem.mediaSources = info.mediaSources;
      safeThis->applySeriesPlayableItemToUi(playableItem, false);
      safeThis->rememberSeriesSelection(
          playableItem, QStringLiteral("series-selection-playback-info"),
          true);
    }
  } catch (...) {
  }
}

QCoro::Task<void> DetailView::onVersionChanged(int index) {
  const bool usePlayableItem =
      m_currentMediaItem.type == "Series" && !m_currentPlayableItem.id.isEmpty();
  MediaItem actionItem = usePlayableItem ? m_currentPlayableItem
                                         : m_currentMediaItem;
  if (index < 0 || index >= actionItem.mediaSources.size())
    co_return;
  MediaSourceInfo source = actionItem.mediaSources[index];
  QPointer<DetailView> safeThis(this);
  const QString expectedPageId = m_currentItemId;
  const QString actionItemId = actionItem.id;

  if (source.mediaStreams.isEmpty()) {
    try {
      PlaybackInfo info =
          co_await m_core->mediaService()->getPlaybackInfo(actionItemId);
      if (!safeThis || safeThis->m_currentItemId != expectedPageId)
        co_return;
      if (!info.mediaSources.isEmpty()) {
        if (usePlayableItem) {
          if (safeThis->m_currentPlayableItem.id != actionItemId)
            co_return;
          safeThis->m_currentPlayableItem.mediaSources = info.mediaSources;
          actionItem = safeThis->m_currentPlayableItem;
        } else {
          safeThis->m_currentMediaItem.mediaSources = info.mediaSources;

          
          
          safeThis->m_currentPlayableItem.mediaSources = info.mediaSources;
          actionItem = safeThis->m_currentMediaItem;
        }

        int selectedIndex = index;
        if (selectedIndex >= actionItem.mediaSources.size())
          selectedIndex = 0;
        if (selectedIndex < actionItem.mediaSources.size()) {
          source = actionItem.mediaSources[selectedIndex];
        }
      }
    } catch (...) {
      co_return;
    }
  }
  if (!safeThis || source.mediaStreams.isEmpty())
    co_return;

  safeThis->m_actionWidget->setStreams(source);
  QList<MediaSourceInfo> singleSource;
  singleSource.append(source);
  const MediaItem infoItem =
      usePlayableItem ? safeThis->m_currentPlayableItem
                      : safeThis->m_currentMediaItem;
  safeThis->m_bottomInfoWidget->setInfo(infoItem, singleSource);
  hideReserveWidget(safeThis->m_bottomInfoReserveWidget);
}

QCoro::Task<void> DetailView::executePlay(MediaItem targetItem,
                                          long long startTicks) {
  if (targetItem.id.isEmpty())
    co_return;
  try {
    MediaItem actualItem = targetItem;
    if (actualItem.mediaSources.isEmpty()) {
      PlaybackInfo info =
          co_await m_core->mediaService()->getPlaybackInfo(actualItem.id);
      actualItem.mediaSources = info.mediaSources;
    }
    if (actualItem.mediaSources.isEmpty())
      co_return;

    int sourceIdx = 0;
    int selectedAudioIdx = -1;
    int selectedSubIdx = -1;
    const bool useUiSelection = isCurrentActionItem(actualItem.id);

    if (useUiSelection) {
      
      sourceIdx = m_actionWidget->currentSourceIndex();
      selectedAudioIdx = m_actionWidget->currentAudioIndex();
      selectedSubIdx = m_actionWidget->currentSubtitleIndex();
    } else {
      sourceIdx = MediaSourcePreferenceUtils::resolvePreferredMediaSourceIndex(
          actualItem.mediaSources,
          ConfigStore::instance()
              ->get<QString>(ConfigKeys::PlayerPreferredVersion)
              .trimmed());
    }

    if (sourceIdx >= actualItem.mediaSources.size())
      sourceIdx = 0;
    MediaSourceInfo modifiedSource = actualItem.mediaSources[sourceIdx];

    if (useUiSelection) {
      
      
      
      for (auto &stream : modifiedSource.mediaStreams) {
        if (stream.type == "Audio" && selectedAudioIdx >= 0)
          stream.isDefault = (stream.index == selectedAudioIdx);
        else if (stream.type == "Subtitle" && selectedSubIdx >= 0)
          stream.isDefault = (stream.index == selectedSubIdx);
      }
    } else {
      PlayerPreferenceUtils::applyPreferredStreamRules(
          modifiedSource,
          ConfigStore::instance()->get<QString>(ConfigKeys::PlayerAudioLang,
                                                "auto"),
          ConfigStore::instance()->get<QString>(ConfigKeys::PlayerSubLang,
                                                "auto"));
    }

    QString streamUrl =
        m_core->mediaService()->getStreamUrl(actualItem.id, modifiedSource.id);

    const QString playTitle =
        MediaItemUtils::playbackTitle(actualItem, m_currentMediaItem.name);

    
    PlaybackManager::instance()->startInternalPlayback(
        actualItem.id, playTitle, streamUrl, startTicks,
        QVariant::fromValue(modifiedSource));
  } catch (...) {
  }
}

QCoro::Task<void> DetailView::executePlaySeason(MediaItem seasonItem) {
  if (seasonItem.id.isEmpty() || m_currentItemId.isEmpty() || !m_core ||
      !m_core->mediaService())
    co_return;

  QPointer<DetailView> safeThis(this);
  const QString seriesId = m_currentItemId;
  const QString seasonId = seasonItem.id;

  try {
    QList<MediaItem> episodes =
        co_await m_core->mediaService()->getEpisodes(seriesId, seasonId);
    if (!safeThis || safeThis->m_currentItemId != seriesId)
      co_return;

    MediaItem targetEpisode = resolveSeasonPlaybackEpisode(episodes);
    if (targetEpisode.id.isEmpty())
      co_return;

    qDebug() << "[DetailView] Play season"
             << "seriesId=" << seriesId << "seasonId=" << seasonId
             << "episodeId=" << targetEpisode.id
             << "episodeTag=" << formatEpisodeTag(targetEpisode)
             << "episodeCount=" << episodes.size()
             << "startTicks="
             << targetEpisode.userData.playbackPositionTicks;

    co_await safeThis->applySeriesPlayableItem(targetEpisode, false);
    if (!safeThis || safeThis->m_currentItemId != seriesId)
      co_return;

    const MediaItem playbackItem =
        safeThis->m_currentPlayableItem.id == targetEpisode.id
            ? safeThis->m_currentPlayableItem
            : targetEpisode;
    co_await safeThis->executePlay(
        playbackItem, playbackItem.userData.playbackPositionTicks);
  } catch (...) {
  }
}

QCoro::Task<void> DetailView::executeExternalPlay(MediaItem targetItem,
                                                  QString playerPath) {
  if (targetItem.id.isEmpty() || playerPath.isEmpty())
    co_return;
  try {
    MediaItem actualItem = targetItem;
    if (actualItem.mediaSources.isEmpty()) {
      PlaybackInfo info =
          co_await m_core->mediaService()->getPlaybackInfo(actualItem.id);
      actualItem.mediaSources = info.mediaSources;
    }
    if (actualItem.mediaSources.isEmpty())
      co_return;

    
    int sourceIdx = 0;
    const bool useUiSelection = isCurrentActionItem(actualItem.id);
    if (useUiSelection) {
      sourceIdx = m_actionWidget->currentSourceIndex();
    }
    if (sourceIdx >= actualItem.mediaSources.size())
      sourceIdx = 0;
    MediaSourceInfo modifiedSource = actualItem.mediaSources[sourceIdx];

    if (useUiSelection) {
      int selectedAudioIdx = m_actionWidget->currentAudioIndex();
      int selectedSubIdx = m_actionWidget->currentSubtitleIndex();
      for (auto &stream : modifiedSource.mediaStreams) {
        if (stream.type == "Audio" && selectedAudioIdx >= 0)
          stream.isDefault = (stream.index == selectedAudioIdx);
        else if (stream.type == "Subtitle" && selectedSubIdx >= 0)
          stream.isDefault = (stream.index == selectedSubIdx);
      }
    } else {
      PlayerPreferenceUtils::applyPreferredStreamRules(
          modifiedSource,
          ConfigStore::instance()->get<QString>(ConfigKeys::PlayerAudioLang,
                                                "auto"),
          ConfigStore::instance()->get<QString>(ConfigKeys::PlayerSubLang,
                                                "auto"));
    }

    QString streamUrl =
        m_core->mediaService()->getStreamUrl(actualItem.id, modifiedSource.id);

    const QString playTitle =
        MediaItemUtils::playbackTitle(actualItem, m_currentMediaItem.name);

    long long startTicks = actualItem.userData.playbackPositionTicks;
    PlaybackManager::instance()->startExternalPlayback(
        playerPath, actualItem.id, playTitle, streamUrl, startTicks,
        QVariant::fromValue(modifiedSource));
  } catch (...) {
  }
}

void DetailView::updateBackdrop() {
  if (m_contentWidget)
    static_cast<DetailContentWidget *>(m_contentWidget)
        ->setBackdrop(m_currentBackdropPix);
}

void DetailView::updateTagLayoutHeight() {
  if (!m_tagsWidget || !m_tagsLayout)
    return;

  int targetWidth = m_tagsWidget->width();
  if (targetWidth <= 0 && m_textContainer) {
    targetWidth = m_textContainer->contentsRect().width();
  }

  if (targetWidth <= 0) {
    m_tagsWidget->updateGeometry();
    QTimer::singleShot(50, this, [this]() { updateTagLayoutHeight(); });
    return;
  }

  m_tagsLayout->invalidate();
  const int targetHeight = m_tagsLayout->heightForWidth(targetWidth);
  if (m_tagsWidget->minimumHeight() != targetHeight) {
    m_tagsWidget->setMinimumHeight(targetHeight);
  }
  m_tagsWidget->updateGeometry();

  if (m_textContainer) {
    m_textContainer->updateGeometry();
  }
}

void DetailView::resizeEvent(QResizeEvent *event) {
  QWidget::resizeEvent(event);
  if (m_overviewLabel && m_overviewLabel->width() > 10 &&
      qAbs(m_overviewLabel->width() - m_lastOverviewWidth) > 5) {
    m_lastOverviewWidth = m_overviewLabel->width();
    updateOverviewElidedText();
  }
  updateTagLayoutHeight();
  updateEpisodeJumpControl(m_currentSeasonEpisodes.size());
}

bool DetailView::eventFilter(QObject *obj, QEvent *event) {
  if (m_mainScrollArea && obj == m_mainScrollArea->viewport() &&
      event->type() == QEvent::Resize) {
    QResizeEvent *re = static_cast<QResizeEvent *>(event);
    int newWidth = re->size().width();
    if (m_contentWidget && newWidth > 100 &&
        m_contentWidget->maximumWidth() != newWidth) {
      m_contentWidget->setMaximumWidth(newWidth);
      QTimer::singleShot(0, this, [this]() {
        updateTagLayoutHeight();
        updateEpisodeJumpControl(m_currentSeasonEpisodes.size());
      });
    }
  }
  if (m_episodeWidget && m_episodeWidget->gallery() &&
      obj == m_episodeWidget->gallery()->listView()->viewport() &&
      event->type() == QEvent::Resize) {
    QTimer::singleShot(0, this, [this]() {
      updateEpisodeJumpControl(m_currentSeasonEpisodes.size());
    });
  }
  if (event->type() == QEvent::Wheel) {
    bool isHorizontalViewport =
        obj->parent() &&
        obj->parent()->property("isHorizontalListView").toBool();
    bool isMainViewport =
        (m_mainScrollArea && obj == m_mainScrollArea->viewport());
    if (isHorizontalViewport || isMainViewport) {
      QWheelEvent *we = static_cast<QWheelEvent *>(event);
      if (m_vScrollController) {
        m_vScrollController->scrollByWheelEvent(we, Qt::Vertical);
      }
      return true;
    }
  }
  return QWidget::eventFilter(obj, event);
}

void DetailView::showEvent(QShowEvent *event) {
  BaseView::showEvent(event);

  
  
  if (m_skipNextSilentRefresh) {
    m_skipNextSilentRefresh = false;
    return;
  }

  
  
  
  m_pendingDeferredFetchId.clear();

  if (!m_currentItemId.isEmpty() && m_core && m_core->mediaService()) {
    executeSilentRefresh(QPointer<DetailView>(this), m_core, m_currentItemId);
  }
}

void DetailView::clearLayout(QLayout *layout) {
  if (!layout)
    return;
  QLayoutItem *child;
  while ((child = layout->takeAt(0)) != nullptr) {
    if (child->widget()) {
      child->widget()->hide();
      child->widget()->setParent(nullptr);
      child->widget()->deleteLater();
    } else if (child->layout()) {
      clearLayout(child->layout());
    }
    delete child;
  }
}

QString DetailView::formatRunTime(long long ticks) {
  long long totalSeconds = ticks / 10000000;
  long long hours = totalSeconds / 3600;
  long long minutes = (totalSeconds % 3600) / 60;
  if (hours > 0)
    return QString(tr("%1 hr %2 min")).arg(hours).arg(minutes);
  return QString(tr("%1 min")).arg(minutes);
}

bool DetailView::shouldShowDisplayNumber(const MediaItem &item) const {
  if (item.type.compare("Movie", Qt::CaseInsensitive) != 0) {
    return false;
  }

  static const QList<QRegularExpression> adultMarkers = {
      QRegularExpression(R"((^|[^0-9])18\+?([^0-9]|$))",
                         QRegularExpression::CaseInsensitiveOption),
      QRegularExpression(R"(\b(?:R[-\s]?18\+?|NC[-\s]?17|JAV|AV)\b)",
                         QRegularExpression::CaseInsensitiveOption),
      QRegularExpression(QStringLiteral("(成人|无码|有码|無碼|有碼)"))};

  const auto containsAdultMarker = [](const QString &value) -> bool {
    const QString trimmedValue = value.trimmed();
    if (trimmedValue.isEmpty()) {
      return false;
    }

    for (const QRegularExpression &pattern : adultMarkers) {
      if (pattern.match(trimmedValue).hasMatch()) {
        return true;
      }
    }

    return false;
  };

  if (containsAdultMarker(item.officialRating)) {
    return true;
  }

  for (const QString &genre : item.genres) {
    if (containsAdultMarker(genre)) {
      return true;
    }
  }

  for (const QString &tag : item.tags) {
    if (containsAdultMarker(tag)) {
      return true;
    }
  }

  return false;
}

bool DetailView::isCurrentActionItem(const QString &itemId) const {
  if (itemId.isEmpty())
    return false;
  if (itemId == m_currentMediaItem.id)
    return true;
  return m_currentMediaItem.type == "Series" &&
         itemId == m_currentPlayableItem.id;
}

QStringList DetailView::buildNumberCandidates(const MediaItem &item) const {
  QStringList candidates;
  const auto appendCandidate = [&candidates](const QString &value) {
    const QString trimmedValue = value.trimmed();
    if (!trimmedValue.isEmpty() && !candidates.contains(trimmedValue)) {
      candidates.append(trimmedValue);
    }
  };

  for (const MediaSourceInfo &source : item.mediaSources) {
    appendCandidate(source.path);
  }

  appendCandidate(item.path);
  appendCandidate(item.name);
  appendCandidate(item.originalTitle);
  appendCandidate(item.sortName);

  for (const QString &tagline : item.taglines) {
    appendCandidate(tagline);
  }

  for (const QString &tag : item.tags) {
    appendCandidate(tag);
  }

  return candidates;
}

QString DetailView::extractDisplayNumber(const MediaItem &item) const {
  static const NumberExtractor extractor(true);
  const QStringList candidates = buildNumberCandidates(item);
  const QString displayNumber = extractor.extractBestNumber(candidates);

  qDebug() << "[DetailView] adult number extraction itemId=" << item.id
           << "officialRating=" << item.officialRating
           << "candidateCount=" << candidates.size()
           << "selected=" << displayNumber;

  return displayNumber;
}

void DetailView::updateDisplayNumber(const QString &number) {
  if (!m_numberButton || !m_metaRowWidget) {
    return;
  }

  const QString trimmedNumber = number.trimmed();
  const bool hasNumber = !trimmedNumber.isEmpty();
  m_numberButton->setVisible(hasNumber);
  m_numberButton->setText(trimmedNumber);
  m_numberButton->setToolTip(hasNumber ? tr("Click to copy number")
                                       : QString());
  m_metaRowWidget->setVisible(!m_metaLabel->text().isEmpty() || hasNumber);
}

void DetailView::copyDisplayedNumber() {
  if (!m_numberButton) {
    return;
  }

  const QString displayNumber = m_numberButton->text().trimmed();
  if (displayNumber.isEmpty()) {
    return;
  }

  if (QClipboard *clipboard = QGuiApplication::clipboard()) {
    clipboard->setText(displayNumber);
  }

  qDebug() << "[DetailView] copied display number" << displayNumber
           << "for itemId" << m_currentItemId;
  ModernToast::showMessage(tr("Number copied: %1").arg(displayNumber), 2000);
}

void DetailView::markManualSeriesSelection(const QString &reason) {
  if (m_currentMediaItem.type != "Series")
    return;

  if (!m_hasManualSeriesSelection) {
    qDebug() << "[DetailView] Manual series selection locked server auto-sync"
             << "seriesId=" << m_currentItemId << "reason=" << reason;
  }
  m_hasManualSeriesSelection = true;
}

void DetailView::rememberSeriesSelection(const MediaItem &episode,
                                         const QString &reason,
                                         bool persist) {
  if (m_currentMediaItem.type != "Series")
    return;

  int selectedSeasonIndex = -1;
  QString selectedSeasonId;
  bool hasSelectedSeason = false;
  if (episode.type == "Episode" && episode.parentIndexNumber >= 0) {
    for (int i = 0; i < m_seriesSeasons.size(); ++i) {
      if (m_seriesSeasons[i].indexNumber == episode.parentIndexNumber) {
        selectedSeasonIndex = i;
        selectedSeasonId = m_seriesSeasons[i].id;
        hasSelectedSeason = true;
        break;
      }
    }
  } else {
    selectedSeasonIndex = m_currentSeasonIndex;
    if (selectedSeasonIndex >= 0 &&
        selectedSeasonIndex < m_seriesSeasons.size()) {
      selectedSeasonId = m_seriesSeasons[selectedSeasonIndex].id;
      hasSelectedSeason = true;
    }
  }

  if (hasSelectedSeason) {
    if (persist) {
      m_cachedSeasonIndex = selectedSeasonIndex;
      m_cachedSeasonId = selectedSeasonId;
    } else {
      m_manualSelectedSeasonIndex = selectedSeasonIndex;
      m_manualSelectedSeasonId = selectedSeasonId;
    }
  }
  if (episode.type == "Episode" && !episode.id.isEmpty()) {
    if (persist) {
      m_cachedSeriesPlayableItem = episode;
      m_hasCachedSeriesPlayableItem = true;
    } else {
      m_manualSelectedEpisodeId = episode.id;
    }
  }

  qDebug() << "[DetailView] Remember series selection"
           << "seriesId=" << m_currentItemId
           << "seasonIndex="
           << (persist ? m_cachedSeasonIndex : m_manualSelectedSeasonIndex)
           << "seasonId="
           << (persist ? m_cachedSeasonId : m_manualSelectedSeasonId)
           << "episodeId="
           << (persist ? m_cachedSeriesPlayableItem.id
                       : m_manualSelectedEpisodeId)
           << "persist=" << persist << "reason=" << reason;

  if (persist)
    persistDetailCacheSnapshot(reason);
}

void DetailView::updateOverviewElidedText() {
  if (m_cleanOverviewText.isEmpty() || !m_overviewLabel)
    return;

  const int generation = ++m_overviewElideGeneration;
  QPointer<DetailView> safeThis(this);
  QTimer::singleShot(0, this, [safeThis, generation]() {
    if (!safeThis || generation != safeThis->m_overviewElideGeneration)
      return;

    int labelWidth = safeThis->m_overviewLabel->contentsRect().width();
    if (labelWidth <= 10 && safeThis->m_textContainer)
      labelWidth = safeThis->m_textContainer->contentsRect().width();
    if (labelWidth <= 10)
      return;

    QFontMetrics fm(safeThis->m_overviewLabel->font());
    int maxHeight = fm.lineSpacing() * 5 + 10;
    QTextDocument doc;
    doc.setDefaultFont(safeThis->m_overviewLabel->font());
    doc.setTextWidth(labelWidth);

    QString fullHtml = safeThis->m_cleanOverviewText.toHtmlEscaped();
    fullHtml.replace("\n", "<br>");
    doc.setHtml(fullHtml);

    if (doc.size().height() <= maxHeight) {
      if (safeThis->m_overviewLabel->text() != fullHtml)
        safeThis->m_overviewLabel->setText(fullHtml);
      return;
    }

    int low = 0, high = safeThis->m_cleanOverviewText.length(), best = 0;
    QString moreLink =
        safeThis->tr("... <a href='more' style='color:#3B82F6; "
                     "text-decoration:none; font-weight:bold;'>More</a>");
    while (low <= high) {
      int mid = low + (high - low) / 2;
      QString testHtml =
          safeThis->m_cleanOverviewText.left(mid).toHtmlEscaped();
      testHtml.replace("\n", "<br>");
      doc.setHtml(testHtml + moreLink);
      if (doc.size().height() <= maxHeight) {
        best = mid;
        low = mid + 1;
      } else {
        high = mid - 1;
      }
    }

    QString finalHtml =
        safeThis->m_cleanOverviewText.left(best).toHtmlEscaped();
    finalHtml.replace("\n", "<br>");
    const QString overviewHtml = finalHtml + moreLink;
    if (safeThis->m_overviewLabel->text() != overviewHtml)
      safeThis->m_overviewLabel->setText(overviewHtml);
  });
}

QCoro::Task<void> DetailView::updateUi(MediaItem item, bool isSilentRefresh) {
  m_currentMediaItem = item;
  if (!isSilentRefresh && item.type == "Series")
    m_deferBottomInfoReveal = true;
  m_titleLabel->setText(item.name);

  const QString metaEpisodeTag =
      item.type == "Series" && m_currentPlayableItem.type == "Episode"
          ? formatEpisodeTag(m_currentPlayableItem)
          : formatEpisodeTag(item);
  updateMetaRow(item, metaEpisodeTag);

  const bool shouldShowNumber = shouldShowDisplayNumber(item);
  const QString displayNumber =
      shouldShowNumber ? extractDisplayNumber(item) : QString();
  updateDisplayNumber(displayNumber);

  qDebug() << "[DetailView] updateUi itemId=" << item.id
           << "adultMovie=" << shouldShowNumber
           << "displayNumber=" << displayNumber;

  QString text = item.overview;
  text.replace(QRegularExpression("<br\\s*/?>",
                                  QRegularExpression::CaseInsensitiveOption),
               "\n");
  text.replace("</p>", "\n\n");
  text.remove(QRegularExpression("<[^>]*>"));
  QTextDocument doc;
  doc.setHtml(text);
  QString cleanText = doc.toPlainText();
  cleanText.replace(QChar::LineSeparator, '\n');
  cleanText.remove(QRegularExpression("<[^>]*>"));
  m_cleanOverviewText = cleanText;
  m_lastOverviewWidth = -1;
  updateOverviewElidedText();

  if (item.type == "Series") {
    const bool hasCurrentEpisode =
        m_currentPlayableItem.type == "Episode" &&
        !m_currentPlayableItem.id.isEmpty();
    if (!isSilentRefresh && !hasCurrentEpisode)
      m_actionWidget->setSeriesLoadingMode();
  } else {
    
    
    m_currentPlayableItem = item;
    m_actionWidget->setupNormalMode(item);
  }

  
  
  const QString newSourcesFingerprint =
      computeSourcesFingerprint(item.mediaSources);
  const bool sourcesUnchanged =
      !m_appliedSourcesFingerprint.isEmpty() &&
      newSourcesFingerprint == m_appliedSourcesFingerprint;
  const bool sourcesOwnedBySeriesEpisode =
      item.type == "Series" && m_currentPlayableItem.type == "Episode" &&
      !m_currentPlayableItem.id.isEmpty();
  if (!sourcesOwnedBySeriesEpisode && !sourcesUnchanged) {
    m_actionWidget->setSources(item.mediaSources, 0);
    m_appliedSourcesFingerprint = newSourcesFingerprint;
    if (item.mediaSources.isEmpty() ||
        item.mediaSources.first().mediaStreams.isEmpty()) {
      m_actionWidget->setStreams(MediaSourceInfo{});
    }
  }
  m_actionWidget->refreshExtPlayerButton();

  if (!item.taglines.isEmpty()) {
    m_taglineLabel->setText(item.taglines.first());
    m_taglineLabel->show();
  } else {
    m_taglineLabel->hide();
  }

  m_isFavorite = item.isFavorite();
  m_actionWidget->setFavoriteState(m_isFavorite);
  m_actionWidget->setPlayedState(item.userData.played);

  
  
  buildTagButtons(item.genres);

  QCoro::connect(executeLoadImages(QPointer<DetailView>(this), m_core, item,
                                   true),
                 this, []() {});

  QPointer<DetailView> safeThis(this);

  
  
  auto applyBottomInfo = [this, &item](const QList<MediaSourceInfo> &srcs) {
    const QString fp = computeBottomInfoFingerprint(item, srcs);
    if (fp == m_appliedBottomInfoFingerprint)
      return;
    m_bottomInfoWidget->setInfo(item, srcs);
    m_appliedBottomInfoFingerprint = fp;
    if (m_deferBottomInfoReveal) {
      showReserveWidget(m_bottomInfoReserveWidget,
                        reserveHeightFor(m_bottomInfoWidget, 220));
    }
  };

  if (sourcesOwnedBySeriesEpisode) {
    applySeriesPlayableItemToUi(m_currentPlayableItem);
  } else if (!item.mediaSources.isEmpty() &&
             !item.mediaSources.first().mediaStreams.isEmpty()) {
    int defaultIndex = m_actionWidget->currentSourceIndex();
    QList<MediaSourceInfo> defaultSource;
    if (defaultIndex < item.mediaSources.size()) {
      const MediaSourceInfo &dSrc = item.mediaSources[defaultIndex];
      
      
      if (!sourcesUnchanged)
        m_actionWidget->setStreams(dSrc);
      defaultSource.append(dSrc);
    }
    applyBottomInfo(defaultSource);
  } else {
    
    
    applyBottomInfo({});
    if (item.mediaType == "Video" || item.mediaType == "Audio" ||
        item.type == "Movie" || item.type == "Episode") {
      try {
        PlaybackInfo info =
            co_await m_core->mediaService()->getPlaybackInfo(item.id);
        if (!safeThis)
          co_return;
        if (item.id == safeThis->m_currentItemId &&
            !info.mediaSources.isEmpty()) {
          safeThis->m_currentMediaItem.mediaSources = info.mediaSources;

          const QString refreshedNumber =
              safeThis->shouldShowDisplayNumber(safeThis->m_currentMediaItem)
                  ? safeThis->extractDisplayNumber(safeThis->m_currentMediaItem)
                  : QString();
          safeThis->updateDisplayNumber(refreshedNumber);

          
          
          if (item.type != "Series") {
            safeThis->m_currentPlayableItem.mediaSources = info.mediaSources;
          }

          int idx = safeThis->m_actionWidget->currentSourceIndex();
          if (idx >= info.mediaSources.size())
            idx = 0;
          safeThis->m_actionWidget->setSources(info.mediaSources, idx);
          safeThis->m_appliedSourcesFingerprint =
              computeSourcesFingerprint(info.mediaSources);
          idx = safeThis->m_actionWidget->currentSourceIndex();
          if (idx >= safeThis->m_currentMediaItem.mediaSources.size())
            idx = 0;
          if (idx < safeThis->m_currentMediaItem.mediaSources.size()) {
            const MediaSourceInfo &singleSource =
                safeThis->m_currentMediaItem.mediaSources[idx];
            safeThis->m_actionWidget->setStreams(singleSource);
            QList<MediaSourceInfo> sList;
            sList.append(singleSource);
            
            const QString fp = computeBottomInfoFingerprint(
                safeThis->m_currentMediaItem, sList);
            if (fp != safeThis->m_appliedBottomInfoFingerprint) {
              safeThis->m_bottomInfoWidget->setInfo(
                  safeThis->m_currentMediaItem, sList);
              safeThis->m_appliedBottomInfoFingerprint = fp;
              if (safeThis->m_deferBottomInfoReveal) {
                showReserveWidget(
                    safeThis->m_bottomInfoReserveWidget,
                    reserveHeightFor(safeThis->m_bottomInfoWidget, 220));
              }
            }
          }
        }
      } catch (...) {
      }
    }
  }

  if (!safeThis)
    co_return;
  safeThis->m_bottomInfoWidget->setVisible(
      !safeThis->m_deferBottomInfoReveal);
  if (safeThis->m_deferBottomInfoReveal) {
    showReserveWidget(safeThis->m_bottomInfoReserveWidget,
                      reserveHeightFor(safeThis->m_bottomInfoWidget, 220));
  } else {
    hideReserveWidget(safeThis->m_bottomInfoReserveWidget);
  }
  if (!isSilentRefresh && safeThis)
    QMetaObject::invokeMethod(safeThis.data(), "scrollToTop",
                              Qt::QueuedConnection);
}

void DetailView::onOverviewMoreClicked(const QString &link) {
  if (link == "more") {
    OverviewDialog dialog(this);
    dialog.setMediaItem(m_currentMediaItem, m_currentPosterPix);
    dialog.exec();
  }
}

QCoro::Task<void>
DetailView::executeSilentRefresh(QPointer<DetailView> safeThis, QEmbyCore *core,
                                 QString itemId) {
  QString cacheServerId;
  QString cacheUserId;
  if (core && core->serverManager()) {
    const ServerProfile profile = core->serverManager()->activeProfile();
    cacheServerId = profile.id;
    cacheUserId = profile.userId;
  }

  try {
    MediaItem item = co_await core->mediaService()->getItemDetail(itemId);
    if (!cacheServerId.isEmpty() && !cacheUserId.isEmpty()) {
      const QString fp = co_await QtConcurrent::run(
          [item]() mutable { return DetailCacheUtils::fingerprint(item); });
      saveDetailCacheAsync(cacheServerId, cacheUserId, item, fp);
      if (safeThis && item.id == safeThis->m_currentItemId)
        safeThis->m_cachedDetailFingerprint = fp;
    }
    if (safeThis && item.id == safeThis->m_currentItemId) {
      co_await safeThis->updateUi(item, true);

      
      if (item.type == "Series") {
        co_await safeThis->fetchSeriesNextUp(
            itemId, !safeThis->m_hasManualSeriesSelection);
        if (!safeThis || safeThis->m_currentItemId != itemId)
          co_return;

        auto seasons = co_await core->mediaService()->getSeasons(itemId);
        if (!safeThis || safeThis->m_currentItemId != itemId)
          co_return;

        safeThis->m_seriesSeasons = seasons;
        safeThis->m_cachedSeriesSeasons = seasons;
        safeThis->m_hasCachedSeriesSeasons = true;

        
        
        bool selectedSeasonResolved = false;
        if (safeThis->m_hasManualSeriesSelection &&
            !safeThis->m_manualSelectedSeasonId.isEmpty()) {
          for (int i = 0; i < seasons.size(); ++i) {
            if (seasons[i].id == safeThis->m_manualSelectedSeasonId) {
              safeThis->m_currentSeasonIndex = i;
              selectedSeasonResolved = true;
              break;
            }
          }
        }
        if (!selectedSeasonResolved &&
            safeThis->m_hasManualSeriesSelection &&
            safeThis->m_manualSelectedSeasonIndex >= 0 &&
            safeThis->m_manualSelectedSeasonIndex < seasons.size()) {
          safeThis->m_currentSeasonIndex =
              safeThis->m_manualSelectedSeasonIndex;
          selectedSeasonResolved = true;
        }
        if (!selectedSeasonResolved &&
            safeThis->m_hasManualSeriesSelection &&
            safeThis->m_currentSeasonIndex >= 0 &&
            safeThis->m_currentSeasonIndex < seasons.size()) {
          selectedSeasonResolved = true;
        }
        int playableParentIdx =
            safeThis->m_currentPlayableItem.parentIndexNumber;
        if (!selectedSeasonResolved &&
            !safeThis->m_hasManualSeriesSelection && playableParentIdx >= 0) {
          for (int i = 0; i < seasons.size(); ++i) {
            if (seasons[i].indexNumber == playableParentIdx) {
              safeThis->m_currentSeasonIndex = i;
              break;
            }
          }
        }

        if (!seasons.isEmpty() && safeThis->m_episodeWidget) {
          const int idx =
              qBound(0, safeThis->m_currentSeasonIndex, seasons.size() - 1);
          safeThis->updateSeasonSwitcher(idx);

          const QString playableItemId =
              safeThis->m_hasManualSeriesSelection &&
                      !safeThis->m_manualSelectedEpisodeId.isEmpty()
                  ? safeThis->m_manualSelectedEpisodeId
                  : safeThis->m_currentPlayableItem.id;
          co_await safeThis->loadEpisodesForSeason(
              idx, playableItemId, !playableItemId.isEmpty(),
              safeThis->m_hasManualSeriesSelection);
        } else if (safeThis->m_episodeWidget) {
          applyReservedSectionItems(safeThis->m_episodeSectionReserveWidget,
                                    safeThis->m_episodeWidget, {});
        }
        if (!safeThis || safeThis->m_currentItemId != itemId)
          co_return;

        if (safeThis->m_seasonWidget) {
          safeThis->m_seasonWidget->setTitle(tr("Seasons"));
          safeThis->m_seasonWidget->setCardStyle(MediaCardDelegate::Poster);
          safeThis->m_seasonWidget->setGalleryHeight(300);
          applyReservedSectionItems(safeThis->m_seasonSectionReserveWidget,
                                    safeThis->m_seasonWidget, seasons);
        }
      }

      safeThis->applyCastSection(item);
      if (safeThis->m_deferBottomInfoReveal) {
        safeThis->m_deferBottomInfoReveal = false;
        hideReserveWidget(safeThis->m_bottomInfoReserveWidget);
        if (safeThis->m_bottomInfoWidget)
          safeThis->m_bottomInfoWidget->show();
      }
      safeThis->persistDetailCacheSnapshot(QStringLiteral("silent-refresh"));
    }
  } catch (...) {
  }
}

QCoro::Task<void> DetailView::executeLoadImages(QPointer<DetailView> safeThis,
                                                QEmbyCore *core,
                                                MediaItem item,
                                                bool allowPrimaryBackdropFallback) {
  bool adaptive =
      ConfigStore::instance()->get<bool>(ConfigKeys::AdaptiveImages, true);
  QString cacheServerId;
  QString cacheUserId;
  if (safeThis) {
    cacheServerId = safeThis->m_detailCacheServerId;
    cacheUserId = safeThis->m_detailCacheUserId;
  } else if (core && core->serverManager()) {
    const ServerProfile profile = core->serverManager()->activeProfile();
    cacheServerId = profile.id;
    cacheUserId = profile.userId;
  }

  
  
  QString posterType = QStringLiteral("Primary");
  QString posterTag = item.images.primaryTag;
  QString posterId = item.id;
  if (posterTag.isEmpty() && adaptive) {
    auto best = item.images.bestPoster();
    posterTag = best.first;
    posterType = best.second;
    if (item.images.isParentTag(posterTag)) {
      posterId = item.images.parentImageItemId.isEmpty()
                     ? item.seriesId
                     : item.images.parentImageItemId;
    }
  }
  const int posterMaxWidth =
      posterType == QStringLiteral("Primary") ? 400 : 800;

  QString backdropType = QStringLiteral("Backdrop");
  QString backdropTag = item.images.backdropTag;
  QString backdropId = item.id;
  if (backdropTag.isEmpty() && adaptive) {
    if (allowPrimaryBackdropFallback) {
      auto best = item.images.bestBackdrop();
      backdropTag = best.first;
      backdropType = best.second;
    } else {
      if (!item.images.thumbTag.isEmpty()) {
        backdropTag = item.images.thumbTag;
        backdropType = QStringLiteral("Thumb");
      } else if (!item.images.parentBackdropTag.isEmpty()) {
        backdropTag = item.images.parentBackdropTag;
        backdropType = QStringLiteral("Backdrop");
      } else if (!item.images.parentThumbTag.isEmpty()) {
        backdropTag = item.images.parentThumbTag;
        backdropType = QStringLiteral("Thumb");
      }
    }
    if (item.images.isParentTag(backdropTag)) {
      backdropId = item.images.parentImageItemId.isEmpty()
                       ? item.seriesId
                       : item.images.parentImageItemId;
    }
  }

  const QString logoId = item.id;
  const QString logoType = QStringLiteral("Logo");
  const QString logoTag = item.images.logoTag;
  constexpr int logoMaxWidth = 400;

  const CachedDetailImages cachedImages = co_await QtConcurrent::run(
      [cacheServerId, cacheUserId, ownerItemId = item.id, posterId,
       posterType, posterTag, posterMaxWidth, backdropId, backdropType,
       backdropTag, logoId, logoType, logoTag, logoMaxWidth]() mutable {
        CachedDetailImages result;
        if (!posterTag.isEmpty()) {
          result.poster = DetailCacheUtils::loadImage(
              cacheServerId, cacheUserId, ownerItemId,
              QStringLiteral("poster"), posterId, posterType, posterTag,
              posterMaxWidth);
        }
        if (!backdropTag.isEmpty()) {
          result.backdrop = DetailCacheUtils::loadImage(
              cacheServerId, cacheUserId, ownerItemId,
              QStringLiteral("backdrop"), backdropId, backdropType,
              backdropTag, 1920);
        }
        if (!logoTag.isEmpty()) {
          result.logo = DetailCacheUtils::loadImage(
              cacheServerId, cacheUserId, ownerItemId, QStringLiteral("logo"),
              logoId, logoType, logoTag, logoMaxWidth);
        }
        return result;
      });
  if (!safeThis || safeThis->m_currentItemId != item.id)
    co_return;

  if (cachedImages.poster.has_value()) {
    safeThis->m_currentPosterPix =
        QPixmap::fromImage(cachedImages.poster.value());
    safeThis->m_posterLabel->setPixmap(safeThis->m_currentPosterPix);
    qDebug() << "[DetailView] Detail poster image cache hit"
             << "itemId=" << item.id << "imageId=" << posterId
             << "type=" << posterType;
  }

  if (cachedImages.backdrop.has_value()) {
    safeThis->m_currentBackdropPix =
        QPixmap::fromImage(cachedImages.backdrop.value());
    safeThis->updateBackdrop();
    qDebug() << "[DetailView] Detail backdrop image cache hit"
             << "itemId=" << item.id << "imageId=" << backdropId
             << "type=" << backdropType;
  }

  if (cachedImages.logo.has_value()) {
    safeThis->m_logoLabel->setPixmap(
        QPixmap::fromImage(cachedImages.logo.value()));
    safeThis->m_logoLabel->show();
    qDebug() << "[DetailView] Detail logo image cache hit"
             << "itemId=" << item.id;
  }

  
  if (!posterTag.isEmpty()) {
    try {
      QPixmap pix = co_await core->mediaService()->fetchImage(
          posterId, posterType, posterTag, posterMaxWidth);
      if (safeThis && safeThis->m_currentItemId == item.id && !pix.isNull()) {
        safeThis->m_currentPosterPix = pix;

        
        
        
        const QSize posterSize(250, 375);
        const QString currentItemId = item.id;
        QImage src = pix.toImage();

        auto *watcher = new QFutureWatcher<QImage>(safeThis.data());
        QObject::connect(
            watcher, &QFutureWatcher<QImage>::finished, safeThis.data(),
            [safeThis, watcher, currentItemId, cacheServerId, cacheUserId,
             posterId, posterType, posterTag, posterMaxWidth]() {
              if (safeThis && safeThis->m_currentItemId == currentItemId) {
                const QImage result = watcher->result();
                if (!result.isNull()) {
                  safeThis->m_posterLabel->setPixmap(
                      QPixmap::fromImage(result));
                  saveDetailImageAsync(cacheServerId, cacheUserId,
                                       currentItemId,
                                       QStringLiteral("poster"), posterId,
                                       posterType, posterTag, posterMaxWidth,
                                       result);
                }
              }
              watcher->deleteLater();
            });
        watcher->setFuture(QtConcurrent::run([src, posterSize]() -> QImage {
          QImage scaled =
              src.scaled(posterSize, Qt::KeepAspectRatioByExpanding,
                         Qt::SmoothTransformation);
          const int cropX = (scaled.width() - posterSize.width()) / 2;
          const int cropY = (scaled.height() - posterSize.height()) / 2;
          QImage cropped =
              scaled.copy(cropX, cropY, posterSize.width(), posterSize.height());

          
          QImage rounded(posterSize, QImage::Format_ARGB32_Premultiplied);
          rounded.fill(Qt::transparent);
          QPainter p(&rounded);
          p.setRenderHint(QPainter::Antialiasing);
          QPainterPath path;
          path.addRoundedRect(
              QRectF(0, 0, posterSize.width(), posterSize.height()), 12, 12);
          p.setClipPath(path);
          p.drawImage(0, 0, cropped);
          p.end();
          return rounded;
        }));
      }
    } catch (...) {
    }
  }

  
  if (!backdropTag.isEmpty()) {
    try {
      QPixmap pix = co_await core->mediaService()->fetchImage(
          backdropId, backdropType, backdropTag, 1920);
      if (safeThis && safeThis->m_currentItemId == item.id && !pix.isNull()) {
        safeThis->m_currentBackdropPix = pix;
        safeThis->updateBackdrop();
        saveDetailImageAsync(cacheServerId, cacheUserId, item.id,
                             QStringLiteral("backdrop"), backdropId,
                             backdropType, backdropTag, 1920, pix.toImage());
      }
    } catch (...) {
    }
  }

  
  
  if (!logoTag.isEmpty()) {
    try {
      QPixmap pix =
          co_await core->mediaService()->fetchImage(logoId, logoType, logoTag,
                                                    logoMaxWidth);
      if (safeThis && safeThis->m_currentItemId == item.id && !pix.isNull()) {
        const QString currentItemId = item.id;
        QImage src = pix.toImage();

        auto *watcher = new QFutureWatcher<QImage>(safeThis.data());
        QObject::connect(
            watcher, &QFutureWatcher<QImage>::finished, safeThis.data(),
            [safeThis, watcher, currentItemId, cacheServerId, cacheUserId,
             logoId, logoType, logoTag, logoMaxWidth]() {
              if (safeThis &&
                  safeThis->m_currentItemId == currentItemId) {
                const QImage result = watcher->result();
                if (!result.isNull()) {
                  safeThis->m_logoLabel->setPixmap(
                      QPixmap::fromImage(result));
                  safeThis->m_logoLabel->show();
                  saveDetailImageAsync(
                      cacheServerId, cacheUserId, currentItemId,
                      QStringLiteral("logo"), logoId, logoType, logoTag,
                      logoMaxWidth, result);
                }
              }
              watcher->deleteLater();
            });
        watcher->setFuture(QtConcurrent::run([src]() -> QImage {
          return src.scaled(250, 100, Qt::KeepAspectRatio,
                            Qt::SmoothTransformation);
        }));
      }
    } catch (...) {
    }
  }
}

void DetailView::onMediaItemUpdated(const MediaItem &item) {
  
  if (m_currentItemId == item.id && m_core && !m_skipSilentRefresh) {
    QCoro::connect(
        executeSilentRefresh(QPointer<DetailView>(this), m_core, item.id), this,
        []() {});
  }

  
  if (item.type == "Season" && !m_currentSeasonEpisodes.isEmpty()) {
    bool belongsToSeries = false;
    for (const MediaItem &season : m_seriesSeasons) {
      if (season.id == item.id) {
        belongsToSeries = true;
        break;
      }
    }
    if (belongsToSeries) {
      for (MediaItem &ep : m_currentSeasonEpisodes) {
        if (ep.parentIndexNumber == item.indexNumber)
          ep.userData.played = item.userData.played;
      }
      m_episodeWidget->setItems(m_currentSeasonEpisodes);
    }
  }

  
  if (item.type == "Episode" && !m_currentSeasonEpisodes.isEmpty() &&
      !m_seriesSeasons.isEmpty()) {
    
    for (MediaItem &ep : m_currentSeasonEpisodes) {
      if (ep.id == item.id) {
        ep.userData.played = item.userData.played;
        break;
      }
    }
    
    const int seasonIndex = item.parentIndexNumber;
    if (seasonIndex >= 0) {
      for (MediaItem &season : m_seriesSeasons) {
        if (season.indexNumber == seasonIndex) {
          int total = 0, played = 0;
          for (const MediaItem &ep : m_currentSeasonEpisodes) {
            if (ep.parentIndexNumber == seasonIndex) {
              total++;
              if (ep.userData.played)
                played++;
            }
          }
          const bool allPlayed = (total > 0 && played == total);
          if (season.userData.played != allPlayed) {
            season.userData.played = allPlayed;
            m_seasonWidget->updateItem(season);
          }
          break;
        }
      }
    }
  }

  
  if (m_currentPlayableItem.id == item.id) {
    m_currentPlayableItem.userData.played = item.userData.played;
    m_actionWidget->setPlayedState(item.userData.played);
  }
  if (m_currentMediaItem.id == item.id) {
    m_currentMediaItem.userData.played = item.userData.played;
    m_actionWidget->setPlayedState(item.userData.played);
  }

  if (m_seasonWidget)
    m_seasonWidget->updateItem(item);
  if (m_episodeWidget)
    m_episodeWidget->updateItem(item);
  if (m_castWidget)
    m_castWidget->updateItem(item);
  if (m_additionalPartsWidget)
    m_additionalPartsWidget->updateItem(item);
  if (m_collectionWidget)
    m_collectionWidget->updateItem(item);
  if (m_similarWidget)
    m_similarWidget->updateItem(item);
}

void DetailView::beginOptimisticPlayedUpdate() { m_skipSilentRefresh = true; }

void DetailView::endOptimisticPlayedUpdate() { m_skipSilentRefresh = false; }

void DetailView::updateSeasonSwitcher(int currentIndex) {
  if (!m_seasonSwitcher)
    return;

  QSignalBlocker blocker(m_seasonSwitcher);
  m_seasonSwitcher->clear();

  for (const MediaItem &season : m_seriesSeasons)
    m_seasonSwitcher->addItem(season.name, "");

  if (!m_seriesSeasons.isEmpty()) {
    const int idx = qBound(0, currentIndex, m_seriesSeasons.size() - 1);
    m_seasonSwitcher->setCurrentIndex(idx);
  }

  m_seasonSwitcher->setVisible(m_seriesSeasons.size() > 1);
  m_seasonSwitcher->adjustSize();
  if (m_episodeJumpEdit) {
    const int switcherHeight =
        qMax(m_seasonSwitcher->height(), m_seasonSwitcher->sizeHint().height());
    m_episodeJumpEdit->setFixedHeight(switcherHeight);
  }
  updateEpisodeHeaderControlsVisibility();
}

void DetailView::updateEpisodeHeaderControlsVisibility() {
  if (!m_episodeHeaderControls)
    return;

  const bool showSeasonSwitcher =
      m_seasonSwitcher && !m_seasonSwitcher->isHidden();
  const bool showEpisodeJump =
      m_episodeJumpEdit && !m_episodeJumpEdit->isHidden();
  m_episodeHeaderControls->setVisible(showSeasonSwitcher || showEpisodeJump);
  m_episodeHeaderControls->adjustSize();
  m_episodeHeaderControls->updateGeometry();
}

void DetailView::updateEpisodeJumpControl(int episodeCount) {
  if (!m_episodeJumpEdit) {
    return;
  }

  const int visibleCardCapacity = episodeGalleryVisibleCardCapacity();
  const bool showJump =
      episodeCount > 0 && visibleCardCapacity > 0 &&
      episodeCount > visibleCardCapacity;
  if (showJump) {
    int explicitMaxEpisodeNumber = 0;
    for (const MediaItem &episode : m_currentSeasonEpisodes)
      explicitMaxEpisodeNumber = qMax(explicitMaxEpisodeNumber,
                                      episode.indexNumber);

    const int maxEpisodeNumber =
        explicitMaxEpisodeNumber > 0 ? explicitMaxEpisodeNumber : episodeCount;
    const QString rangeText = tr("1-%1").arg(maxEpisodeNumber);
    m_episodeJumpEdit->setPlaceholderText(rangeText);
    m_episodeJumpEdit->setToolTip(
        tr("Jump to episode number (1-%1)").arg(maxEpisodeNumber));
    if (m_episodeJumpValidator)
      m_episodeJumpValidator->setTop(maxEpisodeNumber);
    m_episodeJumpEdit->setMaxLength(
        qMax(1, QString::number(maxEpisodeNumber).length()));

    const int textWidth =
        m_episodeJumpEdit->fontMetrics().horizontalAdvance(rangeText);
    m_episodeJumpEdit->setFixedWidth(qBound(64, textWidth + 22, 92));
  } else {
    m_episodeJumpEdit->clear();
    m_episodeJumpEdit->setPlaceholderText(tr("Ep #"));
    m_episodeJumpEdit->setToolTip(tr("Jump to episode number"));
    if (m_episodeJumpValidator)
      m_episodeJumpValidator->setTop(9999);
    m_episodeJumpEdit->setMaxLength(4);
    m_episodeJumpEdit->setFixedWidth(64);
  }

  m_episodeJumpEdit->setVisible(showJump);
  updateEpisodeHeaderControlsVisibility();
}

int DetailView::episodeGalleryVisibleCardCapacity() const {
  if (!m_episodeWidget || !m_episodeWidget->gallery())
    return 0;

  QListView *listView = m_episodeWidget->gallery()->listView();
  if (!listView || !listView->viewport())
    return 0;

  int cardWidth = m_episodeTileWidth;
  if (cardWidth <= 0 && listView->model() && listView->model()->rowCount() > 0) {
    const QSize itemSize =
        listView->sizeHintForIndex(listView->model()->index(0, 0));
    cardWidth = itemSize.width();
  }
  if (cardWidth <= 0)
    return 0;

  const int viewportWidth = listView->viewport()->width();
  if (viewportWidth <= 0)
    return 0;

  return qMax(1, viewportWidth / cardWidth);
}

MediaItem DetailView::episodeForJumpNumber(int episodeNumber) const {
  if (episodeNumber <= 0)
    return MediaItem{};

  bool hasExplicitEpisodeNumbers = false;
  for (const MediaItem &episode : m_currentSeasonEpisodes) {
    if (episode.indexNumber > 0)
      hasExplicitEpisodeNumbers = true;
    if (episode.indexNumber == episodeNumber)
      return episode;
  }

  if (hasExplicitEpisodeNumbers)
    return MediaItem{};

  const int row = episodeNumber - 1;
  if (row >= 0 && row < m_currentSeasonEpisodes.size())
    return m_currentSeasonEpisodes.at(row);

  return MediaItem{};
}

void DetailView::submitEpisodeJump() {
  if (!m_episodeJumpEdit || m_episodeJumpEdit->isHidden() ||
      m_currentSeasonEpisodes.isEmpty()) {
    return;
  }

  const QString text = m_episodeJumpEdit->text().trimmed();
  if (text.isEmpty())
    return;

  bool ok = false;
  const int episodeNumber = text.toInt(&ok);
  if (!ok)
    return;

  const MediaItem targetEpisode = episodeForJumpNumber(episodeNumber);
  if (targetEpisode.id.isEmpty()) {
    qDebug() << "[DetailView] Episode jump target not found"
             << "seriesId=" << m_currentItemId
             << "seasonIndex=" << m_currentSeasonIndex
             << "episodeNumber=" << episodeNumber
             << "episodeCount=" << m_currentSeasonEpisodes.size();
    ModernToast::showMessage(tr("Episode %1 not found").arg(episodeNumber),
                             1600);
    m_episodeJumpEdit->selectAll();
    return;
  }

  qDebug() << "[DetailView] Episode jump"
           << "seriesId=" << m_currentItemId
           << "seasonIndex=" << m_currentSeasonIndex
           << "episodeNumber=" << episodeNumber
           << "episodeId=" << targetEpisode.id;

  m_episodeJumpEdit->clear();
  markManualSeriesSelection(QStringLiteral("episode-jump"));
  QCoro::connect(applySeriesPlayableItem(targetEpisode, true), this, []() {});
}

QCoro::Task<void> DetailView::loadEpisodesForSeason(int idx,
                                                    QString highlightEpisodeId,
                                                    bool scrollToHighlight,
                                                    bool manualSelection) {
  if (idx < 0 || idx >= m_seriesSeasons.size() || !m_episodeWidget)
    co_return;

  m_currentSeasonIndex = idx;
  const QString seasonId = m_seriesSeasons[idx].id;
  if (seasonId.isEmpty())
    co_return;
  if (manualSelection)
    markManualSeriesSelection(QStringLiteral("season-selection"));
  rememberSeriesSelection(MediaItem{}, QStringLiteral("season-selection"),
                          !manualSelection);

  bool showedCachedEpisodes = false;
  QList<MediaItem> shownCachedEpisodes;
  if (m_hasCachedSeasonEpisodes && m_cachedSeasonIndex == idx &&
      m_cachedSeasonId == seasonId &&
      episodesBelongToSeason(m_cachedSeasonEpisodes, m_seriesSeasons[idx])) {
    shownCachedEpisodes = m_cachedSeasonEpisodes;
    if (!m_appliedCachedSeasonEpisodesToUi) {
      m_currentSeasonEpisodes = shownCachedEpisodes;
      applyReservedSectionItems(m_episodeSectionReserveWidget, m_episodeWidget,
                                shownCachedEpisodes);
      updateEpisodeJumpControl(shownCachedEpisodes.size());
      if (!highlightEpisodeId.isEmpty()) {
        m_episodeWidget->gallery()->setHighlightedItemId(highlightEpisodeId);
        if (scrollToHighlight) {
          m_episodeWidget->gallery()->scrollToItemId(highlightEpisodeId);
        }
      }
      m_appliedCachedSeasonEpisodesToUi = true;
    }
    showedCachedEpisodes = true;
    qDebug() << "[DetailView][cache-ui] Applied cached season episodes"
             << "seriesId=" << m_currentItemId << "seasonId=" << seasonId
             << "seasonIndex=" << idx
             << "episodeCount=" << shownCachedEpisodes.size();
  } else {
    m_currentSeasonEpisodes.clear();
    updateEpisodeJumpControl(0);
  }

  if (!m_core || !m_core->mediaService())
    co_return;

  QPointer<DetailView> safeThis(this);
  const QString seriesId = m_currentItemId;
  QEmbyCore *core = m_core;

  try {
    qDebug() << "[DetailView][network] Fetch season episodes"
             << "seriesId=" << seriesId << "seasonId=" << seasonId
             << "seasonIndex=" << idx;
    QList<MediaItem> episodes =
        co_await core->mediaService()->getEpisodes(seriesId, seasonId);

    if (!safeThis || safeThis->m_currentItemId != seriesId ||
        safeThis->m_currentSeasonIndex != idx)
      co_return;

    bool episodesUnchanged = false;
    if (showedCachedEpisodes) {
      episodesUnchanged =
          co_await mediaItemListsEqualAsync(shownCachedEpisodes, episodes);
      if (!safeThis || safeThis->m_currentItemId != seriesId ||
          safeThis->m_currentSeasonIndex != idx)
        co_return;
    }

    safeThis->m_currentSeasonEpisodes = episodes;
    if (!safeThis->m_hasManualSeriesSelection) {
      safeThis->m_cachedSeasonEpisodes = episodes;
      safeThis->m_cachedSeasonIndex = idx;
      safeThis->m_cachedSeasonId = seasonId;
      safeThis->m_hasCachedSeasonEpisodes = true;
    } else {
      qDebug() << "[DetailView] Skip persisting manually selected season "
                  "episodes"
               << "seriesId=" << seriesId << "seasonId=" << seasonId
               << "seasonIndex=" << idx;
    }
    if (!manualSelection && !highlightEpisodeId.isEmpty() &&
        safeThis->m_currentPlayableItem.id != highlightEpisodeId) {
      for (const MediaItem &episode : episodes) {
        if (episode.id == highlightEpisodeId) {
          safeThis->applySeriesPlayableItemToUi(episode);
          break;
        }
      }
    }
    if (!episodesUnchanged) {
      applyReservedSectionItems(safeThis->m_episodeSectionReserveWidget,
                                safeThis->m_episodeWidget, episodes);
    }
    safeThis->updateEpisodeJumpControl(episodes.size());

    qDebug() << "[DetailView][network] Loaded season episodes"
             << "seriesId=" << seriesId << "seasonId=" << seasonId
             << "seasonIndex=" << idx << "episodeCount=" << episodes.size()
             << "fromCache=" << showedCachedEpisodes
             << "unchanged=" << episodesUnchanged
             << "visibleCardCapacity="
             << safeThis->episodeGalleryVisibleCardCapacity()
             << "jumpVisible="
             << (safeThis->m_episodeJumpEdit &&
                 !safeThis->m_episodeJumpEdit->isHidden());

    if (!highlightEpisodeId.isEmpty()) {
      safeThis->m_episodeWidget->gallery()->setHighlightedItemId(
          highlightEpisodeId);
      if (scrollToHighlight) {
        safeThis->m_episodeWidget->gallery()->scrollToItemId(
            highlightEpisodeId);
      }
    }
    safeThis->persistDetailCacheSnapshot(QStringLiteral("episodes"));
  } catch (...) {
    if (safeThis && safeThis->m_currentItemId == seriesId &&
        safeThis->m_currentSeasonIndex == idx) {
      if (!showedCachedEpisodes) {
        safeThis->m_currentSeasonEpisodes.clear();
        safeThis->updateEpisodeJumpControl(0);
        applyReservedSectionItems(safeThis->m_episodeSectionReserveWidget,
                                  safeThis->m_episodeWidget, {});
      }
      qDebug() << "[DetailView] Failed to load season episodes"
               << "seriesId=" << seriesId << "seasonId=" << seasonId
               << "seasonIndex=" << idx
               << "keptCache=" << showedCachedEpisodes;
    }
  }
}


QCoro::Task<void> DetailView::switchToSeason(int idx, bool manualSelection) {
  co_await loadEpisodesForSeason(idx, QString(), false, manualSelection);
}
