#include "shell/control_center/media_tab.h"

#include "core/log.h"
#include "dbus/mpris/mpris_art.h"
#include "dbus/mpris/mpris_service.h"
#include "i18n/i18n.h"
#include "net/http_client.h"
#include "pipewire/pipewire_spectrum.h"
#include "render/core/renderer.h"
#include "render/scene/node.h"
#include "shell/control_center/tab.h"
#include "shell/panel/panel_manager.h"
#include "ui/controls/audio_spectrum.h"
#include "ui/controls/button.h"
#include "ui/controls/context_menu.h"
#include "ui/controls/context_menu_popup.h"
#include "ui/controls/flex.h"
#include "ui/controls/image.h"
#include "ui/controls/label.h"
#include "ui/controls/slider.h"

#include <algorithm>
#include <chrono>
#include <cmath>
#include <filesystem>
#include <format>
#include <memory>
#include <string>
#include <string_view>
#include <vector>

using namespace control_center;
using namespace mpris;

namespace {

  const Logger kLog{"media_tab"};

  // Layout-grid unit for the media tab. Calibrated visual size, decoupled from
  // Style::controlHeightLg which is a control-row height — bumping that token to
  // a roomier value would otherwise inflate the artwork, card, and menu widths
  // and overflow the panel content area.
  constexpr float kMediaUnit = 36.0f;

  constexpr float kArtworkSize = kMediaUnit * 6;
  constexpr float kMediaNowCardMinHeight = kMediaUnit * 11 + Style::spaceSm * 2;
  constexpr float kMediaControlsHeight = kMediaUnit + Style::spaceXs;
  constexpr float kMediaPlayPauseHeight = kMediaUnit + Style::spaceSm;
  constexpr float kMediaArtworkMinHeight = kMediaUnit * 4;
  constexpr auto kNoActivePlayerGrace = std::chrono::milliseconds(2000);

  std::string playPauseGlyph(const std::string& playbackStatus) {
    return playbackStatus == "Playing" ? "media-pause" : "media-play";
  }

  std::string repeatGlyph(const std::string& loopStatus) { return loopStatus == "Track" ? "repeat-once" : "repeat"; }

  ButtonVariant toggleVariant(bool active) { return active ? ButtonVariant::Accent : ButtonVariant::Ghost; }
  constexpr int kVisualizerBandCount = 32;

} // namespace

MediaTab::MediaTab(MprisService* mpris, HttpClient* httpClient, PipeWireSpectrum* spectrum, WaylandConnection* wayland,
                   RenderContext* renderContext)
    : m_mpris(mpris), m_httpClient(httpClient), m_spectrum(spectrum), m_wayland(wayland),
      m_renderContext(renderContext) {}

MediaTab::~MediaTab() = default;

void MediaTab::openPlayerMenu() {
  if (m_playerMenuPopup == nullptr || m_mpris == nullptr || m_playerMenuButton == nullptr) {
    return;
  }

  const auto pinnedBusName = m_mpris->pinnedPlayerPreference();
  std::vector<ContextMenuControlEntry> entries;
  entries.reserve(m_playerBusNames.size() + 1);
  entries.push_back({.id = 0,
                     .label = i18n::tr("control-center.media.active-player"),
                     .enabled = true,
                     .separator = false,
                     .hasSubmenu = false});
  for (std::size_t i = 0; i < m_playerBusNames.size(); ++i) {
    const auto& busName = m_playerBusNames[i];
    const bool selected = pinnedBusName.has_value() && busName == *pinnedBusName;
    std::string identity;
    if (auto it = m_mpris->players().find(busName); it != m_mpris->players().end()) {
      identity = it->second.identity;
    }
    const std::string label = (selected ? "• " : "") + (identity.empty() ? busName : identity);
    entries.push_back({.id = static_cast<std::int32_t>(i + 1),
                       .label = label,
                       .enabled = true,
                       .separator = false,
                       .hasSubmenu = false});
  }

  Flex* anchor = m_playerMenuButton->parent() != nullptr ? static_cast<Flex*>(m_playerMenuButton->parent())
                                                         : static_cast<Flex*>(m_nowCard);
  if (anchor == nullptr) {
    return;
  }

  const auto parentCtx = PanelManager::instance().fallbackPopupParentContext();
  if (!parentCtx.has_value()) {
    return;
  }

  float anchorAbsX = 0.0f;
  float anchorAbsY = 0.0f;
  Node::absolutePosition(anchor, anchorAbsX, anchorAbsY);

  const float scale = contentScale();
  const float menuWidth = std::clamp(kMediaUnit * 6.0f * scale, kMediaUnit * 4.2f * scale,
                                     m_nowCard != nullptr ? std::max(1.0f, m_nowCard->width()) : 240.0f * scale);

  PanelManager::instance().beginAttachedPopup(parentCtx->surface);
  PanelManager::instance().setActivePopup(m_playerMenuPopup.get());

  m_playerMenuPopup->setOnDismissed([parentSurface = parentCtx->surface]() {
    PanelManager::instance().clearActivePopup();
    PanelManager::instance().endAttachedPopup(parentSurface);
  });

  m_playerMenuPopup->open(std::move(entries), menuWidth, 10, static_cast<std::int32_t>(anchorAbsX),
                          static_cast<std::int32_t>(anchorAbsY), static_cast<std::int32_t>(anchor->width()),
                          static_cast<std::int32_t>(anchor->height()), parentCtx->layerSurface, parentCtx->output);

  m_playerMenuOpen = true;
}

