// every.channel web watcher
//
// This uses the upstream moq watch web component (WebTransport + WebCodecs).
// It is intentionally dependency-light: no framework, no bundler.

const DEFAULT_RELAY_URL = "https://relay.every.channel/anon";
const MOQ_WATCH_VERSION = "0.2.10";
const MOQ_WATCH_MODULE_URLS = [
  `https://esm.sh/@moq/watch@${MOQ_WATCH_VERSION}/element`,
  `https://cdn.jsdelivr.net/npm/@moq/watch@${MOQ_WATCH_VERSION}/element/+esm`,
  `https://unpkg.com/@moq/watch@${MOQ_WATCH_VERSION}/element.js?module`,
];
const HLS_MODULE_URLS = [
  "https://esm.sh/hls.js@1.6.2",
  "https://cdn.jsdelivr.net/npm/hls.js@1.6.2/+esm",
  "https://unpkg.com/hls.js@1.6.2/dist/hls.mjs",
];
const PUBLIC_STREAMS_PATH = "/api/public-streams";
const LIVE_JITTER_MS = 1250;
const GUIDE_CACHE_KEY = "every.channel.publicStreams.v2";
const GUIDE_CACHE_MAX_AGE_MS = 5 * 60 * 1000;
const GUIDE_INITIAL_TIMEOUT_MS = 1800;
const GUIDE_REFRESH_TIMEOUT_MS = 5000;
const FIRST_FRAME_TIMEOUT_MS = 12000;
let moqWatchModulePromise = null;
let hlsModulePromise = null;
let disposePlayerSignals = null;
let activeHlsPlayer = null;
const perfState = (window.__ecPerf = window.__ecPerf || {
  events: [],
  marks: {},
  measures: {},
});

function perfNow() {
  return window.performance && typeof window.performance.now === "function"
    ? window.performance.now()
    : Date.now();
}

function markPerf(name, detail) {
  const at = perfNow();
  const safeDetail = { ...(detail || {}) };
  if (Object.prototype.hasOwnProperty.call(safeDetail, "name")) {
    safeDetail.stream_name = safeDetail.name;
    delete safeDetail.name;
  }
  const event = { name, at_ms: Math.round(at), ...safeDetail };
  perfState.marks[name] = event;
  perfState.events.push(event);
  if (window.performance && typeof window.performance.mark === "function") {
    try {
      window.performance.mark(`ec:${name}`);
    } catch (_) {
      // Browser performance marks are best-effort.
    }
  }
  if (window.console && typeof window.console.info === "function") {
    try {
      window.console.info(`[ec-perf] ${name} ${JSON.stringify(event)}`);
    } catch (_) {
      window.console.info(`[ec-perf] ${name}`);
    }
  }
  return event;
}

function elapsedSincePerf(name) {
  const mark = perfState.marks[name];
  if (!mark) return null;
  return Math.max(0, Math.round(perfNow() - mark.at_ms));
}

function measurePerf(name, startName, endName) {
  const start = perfState.marks[startName];
  const end = perfState.marks[endName];
  if (!start || !end) return null;
  const duration = Math.max(0, Math.round(end.at_ms - start.at_ms));
  perfState.measures[name] = duration;
  return duration;
}

function $(id) {
  const el = document.getElementById(id);
  if (!el) throw new Error(`missing element: ${id}`);
  return el;
}

function normalizeRelayUrl(s) {
  const trimmed = (s || "").trim();
  if (!trimmed) return DEFAULT_RELAY_URL;
  // Ensure trailing slash so relative fetches behave consistently.
  return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
}

function normalizeName(s) {
  return (s || "").trim();
}

function entryRelays(entry) {
  const relays = [];
  const addRelay = (relay) => {
    if (!relay?.relay_url || !relay.broadcast_name) return;
    const normalized = {
      relay_url: normalizeRelayUrl(relay.relay_url),
      broadcast_name: normalizeName(relay.broadcast_name),
      track_name: normalizeName(relay.track_name || "video0.m4s"),
    };
    const key = `${normalized.relay_url}\n${normalized.broadcast_name}\n${normalized.track_name}`;
    if (!relays.some((existing) => `${existing.relay_url}\n${existing.broadcast_name}\n${existing.track_name}` === key)) {
      relays.push(normalized);
    }
  };

  addRelay({
    relay_url: entry?.relay_url,
    broadcast_name: entry?.broadcast_name,
    track_name: entry?.track_name,
  });
  if (Array.isArray(entry?.relays)) {
    entry.relays.forEach(addRelay);
  }

  return relays;
}

