(() => {
  const root = document.getElementById("lumi-overlay-root");
  const config = document.getElementById("lumi-overlay-config");
  const initial = document.getElementById("lumi-overlay-state");
  const renderer = window.LumiOverlayRenderer;
  if (!root || !config || !initial || !renderer) return;

  let revision = 0;
  let refreshPromise = null;
  let currentState = null;
  let resizeTimer = null;
  const sourceTimers = new Map();
  const actionQueues = new Map();
  const obsBridge = window.obsstudio || null;
  const bridgeInstanceId = globalThis.crypto?.randomUUID?.() || `${Date.now()}:${Math.random().toString(16).slice(2)}`;
  const handledBridgeCommands = new Set();
  let bridgeSourceActive = null;
  let bridgeSourceVisible = null;
  let bridgeReportTimer = null;

  function clearSourceTimers() {
    for (const timers of sourceTimers.values()) timers.forEach((timer) => clearTimeout(timer));
    sourceTimers.clear();
  }

  function rememberTimer(moduleId, timer) {
    const timers = sourceTimers.get(moduleId) || [];
    timers.push(timer);
    sourceTimers.set(moduleId, timers);
  }

  function frameFor(moduleId) {
    return renderer.findSourceContent(root, moduleId, ".lumi-overlay-frame");
  }

  function mediaFor(moduleId) {
    return renderer.findSourceContent(root, moduleId, "video, audio");
  }

  function sourceFor(moduleId) {
    return root.querySelector(`.lumi-overlay-module[data-module-id="${CSS.escape(moduleId)}"]`);
  }

  function reloadWebsiteSource(moduleId) {
    const frame = frameFor(moduleId);
    if (!frame) return false;
    const replacement = frame.cloneNode(false);
    replacement.src = frame.dataset.originalUrl || frame.src;
    frame.replaceWith(replacement);
    return true;
  }

  function restartSource(moduleId) {
    return reloadWebsiteSource(moduleId) || renderer.restartMediaElement(mediaFor(moduleId));
  }

  function wait(milliseconds) {
    return new Promise((resolve) => setTimeout(resolve, milliseconds));
  }

  async function playMediaOnce(moduleId) {
    const media = mediaFor(moduleId);
    if (!media) return false;
    media.loop = false;
    const startAt = Math.max(0, Number(media.dataset.startAt || 0));
    try { media.currentTime = startAt; } catch {}
    await new Promise((resolve) => {
      let settled = false;
      const finish = () => {
        if (settled) return;
        settled = true;
        clearTimeout(timeout);
        media.removeEventListener("ended", finish);
        media.removeEventListener("error", finish);
        resolve();
      };
      const duration = Number.isFinite(media.duration) && media.duration > startAt
        ? ((media.duration - startAt) / Math.max(0.25, Number(media.playbackRate || 1))) * 1000 + 2000
        : 5 * 60 * 1000;
      const timeout = setTimeout(finish, Math.min(10 * 60 * 1000, Math.max(2000, duration)));
      media.addEventListener("ended", finish, { once: true });
      media.addEventListener("error", finish, { once: true });
      try {
        const playback = media.play();
        playback?.catch?.(finish);
      } catch {
        finish();
      }
    });
    return true;
  }

  async function showSourceTemporarily(moduleId, durationSeconds) {
    const source = sourceFor(moduleId);
    if (!source) return false;
    source.hidden = false;
    source.classList.add("is-event-active");
    await wait(Math.min(3600, Math.max(0.5, Number(durationSeconds || 5))) * 1000);
    source.classList.remove("is-event-active");
    if (source.dataset.eventOnly === "true") source.hidden = true;
    return true;
  }

  function enqueueOverlayAction(payload) {
    const moduleId = String(payload?.module_id || "");
    if (!moduleId || !sourceFor(moduleId)) return;
    const previous = actionQueues.get(moduleId) || Promise.resolve();
    const next = previous.catch(() => {}).then(() => {
      if (payload.action === "play_once") return playMediaOnce(moduleId);
      if (payload.action === "show_temporary") return showSourceTemporarily(moduleId, payload.duration_seconds);
      return false;
    });
    actionQueues.set(moduleId, next);
    next.finally(() => {
      if (actionQueues.get(moduleId) === next) actionQueues.delete(moduleId);
    });
  }

  async function checkWebsiteSource(module, attempt = 0) {
    try {
      const url = new URL(config.dataset.healthUrl, window.location.href);
      url.searchParams.set("module_id", module.id);
      const response = await fetch(url, { cache: "no-store", credentials: "omit" });
      const result = await response.json().catch(() => ({ healthy: false }));
      if (result.healthy) return;
      if (attempt < Number(module.config.retryLimit ?? 3)) {
        const delay = Math.min(60000, 5000 * (3 ** attempt));
        rememberTimer(module.id, setTimeout(() => {
          reloadWebsiteSource(module.id);
          checkWebsiteSource(module, attempt + 1);
        }, delay));
      }
    } catch {
      if (attempt < Number(module.config.retryLimit ?? 3)) {
        const delay = Math.min(60000, 5000 * (3 ** attempt));
        rememberTimer(module.id, setTimeout(() => {
          reloadWebsiteSource(module.id);
          checkWebsiteSource(module, attempt + 1);
        }, delay));
      }
    }
  }

  function startWebsiteRecovery(module) {
    if (!module.config?.autoRefresh) return;
    const interval = Math.max(30, Number(module.config.refreshIntervalSeconds || 300)) * 1000;
    const frame = frameFor(module.id);
    if (frame) {
      let loaded = false;
      frame.addEventListener("load", () => { loaded = true; }, { once: true });
      frame.addEventListener("error", () => {
        reloadWebsiteSource(module.id);
        if (module.config.healthCheck !== false) checkWebsiteSource(module, 0);
      }, { once: true });
      rememberTimer(module.id, setTimeout(() => {
        if (!loaded) {
          reloadWebsiteSource(module.id);
          if (module.config.healthCheck !== false) checkWebsiteSource(module, 0);
        }
      }, 15000));
    }
    if (module.config.healthCheck !== false) {
      rememberTimer(module.id, setTimeout(() => checkWebsiteSource(module, 0), Math.min(30000, interval / 2)));
    }
    const periodic = () => {
      reloadWebsiteSource(module.id);
      if (module.config.healthCheck !== false) checkWebsiteSource(module, 0);
      rememberTimer(module.id, setTimeout(periodic, interval));
    };
    rememberTimer(module.id, setTimeout(periodic, interval));
  }

  function render(state) {
    currentState = state;
    revision = Number(state?.revision || revision || 0);
    clearSourceTimers();
    renderer.render(root, state, { canvas: state?.canvas });
    for (const module of state?.scene?.modules || []) {
      if ((module.renderType || module.type) === "web") startWebsiteRecovery(module);
    }
  }

  async function refresh() {
    if (refreshPromise) return refreshPromise;
    refreshPromise = fetch(config.dataset.stateUrl, { cache: "no-store", credentials: "omit" })
      .then((response) => response.ok ? response.json() : { enabled: false, scene: null })
      .then(render)
      .catch(() => {})
      .finally(() => { refreshPromise = null; });
    return refreshPromise;
  }

  function obsCallback(method, ...args) {
    return new Promise((resolve) => {
      if (!obsBridge || typeof obsBridge[method] !== "function") return resolve(null);
      let settled = false;
      const finish = (value) => {
        if (settled) return;
        settled = true;
        clearTimeout(timeout);
        resolve(value);
      };
      const timeout = setTimeout(() => finish(null), 1800);
      try { obsBridge[method](...args, finish); }
      catch { finish(null); }
    });
  }

  async function reportObsBridge() {
    if (!obsBridge || !config.dataset.bridgeUrl) return;
    const controlLevel = Number(await obsCallback("getControlLevel")) || 0;
    const [outputs, scene, scenes, transitions, currentTransition] = await Promise.all([
      controlLevel >= 1 ? obsCallback("getStatus") : null,
      controlLevel >= 2 ? obsCallback("getCurrentScene") : null,
      controlLevel >= 2 ? obsCallback("getScenes") : null,
      controlLevel >= 2 ? obsCallback("getTransitions") : null,
      controlLevel >= 2 ? obsCallback("getCurrentTransition") : null
    ]);
    try {
      await fetch(config.dataset.bridgeUrl, {
        method: "POST",
        cache: "no-store",
        credentials: "omit",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          instance_id: bridgeInstanceId,
          plugin_version: obsBridge.pluginVersion || null,
          control_level: controlLevel,
          current_scene: scene?.name || null,
          canvas_width: scene?.width || null,
          canvas_height: scene?.height || null,
          scenes: Array.isArray(scenes) ? scenes : [],
          transitions: Array.isArray(transitions) ? transitions : [],
          current_transition: currentTransition || null,
          outputs: outputs || {},
          source_active: bridgeSourceActive,
          source_visible: bridgeSourceVisible
        })
      });
    } catch {}
  }

  function scheduleObsBridgeReport(delay = 150) {
    if (!obsBridge) return;
    clearTimeout(bridgeReportTimer);
    bridgeReportTimer = setTimeout(reportObsBridge, delay);
  }

  function bridgeFlag(event, key) {
    if (typeof event?.detail === "boolean") return event.detail;
    if (typeof event?.detail?.[key] === "boolean") return event.detail[key];
    return null;
  }

  if (obsBridge) {
    window.addEventListener("obsSourceActiveChanged", (event) => {
      bridgeSourceActive = bridgeFlag(event, "active");
      scheduleObsBridgeReport();
    });
    window.addEventListener("obsSourceVisibleChanged", (event) => {
      bridgeSourceVisible = bridgeFlag(event, "visible");
      scheduleObsBridgeReport();
    });
    for (const eventName of [
      "obsSceneChanged", "obsSceneListChanged", "obsTransitionChanged", "obsTransitionListChanged",
      "obsStreamingStarting", "obsStreamingStarted", "obsStreamingStopping", "obsStreamingStopped",
      "obsRecordingStarting", "obsRecordingStarted", "obsRecordingPaused", "obsRecordingUnpaused",
      "obsRecordingStopping", "obsRecordingStopped", "obsReplaybufferStarting", "obsReplaybufferStarted",
      "obsReplaybufferSaved", "obsReplaybufferStopping", "obsReplaybufferStopped",
      "obsVirtualcamStarted", "obsVirtualcamStopped"
    ]) window.addEventListener(eventName, () => scheduleObsBridgeReport());
    scheduleObsBridgeReport(0);
    setInterval(() => scheduleObsBridgeReport(0), 5000);
  }

  try { render(JSON.parse(initial.textContent || "{}")); }
  catch { render({ enabled: false, scene: null }); }

  const events = new EventSource(config.dataset.eventsUrl, { withCredentials: false });
  events.addEventListener("overlay:changed", (event) => {
    try {
      const payload = JSON.parse(event.data || "{}");
      if (!payload.revision || Number(payload.revision) >= revision) refresh();
    } catch {
      refresh();
    }
  });
  events.addEventListener("overlay:module-refresh", (event) => {
    try {
      const payload = JSON.parse(event.data || "{}");
      if (payload.module_id) restartSource(payload.module_id);
    } catch {}
  });
  events.addEventListener("overlay:chat-message", (event) => {
    try { renderer.handleChatMessage(root, JSON.parse(event.data || "{}")); }
    catch {}
  });
  events.addEventListener("overlay:action", (event) => {
    try { enqueueOverlayAction(JSON.parse(event.data || "{}")); }
    catch {}
  });
  events.addEventListener("overlay:obs-browser-command", (event) => {
    if (!obsBridge) return;
    try {
      const payload = JSON.parse(event.data || "{}");
      if (!payload.command_id || payload.target_instance_id !== bridgeInstanceId || handledBridgeCommands.has(payload.command_id)) return;
      handledBridgeCommands.add(payload.command_id);
      if (handledBridgeCommands.size > 100) handledBridgeCommands.delete(handledBridgeCommands.values().next().value);
      if (payload.action === "set_current_scene" && typeof obsBridge.setCurrentScene === "function") {
        obsBridge.setCurrentScene(String(payload.scene_name || ""));
        scheduleObsBridgeReport(350);
      } else if (payload.action === "save_replay_buffer" && typeof obsBridge.saveReplayBuffer === "function") {
        obsBridge.saveReplayBuffer();
        scheduleObsBridgeReport(350);
      }
    } catch {}
  });
  events.addEventListener("server:status", (event) => {
    try {
      const payload = JSON.parse(event.data || "{}");
      if (payload.status === "connected") refresh();
    } catch {}
  });
  window.addEventListener("resize", () => {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(() => { if (currentState) render(currentState); }, 120);
  });
})();