std::unique_ptr<Flex> MediaTab::create() {
  const float scale = contentScale();

  auto tab = std::make_unique<Flex>();
  tab->setDirection(FlexDirection::Horizontal);
  tab->setAlign(FlexAlign::Stretch);
  tab->setGap(Style::spaceSm * scale);
  m_rootLayout = tab.get();

  auto mediaColumn = std::make_unique<Flex>();
  mediaColumn->setDirection(FlexDirection::Vertical);
  mediaColumn->setAlign(FlexAlign::Stretch);
  mediaColumn->setGap(Style::spaceMd * scale);
  mediaColumn->setFlexGrow(3.0f);
  m_mediaColumn = mediaColumn.get();

  auto nowCard = std::make_unique<Flex>();
  applySectionCardStyle(*nowCard, scale);
  nowCard->setGap(Style::spaceMd * scale);
  nowCard->setFlexGrow(1.0f);
  nowCard->setMinHeight(kMediaNowCardMinHeight * scale);
  m_nowCard = nowCard.get();

  auto nowHeader = std::make_unique<Flex>();
  nowHeader->setDirection(FlexDirection::Horizontal);
  nowHeader->setAlign(FlexAlign::Center);
  nowHeader->setJustify(FlexJustify::SpaceBetween);
  nowHeader->setGap(Style::spaceSm * scale);
  nowHeader->setMinHeight(Style::controlHeightSm * scale);

  auto nowLabel = std::make_unique<Label>();
  nowLabel->setText(i18n::tr("control-center.media.now-playing"));
  nowLabel->setBold(true);
  nowLabel->setFontSize(Style::fontSizeTitle * scale);
  nowLabel->setColor(colorSpecFromRole(ColorRole::OnSurface));
  nowLabel->setFlexGrow(1.0f);
  nowHeader->addChild(std::move(nowLabel));

  auto playerMenuButton = std::make_unique<Button>();
  playerMenuButton->setGlyph("headphones");
  playerMenuButton->setVariant(ButtonVariant::Ghost);
  playerMenuButton->setMinWidth(Style::controlHeightSm * scale);
  playerMenuButton->setMinHeight(Style::controlHeightSm * scale);
  playerMenuButton->setGlyphSize(Style::fontSizeBody * scale);
  playerMenuButton->setPadding(Style::spaceXs * scale);
  playerMenuButton->setEnabled(false);
  playerMenuButton->setOnClick([this]() {
    if (m_playerBusNames.empty()) {
      return;
    }
    if (m_playerMenuPopup != nullptr && m_playerMenuPopup->isOpen()) {
      m_playerMenuPopup->close();
      PanelManager::instance().clearActivePopup();
    } else {
      openPlayerMenu();
    }
  });
  m_playerMenuButton = playerMenuButton.get();
  nowHeader->addChild(std::move(playerMenuButton));
  nowCard->addChild(std::move(nowHeader));

  auto mediaStack = std::make_unique<Flex>();
  mediaStack->setDirection(FlexDirection::Vertical);
  mediaStack->setAlign(FlexAlign::Stretch);
  mediaStack->setGap(Style::spaceMd * scale);
  mediaStack->setFlexGrow(1.0f);
  m_mediaStack = mediaStack.get();

  auto artworkRow = std::make_unique<Flex>();
  artworkRow->setDirection(FlexDirection::Horizontal);
  artworkRow->setAlign(FlexAlign::Center);
  artworkRow->setJustify(FlexJustify::Center);
  artworkRow->setGap(0.0f);
  artworkRow->setFlexGrow(1.0f);
  m_artworkRow = artworkRow.get();

  auto artwork = std::make_unique<Image>();
  artwork->setRadius(Style::radiusXl * scale);
  artwork->setFit(ImageFit::Contain);
  artwork->setSize(kArtworkSize * scale, kArtworkSize * scale);
  m_artwork = artwork.get();
  artworkRow->addChild(std::move(artwork));
  mediaStack->addChild(std::move(artworkRow));

  auto metadataStack = std::make_unique<Flex>();
  metadataStack->setDirection(FlexDirection::Vertical);
  metadataStack->setAlign(FlexAlign::Stretch);
  metadataStack->setGap(Style::spaceSm * scale);

  auto title = std::make_unique<Label>();
  title->setText(i18n::tr("control-center.media.nothing-playing"));
  title->setBold(true);
  title->setFontSize((Style::fontSizeTitle + Style::spaceXs) * scale);
  title->setColor(colorSpecFromRole(ColorRole::OnSurface));
  m_trackTitle = title.get();
  metadataStack->addChild(std::move(title));

  auto artist = std::make_unique<Label>();
  artist->setText(i18n::tr("control-center.media.start-playback"));
  artist->setFontSize(Style::fontSizeBody * scale);
  artist->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
  m_trackArtist = artist.get();
  metadataStack->addChild(std::move(artist));

  auto album = std::make_unique<Label>();
  album->setText("");
  album->setCaptionStyle();
  album->setFontSize(Style::fontSizeCaption * scale);
  album->setColor(colorSpecFromRole(ColorRole::Secondary));
  album->setVisible(false);
  m_trackAlbum = album.get();
  metadataStack->addChild(std::move(album));

  mediaStack->addChild(std::move(metadataStack));

  auto progress = std::make_unique<Slider>();
  progress->setRange(0.0f, 100.0f);
  progress->setStep(1.0f);
  progress->setControlHeight((Style::controlHeight + Style::spaceXs) * scale);
  progress->setTrackHeight(7.0f * scale);
  progress->setThumbSize(16.0f * scale);
  progress->setOnValueChanged([this](float value) {
    if (m_syncingProgress || m_mpris == nullptr) {
      return;
    }
    const auto active = m_mpris->activePlayer();
    const std::int64_t targetUs = static_cast<std::int64_t>(std::llround(value * 1000000.0f));
    const auto now = std::chrono::steady_clock::now();
    m_positionUs = targetUs;
    m_positionSampleAt = now;
    const std::string seekBusName =
        active.has_value() ? active->busName : (!m_positionBusName.empty() ? m_positionBusName : std::string{});
    m_pendingSeekBusName = seekBusName;
    m_pendingSeekUs = targetUs;
    m_pendingSeekUntil = now + std::chrono::milliseconds(3000);

    bool seekIssued = false;
    if (!seekBusName.empty()) {
      seekIssued = m_mpris->setPosition(seekBusName, targetUs);
    } else {
      seekIssued = m_mpris->setPositionActive(targetUs);
    }
    if (!seekIssued) {
      // Keep the thumb stable briefly even if transport seek dispatch races.
      m_pendingSeekUntil = now + std::chrono::milliseconds(750);
    }
  });
  m_progressSlider = progress.get();
  mediaStack->addChild(std::move(progress));

  auto controlsRow = std::make_unique<Flex>();
  controlsRow->setDirection(FlexDirection::Horizontal);
  controlsRow->setAlign(FlexAlign::Center);
  controlsRow->setJustify(FlexJustify::Center);
  controlsRow->setFillWidth(true);
  controlsRow->setGap(0.0f);

  auto controls = std::make_unique<Flex>();
  controls->setDirection(FlexDirection::Horizontal);
  controls->setAlign(FlexAlign::Center);
  controls->setGap(Style::spaceMd * scale);

  auto repeat = std::make_unique<Button>();
  repeat->setGlyph("repeat");
  repeat->setVariant(ButtonVariant::Ghost);
  repeat->setMinWidth(kMediaControlsHeight * scale);
  repeat->setMinHeight(kMediaControlsHeight * scale);
  repeat->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
  repeat->setRadius(Style::radiusLg * scale);
  repeat->setOnClick([this]() {
    if (m_mpris == nullptr) {
      return;
    }
    const auto current = m_mpris->loopStatusActive().value_or("None");
    const std::string next = current == "None" ? "Playlist" : (current == "Playlist" ? "Track" : "None");
    m_mpris->setLoopStatusActive(next);
    PanelManager::instance().refresh();
  });
  m_repeatButton = repeat.get();
  controls->addChild(std::move(repeat));

  auto previous = std::make_unique<Button>();
  previous->setGlyph("media-prev");
  previous->setVariant(ButtonVariant::Ghost);
  previous->setMinWidth(kMediaControlsHeight * scale);
  previous->setMinHeight(kMediaControlsHeight * scale);
  previous->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
  previous->setRadius(Style::radiusLg * scale);
  previous->setOnClick([this]() {
    if (m_mpris != nullptr) {
      m_mpris->previousActive();
      PanelManager::instance().refresh();
    }
  });
  m_prevButton = previous.get();
  controls->addChild(std::move(previous));

  auto playPause = std::make_unique<Button>();
  playPause->setGlyph("media-play");
  playPause->setVariant(ButtonVariant::Accent);
  playPause->setMinWidth(kMediaPlayPauseHeight * scale);
  playPause->setMinHeight(kMediaPlayPauseHeight * scale);
  playPause->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
  playPause->setRadius(Style::radiusLg * scale);
  playPause->setOnClick([this]() {
    if (m_mpris != nullptr) {
      m_mpris->playPauseActive();
      PanelManager::instance().refresh();
    }
  });
  m_playPauseButton = playPause.get();
  controls->addChild(std::move(playPause));

  auto next = std::make_unique<Button>();
  next->setGlyph("media-next");
  next->setVariant(ButtonVariant::Ghost);
  next->setMinWidth(kMediaControlsHeight * scale);
  next->setMinHeight(kMediaControlsHeight * scale);
  next->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
  next->setRadius(Style::radiusLg * scale);
  next->setOnClick([this]() {
    if (m_mpris != nullptr) {
      m_mpris->nextActive();
      PanelManager::instance().refresh();
    }
  });
  m_nextButton = next.get();
  controls->addChild(std::move(next));

  auto shuffle = std::make_unique<Button>();
  shuffle->setGlyph("shuffle");
  shuffle->setVariant(ButtonVariant::Ghost);
  shuffle->setMinWidth(kMediaControlsHeight * scale);
  shuffle->setMinHeight(kMediaControlsHeight * scale);
  shuffle->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
  shuffle->setRadius(Style::radiusLg * scale);
  shuffle->setOnClick([this]() {
    if (m_mpris != nullptr) {
      const bool enabled = m_mpris->shuffleActive().value_or(false);
      m_mpris->setShuffleActive(!enabled);
      PanelManager::instance().refresh();
    }
  });
  m_shuffleButton = shuffle.get();
  controls->addChild(std::move(shuffle));

  controlsRow->addChild(std::move(controls));
  mediaStack->addChild(std::move(controlsRow));

  nowCard->addChild(std::move(mediaStack));
  mediaColumn->addChild(std::move(nowCard));

  auto visualizerColumn = std::make_unique<Flex>();
  visualizerColumn->setDirection(FlexDirection::Vertical);
  visualizerColumn->setAlign(FlexAlign::Stretch);
  visualizerColumn->setGap(Style::spaceSm * scale);
  visualizerColumn->setFlexGrow(2.0f);
  applySectionCardStyle(*visualizerColumn, scale);
  visualizerColumn->setClipChildren(true);
  m_visualizerColumn = visualizerColumn.get();

  auto visualizerLabel = std::make_unique<Label>();
  visualizerLabel->setText(i18n::tr("control-center.media.spectrum"));
  visualizerLabel->setBold(true);
  visualizerLabel->setFontSize(Style::fontSizeTitle * scale);
  visualizerLabel->setColor(colorSpecFromRole(ColorRole::OnSurface));
  visualizerColumn->addChild(std::move(visualizerLabel));

  auto visualizerBody = std::make_unique<Flex>();
  visualizerBody->setDirection(FlexDirection::Horizontal);
  visualizerBody->setAlign(FlexAlign::Stretch);
  visualizerBody->setJustify(FlexJustify::Start);
  visualizerBody->setFillWidth(true);
  visualizerBody->setFlexGrow(1.0f);
  m_visualizerBody = visualizerBody.get();

  auto visualizerSpectrum = std::make_unique<AudioSpectrum>();
  visualizerSpectrum->setGradient(colorForRole(ColorRole::Secondary), colorForRole(ColorRole::Tertiary));
  visualizerSpectrum->setSpacingRatio(0.5f);
  visualizerSpectrum->setOrientation(AudioSpectrumOrientation::Vertical);
  visualizerSpectrum->setMirrored(true);
  visualizerSpectrum->setCentered(true);
  visualizerSpectrum->setFlexGrow(1.0f);
  m_visualizerSpectrum = visualizerSpectrum.get();
  visualizerBody->addChild(std::move(visualizerSpectrum));
  visualizerColumn->addChild(std::move(visualizerBody));
  tab->addChild(std::move(mediaColumn));
  tab->addChild(std::move(visualizerColumn));

  if (m_wayland != nullptr && m_renderContext != nullptr) {
    m_playerMenuPopup = std::make_unique<ContextMenuPopup>(*m_wayland, *m_renderContext);
    m_playerMenuPopup->setOnActivate([this](const ContextMenuControlEntry& entry) {
      if (m_mpris == nullptr) {
        return;
      }
      if (entry.id == 0) {
        m_mpris->clearPinnedPlayerPreference();
      } else {
        const std::size_t idx = static_cast<std::size_t>(entry.id - 1);
        if (idx < m_playerBusNames.size()) {
          m_mpris->setPinnedPlayerPreference(m_playerBusNames[idx]);
        }
      }
    });
  }

  return tab;
}

void MediaTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeight) {
  if (m_rootLayout == nullptr || m_nowCard == nullptr || m_mediaStack == nullptr) {
    return;
  }

  const float scale = contentScale();
  m_rootLayout->setSize(contentWidth, bodyHeight);
  m_rootLayout->layout(renderer);

  const float cardInnerWidth =
      std::max(0.0f, m_nowCard->width() - (m_nowCard->paddingLeft() + m_nowCard->paddingRight()));
  const float mediaWidth = std::clamp(cardInnerWidth, 1.0f, kMediaUnit * 11.0f * scale);
  const float mediaStackHeight = m_mediaStack->height();
  m_mediaStack->setSize(mediaWidth, mediaStackHeight);

  if (m_artworkRow != nullptr) {
    // Horizontal Flex with justify Center under-reports its width when the child is narrower than
    // the stretched cross-axis; min width keeps the row full-bleed so art centers.
    m_artworkRow->setMinWidth(mediaWidth);
  }

  if (m_artwork != nullptr) {
    const float sideButtonSize = kMediaControlsHeight * scale;
    const float playPauseButtonSize = kMediaPlayPauseHeight * scale;
    const float sideGlyphSize = Style::fontSizeTitle * scale;
    const float playPauseGlyphSize = (Style::fontSizeTitle + Style::spaceXs) * scale;

    for (auto* button : {m_repeatButton, m_prevButton, m_nextButton, m_shuffleButton}) {
      if (button != nullptr) {
        button->setMinWidth(sideButtonSize);
        button->setMinHeight(sideButtonSize);
        button->setGlyphSize(sideGlyphSize);
        button->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
        button->setRadius(Style::radiusLg * scale);
      }
    }
    if (m_playPauseButton != nullptr) {
      m_playPauseButton->setMinWidth(playPauseButtonSize);
      m_playPauseButton->setMinHeight(playPauseButtonSize);
      m_playPauseButton->setGlyphSize(playPauseGlyphSize);
      m_playPauseButton->setPadding(Style::spaceSm * scale, Style::spaceSm * scale);
      m_playPauseButton->setRadius(Style::radiusLg * scale);
    }
  }

  if (m_trackTitle != nullptr) {
    m_trackTitle->setMaxWidth(mediaWidth);
  }
  if (m_trackArtist != nullptr) {
    m_trackArtist->setMaxWidth(mediaWidth);
  }
  if (m_trackAlbum != nullptr) {
    m_trackAlbum->setMaxWidth(mediaWidth);
  }
  if (m_progressSlider != nullptr) {
    m_progressSlider->setSize(mediaWidth, 0.0f);
  }

  m_mediaStack->layout(renderer);

  if (m_artwork != nullptr && m_artworkRow != nullptr) {
    const float artWidth =
        std::max(1.0f, m_artworkRow->width() - (m_artworkRow->paddingLeft() + m_artworkRow->paddingRight()));
    const float artHeight =
        std::max(kMediaArtworkMinHeight * scale,
                 m_artworkRow->height() - (m_artworkRow->paddingTop() + m_artworkRow->paddingBottom()));
    float targetWidth = artWidth;
    float targetHeight = artHeight;
    if (m_artwork->hasImage()) {
      const float imageAspect = std::max(0.01f, m_artwork->aspectRatio());
      const float boundsAspect = artWidth / std::max(1.0f, artHeight);
      if (imageAspect > boundsAspect) {
        targetHeight = artWidth / imageAspect;
      } else {
        targetWidth = artHeight * imageAspect;
      }
    }
    m_artwork->setSize(targetWidth, targetHeight);
    m_artwork->setRadius(Style::radiusXl * scale);
    m_mediaStack->layout(renderer);
  }

  if (m_visualizerBody != nullptr && m_visualizerSpectrum != nullptr) {
    const float bodyWidth = std::max(0.0f, m_visualizerBody->width() -
                                               (m_visualizerBody->paddingLeft() + m_visualizerBody->paddingRight()));
    const float bodyHeightAvail = std::max(
        0.0f, m_visualizerBody->height() - (m_visualizerBody->paddingTop() + m_visualizerBody->paddingBottom()));
    const float spectrumWidth = std::max(1.0f, bodyWidth);
    const float spectrumHeight = std::max(1.0f, bodyHeightAvail);
    m_visualizerSpectrum->setSize(spectrumWidth, spectrumHeight);
    m_visualizerBody->layout(renderer);
  }
}

