(() => {
  const root = document.getElementById("now-playing-root");
  const initial = JSON.parse(root?.dataset.state || "{}");
  const token = root?.dataset.token || "";
  let state = initial;
  let card = null;
  let hideTimer = null;
  let progressTimer = null;
  let lastTrackKey = "";
  let lastConfigSignature = "";
  let refreshRequest = null;
  let fallbackTimer = null;

  const escapeText = (value) => String(value ?? "");
  const formatTime = (milliseconds) => {
    const seconds = Math.max(0, Math.floor(Number(milliseconds || 0) / 1000));
    const minutes = Math.floor(seconds / 60);
    return `${minutes}:${String(seconds % 60).padStart(2, "0")}`;
  };
  const projectedPosition = () => {
    const playback = state.playback;
    if (!playback) return 0;
    let value = Number(playback.position_ms || 0);
    if (playback.status === "playing") value += Math.max(0, Date.now() - Number(playback.reported_at || Date.now())) * Number(playback.rate || 1);
    return Math.min(Number(playback.duration_ms || value), value);
  };

  function backgroundColors(config, track) {
    const coverBased = String(config.background_mode || "").startsWith("cover-");
    return {
      primary: coverBased ? track.color_primary : config.custom_color_primary,
      secondary: coverBased ? track.color_secondary : config.custom_color_secondary,
      solid: String(config.background_mode || "").endsWith("-solid")
    };
  }

  function buildCard() {
    const config = state.config || {};
    const track = state.track || {};
    const colors = backgroundColors(config, track);
    const element = document.createElement("article");
    element.className = `np-card scope-${String(config.transparency_scope || "background").replaceAll("_", "-")}${colors.solid ? " background-solid" : ""}`;
    element.dataset.animation = config.entry_animation || "fade";
    element.style.setProperty("--np-primary", colors.primary || "#1f2937");
    element.style.setProperty("--np-secondary", colors.secondary || colors.primary || "#111827");
    element.style.setProperty("--np-opacity", String(config.opacity ?? .92));
    element.style.setProperty("--np-radius", `${Number(config.corner_radius || 18)}px`);
    element.style.setProperty("--np-scale", String(config.scale || 1));
    element.style.setProperty("--np-duration", `${Number(config.animation_ms || 450)}ms`);

    if (config.show_cover && track.cover_url) {
      const cover = document.createElement("img");
      cover.className = "np-cover";
      cover.src = track.cover_url;
      cover.alt = "";
      element.appendChild(cover);
    }

    const content = document.createElement("div");
    content.className = "np-content";
    if (config.show_provider && track.provider) {
      const provider = document.createElement("span");
      provider.className = "np-provider";
      provider.textContent = escapeText(track.provider);
      content.appendChild(provider);
    }
    if (config.show_title) {
      const title = document.createElement("strong");
      title.className = "np-title";
      title.textContent = escapeText(track.name || "Unknown song");
      content.appendChild(title);
    }
    if (config.show_artist && track.artist) {
      const artist = document.createElement("span");
      artist.className = "np-artist";
      artist.textContent = escapeText(track.artist);
      content.appendChild(artist);
    }
    if ((config.show_album && track.album) || (config.show_release_year && track.release_year)) {
      const meta = document.createElement("div");
      meta.className = "np-meta";
      if (config.show_album && track.album) {
        const album = document.createElement("span");
        album.textContent = escapeText(track.album);
        meta.appendChild(album);
      }
      if (config.show_release_year && track.release_year) {
        const year = document.createElement("span");
        year.textContent = escapeText(track.release_year);
        meta.appendChild(year);
      }
      content.appendChild(meta);
    }
    if (config.show_timeline && Number(state.playback?.duration_ms || 0) > 0) {
      const timeline = document.createElement("div");
      timeline.className = "np-timeline";
      timeline.innerHTML = '<span data-position>0:00</span><div class="np-track"><div class="np-progress" data-progress></div></div><span data-duration></span>';
      content.appendChild(timeline);
    }
    element.appendChild(content);
    return element;
  }

  function updateProgress() {
    if (!card) return;
    const duration = Number(state.playback?.duration_ms || 0);
    const position = projectedPosition();
    const progress = card.querySelector("[data-progress]");
    const positionLabel = card.querySelector("[data-position]");
    const durationLabel = card.querySelector("[data-duration]");
    if (progress) progress.style.width = `${duration ? Math.min(100, Math.max(0, position / duration * 100)) : 0}%`;
    if (positionLabel) positionLabel.textContent = formatTime(position);
    if (durationLabel) durationLabel.textContent = formatTime(duration);
  }

  function show(nextState) {
    const previousState = state;
    state = nextState || {};
    if (!state.available || !state.track) return hide();

    const nextTrackKey = state.track.key || "";
    const trackChanged = nextTrackKey !== lastTrackKey;
    const configSignature = JSON.stringify(state.config || {});
    const configChanged = configSignature !== lastConfigSignature;
    const timed = state.config?.display_mode === "timed";
    lastTrackKey = nextTrackKey;
    lastConfigSignature = configSignature;

    // Ordinary playback and heartbeat updates should only advance the timeline.
    // Rebuilding the DOM for every delta causes visible jitter and can re-show a
    // timed card after its configured display window has elapsed.
    if (!trackChanged && !configChanged) {
      if (card) updateProgress();
      return;
    }
    if (!trackChanged && timed && !card) return;

    clearTimeout(hideTimer);
    const nextCard = buildCard();
    if (card) card.remove();
    card = nextCard;
    root.replaceChildren(card);
    card.classList.add("is-entering");
    card.addEventListener("animationend", () => card?.classList.remove("is-entering"), { once: true });
    clearInterval(progressTimer);
    progressTimer = setInterval(updateProgress, 250);
    updateProgress();
    if (timed && trackChanged) {
      hideTimer = setTimeout(hide, Math.max(1, Number(state.config.display_seconds || 10)) * 1000);
    }
  }

  function hide() {
    clearTimeout(hideTimer);
    clearInterval(progressTimer);
    if (!card) return;
    const removing = card;
    removing.dataset.animation = state.config?.exit_animation || "fade";
    removing.classList.remove("is-entering");
    removing.classList.add("is-exiting");
    const remove = () => {
      if (card === removing) card = null;
      removing.remove();
    };
    removing.addEventListener("animationend", remove, { once: true });
    setTimeout(remove, Number(state.config?.animation_ms || 450) + 100);
  }

  function refreshState() {
    if (refreshRequest) return refreshRequest;
    refreshRequest = fetch(`/plugins/now_playing/render/${encodeURIComponent(token)}/state`, {
      cache: "no-store",
      credentials: "omit"
    })
      .then((response) => {
        if (!response.ok) throw new Error(`Song state request failed (${response.status}).`);
        return response.json();
      })
      .then(show)
      .catch(() => {})
      .finally(() => { refreshRequest = null; });
    return refreshRequest;
  }

  function startFallbackPolling() {
    if (fallbackTimer) return;
    refreshState();
    fallbackTimer = setInterval(refreshState, 2000);
  }

  function stopFallbackPolling() {
    clearInterval(fallbackTimer);
    fallbackTimer = null;
  }

  show(initial);
  const events = new EventSource(`/plugins/now_playing/render/${encodeURIComponent(token)}/events`);
  events.addEventListener("state", (event) => {
    try { show(JSON.parse(event.data)); } catch {}
  });
  events.addEventListener("changed", refreshState);
  events.addEventListener("open", stopFallbackPolling);
  events.addEventListener("error", startFallbackPolling);
  window.addEventListener("beforeunload", () => {
    stopFallbackPolling();
    events.close();
  });
})();
