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

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

export {};

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

interface AisMessage {
  rig_id?: string | null;
  channel?: string | null;
  message_type?: number | null;
  mmsi?: number | null;
  lat?: number | null;
  lon?: number | null;
  sog_knots?: number | null;
  cog_deg?: number | null;
  heading_deg?: number | null;
  vessel_name?: string | null;
  callsign?: string | null;
  destination?: string | null;
  ts_ms?: number | null;
  _tsMs?: number;
  _ts?: string;
}
interface AisChannelInfo { label: string; badgeClass: string; freqText: string }
interface AisBridge {
  getDecodeHistoryRetentionMs?: () => number;
  trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
  buildAisVesselUrl?: (mmsi: number | null | undefined) => string | null;
  aisMapAddVessel?: (message: AisMessage) => void;
  clearMapMarkersByType?: (type: string) => void;
  trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise<boolean> };
  updateAisBar?: () => void;
  clearAisBar?: () => void;
}
const aisWindow = window as unknown as AisBridge;
const escapeAisHtml = (input: string): string => hostCore.escapeMapHtml(input);

// --- AIS Decoder Plugin (server-side decode) ---
const aisStatus = document.getElementById("ais-status");
const aisMessagesEl = document.getElementById("ais-messages");
const aisFilterInput = document.getElementById("ais-filter") as HTMLInputElement | null;
const aisBarOverlay = document.getElementById("ais-bar-overlay");
const aisChannelSummaryEl = document.getElementById("ais-channel-summary");
const aisVesselCountEl = document.getElementById("ais-vessel-count");
const aisLatestSeenEl = document.getElementById("ais-latest-seen");
const AIS_BAR_WINDOW_MS = 15 * 60 * 1000;
const AIS_DEFAULT_A_HZ = 161_975_000;
const AIS_CHANNEL_SPACING_HZ = 50_000;
let aisFilterText = "";
let aisMessageHistory: AisMessage[] = [];

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

function pruneAisMessageHistory() {
  const cutoffMs = Date.now() - currentAisHistoryRetentionMs();
  aisMessageHistory = aisMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
}

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

function scheduleAisHistoryRender() {
  scheduleAisUi("ais-history", () => { renderAisHistory(); });
}

function scheduleAisBarUpdate() {
  scheduleAisUi("ais-bar", () => { updateAisBar(); });
}

function formatAisMhz(freqHz: number): string {
  return `${(freqHz / 1_000_000).toFixed(3)} MHz`;
}

function currentAisChannelPlan(): { aHz: number; bHz: number } {
  const raw = ((document.getElementById("freq") as HTMLInputElement | null)?.value || "").replace(/[^\d]/g, "");
  const aHz = raw ? Number(raw) : AIS_DEFAULT_A_HZ;
  const safeAHz = Number.isFinite(aHz) && aHz > 0 ? aHz : AIS_DEFAULT_A_HZ;
  return {
    aHz: safeAHz,
    bHz: safeAHz + AIS_CHANNEL_SPACING_HZ,
  };
}

function aisChannelInfo(channel: string | null | undefined): AisChannelInfo {
  const plan = currentAisChannelPlan();
  const ch = (channel ?? "").trim().toUpperCase();
  if (ch === "B") {
    return {
      label: "AIS-B",
      badgeClass: "ais-badge ais-badge-channel-b",
      freqText: formatAisMhz(plan.bHz),
    };
  }
  return {
    label: "AIS-A",
    badgeClass: "ais-badge ais-badge-channel-a",
    freqText: formatAisMhz(plan.aHz),
  };
}

function aisDisplayName(msg: AisMessage): string {
  return msg.vessel_name || msg.callsign || `MMSI ${msg.mmsi}`;
}

function aisDisplayNameHtml(msg: AisMessage): string {
  const label = escapeAisHtml(aisDisplayName(msg));
  const url = aisWindow.buildAisVesselUrl?.(msg.mmsi) ?? null;
  if (!url) return label;
  return `<a class="title-link" href="${escapeAisHtml(url)}" target="_blank" rel="noopener">${label}</a>`;
}

function aisTypeLabel(type: number | null | undefined): string {
  switch (Number(type)) {
    case 1:
    case 2:
    case 3:
      return "Class A Position";
    case 4:
      return "Base Station";
    case 5:
      return "Static/Voyage";
    case 18:
      return "Class B Position";
    case 19:
      return "Class B Extended";
    case 21:
      return "Aid to Nav";
    case 24:
      return "Class B Static";
    default:
      return `Type ${type ?? "--"}`;
  }
}

function aisAgeText(tsMs: number | undefined): string {
  if (typeof tsMs !== "number" || !Number.isFinite(tsMs)) return "just now";
  const deltaMs = Math.max(0, Date.now() - tsMs);
  const seconds = Math.round(deltaMs / 1000);
  if (seconds < 5) return "just now";
  if (seconds < 60) return `${seconds}s ago`;
  const minutes = Math.round(seconds / 60);
  if (minutes < 60) return `${minutes}m ago`;
  const hours = Math.round(minutes / 60);
  return `${hours}h ago`;
}

