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

import { hostState } from "./host.js";

export {};

interface DecoderDescriptor {
  id: string;
  background_decode?: boolean;
  activation?: string;
  active_modes: string[];
}
interface Bookmark {
  id: string;
  name: string;
  freq_hz: number;
  mode: string;
  decoders?: string[];
}
interface BackgroundDecodeConfig {
  remote: string | null;
  enabled: boolean;
  bookmark_ids: string[];
}
interface BackgroundStatusEntry {
  bookmark_name?: string;
  bookmark_id?: string;
  freq_hz?: number;
  mode?: string;
  decoder_kinds?: string[];
  state?: string;
}
interface BackgroundDecodeStatus {
  entries?: BackgroundStatusEntry[];
  active_rig?: boolean;
  center_hz?: number;
  sample_rate?: number;
}
interface BackgroundBridge {
  decoderRegistry?: DecoderDescriptor[];
  trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
  trx?: { modules?: { backgroundDecode?: BackgroundDecodeService } };
}
interface BackgroundDecodeService {
  initialize(rigId: string | null, role: string | null): void;
  wireEvents(): void;
  setRig(rigId: string | null): void;
}
interface WiredElement extends HTMLElement { _wired?: boolean }
const bgdWindow = window as unknown as BackgroundBridge;

(function () {
  "use strict";

  function bgdSupportedIds(): string[] {
    return (bgdWindow.decoderRegistry || [])
      .filter(function (d) { return d.background_decode; })
      .map(function (d) { return d.id; });
  }

  let backgroundDecodeRole: string | null = null;
  let currentRigId: string | null = null;
  let currentConfig: BackgroundDecodeConfig | null = null;
  let bookmarkList: Bookmark[] = [];
  let statusInterval: ReturnType<typeof setInterval> | null = null;
  let bgdDirty = false;

  function initBackgroundDecode(rigId: string | null, role: string | null): void {
    backgroundDecodeRole = role;
    currentRigId = rigId || null;
    if (currentRigId) loadBackgroundDecode();
    startStatusPolling();
  }

  function setBackgroundDecodeRig(rigId: string | null): void {
    const nextRigId = rigId || null;
    if (nextRigId === currentRigId) return;
    currentRigId = nextRigId;
    if (!currentRigId) return;
    loadBackgroundDecode();
  }

  function apiGetConfig(rigId: string): Promise<BackgroundDecodeConfig> {
    return fetch("/background-decode/" + encodeURIComponent(rigId)).then(function (r) {
      if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
      return r.json() as Promise<BackgroundDecodeConfig>;
    });
  }

  function apiPutConfig(rigId: string, config: BackgroundDecodeConfig): Promise<BackgroundDecodeConfig> {
    return fetch("/background-decode/" + encodeURIComponent(rigId), {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(config),
    }).then(function (r) {
      if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
      return r.json() as Promise<BackgroundDecodeConfig>;
    });
  }

  function apiResetConfig(rigId: string): Promise<BackgroundDecodeConfig> {
    return fetch("/background-decode/" + encodeURIComponent(rigId), {
      method: "DELETE",
    }).then(function (r) {
      if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
      return r.json() as Promise<BackgroundDecodeConfig>;
    });
  }

  function apiGetStatus(rigId: string): Promise<BackgroundDecodeStatus> {
    return fetch("/background-decode/" + encodeURIComponent(rigId) + "/status").then(function (r) {
      if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
      return r.json() as Promise<BackgroundDecodeStatus>;
    });
  }

  function apiGetBookmarks(): Promise<Bookmark[]> {
    return fetch("/bookmarks").then(function (r) {
      if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
      return r.json() as Promise<Bookmark[]>;
    });
  }

  function loadBackgroundDecode() {
    const rigId = currentRigId;
    if (!rigId) return;
    Promise.all([apiGetConfig(rigId), apiGetBookmarks()])
      .then(function ([config, bookmarks]) {
        currentConfig = config;
        bookmarkList = Array.isArray(bookmarks) ? bookmarks : [];
        renderBackgroundDecode();
        clearBgdDirty();
        pollBackgroundDecodeStatus();
      })
      .catch(function (err: unknown) {
        console.error("background decode load failed", err);
      });
  }

  function supportedBookmarks(): Bookmark[] {
    return bookmarkList.filter(function (bookmark) {
      return bookmarkDecoderKinds(bookmark).length > 0;
    });
  }

  function bookmarkDecoderKinds(bookmark: Bookmark): string[] {
    const ids = bgdSupportedIds();
    const decoders = bookmark.decoders ?? [];
    const explicit = decoders
      .map(function (item) { return item.trim().toLowerCase(); })
      .filter(function (item, index, arr) {
        return ids.indexOf(item) >= 0 && arr.indexOf(item) === index;
      });
    if (explicit.length > 0) return explicit;
    // Fall back: infer from mode via mode-bound entries in the registry.
    const mode = bookmark.mode.trim().toUpperCase();
    return (bgdWindow.decoderRegistry || [])
      .filter(function (d) {
        return d.activation === "mode_bound" && d.background_decode
          && d.active_modes.indexOf(mode) >= 0;
      })
      .map(function (d) { return d.id; });
  }

  function renderBackgroundDecode() {
    if (!currentConfig) {
      currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
    }
    setCheckbox("background-decode-enabled", currentConfig.enabled);
    renderBookmarkChecklist();

    const isControl = backgroundDecodeRole === "control" || hostState.authEnabled === false;
    const panel = document.getElementById("background-decode-panel");
    if (panel) {
        panel.querySelectorAll<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>("input, select, button.sch-write").forEach(function (el) {
        el.disabled = !isControl;
      });
    }
    const saveBtn = document.getElementById("background-decode-save-btn");
    const resetBtn = document.getElementById("background-decode-reset-btn");
    if (saveBtn) saveBtn.style.display = isControl ? "" : "none";
    if (resetBtn) resetBtn.style.display = isControl ? "" : "none";
  }

  function renderBookmarkChecklist(filterText = ""): void {
    const container = document.getElementById("bgd-bookmark-checklist");
    if (!container) return;
    container.innerHTML = "";

    const selectedIds = new Set(
      currentConfig && Array.isArray(currentConfig.bookmark_ids) ? currentConfig.bookmark_ids : []
    );
    const all = supportedBookmarks();
    const filter = (filterText || "").trim().toLowerCase();

    const filtered = filter
      ? all.filter(function (bm) {
          const text = (bm.name + " " + formatFreq(bm.freq_hz) + " " + bm.mode).toLowerCase();
          return text.indexOf(filter) >= 0;
        })
      : all;

    if (filtered.length === 0) {
      container.innerHTML = '<div class="bgd-checklist-empty">' +
        (all.length === 0 ? "No supported bookmarks available." : "No bookmarks match filter.") +
        '</div>';
      return;
    }

    filtered.forEach(function (bookmark) {
      const row = document.createElement("label");
      row.className = "bgd-checklist-row";
      const decoders = bookmarkDecoderKinds(bookmark);
      const checked = selectedIds.has(bookmark.id) ? " checked" : "";
      row.innerHTML =
        '<input type="checkbox"' + checked + ' data-bm-id="' + escHtml(bookmark.id) + '" />' +
        '<span class="bgd-checklist-name">' + escHtml(bookmark.name) + '</span>' +
        '<span class="bgd-checklist-meta">' + escHtml(formatFreq(bookmark.freq_hz) + " " + bookmark.mode + " · " + decoders.join("/").toUpperCase()) + '</span>';
      row.querySelector<HTMLInputElement>("input")?.addEventListener("change", function (e) {
        onChecklistToggle(bookmark.id, (e.currentTarget as HTMLInputElement).checked);
      });
      container.appendChild(row);
    });
  }

  function onChecklistToggle(bookmarkId: string, checked: boolean): void {
    if (!currentConfig) {
      currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
    }
    if (!Array.isArray(currentConfig.bookmark_ids)) currentConfig.bookmark_ids = [];
    if (checked && !currentConfig.bookmark_ids.includes(bookmarkId)) {
      currentConfig.bookmark_ids.push(bookmarkId);
    } else if (!checked) {
      currentConfig.bookmark_ids = currentConfig.bookmark_ids.filter(function (id) { return id !== bookmarkId; });
    }
    markBgdDirty();
  }

  function saveBackgroundDecode() {
    const rigId = currentRigId;
    if (!rigId) return;
    const payload: BackgroundDecodeConfig = {
      remote: rigId,
      enabled: (document.getElementById("background-decode-enabled") as HTMLInputElement | null)?.checked ?? false,
      bookmark_ids: currentConfig?.bookmark_ids.slice() ?? [],
    };
    const btn = document.getElementById("background-decode-save-btn") as HTMLButtonElement | null;
    if (btn) btn.disabled = true;
    apiPutConfig(rigId, payload)
      .then(function (saved) {
        currentConfig = saved;
        renderBackgroundDecode();
        clearBgdDirty();
        pollBackgroundDecodeStatus();
        showToast("Background decode saved.", false);
      })
      .catch(function (err: unknown) {
        showToast(`Save failed: ${errorMessage(err)}`, true);
      })
      .finally(function () {
        if (btn) btn.disabled = false;
      });
  }

  async function resetBackgroundDecode() {
    const rigId = currentRigId;
    if (!rigId) return;
    if (!await bgdWindow.trxUi.confirm({ title: "Reset background decoding?", message: "The current background decode configuration will be permanently reset.", confirmLabel: "Reset" })) return;
    apiResetConfig(rigId)
      .then(function (saved) {
        currentConfig = saved;
        renderBackgroundDecode();
        clearBgdDirty();
        pollBackgroundDecodeStatus();
        showToast("Background decode reset.", false);
      })
      .catch(function (err: unknown) {
        showToast(`Reset failed: ${errorMessage(err)}`, true);
      });
  }

  function startStatusPolling() {
    if (statusInterval) clearInterval(statusInterval);
    statusInterval = setInterval(pollBackgroundDecodeStatus, 15000);
  }

  function pollBackgroundDecodeStatus() {
    const rigId = currentRigId;
    if (!rigId) return;
    apiGetStatus(rigId)
      .then(renderStatus)
      .catch(function () {});
  }

  function renderStatus(status: BackgroundDecodeStatus): void {
    const card = document.getElementById("background-decode-status-card");
    if (!card) return;
    const entries = status.entries ?? [];
    if (!entries.length) {
      card.textContent = "No background decode bookmarks configured.";
      return;
    }
    const summary = [];
    if (status.active_rig) {
      if (typeof status.center_hz === "number" && Number.isFinite(status.center_hz)) summary.push("Center " + formatFreq(status.center_hz));
      if (typeof status.sample_rate === "number" && Number.isFinite(status.sample_rate) && status.sample_rate > 0) summary.push("Span ±" + formatFreq(status.sample_rate / 2));
    } else {
      summary.push("This rig is not currently selected for audio.");
    }
    let html = summary.length ? '<div style="margin-bottom:0.8rem;color:var(--text-muted);">' + escHtml(summary.join(" · ")) + "</div>" : "";
    html += '<div class="bgd-status-list">';
    entries.forEach(function (entry) {
      const name = entry.bookmark_name || entry.bookmark_id || "Unknown bookmark";
      const parts = [];
      if (typeof entry.freq_hz === "number" && Number.isFinite(entry.freq_hz)) parts.push(formatFreq(entry.freq_hz));
      if (entry.mode) parts.push(entry.mode);
      if (Array.isArray(entry.decoder_kinds) && entry.decoder_kinds.length) {
        parts.push(entry.decoder_kinds.join("/").toUpperCase());
      }
      html +=
        '<div class="bgd-status-row">' +
          '<div>' +
            '<div class="bgd-status-name">' + escHtml(name) + '</div>' +
            '<div class="bgd-status-meta">' + escHtml(parts.join(" · ")) + '</div>' +
          '</div>' +
          '<div class="bgd-status-state" data-state="' + escHtml(entry.state || "inactive") + '">' +
            '<svg class="bgd-state-dot" viewBox="0 0 8 8"><circle cx="4" cy="4" r="3.5"/></svg>' +
            escHtml(prettyState(entry.state)) + '</div>' +
        '</div>';
    });
    html += "</div>";
    card.innerHTML = html;
  }

  function prettyState(state: string | undefined): string {
    switch (state) {
      case "active": return "\u2713 Active";
      case "out_of_span": return "\u25B3 Out of span";
      case "waiting_for_spectrum": return "\u25B3 Waiting";
      case "waiting_for_user": return "\u25B3 No user";
      case "missing_bookmark": return "\u2717 Missing";
      case "no_supported_decoders": return "\u2717 Unsupported";
      case "disabled": return "\u25B3 Disabled";
      case "handled_by_scheduler": return "\u25B3 Scheduler";
      case "scheduler_has_control": return "\u25B3 Scheduler";
      case "handled_by_virtual_channel": return "\u25B3 VChan";
      default: return "\u25B3 Inactive";
    }
  }

  function setCheckbox(id: string, value: boolean): void {
    const el = document.getElementById(id) as HTMLInputElement | null;
    if (el) el.checked = value;
  }

  function formatFreq(hz: number): string {
    if (!Number.isFinite(hz) || hz <= 0) return "--";
    if (hz >= 1e6) return (hz / 1e6).toFixed(3).replace(/\.?0+$/, "") + " MHz";
    if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
    return `${String(hz)} Hz`;
  }

  function escHtml(value: unknown): string {
    const text = typeof value === "string" || typeof value === "number" || typeof value === "boolean"
      ? String(value)
      : "";
    return text
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  function errorMessage(error: unknown): string {
    return error instanceof Error ? error.message : String(error);
  }

  function markBgdDirty() {
    if (bgdDirty) return;
    bgdDirty = true;
    const btn = document.getElementById("background-decode-save-btn");
    if (btn) btn.classList.add("sch-dirty");
  }

  function clearBgdDirty() {
    bgdDirty = false;
    const btn = document.getElementById("background-decode-save-btn");
    if (btn) btn.classList.remove("sch-dirty");
  }

  function showToast(msg: string, isError: boolean): void {
    const el = document.getElementById("background-decode-toast");
    if (!el) return;
    el.textContent = msg;
    el.style.background = isError ? "var(--color-error, #c00)" : "var(--accent-green)";
    el.style.display = "block";
    setTimeout(function () {
      el.style.display = "none";
    }, 3000);
  }

  function selectAllBookmarks() {
    if (!currentConfig) {
      currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
    }
    const ids = supportedBookmarks().map(function (bm) { return bm.id; });
    currentConfig.bookmark_ids = ids;
    renderBookmarkChecklist((document.getElementById("bgd-bookmark-filter") as HTMLInputElement | null)?.value);
    markBgdDirty();
  }

  function deselectAllBookmarks() {
    if (!currentConfig) {
      currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
    }
    currentConfig.bookmark_ids = [];
    renderBookmarkChecklist((document.getElementById("bgd-bookmark-filter") as HTMLInputElement | null)?.value);
    markBgdDirty();
  }

  function wireBackgroundDecodeEvents() {
    const filterInput = document.getElementById("bgd-bookmark-filter") as (HTMLInputElement & WiredElement) | null;
    if (filterInput && !filterInput._wired) {
      filterInput._wired = true;
      filterInput.addEventListener("input", function () {
        renderBookmarkChecklist(filterInput.value);
      });
    }

    const enabledCb = document.getElementById("background-decode-enabled") as (HTMLInputElement & WiredElement) | null;
    if (enabledCb && !enabledCb._wired) {
      enabledCb._wired = true;
      enabledCb.addEventListener("change", function () { markBgdDirty(); });
    }

    const selectAllBtn = document.getElementById("bgd-select-all-btn") as WiredElement | null;
    if (selectAllBtn && !selectAllBtn._wired) {
      selectAllBtn._wired = true;
      selectAllBtn.addEventListener("click", selectAllBookmarks);
    }

    const deselectAllBtn = document.getElementById("bgd-deselect-all-btn") as WiredElement | null;
    if (deselectAllBtn && !deselectAllBtn._wired) {
      deselectAllBtn._wired = true;
      deselectAllBtn.addEventListener("click", deselectAllBookmarks);
    }

    const saveBtn = document.getElementById("background-decode-save-btn") as WiredElement | null;
    if (saveBtn && !saveBtn._wired) {
      saveBtn._wired = true;
      saveBtn.addEventListener("click", saveBackgroundDecode);
    }

    const resetBtn = document.getElementById("background-decode-reset-btn") as WiredElement | null;
    if (resetBtn && !resetBtn._wired) {
      resetBtn._wired = true;
      resetBtn.addEventListener("click", () => { void resetBackgroundDecode(); });
    }
  }

  bgdWindow.trx ??= {};
  bgdWindow.trx.modules ??= {};
  bgdWindow.trx.modules.backgroundDecode = {
    initialize: initBackgroundDecode,
    wireEvents: wireBackgroundDecodeEvents,
    setRig: setBackgroundDecodeRig,
  };
})();
