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

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

import {
  aprsAgeText,
  aprsCategoryLabel,
  aprsHexBytes,
  aprsPacketCategory,
  collapseAprsDuplicates,
  normalizeAprsPacket,
  renderAprsInfo,
  renderLocalAprsSymbol,
  type AprsPacket,
  type AprsTypeFilter,
} from "./aprs-shared";
import type { PluginRuntimeWindow } from "./runtime-contract";
interface AprsBridge {
  getDecodeHistoryRetentionMs?: () => number;
  trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
  navigateToAprsMap?: (lat: number, lon: number) => void;
  clearMapMarkersByType?: (type: string) => void;
  aprsMapAddStation?: (call: string, lat: number, lon: number, info: string, symbolTable: string | null | undefined, symbolCode: string | null | undefined, packet: AprsPacket) => void;
  getDecodeRigMeta?: () => unknown;
  trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
  updateAprsBar?: () => void;
  clearAprsBar?: () => void;
  closeAprsBar?: () => void;
}
const aprsWindow = window as unknown as AprsBridge;
const escapeAprsHtml = (input: string): string => hostCore.escapeMapHtml(input);
const showAprsHint = (message: string, durationMs: number): void => { hostCore.showHint(message, durationMs); };

// --- APRS Decoder Plugin (server-side decode) ---
const aprsStatus = document.getElementById("aprs-status");
const aprsPacketsEl = document.getElementById("aprs-packets");
const aprsFilterInput = document.getElementById("aprs-filter") as HTMLInputElement | null;
const aprsBarOverlay = document.getElementById("aprs-bar-overlay");
const aprsOnlyPosBtn = document.getElementById("aprs-only-pos-btn");
const aprsHideCrcBtn = document.getElementById("aprs-hide-crc-btn");
const aprsCollapseDupBtn = document.getElementById("aprs-collapse-dup-btn");
const aprsTotalCountEl = document.getElementById("aprs-total-count");
const aprsVisibleCountEl = document.getElementById("aprs-visible-count");
const aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
const APRS_BAR_WINDOW_MS = 15 * 60 * 1000;
let aprsFilterText = "";
let aprsPacketHistory: AprsPacket[] = [];
let aprsBarDismissedAtMs = 0;
let aprsOnlyPos = false;
let aprsHideCrc = false;
let aprsCollapseDup = false;
let aprsTypeFilter: AprsTypeFilter = "all";

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

function pruneAprsPacketHistory() {
  const cutoffMs = Date.now() - currentAprsHistoryRetentionMs();
  aprsPacketHistory = aprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs);
}

function scheduleAprsUi(key: string, job: () => void): void {
  if (typeof aprsWindow.trxScheduleUiFrameJob === "function") {
    aprsWindow.trxScheduleUiFrameJob(key, job);
    return;
  }
  job();
}

function scheduleAprsHistoryRender() {
  scheduleAprsUi("aprs-history", () => { renderAprsHistory(); });
}

function scheduleAprsBarUpdate() {
  scheduleAprsUi("aprs-bar", () => { updateAprsBar(); });
}

function aprsDistanceText(pkt: AprsPacket): string {
  if (hostState.serverLat == null || hostState.serverLon == null || pkt.lat == null || pkt.lon == null) return "";
  const distKm = hostCore.haversineKm(hostState.serverLat, hostState.serverLon, pkt.lat, pkt.lon);
  if (!Number.isFinite(distKm)) return "";
  if (distKm < 1) return `${Math.round(distKm * 1000)} m from TRX`;
  return `${distKm.toFixed(1)} km from TRX`;
}