void MediaTab::doUpdate(Renderer& renderer) {
  if (!m_active) {
    return;
  }
  if (m_visualizerSpectrum != nullptr && m_spectrum != nullptr && m_spectrumListenerId != 0) {
    if (!m_spectrum->idle() || !m_visualizerSpectrum->converged()) {
      m_visualizerSpectrum->setValues(m_spectrum->values(m_spectrumListenerId));
    }
  }
  refresh(renderer);
}

void MediaTab::onFrameTick(float deltaMs) {
  if (!m_active || m_visualizerSpectrum == nullptr) {
    return;
  }
  if (m_spectrum != nullptr && m_spectrumListenerId != 0) {
    if (m_spectrum->idle() && m_visualizerSpectrum->converged()) {
      return;
    }
    m_visualizerSpectrum->setValues(m_spectrum->values(m_spectrumListenerId));
  }
  m_visualizerSpectrum->tick(deltaMs);
}

void MediaTab::setActive(bool active) {
  const bool becameActive = active && !m_active;
  m_active = active;
  if (m_spectrum != nullptr) {
    if (active && m_spectrumListenerId == 0) {
      m_spectrumListenerId = m_spectrum->addChangeListener(kVisualizerBandCount, [this]() {
        if (!m_active || m_spectrum->idle()) {
          return;
        }
        PanelManager::instance().requestRedraw();
      });
    } else if (!active && m_spectrumListenerId != 0) {
      m_spectrum->removeChangeListener(m_spectrumListenerId);
      m_spectrumListenerId = 0;
    }
  }
  if (!active) {
    m_positionSampleAt = {};
  }
  if (becameActive && m_mpris != nullptr) {
    // Pull a fresh snapshot (including Position) when the tab opens so the
    // progress slider starts at the current playback position.
    m_positionSampleAt = {};
    m_mpris->refreshPlayers();
    m_lastMprisRefreshAttempt = std::chrono::steady_clock::now();
  }
}