function entryPrimaryRelay(entry) {
  return entryRelays(entry)[0] || {
    relay_url: DEFAULT_RELAY_URL,
    broadcast_name: normalizeName(entry?.broadcast_name || ""),
    track_name: "video0.m4s",
  };
}

function normalizeGuideEntries(entries) {
  if (!Array.isArray(entries)) return [];
  return entries
    .filter((entry) => entry && (entry.stream_id || entry.broadcast_name || entry.title))
    .map((entry) => ({
      ...entry,
      relay_url: normalizeRelayUrl(entry.relay_url),
      broadcast_name: normalizeName(entry.broadcast_name || entry.stream_id || ""),
      title: normalizeName(entry.title || entry.stream_id || entry.broadcast_name || "Live"),
      relays: Array.isArray(entry.relays)
        ? entry.relays
          .filter((relay) => relay?.relay_url && relay.broadcast_name)
          .map((relay) => ({
            ...relay,
            relay_url: normalizeRelayUrl(relay.relay_url),
            broadcast_name: normalizeName(relay.broadcast_name),
            track_name: normalizeName(relay.track_name || "video0.m4s"),
          }))
        : entry.relays,
    }));
}

function guideEntryKey(entry) {
  return normalizeName(entry?.stream_id || entry?.broadcast_name || entry?.title || "");
}

function mergeGuideEntries(primary, fallback) {
  const merged = [];
  const seen = new Set();
  for (const entry of [...normalizeGuideEntries(primary), ...normalizeGuideEntries(fallback)]) {
    const key = guideEntryKey(entry);
    if (!key || seen.has(key)) continue;
    seen.add(key);
    merged.push(entry);
  }
  return merged;
}

function readInitialPublicStreams() {
  const el = document.getElementById("initialPublicStreams");
  if (!el) return [];
  try {
    const body = JSON.parse(el.textContent || "{}");
    return normalizeGuideEntries(body.entries);
  } catch (err) {
    markPerf("guide.seed.error", { error: String(err) });
    return [];
  }
}

function readGuideCache() {
  try {
    const raw = window.localStorage?.getItem(GUIDE_CACHE_KEY);
    if (!raw) return [];
    const cached = JSON.parse(raw);
    if (!cached?.saved_ms || Date.now() - cached.saved_ms > GUIDE_CACHE_MAX_AGE_MS) return [];
    return normalizeGuideEntries(cached.entries);
  } catch (_) {
    return [];
  }
}

function writeGuideCache(entries) {
  if (!entries.length) return;
  try {
    window.localStorage?.setItem(
      GUIDE_CACHE_KEY,
      JSON.stringify({ saved_ms: Date.now(), entries: normalizeGuideEntries(entries) }),
    );
  } catch (_) {
    // Ignore private-mode or quota failures; the HTML seed remains available.
  }
}

function currentShareLink(relayUrl, name, mode) {
  const u = new URL(window.location.href);
  u.pathname = "/watch";
  u.searchParams.set("url", relayUrl);
  u.searchParams.set("name", name);
  if (mode === "archive") u.searchParams.set("mode", "archive");
  else u.searchParams.delete("mode");
  // Avoid leaking other params.
  for (const k of [...u.searchParams.keys()]) {
    if (k !== "url" && k !== "name" && k !== "mode") u.searchParams.delete(k);
  }
  return u.toString();
}

function setHint(text, kind) {
  const el = $("hint");
  el.textContent = text || "";
  el.dataset.kind = kind || "";
}

function setNowTitle(text) {
  const el = document.getElementById("nowTitle");
  if (el) el.textContent = text || "Ready";
}

function setShareLink(text) {
  const el = $("shareLink");
  el.textContent = text || "";
}

function setListHint(text, kind) {
  const el = $("listHint");
  el.textContent = text || "";
  el.dataset.kind = kind || "";
}

function openSignalDrawer() {
  const drawer = $("signalDrawer");
  drawer.open = true;
  $("broadcastName").focus();
}

function renderManualTunePrompt(message) {
  const mount = $("liveList");
  mount.textContent = "";

  const empty = document.createElement("div");
  empty.className = "emptyState";

  const title = document.createElement("div");
  title.className = "emptyTitle";
  title.textContent = message;

  const action = document.createElement("button");
  action.className = "secondaryButton";
  action.type = "button";
  action.textContent = "Add channel";
  action.addEventListener("click", openSignalDrawer);

  empty.appendChild(title);
  empty.appendChild(action);
  mount.appendChild(empty);
}

function setThumbPosition(el, index) {
  const positions = [
    ["0%", "0%"],
    ["100%", "0%"],
    ["0%", "100%"],
    ["100%", "100%"],
  ];
  const [x, y] = positions[index % positions.length];
  el.style.setProperty("--thumb-x", x);
  el.style.setProperty("--thumb-y", y);
}