function aprsFilterMatch(pkt: AprsPacket): boolean {
  if (aprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
  if (aprsHideCrc && !pkt.crcOk) return false;
  if (aprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== aprsTypeFilter) return false;
  if (!aprsFilterText) return true;
  const haystack = [
    pkt.srcCall,
    pkt.destCall,
    pkt.path,
    pkt.info,
    pkt.type,
    pkt.lat != null ? pkt.lat.toFixed(4) : "",
    pkt.lon != null ? pkt.lon.toFixed(4) : "",
    aprsPacketCategory(pkt),
  ]
    .filter(Boolean)
    .join(" ")
    .toUpperCase();
  return haystack.includes(aprsFilterText);
}

function aprsVisiblePackets(): AprsPacket[] {
  const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
  return packets.filter(aprsFilterMatch);
}

function updateAprsSummary() {
  const visible = aprsVisiblePackets();
  if (aprsTotalCountEl) {
    aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
  }
  if (aprsVisibleCountEl) {
    aprsVisibleCountEl.textContent = `${visible.length} shown`;
  }
  if (aprsLatestSeenEl) {
    const latest = aprsPacketHistory[0];
    if (!latest) {
      aprsLatestSeenEl.textContent = "No packets yet";
    } else {
      aprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
    }
  }
}

function updateAprsChipState() {
  document.querySelectorAll("[id^='aprs-type-']").forEach((btn) => {
    btn.classList.toggle("active", btn.id === `aprs-type-${aprsTypeFilter}`);
  });
  aprsOnlyPosBtn?.classList.toggle("active", aprsOnlyPos);
  aprsHideCrcBtn?.classList.toggle("active", aprsHideCrc);
  aprsCollapseDupBtn?.classList.toggle("active", aprsCollapseDup);
}

function renderAprsRow(pkt: AprsPacket, isFresh: boolean): HTMLElement {
  const row = document.createElement("div");
  row.className = "aprs-packet";
  if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
  if (isFresh) row.classList.add("aprs-packet-new");

  const ts = pkt._ts || new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
  const age = aprsAgeText(pkt._tsMs);
  const category = aprsPacketCategory(pkt);
  const categoryLabel = aprsCategoryLabel(category);
  const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`;
  const pathBadge = pkt.path ? `<span class="aprs-badge">${escapeAprsHtml(pkt.path)}</span>` : "";
  const crcBadge = pkt.crcOk ? "" : '<span class="aprs-badge aprs-badge-crc">CRC Fail</span>';
  const symbolHtml = renderLocalAprsSymbol(pkt, escapeAprsHtml);
  const posLink = pkt.lat != null && pkt.lon != null
    ? `<a class="aprs-pos" href="javascript:void(0)" data-aprs-map="${pkt.lat},${pkt.lon}">${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}</a>`
    : "";
  const distance = aprsDistanceText(pkt);
  const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;

  row.innerHTML =
    `<div class="aprs-row-head">` +
      `<span class="aprs-time">${ts}</span>` +
      symbolHtml +
      `<span class="aprs-call">${escapeAprsHtml(pkt.srcCall ?? "")}</span>` +
      `<span>&gt;${escapeAprsHtml(pkt.destCall || "")}</span>` +
      `<span class="${categoryClass}">${escapeAprsHtml(categoryLabel)}</span>` +
      pathBadge +
      crcBadge +
    `</div>` +
    `<div class="aprs-row-meta">` +
      `<span class="aprs-meta-text">${escapeAprsHtml(age)}</span>` +
      (distance ? `<span class="aprs-meta-text">${escapeAprsHtml(distance)}</span>` : "") +
      `<span class="aprs-meta-text">${escapeAprsHtml(pkt.type || "--")}</span>` +
    `</div>` +
    `<div class="aprs-row-detail">` +
      `<span title="${escapeAprsHtml(pkt.type || "")}">${renderAprsInfo(pkt)}</span>` +
      (posLink ? `<span>${posLink}</span>` : "") +
    `</div>` +
    `<div class="aprs-row-actions">` +
      (pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-map="${pkt.lat},${pkt.lon}">Map</button>` : "") +
      (pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-copy="${pkt.lat},${pkt.lon}">Copy Coords</button>` : "") +
      `<a class="aprs-inline-btn" href="${qrzHref}" target="_blank" rel="noopener">QRZ</a>` +
    `</div>` +
    `<details class="aprs-details">` +
      `<summary>Details</summary>` +
      `<div class="aprs-details-grid">` +
        `<span class="aprs-detail-label">Source</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.srcCall || "--")}</span>` +
        `<span class="aprs-detail-label">Destination</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.destCall || "--")}</span>` +
        `<span class="aprs-detail-label">Type</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.type || "--")}</span>` +
        `<span class="aprs-detail-label">Path</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.path || "--")}</span>` +
        `<span class="aprs-detail-label">Age</span><span class="aprs-detail-value">${escapeAprsHtml(age)}</span>` +
        `<span class="aprs-detail-label">CRC</span><span class="aprs-detail-value">${pkt.crcOk ? "OK" : "Failed"}</span>` +
        `<span class="aprs-detail-label">Position</span><span class="aprs-detail-value">${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}</span>` +
        `<span class="aprs-detail-label">Info</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.info || "--")}</span>` +
        `<span class="aprs-detail-label">Info Bytes</span><span class="aprs-detail-value">${escapeAprsHtml(aprsHexBytes(pkt.info_bytes))}</span>` +
      `</div>` +
    `</details>`;

  row.querySelectorAll<HTMLElement>("[data-aprs-map]").forEach((el) => {
    el.addEventListener("click", (evt) => {
      evt.preventDefault();
      const raw = el.dataset.aprsMap ?? "";
      const [lat, lon] = raw.split(",").map(Number);
      if (aprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) {
        aprsWindow.navigateToAprsMap(lat, lon);
      }
    });
  });

  const copyBtn = row.querySelector<HTMLElement>("[data-aprs-copy]");
  if (copyBtn) {
    copyBtn.addEventListener("click", () => { void (async () => {
      const raw = copyBtn.dataset.aprsCopy ?? "";
      try {
        const clipboard = Reflect.get(navigator, "clipboard") as Clipboard | undefined;
        if (clipboard) {
          await clipboard.writeText(raw);
          showAprsHint("Coordinates copied", 1200);
        }
      } catch {
        showAprsHint("Copy failed", 1500);
      }
    })(); });
  }

  return row;
}

function renderAprsHistory() {
  pruneAprsPacketHistory();
  if (!aprsPacketsEl) {
    updateAprsSummary();
    updateAprsChipState();
    return;
  }
  const visible = aprsVisiblePackets();
  const fragment = document.createDocumentFragment();
  for (const [index, packet] of visible.entries()) {
    fragment.appendChild(renderAprsRow(packet, index === 0));
  }
  aprsPacketsEl.replaceChildren(fragment);
  updateAprsSummary();
  updateAprsChipState();
}

function updateAprsBar() {
  if (!aprsBarOverlay) return;
  const isPkt = ((document.getElementById("mode") as HTMLSelectElement | null)?.value || "").toUpperCase() === "PKT";
  const cutoffMs = Date.now() - APRS_BAR_WINDOW_MS;
  const okFrames = aprsPacketHistory.filter((p) => p.crcOk && (p._tsMs ?? 0) >= cutoffMs);
  const frames = collapseAprsDuplicates(okFrames).slice(0, 8);
  const newestTsMs = frames.reduce((latest, pkt) => Math.max(latest, Number(pkt._tsMs) || 0), 0);
  if (!isPkt || frames.length === 0 || newestTsMs <= aprsBarDismissedAtMs) {
    aprsBarOverlay.style.display = "none";
    aprsBarOverlay.innerHTML = "";
    return;
  }
  let html = '<div class="aprs-bar-header"><span class="aprs-bar-title"><span class="aprs-bar-title-word">APRS</span><span class="aprs-bar-title-word">Live</span></span><span class="aprs-bar-actions"><span class="aprs-bar-window">Last 15 minutes</span><span class="aprs-bar-clear-wrap"><span class="aprs-bar-clear" role="button" tabindex="0" onclick="window.clearAprsBar()" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();window.clearAprsBar();}" aria-label="Clear APRS overlay">Clear</span></span><button class="aprs-bar-close" type="button" onclick="window.closeAprsBar()" aria-label="Close APRS overlay">&times;</button></span></div>';
  for (const pkt of frames) {
    const ts = pkt._ts ? `<span class="aprs-bar-time">${pkt._ts}</span>` : "";
    const call = `<span class="aprs-bar-call">${escapeAprsHtml(pkt.srcCall ?? "")}</span>`;
    const dest = escapeAprsHtml(pkt.destCall || "");
    const info = escapeAprsHtml(pkt.info || "");
    const pin = pkt.lat != null && pkt.lon != null
      ? `<button class="aprs-bar-pin" title="${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}" onclick="window.navigateToAprsMap(${pkt.lat},${pkt.lon})">📍</button>`
      : "";
    html += `<div class="aprs-bar-frame">` +
      `<div class="aprs-bar-frame-main">${ts}${pin}${call}>${dest}: ${info}</div>` +
      `</div>`;
  }
  aprsBarOverlay.innerHTML = html;
  aprsBarOverlay.style.display = "flex";
}
aprsWindow.updateAprsBar = updateAprsBar;
aprsWindow.clearAprsBar = function() {
  resetAprsHistoryView();
};
aprsWindow.closeAprsBar = function() {
  aprsBarDismissedAtMs = Date.now();
  if (aprsBarOverlay) {
    aprsBarOverlay.style.display = "none";
    aprsBarOverlay.innerHTML = "";
  }
};

function resetAprsHistoryView(): void {
  if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
  aprsPacketHistory = [];
  updateAprsBar();
  renderAprsHistory();
  aprsWindow.clearMapMarkersByType?.("aprs");
}

function pruneAprsHistoryView(): void {
  pruneAprsPacketHistory();
  updateAprsBar();
  renderAprsHistory();
}

function addAprsPacket(pkt: AprsPacket): void {
  const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
  pkt._tsMs = tsMs;
  pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });

  aprsPacketHistory.unshift(pkt);
  pruneAprsPacketHistory();

  if (pkt.lat != null && pkt.lon != null && aprsWindow.aprsMapAddStation) {
    aprsWindow.aprsMapAddStation(pkt.srcCall ?? "", pkt.lat, pkt.lon, pkt.info ?? "", pkt.symbolTable, pkt.symbolCode, pkt);
  }

  if (pkt.crcOk) scheduleAprsBarUpdate();

  scheduleAprsHistoryRender();
}

function normalizeServerAprsPacket(pkt: AprsPacket): AprsPacket {
  return normalizeAprsPacket(pkt, aprsWindow.getDecodeRigMeta?.() ?? null);
}

function onServerAprsBatch(packets: AprsPacket[]): void {
  if (!Array.isArray(packets) || packets.length === 0) return;
  if (aprsStatus) aprsStatus.textContent = "Receiving";
  const normalized: AprsPacket[] = [];
  let hasCrcOk = false;
  for (const pkt of packets) {
    const next = normalizeServerAprsPacket(pkt);
    const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
    next._tsMs = tsMs;
    next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
    if (next.lat != null && next.lon != null && aprsWindow.aprsMapAddStation) {
      aprsWindow.aprsMapAddStation(next.srcCall ?? "", next.lat, next.lon, next.info ?? "", next.symbolTable, next.symbolCode, next);
    }
    if (next.crcOk) hasCrcOk = true;
    normalized.push(next);
  }
  normalized.reverse();
  aprsPacketHistory = normalized.concat(aprsPacketHistory);
  pruneAprsPacketHistory();
  if (hasCrcOk) scheduleAprsBarUpdate();
  scheduleAprsHistoryRender();
}

document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => { void (async () => {
  if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
  try {
    await hostCore.postPath("/clear_aprs_decode");
    resetAprsHistoryView();
  } catch (e) {
    console.error("APRS history clear failed", e);
  }
})(); });

if (aprsOnlyPosBtn) {
  aprsOnlyPosBtn.addEventListener("click", () => {
    aprsOnlyPos = !aprsOnlyPos;
    renderAprsHistory();
  });
}

if (aprsHideCrcBtn) {
  aprsHideCrcBtn.addEventListener("click", () => {
    aprsHideCrc = !aprsHideCrc;
    renderAprsHistory();
  });
}

if (aprsCollapseDupBtn) {
  aprsCollapseDupBtn.addEventListener("click", () => {
    aprsCollapseDup = !aprsCollapseDup;
    renderAprsHistory();
  });
}

(["all", "position", "message", "weather", "telemetry", "other"] as const).forEach((type) => {
  const btn = document.getElementById(`aprs-type-${type}`);
  if (!btn) return;
  btn.addEventListener("click", () => {
    aprsTypeFilter = type;
    renderAprsHistory();
  });
});

if (aprsFilterInput) {
  aprsFilterInput.addEventListener("input", () => {
    aprsFilterText = aprsFilterInput.value.trim().toUpperCase();
    renderAprsHistory();
  });
}

// --- Server-side APRS decode handler ---
function onServerAprs(pkt: AprsPacket): void {
  if (aprsStatus) aprsStatus.textContent = "Receiving";
  addAprsPacket(normalizeServerAprsPacket(pkt));
}

renderAprsHistory();
(window as unknown as PluginRuntimeWindow).trxPluginRuntime.registerDecoder<AprsPacket>({
  id: "aprs",
  onMessage: onServerAprs,
  onBatch: onServerAprsBatch,
  restore: onServerAprsBatch,
  reset: resetAprsHistoryView,
  prune: pruneAprsHistoryView,
});