void MediaTab::onClose() {
  if (m_spectrum != nullptr) {
    if (m_spectrumListenerId != 0) {
      m_spectrum->removeChangeListener(m_spectrumListenerId);
      m_spectrumListenerId = 0;
    }
  }
  m_active = false;
  m_rootLayout = nullptr;
  m_mediaColumn = nullptr;
  m_visualizerColumn = nullptr;
  m_visualizerBody = nullptr;
  m_visualizerSpectrum = nullptr;
  m_artwork = nullptr;
  m_artworkRow = nullptr;
  m_nowCard = nullptr;
  m_mediaStack = nullptr;
  m_playerMenuButton = nullptr;
  if (m_playerMenuPopup != nullptr) {
    PanelManager::instance().clearActivePopup();
    m_playerMenuPopup->close();
  }
  m_playerMenuOpen = false;
  m_trackTitle = nullptr;
  m_trackArtist = nullptr;
  m_trackAlbum = nullptr;
  m_progressSlider = nullptr;
  m_prevButton = nullptr;
  m_playPauseButton = nullptr;
  m_nextButton = nullptr;
  m_repeatButton = nullptr;
  m_shuffleButton = nullptr;
  m_lastArtPath.clear();
  m_lastBusName.clear();
  m_lastPlaybackStatus.clear();
  m_lastLoopStatus.clear();
  m_playerBusNames.clear();
  m_lastActiveSnapshot.reset();
  m_pendingSeekBusName.clear();
  m_pendingSeekUs = -1;
}