function renderDefaultMultiview() {
  const grid = document.getElementById("multiViewGrid");
  if (!grid) return;
  const labels = ["News", "Game", "Weather", "Kitchen"];
  grid.textContent = "";
  labels.forEach((label, index) => {
    const button = document.createElement("button");
    button.className = `miniScreen tile${String.fromCharCode(65 + index)}`;
    button.type = "button";
    button.textContent = label;
    button.addEventListener("click", () => {
      setHint("Add a channel key to tune this slot.", "warn");
      openSignalDrawer();
    });
    grid.appendChild(button);
  });
}

function renderMultiview(entries, onWatchLive) {
  const grid = document.getElementById("multiViewGrid");
  if (!grid) return;
  if (!entries.length) {
    renderDefaultMultiview();
    return;
  }
  grid.textContent = "";
  entries.slice(0, 4).forEach((entry, index) => {
    const title = entry.title || entry.stream_id || entry.broadcast_name || "Live";
    const button = document.createElement("button");
    button.className = "miniScreen";
    button.type = "button";
    button.textContent = title;
    setThumbPosition(button, index);
    button.addEventListener("click", () => onWatchLive(entry));
    grid.appendChild(button);
  });
}

function clearPlayerSignals() {
  if (typeof disposePlayerSignals === "function") {
    disposePlayerSignals();
  }
  disposePlayerSignals = null;
}

function destroyArchivePlayer() {
  if (activeHlsPlayer && typeof activeHlsPlayer.destroy === "function") {
    try {
      activeHlsPlayer.destroy();
    } catch (_) {
      // Ignore teardown errors.
    }
  }
  activeHlsPlayer = null;
}

function bindPlayerSignals(watch, name, extraCleanup) {
  const cleanup = Array.isArray(extraCleanup) ? [...extraCleanup] : [];
  let offlineTimer = null;
  let markedLive = false;
  let markedCatalog = false;

  const clearOfflineTimer = () => {
    if (offlineTimer) {
      window.clearTimeout(offlineTimer);
      offlineTimer = null;
    }
  };

  const maybeWatch = (signal, onValue) => {
    if (!signal || typeof signal.watch !== "function") return;
    const dispose = signal.watch(onValue);
    if (typeof dispose === "function") cleanup.push(dispose);
  };

  let sawLoading = false;
  maybeWatch(watch?.broadcast?.status, (status) => {
    if (status === "loading") {
      clearOfflineTimer();
      sawLoading = true;
      setNowTitle(name);
      setHint(`Tuning ${name}...`, "ok");
      return;
    }
    if (status === "live") {
      clearOfflineTimer();
      setNowTitle(name);
      setHint(`On air: ${name}`, "ok");
      if (!markedLive) {
        markedLive = true;
        markPerf("watch.live-status", {
          name,
          elapsed_ms: elapsedSincePerf("watch.requested"),
        });
        measurePerf("watch.request-to-live", "watch.requested", "watch.live-status");
      }
      return;
    }
    if (status === "offline" && sawLoading) {
      clearOfflineTimer();
      // Avoid flashing a false negative during short reconnect windows.
      offlineTimer = window.setTimeout(() => {
        setHint(`Signal faded, retrying ${name}...`, "warn");
        offlineTimer = null;
      }, 8000);
    }
  });

  maybeWatch(watch?.broadcast?.catalog, (catalog) => {
    if (!catalog) return;
    const hasVideo = Boolean(catalog.video && catalog.video.renditions);
    const hasAudio = Boolean(catalog.audio && catalog.audio.renditions);
    if (hasVideo || hasAudio) {
      setNowTitle(name);
      setHint(`On air: ${name}`, "ok");
      if (!markedCatalog) {
        markedCatalog = true;
        markPerf("watch.catalog-ready", {
          name,
          has_video: hasVideo,
          has_audio: hasAudio,
          elapsed_ms: elapsedSincePerf("watch.requested"),
        });
        measurePerf("watch.request-to-catalog", "watch.requested", "watch.catalog-ready");
      }
    }
  });

  if (cleanup.length) {
    disposePlayerSignals = () => {
      clearOfflineTimer();
      for (const fn of cleanup) {
        try {
          fn();
        } catch (_) {
          // Best-effort cleanup only.
        }
      }
    };
  }
}

