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

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

import type { PluginRuntimeWindow } from "./runtime-contract.js";

export {};

interface WsprMessage {
  message?: string | undefined;
  ts_ms?: number | undefined;
  _tsMs?: number | undefined;
  snr_db?: number | undefined;
  dt_s?: number | undefined;
  freq_hz?: number | undefined;
  receiver?: unknown;
  [key: string]: unknown;
}
interface WsprBridge {
  getDecodeHistoryRetentionMs?: () => number;
  trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
  ft8BaseHz?: number;
  getDecodeRigMeta?: () => unknown;
  mapAddLocator?: (raw: string, grids: string[], type: "wspr", station: string | null, message: WsprMessage) => void;
  navigateToMapLocator?: (grid: string, type: "wspr") => void;
  clearMapMarkersByType?: (type: "wspr") => void;
  takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<void>;
  trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
  [key: string]: unknown;
}
const wsprWindow = window as unknown as WsprBridge & PluginRuntimeWindow;

// --- WSPR Decoder Plugin (server-side decode) ---
const wsprStatus = document.getElementById("wspr-status");
const wsprPeriodEl = document.getElementById("wspr-period");
const wsprMessagesEl = document.getElementById("wspr-messages");
const wsprFilterInput = document.getElementById("wspr-filter") as HTMLInputElement | null;
const WSPR_PERIOD_SECONDS = 120;
let wsprFilterText = "";
let wsprMessageHistory: WsprMessage[] = [];

function finiteNumber(value: unknown): number | null {
  const number = typeof value === "number" ? value : Number(value);
  return Number.isFinite(number) ? number : null;
}

function currentWsprHistoryRetentionMs(): number {
  return typeof wsprWindow.getDecodeHistoryRetentionMs === "function"
    ? wsprWindow.getDecodeHistoryRetentionMs()
    : 24 * 60 * 60 * 1000;
}

function pruneWsprMessageHistory(): void {
  const cutoffMs = Date.now() - currentWsprHistoryRetentionMs();
  wsprMessageHistory = wsprMessageHistory.filter((msg) => Number(msg._tsMs ?? msg.ts_ms) >= cutoffMs);
}

function scheduleWsprHistoryRender(): void {
  if (typeof wsprWindow.trxScheduleUiFrameJob === "function") {
    wsprWindow.trxScheduleUiFrameJob("wspr-history", () => { renderWsprHistory(); });
    return;
  }
  renderWsprHistory();
}

function fmtWsprTime(tsMs: number | undefined): string {
  if (!tsMs) return "--:--:--";
  return new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}

function updateWsprPeriodTimer(): void {
  if (!wsprPeriodEl) return;
  const nowSec = Math.floor(Date.now() / 1000);
  const remaining = WSPR_PERIOD_SECONDS - (nowSec % WSPR_PERIOD_SECONDS);
  const mm = String(Math.floor(remaining / 60)).padStart(2, "0");
  const ss = String(remaining % 60).padStart(2, "0");
  wsprPeriodEl.textContent = `Next slot ${mm}:${ss}`;
}

updateWsprPeriodTimer();
setInterval(updateWsprPeriodTimer, 500);

function renderWsprRow(msg: WsprMessage): HTMLDivElement {
  const row = document.createElement("div");
  row.className = "ft8-row";
  row.dataset.decoder = "wspr";
  const snr = finiteNumber(msg.snr_db);
  const delta = finiteNumber(msg.dt_s);
  const baseHz = finiteNumber(wsprWindow.ft8BaseHz);
  const offsetHz = finiteNumber(msg.freq_hz);
  const rfHz = offsetHz !== null && baseHz !== null ? baseHz + offsetHz : null;
  const freq = rfHz?.toFixed(0) ?? "--";
  const message = msg.message ?? "";
  row.dataset.message = message.toUpperCase();
  row.innerHTML = `<span class="ft8-time">${fmtWsprTime(msg.ts_ms)}</span><span class="ft8-snr">${snr?.toFixed(1) ?? "--"}</span><span class="ft8-dt">${delta?.toFixed(2) ?? "--"}</span><span class="ft8-freq">${freq}</span><span class="ft8-msg">${renderWsprMessage(message)}</span>`;
  applyWsprFilterToRow(row);
  return row;
}

