// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later

// --- Virtual Channels Plugin ---
//
// Handles the `session` and `channels` SSE events emitted by /events and
// provides the channel picker UI (SDR-only, shown when filter_controls is set).

let vchanSessionId = null;
let vchanRigId = null;
let vchanChannels = [];
let vchanActiveId = null;
let schedulerReleaseState = null;
let schedulerReleasePollTimer = null;

function vchanFmtFreq(hz) {
  if (!Number.isFinite(hz) || hz <= 0) return "--";
  if (hz >= 1e9) return (hz / 1e9).toFixed(4).replace(/\.?0+$/, "") + "\u202fGHz";
  if (hz >= 1e6) return (hz / 1e6).toFixed(4).replace(/\.?0+$/, "") + "\u202fMHz";
  if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + "\u202fkHz";
  return hz + "\u202fHz";
}

function schedulerReleaseSummaryText(state) {
  if (!state) return "Scheduler is controlling the rig.";
  const connected = Number(state.connected_sessions) || 0;
  const released = Number(state.released_sessions) || 0;
  if (connected === 0) return "Scheduler can control the rig.";
  if (state.all_released) {
    return connected === 1
      ? "Scheduler is controlling the rig."
      : `Scheduler is controlling the rig for all ${connected} users.`;
  }
  if (!state.current_session_released) {
    const othersReleased = Math.max(released, 0);
    return othersReleased > 0
      ? `You are holding control. ${othersReleased} other user${othersReleased === 1 ? "" : "s"} already released it.`
      : "You are holding control. Release it to return control to the scheduler.";
  }
  const blocking = Math.max(connected - released, 0);
  return blocking > 0
    ? `Scheduler is waiting for ${blocking} user${blocking === 1 ? "" : "s"} to stop manual tuning.`
    : "Scheduler can control the rig.";
}

function vchanRenderSchedulerRelease() {
  const btn = document.getElementById("scheduler-release-btn");
  const status = document.getElementById("scheduler-release-status");
  if (!btn || !status) return;
  const currentReleased = !!(schedulerReleaseState && schedulerReleaseState.current_session_released);
  btn.disabled = !vchanSessionId || currentReleased;
  btn.classList.toggle("active", !currentReleased);
  btn.textContent = "Release to Scheduler";
  status.textContent = schedulerReleaseSummaryText(schedulerReleaseState);
}

async function vchanPollSchedulerRelease() {
  if (!vchanSessionId) {
    schedulerReleaseState = null;
    vchanRenderSchedulerRelease();
    return;
  }
  try {
    const resp = await fetch(`/scheduler-control?session_id=${encodeURIComponent(vchanSessionId)}`);
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    schedulerReleaseState = await resp.json();
    vchanRenderSchedulerRelease();
  } catch (e) {
    console.error("scheduler release status failed", e);
  }
}

function vchanStartSchedulerReleasePolling() {
  if (schedulerReleasePollTimer) {
    clearInterval(schedulerReleasePollTimer);
  }
  schedulerReleasePollTimer = setInterval(vchanPollSchedulerRelease, 10000);
}

async function vchanToggleSchedulerRelease() {
  if (!vchanSessionId) return;
  const rigId = vchanRigId || (typeof lastActiveRigId !== "undefined" ? lastActiveRigId : null);
  try {
    const resp = await fetch("/scheduler-control", {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ session_id: vchanSessionId, released: true, remote: rigId }),
    });
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    schedulerReleaseState = await resp.json();
    vchanRenderSchedulerRelease();
  } catch (e) {
    console.error("scheduler release toggle failed", e);
  }
}

async function vchanTakeSchedulerControl() {
  if (!vchanSessionId) return;
  if (schedulerReleaseState && !schedulerReleaseState.current_session_released) return;
  try {
    const resp = await fetch("/scheduler-control", {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ session_id: vchanSessionId, released: false }),
    });
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    schedulerReleaseState = await resp.json();
    vchanRenderSchedulerRelease();
  } catch (e) {
    console.error("scheduler control takeover failed", e);
  }
}
window.vchanTakeSchedulerControl = vchanTakeSchedulerControl;