function observeFirstCanvasFrame(canvas, name, cleanup) {
  if (!canvas || typeof canvas.captureStream !== "function") return;
  let stream = null;
  let done = false;
  let timeoutId = null;
  const video = document.createElement("video");
  video.muted = true;
  video.playsInline = true;
  video.setAttribute("aria-hidden", "true");
  video.style.cssText =
    "position:fixed;width:1px;height:1px;left:-9999px;top:-9999px;opacity:0;pointer-events:none";

  const cleanupFrameObserver = () => {
    if (done) return;
    done = true;
    if (timeoutId) window.clearTimeout(timeoutId);
    try {
      stream?.getTracks?.().forEach((track) => track.stop());
    } catch (_) {
      // Best-effort cleanup.
    }
    video.remove();
  };

  const finish = (state) => {
    if (done) return;
    markPerf("watch.first-canvas-frame", {
      name,
      state,
      elapsed_ms: elapsedSincePerf("watch.requested"),
    });
    cleanupFrameObserver();
  };

  try {
    stream = canvas.captureStream();
    video.srcObject = stream;
  } catch (err) {
    markPerf("watch.first-canvas-frame.error", { name, error: String(err) });
    return;
  }

  timeoutId = window.setTimeout(() => finish("timeout"), FIRST_FRAME_TIMEOUT_MS);
  if (typeof video.requestVideoFrameCallback === "function") {
    video.requestVideoFrameCallback(() => finish("captured"));
  } else {
    video.addEventListener("playing", () => finish("playing"), { once: true });
  }
  cleanup.push(cleanupFrameObserver);
  document.body.appendChild(video);
  void video.play().catch(() => finish("play-blocked"));
}

function mountPlayer(relayUrl, name) {
  clearPlayerSignals();
  destroyArchivePlayer();

  const mount = $("playerMount");
  mount.textContent = "";
  setNowTitle(name);

  const watch = document.createElement("moq-watch");
  watch.setAttribute("url", relayUrl);
  // Support both @moq/watch attribute variants across minor versions.
  watch.setAttribute("name", name);
  watch.setAttribute("path", name);
  watch.setAttribute("volume", "1");
  watch.setAttribute("jitter", String(LIVE_JITTER_MS));

  const canvas = document.createElement("canvas");
  canvas.className = "canvas";
  canvas.width = 1280;
  canvas.height = 720;
  watch.appendChild(canvas);
  mount.appendChild(watch);
  markPerf("watch.element-mounted", {
    name,
    elapsed_ms: elapsedSincePerf("watch.requested"),
  });
  const cleanup = [];
  observeFirstCanvasFrame(canvas, name, cleanup);
  let audioUnlocked = false;
  const forceAudioOn = () => {
    try {
      watch.backend?.audio?.muted?.set?.(false);
      watch.backend?.audio?.volume?.set?.(1);
      watch.backend?.muted?.set?.(false);
      watch.backend?.volume?.set?.(1);
    } catch (_) {
      // Best effort only.
    }
    watch.removeAttribute("muted");
    watch.muted = false;
  };
  const unlockAudio = () => {
    audioUnlocked = true;
    forceAudioOn();
    watch.backend?.paused?.set?.(true);
    watch.backend?.paused?.set?.(false);
    setHint(`On air: ${name} (sound on)`, "ok");
  };
  document.addEventListener("pointerdown", unlockAudio, { once: true });
  canvas.addEventListener("pointerdown", unlockAudio, { once: true });
  cleanup.push(() => {
    document.removeEventListener("pointerdown", unlockAudio);
    canvas.removeEventListener("pointerdown", unlockAudio);
  });
  setHint(`On air: ${name} (tap picture for sound)`, "warn");
  bindPlayerSignals(watch, name, cleanup);
}

async function ensureMoqWatchElement() {
  if (window.customElements && window.customElements.get("moq-watch")) return;
  if (!moqWatchModulePromise) {
    moqWatchModulePromise = (async () => {
      let lastErr = null;
      for (const moduleUrl of MOQ_WATCH_MODULE_URLS) {
        try {
          await import(moduleUrl);
        } catch (err) {
          lastErr = err;
          continue;
        }
        if (window.customElements && window.customElements.get("moq-watch")) {
          return;
        }
      }
      throw lastErr || new Error("moq-watch custom element is unavailable");
    })();
  }
  try {
    await moqWatchModulePromise;
  } catch (err) {
    moqWatchModulePromise = null;
    throw err;
  }
  if (!(window.customElements && window.customElements.get("moq-watch"))) {
    throw new Error("moq-watch custom element is unavailable");
  }
}