function aisMotionText(msg: AisMessage): string {
  const parts = [
    msg.sog_knots != null ? `${msg.sog_knots.toFixed(1)} kn` : null,
    msg.cog_deg != null ? `${msg.cog_deg.toFixed(1)}° COG` : null,
    msg.heading_deg != null ? `${msg.heading_deg.toFixed(0)}° HDG` : null,
  ].filter(Boolean);
  return parts.join(" · ");
}

function aisRouteText(msg: AisMessage): string {
  return [msg.callsign, msg.destination].filter(Boolean).join(" -> ");
}

function aisDistanceText(msg: AisMessage): string {
  if (hostState.serverLat == null || hostState.serverLon == null || msg.lat == null || msg.lon == null) {
    return "";
  }
  const distKm = hostCore.haversineKm(hostState.serverLat, hostState.serverLon, msg.lat, msg.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 aisLatestByVessel(messages: AisMessage[]): AisMessage[] {
  const byMmsi = new Map<string, AisMessage>();
  for (const msg of messages) {
    const key = Number.isFinite(msg.mmsi) ? String(msg.mmsi) : `${msg.channel || "?"}:${msg._tsMs || 0}`;
    if (!byMmsi.has(key)) byMmsi.set(key, msg);
  }
  return Array.from(byMmsi.values());
}

function updateAisSummary() {
  const plan = currentAisChannelPlan();
  if (aisChannelSummaryEl) {
    aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
  }

  const vessels = aisLatestByVessel(aisMessageHistory);
  if (aisVesselCountEl) {
    const count = vessels.length;
    aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`;
  }

  if (aisLatestSeenEl) {
    const latest = aisMessageHistory[0];
    if (!latest) {
      aisLatestSeenEl.textContent = "No traffic yet";
    } else {
      const channel = aisChannelInfo(latest.channel);
      aisLatestSeenEl.textContent = `${channel.label} ${aisAgeText(latest._tsMs)}`;
    }
  }
}

function renderAisRow(msg: AisMessage): HTMLElement {
  const row = document.createElement("div");
  row.className = "ais-message";
  const ts = msg._ts || new Date().toLocaleTimeString([], {
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
  });
  const name = aisDisplayName(msg);
  const nameHtml = aisDisplayNameHtml(msg);
  const channel = aisChannelInfo(msg.channel);
  const motion = aisMotionText(msg);
  const route = aisRouteText(msg);
  const distance = aisDistanceText(msg);
  const pos = msg.lat != null && msg.lon != null
    ? `<a class="ais-pos-link" href="javascript:void(0)" onclick="window.navigateToAprsMap(${msg.lat},${msg.lon})">${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}</a>`
    : "";
  row.dataset.filterText = [
    name,
    msg.mmsi,
    msg.channel,
    channel.label,
    msg.vessel_name,
    msg.callsign,
    msg.destination,
    aisTypeLabel(msg.message_type),
  ]
    .filter(Boolean)
    .join(" ")
    .toUpperCase();
  row.innerHTML =
    `<div class="ais-row-head">` +
      `<span class="ais-time">${ts}</span>` +
      `<span class="ais-call">${nameHtml}</span>` +
      `<span class="${channel.badgeClass}">${escapeAisHtml(channel.label)}</span>` +
      `<span class="ais-badge ais-badge-type">${escapeAisHtml(aisTypeLabel(msg.message_type))}</span>` +
    `</div>` +
    `<div class="ais-row-meta">` +
      `<span>MMSI ${escapeAisHtml(String(msg.mmsi))}</span>` +
      (route ? `<span class="ais-meta-text">${escapeAisHtml(route)}</span>` : "") +
      `<span class="ais-meta-text">${escapeAisHtml(channel.freqText)}</span>` +
    `</div>` +
    `<div class="ais-row-detail">` +
      (motion ? `<span>${escapeAisHtml(motion)}</span>` : `<span>No motion data</span>`) +
      (distance ? `<span>${escapeAisHtml(distance)}</span>` : "") +
      (pos ? `<span>${pos}</span>` : "") +
      `<span>${escapeAisHtml(aisAgeText(msg._tsMs))}</span>` +
    `</div>`;
  applyAisFilterToRow(row);
  return row;
}

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

function updateAisBar() {
  if (!aisBarOverlay) return;
  updateAisSummary();

  const isAis = ((document.getElementById("mode") as HTMLSelectElement | null)?.value || "").toUpperCase() === "AIS";
  const cutoffMs = Date.now() - AIS_BAR_WINDOW_MS;
  const recent = aisMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
  const messages = aisLatestByVessel(recent).slice(0, 8);
  if (!isAis || messages.length === 0) {
    aisBarOverlay.style.display = "none";
    aisBarOverlay.innerHTML = "";
    return;
  }

  let html = '<div class="aprs-bar-header"><span class="aprs-bar-title"><span class="aprs-bar-title-word">AIS</span><span class="aprs-bar-title-word">Live</span></span><span class="aprs-bar-clear-wrap"><span class="aprs-bar-clear" role="button" tabindex="0" onclick="window.clearAisBar()" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();window.clearAisBar();}" aria-label="Clear AIS overlay">Clear</span></span><span class="aprs-bar-window">Last 15 minutes</span></div>';
  for (const msg of messages) {
    const ts = msg._ts ? `<span class="aprs-bar-time">${msg._ts}</span>` : "";
    const pin = msg.lat != null && msg.lon != null
      ? `<button class="aprs-bar-pin" title="${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}" onclick="window.navigateToAprsMap(${msg.lat},${msg.lon})">📍</button>`
      : "";
    const name = `<span class="ais-call">${aisDisplayNameHtml(msg)}</span>`;
    const channel = aisChannelInfo(msg.channel);
    const distance = aisDistanceText(msg);
    const details = [
      `MMSI ${escapeAisHtml(String(msg.mmsi))}`,
      escapeAisHtml(channel.label),
      msg.sog_knots != null ? `${msg.sog_knots.toFixed(1)} kn` : null,
      msg.cog_deg != null ? `${msg.cog_deg.toFixed(1)}°` : null,
      distance ? escapeAisHtml(distance) : null,
      escapeAisHtml(aisAgeText(msg._tsMs)),
    ]
      .filter(Boolean)
      .join(" · ");
    html += `<div class="aprs-bar-frame">` +
      `<div class="aprs-bar-frame-main">${ts}${pin}${name}: ${details}</div>` +
      `</div>`;
  }
  aisBarOverlay.innerHTML = html;
  aisBarOverlay.style.display = "flex";
}
aisWindow.updateAisBar = updateAisBar;
aisWindow.clearAisBar = function() {
  resetAisHistoryView();
};

function resetAisHistoryView(): void {
  if (aisMessagesEl) aisMessagesEl.innerHTML = "";
  aisMessageHistory = [];
  updateAisBar();
  renderAisHistory();
  aisWindow.clearMapMarkersByType?.("ais");
}

function renderAisHistory() {
  pruneAisMessageHistory();
  if (!aisMessagesEl) {
    updateAisSummary();
    return;
  }
  const fragment = document.createDocumentFragment();
  for (const message of aisMessageHistory) {
    fragment.appendChild(renderAisRow(message));
  }
  aisMessagesEl.replaceChildren(fragment);
  updateAisSummary();
}

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

  aisMessageHistory.unshift(msg);
  pruneAisMessageHistory();
  scheduleAisBarUpdate();
  scheduleAisHistoryRender();

  if (msg.lat != null && msg.lon != null && aisWindow.aisMapAddVessel) {
    aisWindow.aisMapAddVessel(msg);
  }
}

function normalizeServerAisMessage(msg: AisMessage): AisMessage {
  return {
    ...msg,
    rig_id: msg.rig_id || null,
  };
}

function onServerAisBatch(messages: AisMessage[]): void {
  if (!Array.isArray(messages) || messages.length === 0) return;
  if (aisStatus) aisStatus.textContent = "Receiving";
  const normalized: AisMessage[] = [];
  for (const msg of messages) {
    const next = normalizeServerAisMessage(msg);
    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 && aisWindow.aisMapAddVessel) {
      aisWindow.aisMapAddVessel(next);
    }
    normalized.push(next);
  }
  normalized.reverse();
  aisMessageHistory = normalized.concat(aisMessageHistory);
  pruneAisMessageHistory();
  scheduleAisBarUpdate();
  scheduleAisHistoryRender();
}

function pruneAisHistoryView(): void {
  pruneAisMessageHistory();
  updateAisBar();
  renderAisHistory();
}

document.getElementById("settings-clear-ais-history")?.addEventListener("click", () => { void (async () => {
  if (!await aisWindow.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return;
  try {
    await hostCore.postPath("/clear_ais_decode");
    resetAisHistoryView();
  } catch (e) {
    console.error("AIS history clear failed", e);
  }
})(); });

if (aisFilterInput) {
  aisFilterInput.addEventListener("input", () => {
    aisFilterText = aisFilterInput.value.trim().toUpperCase();
    renderAisHistory();
  });
}

function onServerAis(msg: AisMessage): void {
  if (aisStatus) aisStatus.textContent = "Receiving";
  addAisMessage(normalizeServerAisMessage(msg));
}

updateAisSummary();
(window as unknown as PluginRuntimeWindow).trxPluginRuntime.registerDecoder<AisMessage>({
  id: "ais",
  onMessage: onServerAis,
  onBatch: onServerAisBatch,
  restore: onServerAisBatch,
  reset: resetAisHistoryView,
  prune: pruneAisHistoryView,
});