// Called by app.js when the SSE `session` event arrives.
function vchanHandleSession(data) {
  try {
    const d = JSON.parse(data);
    vchanSessionId = d.session_id || null;
    vchanPollSchedulerRelease();
  } catch (e) {
    console.warn("vchan: bad session event", e);
  }
}

// Called by app.js when the SSE `channels` event arrives.
function vchanHandleChannels(data) {
  try {
    const d = JSON.parse(data);
    vchanRigId = d.remote || null;
    vchanChannels = d.channels || [];
    const ids = new Set(vchanChannels.map(c => c.id));
    if (!vchanActiveId && vchanChannels.length > 0 && vchanSessionId) {
      // First channels event for this session — auto-subscribe to channel 0
      // so we join the same tuned channel as other users on this rig.
      // Use a direct subscribe (no scheduler control takeover) to avoid
      // side-effects on initial connect.
      vchanAutoJoinPrimary(vchanChannels[0].id);
    } else if (vchanActiveId && !ids.has(vchanActiveId)) {
      // Active channel was evicted — fall back to channel 0 and reconnect audio.
      vchanActiveId = vchanChannels.length > 0 ? vchanChannels[0].id : null;
      vchanReconnectAudio();
    }
    vchanRender();
    vchanRenderSchedulerRelease();
    if (typeof renderRdsOverlays === "function") renderRdsOverlays();
  } catch (e) {
    console.warn("vchan: bad channels event", e);
  }
}

function vchanRender() {
  const picker = document.getElementById("vchan-picker");
  if (!picker) return;
  picker.innerHTML = "";

  vchanChannels.forEach(ch => {
    const btn = document.createElement("button");
    btn.type = "button";
    btn.title = `Ch ${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode} · ${ch.subscribers} subscriber${ch.subscribers !== 1 ? "s" : ""}`;
    if (ch.id === vchanActiveId) btn.classList.add("active");

    const label = document.createElement("span");
    label.className = "vchan-label";
    label.textContent = `${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode}`;
    btn.appendChild(label);

    if (!ch.permanent) {
      const del = document.createElement("span");
      del.className = "vchan-del";
      del.textContent = "\u00d7";
      del.title = "Delete channel";
      del.addEventListener("click", e => {
        e.stopPropagation();
        vchanDelete(ch.id);
      });
      btn.appendChild(del);
    }

    btn.addEventListener("click", () => {
      if (ch.id !== vchanActiveId) vchanSubscribe(ch.id);
    });

    picker.appendChild(btn);
  });

  // "+" button — allocate a new channel at the current VFO frequency.
  const addBtn = document.createElement("button");
  addBtn.type = "button";
  addBtn.className = "vchan-add";
  addBtn.textContent = "+";
  addBtn.title = "Allocate new virtual channel at current frequency";
  addBtn.addEventListener("click", vchanAllocate);
  picker.appendChild(addBtn);

  vchanSyncAccentUI();
  if (typeof updateDocumentTitle === "function" && typeof activeChannelRds === "function") {
    updateDocumentTitle(activeChannelRds());
  }
  vchanRenderSchedulerRelease();
}