function warmMoqWatchElement() {
  if (!hasLiveTransport()) return;
  const warm = async () => {
    markPerf("player.module.warm.start");
    try {
      await ensureMoqWatchElement();
      markPerf("player.module.ready", {
        elapsed_ms: elapsedSincePerf("player.module.warm.start"),
      });
    } catch (err) {
      markPerf("player.module.warm.error", { error: String(err) });
    }
  };
  if (typeof window.requestIdleCallback === "function") {
    window.requestIdleCallback(() => {
      void warm();
    }, { timeout: 900 });
    return;
  }
  window.setTimeout(() => {
    void warm();
  }, 0);
}

async function ensureHlsPlayerCtor() {
  if (window.Hls) return window.Hls;
  if (!hlsModulePromise) {
    hlsModulePromise = (async () => {
      let lastErr = null;
      for (const moduleUrl of HLS_MODULE_URLS) {
        try {
          const mod = await import(moduleUrl);
          if (mod?.default) {
            window.Hls = mod.default;
          } else if (mod?.Hls) {
            window.Hls = mod.Hls;
          }
        } catch (err) {
          lastErr = err;
          continue;
        }
        if (window.Hls) return window.Hls;
      }
      throw lastErr || new Error("hls.js module is unavailable");
    })();
  }
  return hlsModulePromise;
}

function shouldUseNativeHls(video) {
  const ua = navigator.userAgent || "";
  const vendor = navigator.vendor || "";
  const isIos = /iPhone|iPad|iPod/i.test(ua);
  const isSafari =
    /Safari/i.test(ua) &&
    /Apple/i.test(vendor) &&
    !/CriOS|FxiOS|EdgiOS|OPiOS|Chrome|Chromium/i.test(ua);
  return (isIos || isSafari) && Boolean(video.canPlayType("application/vnd.apple.mpegurl"));
}

async function mountArchivePlayer(name) {
  clearPlayerSignals();
  destroyArchivePlayer();

  const mount = $("playerMount");
  mount.textContent = "";
  setNowTitle(name);

  const video = document.createElement("video");
  video.className = "archiveVideo";
  video.controls = true;
  video.autoplay = true;
  video.muted = false;
  video.playsInline = true;
  video.addEventListener("error", () => {
    setHint(
      "This replay is not ready for browser playback yet. Live tuning is unaffected.",
      "warn",
    );
  });
  mount.appendChild(video);

  const archiveUrl = `/api/archive/${encodeURIComponent(name)}/master.m3u8`;
  if (shouldUseNativeHls(video)) {
    video.src = archiveUrl;
    void video.play().catch(() => {});
    return;
  }

  const HlsCtor = await ensureHlsPlayerCtor();
  if (!HlsCtor || typeof HlsCtor.isSupported !== "function" || !HlsCtor.isSupported()) {
    throw new Error("HLS playback is unsupported in this browser");
  }

  const hls = new HlsCtor({
    liveDurationInfinity: true,
    lowLatencyMode: false,
    backBufferLength: 120,
  });
  activeHlsPlayer = hls;
  hls.on(HlsCtor.Events.ERROR, (_event, data) => {
    if (!data?.fatal) return;
    if (data.type === HlsCtor.ErrorTypes.NETWORK_ERROR) {
      setHint("Replay hiccup, retrying...", "warn");
      try {
        hls.startLoad();
      } catch (_) {
        // best-effort recovery
      }
      return;
    }
    if (data.type === HlsCtor.ErrorTypes.MEDIA_ERROR) {
      setHint("Replay picture hiccup, recovering...", "warn");
      try {
        hls.recoverMediaError();
      } catch (_) {
        // best-effort recovery
      }
      return;
    }
    setHint(`Replay unavailable: ${data.type || "fatal"}`, "warn");
  });
  hls.loadSource(archiveUrl);
  hls.attachMedia(video);
  void video.play().catch(() => {});
}

async function copyToClipboard(text) {
  if (!text) return;
  if (navigator.clipboard && navigator.clipboard.writeText) {
    await navigator.clipboard.writeText(text);
    return;
  }
  // Fallback: best-effort.
  const ta = document.createElement("textarea");
  ta.value = text;
  document.body.appendChild(ta);
  ta.select();
  document.execCommand("copy");
  document.body.removeChild(ta);
}

function readParams() {
  const u = new URL(window.location.href);
  // Accept legacy/share-link aliases for compatibility.
  const relay =
    u.searchParams.get("url") ||
    u.searchParams.get("relay") ||
    u.searchParams.get("relayUrl");
  const name =
    u.searchParams.get("name") ||
    u.searchParams.get("broadcast") ||
    u.searchParams.get("path");
  const mode = u.searchParams.get("mode") === "archive" ? "archive" : "live";
  return {
    relayUrl: normalizeRelayUrl(relay || DEFAULT_RELAY_URL),
    name: normalizeName(name || ""),
    mode,
  };
}