function renderWsprHistory(): void {
  pruneWsprMessageHistory();
  if (!wsprMessagesEl) return;
  const fragment = document.createDocumentFragment();
  for (let i = 0; i < wsprMessageHistory.length; i += 1) {
    const message = wsprMessageHistory[i];
    if (message) fragment.appendChild(renderWsprRow(message));
  }
  wsprMessagesEl.replaceChildren(fragment);
}

function addWsprMessage(msg: WsprMessage): void {
  msg._tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
  wsprMessageHistory.unshift(msg);
  pruneWsprMessageHistory();
  scheduleWsprHistoryRender();
}

function normalizeServerWsprMessage(msg: WsprMessage): { raw: string; grids: string[]; station: string | null; rfHz: number | null; history: WsprMessage } {
  const raw = msg.message ?? "";
  const grids = extractAllGrids(raw);
  const station = extractLikelyCallsign(raw);
  const baseHz = finiteNumber(wsprWindow.ft8BaseHz);
  const offsetHz = finiteNumber(msg.freq_hz);
  const rfHz = offsetHz !== null && baseHz !== null ? baseHz + offsetHz : offsetHz;
  return {
    raw,
    grids,
    station,
    rfHz,
    history: {
      receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
      ts_ms: msg.ts_ms,
      snr_db: msg.snr_db,
      dt_s: msg.dt_s,
      freq_hz: msg.freq_hz,
      message: raw,
    },
  };
}

function onServerWsprBatch(messages: WsprMessage[]): void {
  if (!Array.isArray(messages) || messages.length === 0) return;
  if (wsprStatus) wsprStatus.textContent = "Receiving";
  const normalized: WsprMessage[] = [];
  for (const msg of messages) {
    const next = normalizeServerWsprMessage(msg);
    if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
      wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
        ...msg,
        ...(next.rfHz === null ? {} : { freq_hz: next.rfHz }),
      });
    }
    next.history._tsMs = Number.isFinite(next.history.ts_ms) ? Number(next.history.ts_ms) : Date.now();
    normalized.push(next.history);
  }
  normalized.reverse();
  wsprMessageHistory = normalized.concat(wsprMessageHistory);
  pruneWsprMessageHistory();
  scheduleWsprHistoryRender();
}

function pruneWsprHistoryView(): void {
  pruneWsprMessageHistory();
  renderWsprHistory();
}

function escapeWsprHtml(input: string): string {
  return input
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll("\"", "&quot;");
}

function renderWsprMessage(message: string): string {
  let out = "";
  let i = 0;
  while (i < message.length) {
    const ch = message[i];
    if (isAlphaNum(ch)) {
      let j = i + 1;
      while (j < message.length && isAlphaNum(message[j])) j++;
      const token = message.slice(i, j);
      const grid = token.toUpperCase();
      if (isMaidenheadGridToken(grid)) {
        out += `<span class="ft8-locator" data-locator-grid="${grid}" role="button" tabindex="0" aria-label="Show locator ${grid} on map">${grid}</span>`;
      } else {
        out += escapeWsprHtml(token);
      }
      i = j;
    } else {
      out += escapeWsprHtml(ch ?? "");
      i += 1;
    }
  }
  return out;
}

function extractAllGrids(message: string): string[] {
  const out: string[] = [];
  const seen = new Set<string>();
  const parts = message.toUpperCase().split(/[^A-Z0-9]+/);
  for (const token of parts) {
    if (!token) continue;
    if (isMaidenheadGridToken(token) && !seen.has(token)) {
      seen.add(token);
      out.push(token);
    }
  }
  return out;
}