void MediaTab::clearArt(Renderer& renderer) {
  if (m_artwork != nullptr) {
    m_artwork->clear(renderer);
  }
}

void MediaTab::refresh(Renderer& renderer) {
  std::vector<MprisPlayerInfo> players;
  std::optional<MprisPlayerInfo> active;
  const auto now = std::chrono::steady_clock::now();
  if (m_mpris != nullptr) {
    players = m_mpris->listPlayers();
    active = m_mpris->activePlayer();
    kLog.debug("media tab refresh initial players={} active={} active_bus=\"{}\"", players.size(), active.has_value(),
               active.has_value() ? active->busName : std::string{});

    const bool shouldRetryMpris =
        (!active.has_value() || players.empty()) && (m_lastMprisRefreshAttempt.time_since_epoch().count() == 0 ||
                                                     now - m_lastMprisRefreshAttempt >= std::chrono::milliseconds(750));
    if (shouldRetryMpris) {
      m_lastMprisRefreshAttempt = now;
      kLog.debug("media tab retrying mpris discovery players={} active={}", players.size(), active.has_value());
      m_mpris->refreshPlayers();
      players = m_mpris->listPlayers();
      active = m_mpris->activePlayer();
      kLog.debug("media tab refresh after retry players={} active={} active_bus=\"{}\"", players.size(),
                 active.has_value(), active.has_value() ? active->busName : std::string{});
    }
  }

  if (!active.has_value() && m_lastActiveSnapshot.has_value() && now - m_lastActiveSeenAt <= kNoActivePlayerGrace) {
    // Keep last player briefly to hide transient MPRIS discovery gaps.
    active = m_lastActiveSnapshot;
  }

  if (m_playerMenuButton != nullptr) {
    const auto pinnedBusName = m_mpris != nullptr ? m_mpris->pinnedPlayerPreference() : std::nullopt;
    std::vector<std::string> playerBusNames;
    playerBusNames.reserve(players.size());
    std::vector<ContextMenuControlEntry> entries;
    entries.reserve(players.size() + 1);
    entries.push_back({.id = 0,
                       .label = i18n::tr("control-center.media.active-player"),
                       .enabled = true,
                       .separator = false,
                       .hasSubmenu = false});

    for (std::size_t i = 0; i < players.size(); ++i) {
      const auto& player = players[i];
      playerBusNames.push_back(player.busName);
      const bool selected = pinnedBusName.has_value() && player.busName == *pinnedBusName;
      const std::string label = (selected ? "• " : "") + (player.identity.empty() ? player.busName : player.identity);
      entries.push_back({.id = static_cast<std::int32_t>(i + 1),
                         .label = label,
                         .enabled = true,
                         .separator = false,
                         .hasSubmenu = false});
    }

    m_playerBusNames = std::move(playerBusNames);
    m_playerMenuButton->setEnabled(!m_playerBusNames.empty());
    m_playerMenuButton->setVariant(!m_playerBusNames.empty() ? ButtonVariant::Ghost : ButtonVariant::Default);
    if (m_playerBusNames.empty() && m_playerMenuPopup != nullptr && m_playerMenuPopup->isOpen()) {
      m_playerMenuPopup->close();
      PanelManager::instance().clearActivePopup();
    }
  }

  if (m_trackTitle == nullptr || m_trackArtist == nullptr || m_progressSlider == nullptr ||
      m_playPauseButton == nullptr || m_repeatButton == nullptr || m_shuffleButton == nullptr) {
    return;
  }

  if (active.has_value()) {
    const auto& player = *active;
    m_lastActiveSnapshot = player;
    m_lastActiveSeenAt = now;
    const bool sameBus = m_positionBusName == player.busName && m_positionSampleAt.time_since_epoch().count() != 0;
    const bool trackComparable =
        player.trackId.empty() || m_positionTrackId.empty() || m_positionTrackId == player.trackId;
    const bool canExtrapolate = sameBus && trackComparable && player.playbackStatus == "Playing";
    std::int64_t livePositionUs = player.positionUs;
    if (canExtrapolate) {
      const auto elapsedUs = std::chrono::duration_cast<std::chrono::microseconds>(now - m_positionSampleAt).count();
      if (elapsedUs > 0 && elapsedUs <= 5'000'000) {
        const std::int64_t predictedUs = m_positionUs + elapsedUs;
        // Keep local timeline smooth for players with stale Position values.
        // Only trust transport when it clearly advances ahead.
        livePositionUs = (player.positionUs > predictedUs + 2'000'000) ? player.positionUs : predictedUs;
      }
    }
    if (player.lengthUs > 0) {
      livePositionUs = std::clamp<std::int64_t>(livePositionUs, 0, player.lengthUs);
    } else {
      livePositionUs = std::max<std::int64_t>(0, livePositionUs);
    }
    m_positionBusName = player.busName;
    m_positionTrackId = player.trackId;
    m_positionUs = livePositionUs;
    m_positionSampleAt = now;

    const bool pendingMatchesPlayer = m_pendingSeekBusName.empty() || m_pendingSeekBusName == player.busName;
    const bool seekPending = pendingMatchesPlayer && now < m_pendingSeekUntil && m_pendingSeekUs >= 0;
    const bool seekReached = seekPending && std::llabs(livePositionUs - m_pendingSeekUs) <= 1500000;
    const std::int64_t displayPositionUs = seekPending && !seekReached ? m_pendingSeekUs : livePositionUs;
    if (!seekPending || seekReached) {
      m_pendingSeekBusName.clear();
      m_pendingSeekUs = -1;
    }
    m_positionUs = displayPositionUs;
    m_positionSampleAt = now;

    m_trackTitle->setText(player.title.empty() ? player.identity : player.title);
    m_trackArtist->setText(joinArtists(player.artists).empty() ? player.identity : joinArtists(player.artists));
    if (m_trackAlbum != nullptr) {
      m_trackAlbum->setText(player.album);
      m_trackAlbum->setVisible(!player.album.empty());
    }

    const std::string resolvedArtUrl = effectiveArtUrl(player);
    std::string artPath = normalizeArtPath(resolvedArtUrl);
    if (artPath.empty() && isRemoteArtUrl(resolvedArtUrl)) {
      const auto cached = artCachePath(resolvedArtUrl);
      std::error_code ec;
      if (std::filesystem::exists(cached, ec) && std::filesystem::file_size(cached, ec) > 0) {
        artPath = cached.string();
      } else if (m_httpClient != nullptr && m_pendingArtDownloads.find(resolvedArtUrl) == m_pendingArtDownloads.end()) {
        std::filesystem::create_directories(cached.parent_path(), ec);
        m_pendingArtDownloads.insert(resolvedArtUrl);
        m_httpClient->download(resolvedArtUrl, cached, [this, url = resolvedArtUrl](bool success) {
          m_pendingArtDownloads.erase(url);
          if (success) {
            m_lastArtPath.clear();
            PanelManager::instance().refresh();
          }
        });
      }
    }

    if (m_artwork != nullptr &&
        (!resolvedArtUrl.empty() && (resolvedArtUrl != m_lastArtPath || !m_artwork->hasImage()))) {
      bool loaded = false;
      if (artPath.empty()) {
        kLog.debug("artwork unresolved url=\"{}\"", resolvedArtUrl);
        clearArt(renderer);
      } else if (!m_artwork->setSourceFile(renderer, artPath, static_cast<int>(kArtworkSize))) {
        kLog.warn("artwork load failed url=\"{}\" path=\"{}\"", resolvedArtUrl, artPath);
        clearArt(renderer);
      } else {
        kLog.debug("artwork loaded url=\"{}\" path=\"{}\"", resolvedArtUrl, artPath);
        loaded = true;
      }

      // Only lock this URL once we actually have an image.
      // Otherwise keep retrying while metadata/download catches up.
      m_lastArtPath = loaded ? resolvedArtUrl : std::string{};
    } else if (m_artwork != nullptr && resolvedArtUrl.empty()) {
      clearArt(renderer);
      m_lastArtPath.clear();
    }

    m_syncingProgress = true;
    m_progressSlider->setEnabled(player.canSeek && player.lengthUs > 0);
    m_progressSlider->setRange(0.0f, std::max(1.0f, static_cast<float>(player.lengthUs) / 1000000.0f));
    if (!m_progressSlider->dragging()) {
      m_progressSlider->setValue(static_cast<float>(displayPositionUs) / 1000000.0f);
    }
    m_syncingProgress = false;

    m_playPauseButton->setGlyph(playPauseGlyph(player.playbackStatus));
    m_playPauseButton->setVariant(ButtonVariant::Accent);
    if (m_prevButton != nullptr) {
      m_prevButton->setEnabled(player.canGoPrevious);
    }
    if (m_nextButton != nullptr) {
      m_nextButton->setEnabled(player.canGoNext);
    }
    m_repeatButton->setGlyph(repeatGlyph(player.loopStatus));
    m_repeatButton->setVariant(toggleVariant(player.loopStatus != "None"));
    m_shuffleButton->setVariant(toggleVariant(player.shuffle));

    m_lastBusName = player.busName;
    m_lastPlaybackStatus = player.playbackStatus;
    m_lastLoopStatus = player.loopStatus;
    m_lastShuffle = player.shuffle;
    return;
  }

  m_pendingSeekBusName.clear();
  m_pendingSeekUs = -1;
  m_lastActiveSnapshot.reset();
  m_positionBusName.clear();
  m_positionTrackId.clear();
  m_positionUs = 0;
  m_positionSampleAt = {};
  m_trackTitle->setText(i18n::tr("control-center.media.nothing-playing"));
  m_trackArtist->setText(i18n::tr("control-center.media.start-playback"));
  if (m_trackAlbum != nullptr) {
    m_trackAlbum->setText("");
    m_trackAlbum->setVisible(false);
  }
  clearArt(renderer);
  m_lastArtPath.clear();
  m_syncingProgress = true;
  m_progressSlider->setEnabled(false);
  m_progressSlider->setRange(0.0f, 100.0f);
  m_progressSlider->setValue(0.0f);
  m_syncingProgress = false;
  m_playPauseButton->setGlyph("media-play");
  if (m_prevButton != nullptr) {
    m_prevButton->setEnabled(false);
  }
  if (m_nextButton != nullptr) {
    m_nextButton->setEnabled(false);
  }
  m_repeatButton->setGlyph("repeat");
  m_repeatButton->setVariant(ButtonVariant::Ghost);
  m_shuffleButton->setVariant(ButtonVariant::Ghost);
  m_lastBusName.clear();
  m_lastPlaybackStatus.clear();
  m_lastLoopStatus.clear();
  m_lastShuffle = false;
}