function writeParams(relayUrl, name, mode) {
  const u = new URL(window.location.href);
  u.pathname = "/watch";
  u.searchParams.set("url", relayUrl);
  u.searchParams.set("name", name);
  if (mode === "archive") u.searchParams.set("mode", "archive");
  else u.searchParams.delete("mode");
  // Canonicalize by dropping stale aliases/extra params.
  for (const k of [...u.searchParams.keys()]) {
    if (k !== "url" && k !== "name" && k !== "mode") u.searchParams.delete(k);
  }
  window.history.replaceState({}, "", u.toString());
}

function hasLiveTransport() {
  return typeof window.WebTransport !== "undefined" || typeof window.WebSocket !== "undefined";
}

function renderLiveList(entries, onWatchLive, onWatchArchive) {
  const mount = $("liveList");
  mount.textContent = "";
  if (!entries.length) {
    setListHint("No stations are on air yet.", "");
    renderMultiview([], onWatchLive);
    renderManualTunePrompt("No channels found");
    return;
  }
  setListHint(`${entries.length} on air`, "ok");
  renderMultiview(entries, onWatchLive);

  for (const [index, entry] of entries.entries()) {
    const titleText = entry.title || entry.stream_id || entry.broadcast_name || "Live stream";
    const row = document.createElement("div");
    row.className = "liveItem";

    const tuneButton = document.createElement("button");
    tuneButton.className = "liveTune";
    tuneButton.type = "button";
    tuneButton.setAttribute("aria-label", `Watch ${titleText}`);
    setThumbPosition(tuneButton, index);
    tuneButton.addEventListener("click", () => {
      onWatchLive(entry);
    });

    const info = document.createElement("div");
    const title = document.createElement("div");
    title.className = "liveTitle";
    title.textContent = titleText;
    info.appendChild(title);

    const meta = document.createElement("div");
    meta.className = "liveMeta";
    const relayCount = entryRelays(entry).length;
    const relayText = relayCount > 1 ? ` · ${relayCount} relays` : "";
    meta.textContent = `${entry.broadcast_name || "Ready"}${relayText}`;
    info.appendChild(meta);

    const watchBadge = document.createElement("span");
    watchBadge.className = "watchBadge";
    watchBadge.textContent = "Watch";

    const actions = document.createElement("div");
    actions.className = "liveActions";

    const archiveBtn = document.createElement("button");
    archiveBtn.className = "secondaryButton";
    archiveBtn.type = "button";
    archiveBtn.textContent = "DVR";
    archiveBtn.addEventListener("click", () => {
      onWatchArchive(entry);
    });

    actions.appendChild(archiveBtn);

    tuneButton.appendChild(info);
    tuneButton.appendChild(watchBadge);
    row.appendChild(tuneButton);
    row.appendChild(actions);
    mount.appendChild(row);
  }
}

async function fetchLiveList(options) {
  const timeoutMs = options?.timeoutMs ?? GUIDE_REFRESH_TIMEOUT_MS;
  const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
  const timeoutId = controller
    ? window.setTimeout(() => controller.abort(), timeoutMs)
    : null;
  markPerf("guide.fetch.start", { timeout_ms: timeoutMs });
  try {
    const res = await fetch(PUBLIC_STREAMS_PATH, {
      cache: "no-store",
      signal: controller?.signal,
    });
    if (!res.ok) {
      throw new Error(`HTTP ${res.status}`);
    }
    const body = await res.json();
    const entries = normalizeGuideEntries(body?.entries);
    markPerf("guide.fetch.done", {
      count: entries.length,
      elapsed_ms: elapsedSincePerf("guide.fetch.start"),
    });
    return entries;
  } catch (err) {
    markPerf("guide.fetch.error", {
      error: controller?.signal?.aborted ? "timeout" : String(err),
      elapsed_ms: elapsedSincePerf("guide.fetch.start"),
    });
    throw err;
  } finally {
    if (timeoutId) window.clearTimeout(timeoutId);
  }
}