function extractLikelyCallsign(message: string): string | null {
  const parts = message.toUpperCase().split(/[^A-Z0-9/]+/);
  for (const token of parts) {
    if (!token) continue;
    if (token.length < 3 || token.length > 12) continue;
    if (token === "CQ" || token === "DE" || token === "QRZ" || token === "DX") continue;
    if (isMaidenheadGridToken(token)) continue;
    if (/^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token)) return token;
  }
  return null;
}

function isFtxFarewellToken(token: string): boolean {
  const normalized = token.trim().toUpperCase();
  return normalized === "RR73" || normalized === "73" || normalized === "RR";
}

function isMaidenheadGridToken(token: string): boolean {
  const normalized = token.trim().toUpperCase();
  return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && !isFtxFarewellToken(normalized);
}

function isAlphaNum(ch: string | undefined): boolean {
  return ch !== undefined && /[A-Za-z0-9]/.test(ch);
}

function activateWsprHistoryLocator(target: EventTarget | null): boolean {
  if (!(target instanceof Element)) return false;
  const locatorEl = target.closest<HTMLElement>(".ft8-locator[data-locator-grid]");
  if (!locatorEl) return false;
  const grid = (locatorEl.dataset.locatorGrid || "").toUpperCase();
  if (!grid) return false;
  if (typeof wsprWindow.navigateToMapLocator === "function") {
    wsprWindow.navigateToMapLocator(grid, "wspr");
  }
  return true;
}

function applyWsprFilterToRow(row: HTMLElement): void {
  if (!wsprFilterText) {
    row.style.display = "";
    return;
  }
  const message = row.dataset.message || "";
  row.style.display = message.includes(wsprFilterText) ? "" : "none";
}

function resetWsprHistoryView(): void {
  if (wsprMessagesEl) wsprMessagesEl.innerHTML = "";
  wsprMessageHistory = [];
  renderWsprHistory();
  if (wsprWindow.clearMapMarkersByType) wsprWindow.clearMapMarkersByType("wspr");
}

if (wsprFilterInput) {
  wsprFilterInput.addEventListener("input", () => {
    wsprFilterText = wsprFilterInput.value.trim().toUpperCase();
    renderWsprHistory();
  });
}

if (wsprMessagesEl) {
  wsprMessagesEl.addEventListener("click", (event) => {
    if (!activateWsprHistoryLocator(event.target)) return;
    event.preventDefault();
    event.stopPropagation();
  });
  wsprMessagesEl.addEventListener("keydown", (event) => {
    if (event.key !== "Enter" && event.key !== " ") return;
    if (!activateWsprHistoryLocator(event.target)) return;
    event.preventDefault();
    event.stopPropagation();
  });
}

const wsprDecodeToggleBtn = document.getElementById("wspr-decode-toggle-btn");
wsprDecodeToggleBtn?.addEventListener("click", () => {
  void (async () => {
    try {
      await wsprWindow.takeSchedulerControlForDecoderDisable?.(wsprDecodeToggleBtn);
      await hostCore.postPath("/toggle_wspr_decode");
    } catch (error: unknown) {
      console.error("WSPR toggle failed", error);
    }
  })();
});

document.getElementById("settings-clear-wspr-history")?.addEventListener("click", () => {
  void (async () => {
    if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
    try {
      await hostCore.postPath("/clear_wspr_decode");
      resetWsprHistoryView();
    } catch (error: unknown) {
      console.error("WSPR history clear failed", error);
    }
  })();
});

function onServerWspr(msg: WsprMessage): void {
  if (wsprStatus) wsprStatus.textContent = "Receiving";
  const next = normalizeServerWsprMessage(msg);
  if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
    wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
      ...msg,
      ...(next.rfHz === null ? {} : { freq_hz: next.rfHz }),
    });
  }
  addWsprMessage(next.history);
}

wsprWindow.trxPluginRuntime.registerDecoder({
  id: "wspr",
  onMessage: onServerWspr,
  onBatch: onServerWsprBatch,
  restore: onServerWsprBatch,
  prune: pruneWsprHistoryView,
  reset: resetWsprHistoryView,
});
