(() => {
  let pollTimer = null;
  let audioDetectorRunning = false;
  let lastBeatSeq = 0;
  let lastLoggedSequenceBeatFractions = "";
  /** Prior poll had server zone sequence playback active (`status.sequence.active === true`). */
  let prevZoneSequencePlaybackActive = false;
  /**
   * After sequence playback ends/stops while audio keeps running, keep header # idle until the
   * next beat bumps `beat_seq` (avoids the stuck final cumulative value vs sequence readout).
   */
  let headerBeatStickyIdleAfterSeq = false;
  /** Suppresses duplicate `console.log` when the same `beat_seq` + server `beat_readout` repeats. */
  let lastBeatConsoleKey = "";
  /** @type {Set<ReturnType<typeof setTimeout>>} */
  const pendingBeatPhaseTimers = new Set();

  function el(id) {
    return document.getElementById(id);
  }

  /** @param {Record<string, unknown>} status */
  function updateBeatReadoutDisplays(status) {
    const text = String((status && status.beat_readout) || "").trim();
    for (const id of ["audio-top-beat-readout", "audio-modal-beat-readout"]) {
      const n = el(id);
      if (n) n.textContent = text;
    }
  }

  /**
   * On each new audio `beat_seq`, log server `beat_readout` once (deduped when poll repeats the
   * same `beat_seq` + line).
   * @param {Record<string, unknown>} status
   */
  function logServerBeatConsoleOnPollEdge(status) {
    const beatSeq = Number((status && status.beat_seq) || 0);
    const line = String((status && status.beat_readout) || "").trim();
    const key = `${beatSeq}\t${line}`;
    if (key !== lastBeatConsoleKey) {
      lastBeatConsoleKey = key;
      if (!line) return;
      const seq = /** @type {Record<string, unknown>|undefined} */ (status && status.sequence);
      const seqBeats = !!seq && !!seq.active;
      let out = line;
      if (seqBeats) {
        const nLanes = Number(seq && seq.num_lanes);
        const lanesNote =
          Number.isFinite(nLanes) && nLanes > 1
            ? `lane 1 of ${nLanes} (readout is for this lane only)`
            : "lane 1";
        out = `${line} — ${lanesNote}`;
      }
      console.log(out);
    }
  }

  function updateBpmDisplay(bpm) {
    const node = el("audio-bpm-value");
    if (!node) return;
    node.textContent = Number.isFinite(bpm) ? bpm.toFixed(1) : "--";
    const topNode = el("audio-top-bpm-value");
    if (topNode) {
      topNode.textContent = Number.isFinite(bpm) ? bpm.toFixed(1) : "--";
    }
  }

  /** Zone sequence playback (server); only when `active === true` is beat X/Y meaningful. */
  function sequencePlaybackActiveFromStatus(status) {
    const seq = /** @type {Record<string, unknown>|undefined} */ (
      status && status.sequence
    );
    return !!(seq && seq.active);
  }

  /** Build sequence beat fractions for debug logging (browser console only). */
  function formatSequenceBeatFractionsForLog(status) {
    const seq = /** @type {Record<string, unknown>|undefined} */ (status && status.sequence);
    if (!seq || !seq.active) return null;

    const laneBeatAt = Number(seq.lane0_beat_in_step);
    const laneBeatsPerStep = Number(seq.lane0_beats_per_step);
    if (
      !Number.isFinite(laneBeatAt) ||
      laneBeatAt <= 0 ||
      !Number.isFinite(laneBeatsPerStep) ||
      laneBeatsPerStep <= 0
    ) {
      return null;
    }
    const presetFraction = `${Math.floor(laneBeatAt)}/${Math.floor(laneBeatsPerStep)}`;

    const sequenceBeatAt = Number(seq.sequence_beat_at);
    const sequenceBeatsPerPass = Number(seq.sequence_beats_per_pass);
    if (
      !Number.isFinite(sequenceBeatAt) ||
      sequenceBeatAt <= 0 ||
      !Number.isFinite(sequenceBeatsPerPass) ||
      sequenceBeatsPerPass <= 0
    ) {
      return null;
    }
    const sequenceFraction = `${Math.floor(sequenceBeatAt)}/${Math.floor(sequenceBeatsPerPass)}`;

    return `${presetFraction} ${sequenceFraction}`;
  }

  function updateHitTypeDisplay(hitType, confidence) {
    const node = el("audio-hit-type-value");
    if (!node) return;
    const label = String(hitType || "unknown").toLowerCase();
    const conf = Number.isFinite(confidence) ? ` (${confidence.toFixed(2)})` : "";
    node.textContent = `${label}${conf}`;
  }

  /** @param {Record<string, unknown>} status */
  function updateBarPhaseDisplay(status) {
    const readout = String((status && status.bar_phase_readout) || "").trim();
    const phaseConf = Number((status && status.phase_confidence) || 0);
    const downbeat = !!(status && status.is_downbeat);
    let text = readout || "--";
    if (readout && Number.isFinite(phaseConf) && phaseConf > 0) {
      text = `${text} (${Math.round(phaseConf * 100)}%)`;
    }
    for (const id of ["audio-bar-phase-value", "audio-top-bar-phase"]) {
      const node = el(id);
      if (!node) continue;
      node.textContent = status && status.running ? text : "";
      node.classList.toggle("is-downbeat", downbeat && !!readout);
    }
  }

  function setTopBpmVisible(on) {
    const top = el("audio-top-indicator");
    if (!top) return;
    top.classList.toggle("audio-running", !!on);
  }

  function setNavResetVisible(on) {
    for (const id of ["audio-nav-reset-btn", "audio-nav-reset-mobile"]) {
      const node = el(id);
      if (node) node.hidden = !on;
    }
  }

  async function resetAudioTracking() {
    try {
      const res = await fetch("/api/audio/reset", {
        method: "POST",
        headers: { Accept: "application/json" },
      });
      if (!res.ok) {
        const data = await res.json().catch(() => ({}));
        console.warn("audio reset failed", data.error || res.status);
        return;
      }
      await pollStatus();
    } catch (e) {
      console.warn("audio reset failed", e);
    }
  }

  function updateSequenceSyncControls(zoneSeqActive) {
    const topSync = el("audio-top-beat-sync");
    if (topSync) {
      topSync.disabled = audioDetectorRunning && !zoneSeqActive;
      topSync.title = !audioDetectorRunning
        ? "Start beat detection"
        : zoneSeqActive
          ? "Sync step to music (S)"
          : "Beat detection running";
    }
    const modalBeat = el("audio-modal-beat-readout");
    if (modalBeat) modalBeat.disabled = !zoneSeqActive;
    const passBtn = el("audio-sync-pass-btn");
    if (passBtn) passBtn.disabled = !zoneSeqActive;
  }

  async function handleTopBpmButtonClick() {
    if (!audioDetectorRunning) {
      try {
        await startAudio();
      } catch (e) {
        console.error("audio start failed", e);
        alert("Failed to start audio input. Check mic permissions.");
      }
      return;
    }
    try {
      await syncSequenceBeatPhase("step");
    } catch (e) {
      console.warn("sequence beat sync failed", e);
    }
  }

  async function syncSequenceBeatPhase(mode) {
    const res = await fetch("/sequences/sync-phase", {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body: JSON.stringify({ mode: mode || "step" }),
    });
    if (!res.ok) {
      const data = await res.json().catch(() => ({}));
      throw new Error(data.error || `Sync failed (${res.status})`);
    }
    await pollStatus();
  }

  function isTypingTarget(target) {
    if (!target || typeof target !== "object") return false;
    const tag = String(target.tagName || "").toLowerCase();
    return tag === "input" || tag === "textarea" || tag === "select" || target.isContentEditable;
  }

  function flashBeat() {
    const node = el("audio-beat-flash");
    if (!node) return;
    node.classList.add("active");
    setTimeout(() => node.classList.remove("active"), 80);
    const syncBtn = el("audio-top-beat-sync");
    const top = el("audio-top-indicator");
    if (syncBtn && top && top.classList.contains("audio-running")) {
      syncBtn.classList.add("flash");
      setTimeout(() => syncBtn.classList.remove("flash"), 90);
    }
  }

  function clearBeatPhaseTimers() {
    pendingBeatPhaseTimers.forEach((t) => clearTimeout(t));
    pendingBeatPhaseTimers.clear();
  }

  function getBeatPhaseDelayMs() {
    const inp = el("audio-beat-phase-ms");
    if (inp && String(inp.value).trim() !== "") {
      const n = parseInt(String(inp.value).trim(), 10);
      if (Number.isFinite(n)) return Math.min(500, Math.max(0, n));
    }
    return 0;
  }

  async function persistBeatPhaseMs() {
    const ms = getBeatPhaseDelayMs();
    try {
      await fetch("/settings", {
        method: "PUT",
        headers: { "Content-Type": "application/json", Accept: "application/json" },
        body: JSON.stringify({ audio_beat_phase_ms: ms }),
      });
    } catch (e) {
      console.warn("beat phase ms save failed", e);
    }
  }

  function scheduleBeatPhaseFire(seq, delayMs) {
    let tid = null;
    const run = () => {
      if (tid != null) pendingBeatPhaseTimers.delete(tid);
      flashBeat();
      try {
        window.dispatchEvent(
          new CustomEvent("ledControllerAudioBeat", { detail: { beatSeq: seq } }),
        );
      } catch (e) {
        /* ignore */
      }
    };
    if (delayMs <= 0) {
      run();
      return;
    }
    tid = setTimeout(run, delayMs);
    pendingBeatPhaseTimers.add(tid);
  }

  /** Stop detector and polling; does not clear “resume on load” prefs (used before restart). */
  async function stopAudioOnly() {
    audioDetectorRunning = false;
    setTopBpmVisible(false);
    setNavResetVisible(false);
    clearBeatPhaseTimers();
    if (pollTimer) {
      clearInterval(pollTimer);
      pollTimer = null;
    }
    lastBeatSeq = 0;
    prevZoneSequencePlaybackActive = false;
    headerBeatStickyIdleAfterSeq = false;
    lastBeatConsoleKey = "";
    updateBeatReadoutDisplays({});
    try {
      await fetch("/api/audio/stop", { method: "POST" });
    } catch (e) {
      console.warn("audio stop failed", e);
    }
  }

  /** User-initiated stop (run intent cleared on server). */
  async function stopAudio() {
    await stopAudioOnly();
  }

  async function pollStatus() {
    try {
      const res = await fetch("/api/audio/status", { cache: "no-store" });
      const data = await res.json();
      const status = data?.status || {};
      if (status.error && String(status.error).trim()) {
        const node = el("audio-hit-type-value");
        if (node) {
          node.textContent = String(status.error).trim().slice(0, 120);
        }
        updateBeatReadoutDisplays({});
        audioDetectorRunning = !!status.running;
        updateBpmDisplay(null);
        setTopBpmVisible(!!status.running);
        setNavResetVisible(!!status.running);
        if (!status.running && pollTimer) {
          clearInterval(pollTimer);
          pollTimer = null;
        }
        return;
      }
      audioDetectorRunning = !!status.running;
      const zoneSeqActive = sequencePlaybackActiveFromStatus(status);
      setTopBpmVisible(!!status.running || zoneSeqActive);
      setNavResetVisible(!!status.running);
      updateSequenceSyncControls(zoneSeqActive);
      updateBpmDisplay(status.bpm);
      updateHitTypeDisplay(status.beat_type, Number(status.beat_type_confidence));
      updateBarPhaseDisplay(status);
      applyServerAudioUiFields(status);
      if (typeof window.applySequenceSwitchWaitFromServer === "function") {
        window.applySequenceSwitchWaitFromServer(status.sequence_switch_wait);
      }
      /*
       * `status.beat_seq` is cumulative since Audio Start — used only for flash / sticky idle
       * after sequence ends. Preset and sequence loop counts come from `manual_beat_stride` /
       * `sequence` on each poll.
       */
      const beatSeq = Number(status.beat_seq || 0);
      const endedSeq = prevZoneSequencePlaybackActive && !zoneSeqActive;
      const startedSeq = !prevZoneSequencePlaybackActive && zoneSeqActive;
      prevZoneSequencePlaybackActive = zoneSeqActive;
      if (startedSeq) {
        headerBeatStickyIdleAfterSeq = false;
        lastLoggedSequenceBeatFractions = "";
      }
      if (endedSeq) {
        headerBeatStickyIdleAfterSeq = true;
        clearBeatPhaseTimers();
        lastBeatSeq = beatSeq;
      }
      if (!zoneSeqActive && headerBeatStickyIdleAfterSeq) {
        if (beatSeq > lastBeatSeq) {
          lastBeatSeq = beatSeq;
          logServerBeatConsoleOnPollEdge(status);
          scheduleBeatPhaseFire(beatSeq, getBeatPhaseDelayMs());
          headerBeatStickyIdleAfterSeq = false;
        }
      } else if (beatSeq > lastBeatSeq) {
        lastBeatSeq = beatSeq;
        logServerBeatConsoleOnPollEdge(status);
        scheduleBeatPhaseFire(beatSeq, getBeatPhaseDelayMs());
      }
      const beatFractions = formatSequenceBeatFractionsForLog(status);
      if (beatFractions) {
        if (beatFractions !== lastLoggedSequenceBeatFractions) {
          lastLoggedSequenceBeatFractions = beatFractions;
        }
      } else {
        lastLoggedSequenceBeatFractions = "";
      }
      updateBeatReadoutDisplays(status);
    } catch (e) {
      console.warn("audio status poll failed", e);
    }
  }

  async function startAudio() {
    await stopAudioOnly();
    const override = (el("audio-device-override")?.value || "").trim();
    const selected = el("audio-device-select")?.value || "";
    const rawDevice = override !== "" ? override : selected;
    const numeric = rawDevice !== "" && /^-?\d+$/.test(rawDevice) ? Number(rawDevice) : rawDevice;
    const body = {
      device: rawDevice === "" ? null : numeric,
      device_override: override,
      device_select: selected,
    };
    const res = await fetch("/api/audio/start", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!res.ok) {
      const data = await res.json().catch(() => ({}));
      throw new Error(data.error || "Failed to start audio detector");
    }
    updateBpmDisplay(null);
    updateHitTypeDisplay("unknown", NaN);
    pollTimer = setInterval(pollStatus, 250);
    await pollStatus();
  }

  async function refreshDevices() {
    const select = el("audio-device-select");
    const debug = el("audio-devices-debug");
    if (!select) return;
    const current = select.value;
    const res = await fetch("/api/audio/devices");
    const data = await res.json();
    const inputs = Array.isArray(data?.devices) ? data.devices.slice() : [];
    if (debug) {
      debug.value = JSON.stringify(data?.diagnostics || data, null, 2);
    }
    inputs.sort((a, b) => {
      const am = String(a?.name || "").toLowerCase().includes("monitor");
      const bm = String(b?.name || "").toLowerCase().includes("monitor");
      if (am !== bm) return am ? -1 : 1;
      return Number(a?.id || 0) - Number(b?.id || 0);
    });
    select.innerHTML = '<option value="">System default input</option>';
    let defaultId = "";
    inputs.forEach((d, idx) => {
      const option = document.createElement("option");
      option.value = String(d.id);
      option.textContent = d.label || d.name || `Input ${idx + 1}`;
      if (d.is_default) {
        defaultId = String(d.id);
      }
      select.appendChild(option);
    });
    if (current) {
      select.value = current;
    } else if (defaultId) {
      select.value = defaultId;
    }
  }

  function bind() {
    const modal = el("audio-modal");
    const openBtn = el("audio-btn");
    const closeBtn = el("audio-close-btn");
    const startBtn = el("audio-start-btn");
    const stopBtn = el("audio-stop-btn");
    const navResetBtn = el("audio-nav-reset-btn");
    const refreshBtn = el("audio-refresh-btn");
    if (!modal || !openBtn) return;

    openBtn.addEventListener("click", async () => {
      modal.classList.add("active");
      try {
        await refreshDevices();
      } catch (e) {
        console.warn("audio device refresh failed", e);
      }
    });
    if (closeBtn) {
      closeBtn.addEventListener("click", () => {
        modal.classList.remove("active");
      });
    }
    if (startBtn) {
      startBtn.addEventListener("click", async () => {
        try {
          await startAudio();
          await refreshDevices();
        } catch (e) {
          console.error("audio start failed", e);
          alert("Failed to start audio input. Check mic permissions.");
        }
      });
    }
    if (stopBtn) {
      stopBtn.addEventListener("click", async () => {
        await stopAudio();
      });
    }
    if (navResetBtn) {
      navResetBtn.addEventListener("click", () => resetAudioTracking());
    }
    if (refreshBtn) {
      refreshBtn.addEventListener("click", async () => {
        try {
          await refreshDevices();
        } catch (e) {
          console.error("refresh devices failed", e);
        }
      });
    }

    const phaseInp = el("audio-beat-phase-ms");
    if (phaseInp) {
      phaseInp.addEventListener("change", () => {
        void persistBeatPhaseMs();
      });
      phaseInp.addEventListener("input", () => {
        void persistBeatPhaseMs();
      });
    }

    const bindSync = (node, mode) => {
      if (!node) return;
      node.addEventListener("click", async () => {
        try {
          await syncSequenceBeatPhase(mode);
        } catch (e) {
          console.warn("sequence beat sync failed", e);
        }
      });
    };
    const topBpm = el("audio-top-beat-sync");
    if (topBpm) {
      topBpm.addEventListener("click", () => {
        void handleTopBpmButtonClick();
      });
    }
    bindSync(el("audio-modal-beat-readout"), "step");
    bindSync(el("audio-sync-pass-btn"), "pass");

    document.addEventListener("keydown", (ev) => {
      if (ev.defaultPrevented || ev.repeat || isTypingTarget(ev.target)) return;
      const k = String(ev.key || "").toLowerCase();
      if (k !== "s") return;
      ev.preventDefault();
      const mode = ev.shiftKey ? "pass" : "step";
      void syncSequenceBeatPhase(mode).catch((e) => console.warn("sequence beat sync failed", e));
    });
  }

  async function resumePollingIfDetectorRunning() {
    try {
      const res = await fetch("/api/audio/status", { cache: "no-store" });
      const data = await res.json();
      const status = data?.status || {};
      audioDetectorRunning = !!status.running;
      if (status.running && !pollTimer) {
        pollTimer = setInterval(pollStatus, 250);
        lastBeatSeq = Number(status.beat_seq || 0);
        prevZoneSequencePlaybackActive = sequencePlaybackActiveFromStatus(status);
        await pollStatus();
      } else {
        updateSequenceSyncControls(sequencePlaybackActiveFromStatus(status));
      }
    } catch (e) {
      console.warn("audio resume poll check failed", e);
    }
  }

  /** Apply server-owned audio UI fields from status (device form, beat phase delay). */
  function applyServerAudioUiFields(status) {
    if (!status || typeof status !== "object") return;
    const run = status.audio_run;
    if (run && typeof run === "object") {
      const ov = el("audio-device-override");
      const sel = el("audio-device-select");
      if (ov && run.device_override != null) ov.value = String(run.device_override);
      if (sel && run.device_select) sel.value = String(run.device_select);
    }
    const phaseInp = el("audio-beat-phase-ms");
    if (
      phaseInp &&
      status.beat_phase_ms != null &&
      document.activeElement !== phaseInp
    ) {
      const ms = parseInt(String(status.beat_phase_ms), 10);
      if (Number.isFinite(ms)) {
        phaseInp.value = String(Math.min(500, Math.max(0, ms)));
      }
    }
  }

  async function loadServerAudioUiFields() {
    try {
      await refreshDevices();
    } catch (e) {
      console.warn("audio device list refresh failed", e);
    }
    try {
      const res = await fetch("/api/audio/status", { cache: "no-store" });
      const data = await res.json();
      applyServerAudioUiFields(data?.status || {});
    } catch (e) {
      console.warn("audio status load failed", e);
    }
  }

  /** Called from sequences.js when server playback starts/stops without audio polling. */
  window.ledControllerSequencePlaybackChanged = (active) => {
    updateSequenceSyncControls(!!active);
    if (active) {
      setTopBpmVisible(true);
      return;
    }
    if (!pollTimer) {
      setTopBpmVisible(false);
      updateSequenceSyncControls(false);
    }
  };

  document.addEventListener("DOMContentLoaded", async () => {
    bind();
    await loadServerAudioUiFields();
    await resumePollingIfDetectorRunning();
  });
})();