function main() {
  markPerf("app.start");
  const relayInput = $("relayUrl");
  const nameInput = $("broadcastName");
  const archiveModeInput = $("archiveMode");
  const watchBtn = $("watchBtn");
  const copyBtn = $("copyLinkBtn");
  const refreshListBtn = $("refreshListBtn");
  const liveModeBtn = $("liveModeBtn");
  const dvrModeBtn = $("dvrModeBtn");
  const scrubber = $("watchScrubber");
  const clipStartBtn = $("clipStartBtn");
  const clipEndBtn = $("clipEndBtn");
  const copyClipBtn = $("copyClipBtn");
  const multiViewBtn = $("multiViewBtn");
  const multiView = document.querySelector(".multiview");
  let clipStart = Number(scrubber.value || 0);
  let clipEnd = Number(scrubber.value || 0);

  const initial = readParams();
  relayInput.value = initial.relayUrl;
  nameInput.value = initial.name;
  archiveModeInput.checked = initial.mode === "archive";
  setNowTitle(initial.name || "Ready");

  function updateModeButtons() {
    const isArchive = archiveModeInput.checked;
    liveModeBtn.classList.toggle("pressed", !isArchive);
    dvrModeBtn.classList.toggle("pressed", isArchive);
    liveModeBtn.setAttribute("aria-pressed", String(!isArchive));
    dvrModeBtn.setAttribute("aria-pressed", String(isArchive));
  }

  function updateSharePreview() {
    const relayUrl = normalizeRelayUrl(relayInput.value);
    const name = normalizeName(nameInput.value);
    const mode = archiveModeInput.checked ? "archive" : "live";
    if (!name) {
      setShareLink("");
      return;
    }
    setShareLink(currentShareLink(relayUrl, name, mode));
    updateModeButtons();
  }

  function currentClipLink() {
    const relayUrl = normalizeRelayUrl(relayInput.value);
    const name = normalizeName(nameInput.value);
    const mode = archiveModeInput.checked ? "archive" : "live";
    if (!name) return "";
    const u = new URL(currentShareLink(relayUrl, name, mode));
    const start = Math.min(clipStart, clipEnd);
    const end = Math.max(clipStart, clipEnd);
    u.searchParams.set("clipStart", String(start));
    u.searchParams.set("clipEnd", String(end));
    return u.toString();
  }

  async function start() {
    const relayUrl = normalizeRelayUrl(relayInput.value);
    const name = normalizeName(nameInput.value);
    const mode = archiveModeInput.checked ? "archive" : "live";

    updateSharePreview();
    markPerf("watch.requested", { name, mode, relay_url: relayUrl });

    if (!name) {
      setHint("Pick a station or enter a channel key.", "warn");
      setNowTitle("Ready");
      openSignalDrawer();
      return;
    }

    if (mode === "archive") {
      writeParams(relayUrl, name, mode);
      setNowTitle(name);
      setHint(`Loading replay: ${name}`, "ok");
      try {
        await mountArchivePlayer(name);
      } catch (e) {
        setHint(
          `Replay unavailable: ${String(e)}.`,
          "warn",
        );
      }
      return;
    }

    if (!hasLiveTransport()) {
      setHint(
        "Live playback needs a browser live transport. Try DVR replay or another browser.",
        "warn",
      );
      return;
    }

    try {
      markPerf("player.module.await.start", {
        elapsed_ms: elapsedSincePerf("watch.requested"),
      });
      await ensureMoqWatchElement();
      markPerf("player.module.ready-for-watch", {
        elapsed_ms: elapsedSincePerf("watch.requested"),
      });
    } catch (e) {
      setHint(
        `The player did not load: ${String(e)}. Try again after allowing the page scripts.`,
        "warn",
      );
      return;
    }

    writeParams(relayUrl, name, mode);
    setNowTitle(name);
    setHint(`Tuning ${name}...`, "ok");
    mountPlayer(relayUrl, name);
  }

  relayInput.addEventListener("input", updateSharePreview);
  nameInput.addEventListener("input", updateSharePreview);
  archiveModeInput.addEventListener("input", updateSharePreview);
  archiveModeInput.addEventListener("change", updateModeButtons);
  liveModeBtn.addEventListener("click", () => {
    archiveModeInput.checked = false;
    updateSharePreview();
  });
  dvrModeBtn.addEventListener("click", () => {
    archiveModeInput.checked = true;
    updateSharePreview();
  });
  scrubber.addEventListener("input", () => {
    setHint(`Marker ${scrubber.value}%`, "");
  });
  clipStartBtn.addEventListener("click", () => {
    clipStart = Number(scrubber.value || 0);
    setHint(`Clip starts at ${clipStart}%`, "ok");
  });
  clipEndBtn.addEventListener("click", () => {
    clipEnd = Number(scrubber.value || 0);
    setHint(`Clip ends at ${clipEnd}%`, "ok");
  });
  copyClipBtn.addEventListener("click", async () => {
    const link = currentClipLink();
    if (!link) {
      setHint("Pick a channel first.", "warn");
      return;
    }
    try {
      await copyToClipboard(link);
      setHint("Clip link copied.", "ok");
      setShareLink(link);
    } catch (e) {
      setHint(`Copy failed: ${String(e)}`, "warn");
      setShareLink(link);
    }
  });
  multiViewBtn.addEventListener("click", () => {
    const isHidden = multiView?.hasAttribute("hidden");
    if (isHidden) {
      multiView?.removeAttribute("hidden");
      multiViewBtn.classList.add("pressed");
      multiViewBtn.setAttribute("aria-pressed", "true");
    } else {
      multiView?.setAttribute("hidden", "");
      multiViewBtn.classList.remove("pressed");
      multiViewBtn.setAttribute("aria-pressed", "false");
    }
  });

  watchBtn.addEventListener("click", () => {
    void start();
  });
  nameInput.addEventListener("keydown", (e) => {
    if (e.key === "Enter") void start();
  });

  copyBtn.addEventListener("click", async () => {
    const relayUrl = normalizeRelayUrl(relayInput.value);
    const name = normalizeName(nameInput.value);
    const mode = archiveModeInput.checked ? "archive" : "live";
    if (!name) {
      setHint("Pick a station first.", "warn");
      return;
    }
    const link = currentShareLink(relayUrl, name, mode);
    try {
      await copyToClipboard(link);
      setHint("Link copied.", "ok");
      setShareLink(link);
    } catch (e) {
      setHint(`Copy failed: ${String(e)}`, "warn");
      setShareLink(link);
    }
  });

  let lastRenderedEntries = [];
  let guideFirstRendered = false;

  const watchLiveEntry = (entry) => {
    const relay = entryPrimaryRelay(entry);
    archiveModeInput.checked = false;
    relayInput.value = relay.relay_url;
    nameInput.value = relay.broadcast_name;
    updateSharePreview();
    void start();
  };

  const watchArchiveEntry = (entry) => {
    const relay = entryPrimaryRelay(entry);
    archiveModeInput.checked = true;
    relayInput.value = relay.relay_url;
    nameInput.value = relay.broadcast_name;
    updateSharePreview();
    void start();
  };

  function renderGuide(entries, source) {
    const normalized = normalizeGuideEntries(entries);
    lastRenderedEntries = normalized;
    renderLiveList(normalized, watchLiveEntry, watchArchiveEntry);
    const detail = {
      source,
      count: normalized.length,
      elapsed_ms: elapsedSincePerf("app.start"),
    };
    if (!guideFirstRendered && normalized.length) {
      guideFirstRendered = true;
      markPerf("guide.first-render", detail);
      measurePerf("app-to-guide-first-render", "app.start", "guide.first-render");
      return;
    }
    markPerf("guide.render", detail);
  }

  async function refreshLiveList(options) {
    const manual = Boolean(options?.manual);
    const hadEntries = lastRenderedEntries.length > 0;
    if (manual || !hadEntries) {
      setListHint(hadEntries ? "Checking channels..." : "Scanning stations...", "");
    }
    try {
      const entries = await fetchLiveList({
        timeoutMs: manual ? GUIDE_REFRESH_TIMEOUT_MS : GUIDE_INITIAL_TIMEOUT_MS,
      });
      if (!entries.length && hadEntries) {
        setListHint("Using starter channel guide.", "warn");
        return;
      }
      if (entries.length) {
        writeGuideCache(entries);
        renderGuide(entries, "network");
        return;
      }
      renderGuide([], "network-empty");
    } catch (e) {
      const fallback = mergeGuideEntries(readGuideCache(), readInitialPublicStreams());
      if (hadEntries) {
        setListHint("Using saved channel guide.", "warn");
        return;
      }
      if (fallback.length) {
        renderGuide(fallback, "fallback");
        setListHint(`${fallback.length} on air`, "ok");
        return;
      }
      $("liveList").textContent = "";
      setListHint("Station guide is unavailable.", "warn");
      renderMultiview([], () => {});
      renderManualTunePrompt("Have a channel key?");
    }
  }
  refreshListBtn.addEventListener("click", () => {
    void refreshLiveList({ manual: true });
  });

  updateSharePreview();
  updateModeButtons();
  const seededEntries = readInitialPublicStreams();
  const cachedEntries = readGuideCache();
  const initialEntries = mergeGuideEntries(cachedEntries, seededEntries);
  if (initialEntries.length) {
    renderGuide(initialEntries, cachedEntries.length ? "cache" : "html-seed");
  } else {
    renderDefaultMultiview();
  }
  warmMoqWatchElement();
  void refreshLiveList();
  window.setInterval(() => {
    void refreshLiveList();
  }, 15000);

  // Auto-start if a name was provided.
  if (initial.name) void start();
}

main();