async function vchanAllocate() {
  if (!vchanSessionId || !vchanRigId) return;

  // Use the last known rig frequency and mode as the starting point.
  const freqHz = (typeof lastFreqHz === "number" && lastFreqHz > 0)
    ? lastFreqHz
    : 0;
  const modeEl = document.getElementById("mode");
  const mode = modeEl ? (modeEl.value || "USB") : "USB";

  try {
    const resp = await fetch(`/channels/${encodeURIComponent(vchanRigId)}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ session_id: vchanSessionId, freq_hz: freqHz, mode }),
    });
    if (!resp.ok) {
      const msg = await resp.text().catch(() => String(resp.status));
      console.warn("vchan: allocate failed —", msg);
      return;
    }
    const ch = await resp.json();
    vchanActiveId = ch.id;
    // The SSE `channels` event will trigger vchanRender(); optimistically
    // mark active so the picker feels responsive even before the event arrives.
    vchanRender();
    vchanReconnectAudio();
  } catch (e) {
    console.error("vchan: allocate error", e);
  }
}

async function vchanDelete(channelId) {
  if (!vchanRigId) return;
  try {
    const resp = await fetch(
      `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}`,
      { method: "DELETE" }
    );
    if (!resp.ok) {
      console.warn("vchan: delete failed", resp.status);
    }
    // Channel list updates via SSE `channels` event.
  } catch (e) {
    console.error("vchan: delete error", e);
  }
}

// Lightweight auto-join for initial connect: registers the session on
// channel 0 without taking scheduler control or reconnecting audio
// (audio isn't started yet at this point).
async function vchanAutoJoinPrimary(channelId) {
  if (!vchanSessionId || !vchanRigId) return;
  try {
    const resp = await fetch(
      `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}/subscribe`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ session_id: vchanSessionId }),
      }
    );
    if (!resp.ok) {
      console.warn("vchan: auto-join primary failed", resp.status);
      return;
    }
    vchanActiveId = channelId;
    vchanRender();
  } catch (e) {
    console.error("vchan: auto-join error", e);
  }
}

async function vchanSubscribe(channelId) {
  if (!vchanSessionId || !vchanRigId) return;
  try {
    await vchanTakeSchedulerControl();
    const resp = await fetch(
      `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}/subscribe`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ session_id: vchanSessionId }),
      }
    );
    if (!resp.ok) {
      console.warn("vchan: subscribe failed", resp.status);
      return;
    }
    vchanActiveId = channelId;
    vchanRender();
    vchanSyncModeDisplay();
    vchanReconnectAudio();
  } catch (e) {
    console.error("vchan: subscribe error", e);
  }
}

// Reconnect the audio WebSocket to the appropriate endpoint:
// - virtual channel: /audio?channel_id=<uuid>
// - primary channel: /audio (no param)
// Always updates _audioChannelOverride so that starting audio later
// connects to the correct channel. Only reconnects if RX audio is active.
function vchanReconnectAudio() {
  // Always update the override so startRxAudio picks up the right URL,
  // even when audio isn't currently running.
  const ch = vchanIsOnVirtual() ? vchanActiveChannel() : null;
  if (typeof _audioChannelOverride !== "undefined") {
    _audioChannelOverride = ch ? ch.id : null;
  }
  if (typeof rxActive === "undefined" || !rxActive) return;
  if (typeof stopRxAudio === "function") stopRxAudio();
  // Delay so the server has time to set up the per-channel encoder.
  // The server-side audio_ws handler also polls for up to 2 s, so this
  // just needs to be long enough for the WS upgrade to reach the server.
  setTimeout(() => {
    if (typeof startRxAudio === "function") startRxAudio();
  }, 300);
}

// Called by app.js from applyCapabilities().
// Shows the channel picker only for SDR rigs.
function vchanApplyCapabilities(caps) {
  const picker = document.getElementById("vchan-picker");
  if (!picker) return;
  picker.style.display = (caps && caps.filter_controls) ? "" : "none";
  vchanRenderSchedulerRelease();
}

// ---------------------------------------------------------------------------
// Freq / mode interception + UI accent
// ---------------------------------------------------------------------------

// Returns true when the active channel is a non-primary (virtual) channel.
function vchanIsOnVirtual() {
  if (!vchanActiveId || vchanChannels.length === 0) return false;
  return vchanActiveId !== vchanChannels[0].id;
}

function vchanActiveChannel() {
  return vchanChannels.find(c => c.id === vchanActiveId) || null;
}

// Update the main freq input to show the virtual channel's frequency.
function vchanUpdateFreqDisplay() {
  const ch = vchanActiveChannel();
  if (!ch) return;
  const el = document.getElementById("freq");
  if (!el) return;
  if (typeof formatFreqForStep === "function" && typeof jogUnit !== "undefined") {
    el.value = formatFreqForStep(ch.freq_hz, jogUnit);
  } else {
    el.value = (ch.freq_hz / 1e6).toFixed(6).replace(/\.?0+$/, "");
  }
}

// Sync the mode picker to the active virtual channel's mode.
// Called whenever the active channel changes or the channel list is refreshed.
function vchanSyncModeDisplay() {
  const modeEl = document.getElementById("mode");
  if (!modeEl) return;
  if (vchanIsOnVirtual()) {
    const ch = vchanActiveChannel();
    if (ch && ch.mode) modeEl.value = ch.mode.toUpperCase();
  }
  // When on primary channel, app.js rig-state updates handle the picker.
  const modeUpper = (modeEl.value || "").toUpperCase();
  if (typeof lastModeName !== "undefined") {
    if (modeUpper === "WFM" && lastModeName !== "WFM") {
      if (typeof setJogDivisor === "function") setJogDivisor(10);
      if (typeof resetRdsDisplay === "function") resetRdsDisplay();
    } else if (modeUpper !== "WFM" && lastModeName === "WFM") {
      if (typeof resetRdsDisplay === "function") resetRdsDisplay();
    }
    lastModeName = modeUpper;
  }
  if (typeof updateWfmControls === "function") updateWfmControls();
  if (typeof updateSdrSquelchControlVisibility === "function") {
    updateSdrSquelchControlVisibility();
  }
  if (typeof refreshRdsUi === "function") {
    refreshRdsUi();
  } else if (typeof positionRdsPsOverlay === "function") {
    positionRdsPsOverlay();
  }
}

// Sync the BW input to the active virtual channel's bandwidth.
function vchanSyncBwDisplay() {
  if (!vchanIsOnVirtual()) return;
  const ch = vchanActiveChannel();
  if (!ch) return;
  const bwEl = document.getElementById("spectrum-bw-input");
  if (!bwEl) return;
  // bandwidth_hz == 0 means mode-default; derive it from the channel mode.
  let bwHz = ch.bandwidth_hz || 0;
  if (bwHz === 0 && typeof mwDefaultsForMode === "function") {
    bwHz = mwDefaultsForMode(ch.mode)[0] || 0;
  }
  if (bwHz > 0) {
    bwEl.value = (bwHz / 1000).toFixed(3).replace(/\.?0+$/, "");
    if (typeof currentBandwidthHz !== "undefined") {
      currentBandwidthHz = bwHz;
      window.currentBandwidthHz = bwHz;
    } else {
      window.currentBandwidthHz = bwHz;
    }
  }
}

// Add / remove the vchan accent class from the freq and BW inputs.
function vchanSyncAccentUI() {
  const onVirtual = vchanIsOnVirtual();
  const freqEl = document.getElementById("freq");
  const bwEl   = document.getElementById("spectrum-bw-input");
  if (freqEl) freqEl.classList.toggle("vchan-ch-active", onVirtual);
  if (bwEl)   bwEl.classList.toggle("vchan-ch-active", onVirtual);
  if (onVirtual) {
    vchanUpdateFreqDisplay();
    vchanSyncModeDisplay();
    vchanSyncBwDisplay();
  } else if (typeof _origRefreshFreqDisplay === "function") {
    _origRefreshFreqDisplay();
  }
  if (typeof updateDocumentTitle === "function" && typeof activeChannelRds === "function") {
    updateDocumentTitle(activeChannelRds());
  }
}

// Saved reference to the original refreshFreqDisplay from app.js.
let _origRefreshFreqDisplay = null;

function vchanSetChannelFreq(freqHz) {
  if (!vchanRigId || !vchanActiveId) return;
  // Validate against current SDR capture window.
  if (typeof lastSpectrumData !== "undefined" && lastSpectrumData &&
      lastSpectrumData.sample_rate > 0) {
    const halfSpan = Number(lastSpectrumData.sample_rate) / 2;
    const center   = Number(lastSpectrumData.center_hz);
    if (Math.abs(freqHz - center) > halfSpan) {
      if (typeof showHint === "function") {
        showHint(
          `Out of SDR bandwidth (center ${(center / 1e6).toFixed(3)} MHz ±${(halfSpan / 1e3).toFixed(0)} kHz)`,
          3000
        );
      }
      return;
    }
  }
  // Fire-and-forget: scheduler control + channel freq PUT run in background.
  vchanTakeSchedulerControl();
  fetch(
    `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/freq`,
    {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ freq_hz: Math.round(freqHz) }),
    }
  ).catch(e => console.error("vchan: set freq error", e));
}

async function vchanSetChannelBandwidth(bwHz) {
  if (!vchanRigId || !vchanActiveId) return;
  try {
    await vchanTakeSchedulerControl();
    const resp = await fetch(
      `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/bw`,
      {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ bandwidth_hz: Math.round(bwHz) }),
      }
    );
    if (!resp.ok) console.warn("vchan: set bw failed", resp.status);
  } catch (e) {
    console.error("vchan: set bw error", e);
  }
}

async function vchanSetChannelMode(mode) {
  if (!vchanRigId || !vchanActiveId) return;
  try {
    await vchanTakeSchedulerControl();
    const resp = await fetch(
      `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/mode`,
      {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ mode }),
      }
    );
    if (!resp.ok) console.warn("vchan: set mode failed", resp.status);
  } catch (e) {
    console.error("vchan: set mode error", e);
  }
}

// Called by app.js (applyModeFromPicker) and bookmarks.js (bmApply) before
// sending /set_mode to the server.  Returns true if the change was handled
// by the virtual channel (caller should skip the server request).
window.vchanInterceptMode = async function(mode) {
  if (!vchanIsOnVirtual()) return false;
  await vchanSetChannelMode(mode);
  return true;
};

// Called by app.js bandwidth setters before sending /set_bandwidth to the
// server.  Returns true if the change was handled by the virtual channel.
window.vchanInterceptBandwidth = async function(bwHz) {
  if (!vchanIsOnVirtual()) return false;
  await vchanSetChannelBandwidth(bwHz);
  return true;
};

// Wrap setRigFrequency (defined in app.js, loaded before this file) so that
// frequency changes are redirected to the active virtual channel instead of
// the server when on a non-primary channel.
(function() {
  const _orig = window.setRigFrequency;
  window.setRigFrequency = function(freqHz) {
    if (vchanIsOnVirtual()) {
      // Optimistic local update first, then fire-and-forget channel API.
      if (typeof applyLocalTunedFrequency === "function") {
        if (typeof _freqOptimisticSeq !== "undefined") {
          ++_freqOptimisticSeq;
          _freqOptimisticHz = Math.round(freqHz);
        }
        applyLocalTunedFrequency(Math.round(freqHz));
      }
      vchanSetChannelFreq(freqHz);
      return;
    }
    // Scheduler control is fire-and-forget — don't block the freq change.
    vchanTakeSchedulerControl();
    if (typeof _orig === "function") _orig(freqHz);
  };
})();

(function initSchedulerReleaseControl() {
  const btn = document.getElementById("scheduler-release-btn");
  if (btn) {
    btn.addEventListener("click", () => {
      vchanToggleSchedulerRelease();
    });
  }
  vchanStartSchedulerReleasePolling();
  vchanRenderSchedulerRelease();
})();

// Wrap refreshFreqDisplay so the main freq field stays in sync with the
// active virtual channel's frequency (SSE rig-state updates would otherwise
// constantly overwrite it with channel 0's freq).
(function() {
  _origRefreshFreqDisplay = window.refreshFreqDisplay;
  window.refreshFreqDisplay = function() {
    if (vchanIsOnVirtual()) {
      vchanUpdateFreqDisplay();
      return;
    }
    if (typeof _origRefreshFreqDisplay === "function") _origRefreshFreqDisplay();
  };
})();
