// Import type definitions
import type {
  LiveViewHookContext,
  MarkerData,
  BoundsData,
  CenterData,
  MarkerState,
  MapEventData,
} from "./types/map";
import type {
  Map as LeafletMap,
  Marker,
  TileLayer,
  LayerGroup,
  DivIcon,
  LatLngBounds,
  Polyline,
} from "leaflet";
import type {
  LeafletTouchEvent,
  LeafletPopupEvent,
} from "./types/leaflet-events";
import type {
  HeatLayer,
  MarkerClusterGroup,
  OverlappingMarkerSpiderfier,
  MarkerClusterGroupOptions,
  HeatLayerOptions,
  MarkerCluster,
  HeatLatLng,
} from "./types/leaflet-plugins";
import type { APRSMarker } from "./types/marker-extensions";
import type { BaseEventPayload } from "./types/events";

// Leaflet and plugins are loaded globally from vendor bundle
// We'll access window.L dynamically when needed instead of storing it at module load time

// CSS files are bundled in vendor.js

// The OverlappingMarkerSpiderfier is attached to window by the UMD module
declare global {
  interface Window {
    OverlappingMarkerSpiderfier: any;
  }
}

// Add plugin types to Leaflet
declare module "leaflet" {
  export function heatLayer(
    latlngs: HeatLatLng[],
    options?: HeatLayerOptions,
  ): HeatLayer;
  export function markerClusterGroup(
    options?: MarkerClusterGroupOptions,
  ): MarkerClusterGroup;
}

// Import trail management functionality
import { TrailManager } from "./features/trail_manager";
// Import helper functions
import {
  parseTimestamp,
  getTrailId,
  saveMapState,
  safePushEvent,
  getLiveSocket,
  escapeHtml,
  isValidCoordinate,
  unwrapLongitudes,
} from "./map_helpers";

// APRS Map Hook - handles only basic map interaction
// All data logic handled by LiveView

// Zoom level at which marker clustering is disabled and individual markers are shown.
// Trails are only visible at this zoom level and above to avoid showing trails
// for markers hidden inside cluster bubbles.
const DISABLE_CLUSTERING_AT_ZOOM = 11;

let MapAPRSMap = {
  mounted() {
    const self = this as unknown as LiveViewHookContext;
    // Initialize error tracking
    self.errors = [];
    self.initializationAttempts = 0;
    self.maxInitializationAttempts = 3;

    self.attemptInitialization();
  },

  attemptInitialization() {
    const self = this as unknown as LiveViewHookContext;
    self.initializationAttempts!++;

    // Check if Leaflet is available
    if (typeof window.L === "undefined") {
      console.error("Leaflet library not loaded!");
      self.errors!.push("Leaflet library not available");

      if (self.initializationAttempts! < self.maxInitializationAttempts!) {
        setTimeout(() => self.attemptInitialization(), 1000);
        return;
      } else {
        self.handleFatalError(
          "Leaflet library failed to load after multiple attempts",
        );
        return;
      }
    }

    // Create a local reference to Leaflet for this function
    const L = window.L;

    // Initialize with URL parameters first (from data attributes)
    let initialCenter: CenterData, initialZoom: number;
    let useUrlParams = false;

    try {
      const centerData = self.el.dataset.center;
      const zoomData = self.el.dataset.zoom;

      if (!centerData || !zoomData)
        throw new Error("Missing map data attributes");
      initialCenter = JSON.parse(centerData);
      initialZoom = parseInt(zoomData);

      if (
        !initialCenter ||
        typeof initialCenter.lat !== "number" ||
        typeof initialCenter.lng !== "number"
      )
        throw new Error("Invalid center data");
      if (isNaN(initialZoom) || initialZoom < 1 || initialZoom > 20)
        throw new Error("Invalid zoom data");

      // Check if URL has explicit parameters (not default values)
      const urlParams = new URLSearchParams(window.location.search);
      useUrlParams =
        urlParams.has("lat") || urlParams.has("lng") || urlParams.has("z");
    } catch (error) {
      console.error("Error parsing map data attributes:", error);
      initialCenter = { lat: 39.8283, lng: -98.5795 };
      initialZoom = 5;
    }

    self.initializeMap(initialCenter, initialZoom);
  },

  initializeMap(initialCenter: CenterData, initialZoom: number) {
    const self = this as unknown as LiveViewHookContext;

    // Check if Leaflet is still available
    if (typeof window.L === "undefined") {
      console.error("Leaflet library not loaded!");
      self.handleFatalError("Leaflet library not available");
      return;
    }

    // Create a local reference to Leaflet for this function
    const L = window.L;

    // Ensure the element and its parent exist
    if (!self.el || !self.el.parentNode) {
      console.warn("Map element or parent not found, retrying...");
      if (self.initializationAttempts! < self.maxInitializationAttempts!) {
        setTimeout(() => self.attemptInitialization(), 500);
        return;
      } else {
        self.handleFatalError("Map element structure invalid");
        return;
      }
    }

    // Check element dimensions
    const rect = self.el.getBoundingClientRect();

    // Validate element has dimensions
    if (rect.width === 0 || rect.height === 0) {
      console.warn("Map element has no dimensions, retrying...");
      self.errors!.push("Map element has zero dimensions");

      // Try to force dimensions
      self.el.style.width = "100%";
      self.el.style.height = "100vh";

      if (self.initializationAttempts! < self.maxInitializationAttempts!) {
        setTimeout(() => self.attemptInitialization(), 500);
        return;
      } else {
        self.handleFatalError("Map element never gained proper dimensions");
        return;
      }
    }

    // Initialize basic map
    try {
      // Check if map is already initialized
      if (self.map) {
        console.warn("Map already exists, reinitializing...");
        self.map.remove();
        self.map = null;
      }

      // Ensure element ID is unique and not already initialized
      if (self.el._leaflet_id) {
        console.warn("Map element already has Leaflet ID, cleaning up...");
        const el = L.DomUtil.get(self.el.id) as HTMLElement | null;
        if (el !== null) {
          L.DomUtil.remove(el);
        }
        delete self.el._leaflet_id;
      }

      // Detect if mobile device
      const isMobile =
        /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
          navigator.userAgent,
        );

      self.map = L.map(self.el, {
        zoomControl: !isMobile, // Hide default zoom control on mobile, we'll add a better one
        attributionControl: true,
        closePopupOnClick: true,
        tap: true, // Enable tap events for mobile
        tapTolerance: 30, // Increase tap tolerance for mobile
        touchZoom: true,
        bounceAtZoomLimits: false, // Disable bouncing at zoom limits
        worldCopyJump: true, // Better panning behavior
        preferCanvas: isMobile, // Use canvas renderer on mobile for better performance
        zoomAnimation: !isMobile, // Disable zoom animations on mobile for performance
        fadeAnimation: !isMobile, // Disable fade animations on mobile
        markerZoomAnimation: !isMobile, // Disable marker animations on mobile
      }).setView([initialCenter.lat, initialCenter.lng], initialZoom);

      // Add mobile-friendly zoom control if on mobile
      if (isMobile) {
        L.control
          .zoom({
            position: "bottomright",
          })
          .addTo(self.map);

        // Add touch gesture handling
        self.setupMobileGestures();
      }
    } catch (error) {
      console.error("Error initializing map:", error);
      self.errors!.push(
        "Map initialization failed: " +
          (error instanceof Error ? error.message : error),
      );

      if (self.initializationAttempts! < self.maxInitializationAttempts!) {
        setTimeout(() => self.attemptInitialization(), 1000);
        return;
      } else {
        self.handleFatalError(
          "Map initialization failed after multiple attempts",
        );
        return;
      }
    }

    // Add OpenStreetMap tile layer with improved settings
    try {
      // Tile provider options
      const tileProviders = {
        osm: {
          url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
          attribution:
            '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors | APRS.me',
          subdomains: ["a", "b", "c"],
        },
        osmDE: {
          url: "https://{s}.tile.openstreetmap.de/{z}/{x}/{y}.png",
          attribution:
            '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors | APRS.me',
          subdomains: ["a", "b", "c"],
        },
        carto: {
          url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
          attribution:
            '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a> | APRS.me',
          subdomains: ["a", "b", "c", "d"],
        },
      };

      // Create raster tile layer
      const provider = tileProviders.osm;
      const tileLayer = L.tileLayer(provider.url, {
        attribution: provider.attribution,
        maxZoom: 19,
        subdomains: provider.subdomains,
        tileSize: 256,
        keepBuffer: 2,
        updateWhenZooming: false,
        updateInterval: 200,
        // Remove errorTileUrl to let Leaflet handle tile errors gracefully
        // errorTileUrl causes issues with some browsers when using data URLs
      });

      // Add event handler for tile errors with retry logic
      let retryCount = new Map<string, number>();
      tileLayer.on("tileerror", function (error: any) {
        const src = error.tile.src;
        const count = retryCount.get(src) || 0;

        if (count < 3) {
          console.warn(`Tile load error (attempt ${count + 1}/3):`, src);
          retryCount.set(src, count + 1);

          // Exponential backoff
          setTimeout(
            () => {
              error.tile.src =
                src + (src.includes("?") ? "&" : "?") + "_retry=" + Date.now();
            },
            Math.pow(2, count) * 500,
          );
        } else {
          console.error("Tile failed after 3 attempts:", src);
          retryCount.delete(src);
        }
      });

      tileLayer.addTo(self.map);
    } catch (error) {
      self.errors!.push(
        "Tile layer failed: " +
          (error instanceof Error ? error.message : error),
      );
    }

    // Store markers for management
    self.markers = new Map<string, APRSMarker>();

    // Create marker cluster group for medium zoom levels (9-14)
    if (L.markerClusterGroup) {
      self.markerClusterGroup = L.markerClusterGroup({
        showCoverageOnHover: false,
        zoomToBoundsOnClick: true,
        spiderfyOnMaxZoom: true,
        removeOutsideVisibleBounds: true,
        disableClusteringAtZoom: DISABLE_CLUSTERING_AT_ZOOM,
        maxClusterRadius: 80,
        iconCreateFunction: function (cluster: MarkerCluster) {
          const count = cluster.getChildCount();
          let className = "marker-cluster-small";

          if (count > 10) {
            className = "marker-cluster-medium";
          }
          if (count > 50) {
            className = "marker-cluster-large";
          }

          return createDivIcon("<div><span>" + count + "</span></div>", {
            className: "marker-cluster " + className,
            iconSize: [40, 40],
            iconAnchor: [20, 20],
          });
        },
      });
      self.markerClusterGroup.addTo(self.map);
      self.markerLayer = self.markerClusterGroup;
    } else {
      // Fallback to regular layer group if clustering not available
      self.markerLayer = L.layerGroup().addTo(self.map);
    }

    // Create heat layer (hidden by default)
    try {
      if (L.heatLayer) {
        self.heatLayer = L.heatLayer([], {
          radius: 25,
          blur: 15,
          maxZoom: 8,
          gradient: {
            0.4: "blue",
            0.65: "lime",
            0.85: "yellow",
            1.0: "red",
          },
        });
      }
    } catch (error) {
      console.error("Error creating heat layer:", error);
    }

    // Create trail layer and manager with RF path hover callbacks
    // Only add to map if zoom is high enough (trails hidden when markers are clustered)
    const trailLayer = L.layerGroup();
    if (self.map!.getZoom() >= DISABLE_CLUSTERING_AT_ZOOM) {
      trailLayer.addTo(self.map!);
    }
    self.trailLayer = trailLayer;
    self.trailManager = new TrailManager(
      trailLayer,
      60 * 60 * 1000,
      // onTrailHover: send RF path visualization event to backend
      (lat: number, lng: number, path: string) => {
        if (
          self.pushEvent &&
          typeof self.pushEvent === "function" &&
          !self.isDestroyed
        ) {
          try {
            self.pushEvent.call(self, "marker_hover_start", {
              id: "trail_hover",
              callsign: "",
              path: path,
              lat: lat,
              lng: lng,
            });
          } catch (error) {
            console.warn("Failed to send trail hover event:", error);
          }
        }
      },
      // onTrailHoverEnd: clear RF path lines
      () => {
        if (
          self.pushEvent &&
          typeof self.pushEvent === "function" &&
          !self.isDestroyed
        ) {
          try {
            self.pushEvent.call(self, "marker_hover_end", {
              id: "trail_hover",
            });
          } catch (error) {
            console.warn("Failed to send trail hover end event:", error);
          }
        }
      },
    );

    // Track marker states to prevent unnecessary operations
    self.markerStates = new Map<string, MarkerState>();

    // Reverse index: callsign_group → Set of marker IDs for O(1) lookup
    self.callsignIndex = new Map<string, Set<string>>();

    // Store RF path lines for hover visualization
    self.rfPathLines = [];

    // Force initial size calculation
    try {
      self.map.invalidateSize();
    } catch (error) {
      console.error("Error invalidating map size:", error);
    }

    // Helper function to send map ready events with retry
    self.sendMapReadyEvents = (retryCount = 0) => {
      if (
        self.pushEvent &&
        typeof self.pushEvent === "function" &&
        !self.isDestroyed
      ) {
        try {
          self.pushEvent("map_ready", {});
        } catch (e) {
          console.debug("Failed to push map_ready event:", e);
        }
        if (self.map) {
          saveMapState(self.map, self.pushEvent.bind(self));
          self.sendBoundsToServer();

          // Send map state update after a delay
          setTimeout(() => {
            if (self.map && self.pushEvent && !self.isDestroyed) {
              saveMapState(self.map, self.pushEvent.bind(self));
            }
          }, 500);
        }
      } else if (retryCount < 3) {
        // Retry up to 3 times
        console.warn(
          `pushEvent not available, retrying... (attempt ${retryCount + 1})`,
        );
        setTimeout(() => self.sendMapReadyEvents(retryCount + 1), 200);
      }
    };

    // Track when map is ready
    self.map!.whenReady(() => {
      if (self.isDestroyed) return;
      try {
        self.lastZoom = self.map!.getZoom();
        self.sendMapReadyEvents();

        // Process any pending markers that were queued before map was ready
        if (self.pendingMarkers && self.pendingMarkers.length > 0) {
          console.debug(
            `Processing ${self.pendingMarkers.length} pending markers`,
          );
          self.pendingMarkers.forEach((markerData: MarkerData) => {
            try {
              self.addMarker(markerData);
            } catch (error) {
              console.error("Error adding pending marker:", error, markerData);
            }
          });
          self.pendingMarkers = [];
        }
      } catch (error) {
        console.error("Error in map ready callback:", error);
      }
    });

    // Start periodic cleanup of old trail positions (every 5 minutes)
    self.cleanupInterval = setInterval(
      () => {
        if (!self.isDestroyed && self.trailManager) {
          self.trailManager.cleanupOldPositions();
        }
      },
      5 * 60 * 1000,
    );

    // Track map event handlers for cleanup
    self.mapEventHandlers = new Map();
    self.isDestroyed = false;

    // Send bounds to LiveView when map moves
    const moveEndHandler = () => {
      // Skip if this is a programmatic move from the server
      if (self.programmaticMoveId) {
        return;
      }

      if (self.boundsTimer) clearTimeout(self.boundsTimer);
      self.boundsTimer = setTimeout(() => {
        if (
          self.map &&
          !self.isDestroyed &&
          self.pushEvent &&
          typeof self.pushEvent === "function"
        ) {
          saveMapState(self.map, self.pushEvent.bind(self));
        }
      }, 300);
    };
    self.map!.on("moveend", moveEndHandler);
    self.mapEventHandlers!.set("moveend", moveEndHandler);

    // Handle zoom changes with optimization for large zoom differences
    const zoomEndHandler = () => {
      // Skip if this is a programmatic move from the server
      if (self.programmaticMoveId) {
        return;
      }

      if (self.boundsTimer) clearTimeout(self.boundsTimer);
      self.boundsTimer = setTimeout(() => {
        const currentZoom = self.map!.getZoom();
        const zoomDifference = self.lastZoom
          ? Math.abs(currentZoom - self.lastZoom)
          : 0;

        // Only rebuild OMS when crossing the clustering threshold
        const wasUnclustered = self.lastZoom !== undefined && self.lastZoom >= DISABLE_CLUSTERING_AT_ZOOM;
        const isUnclustered = currentZoom >= DISABLE_CLUSTERING_AT_ZOOM;
        if (self.oms && (wasUnclustered !== isUnclustered || self.lastZoom === undefined)) {
          self.oms.clearMarkers();
          self.markers.forEach((marker, id) => {
            const markerState = self.markerStates.get(String(id));
            const shouldAddToOms =
              markerState?.is_most_recent_for_callsign === true ||
              (markerState?.is_most_recent_for_callsign == null &&
                !marker._isHistorical);
            if (
              marker &&
              !marker._isClusterMarker &&
              markerState &&
              shouldAddToOms
            ) {
              self.oms.addMarker(marker);
            }
          });
        }

        // Hide trails when markers are clustered, show when unclustered
        if (self.trailLayer && self.map) {
          if (currentZoom >= DISABLE_CLUSTERING_AT_ZOOM) {
            if (!self.map.hasLayer(self.trailLayer)) {
              self.trailLayer.addTo(self.map);
              self.trailLayer.bringToFront();
            }
          } else {
            if (self.map.hasLayer(self.trailLayer)) {
              self.map.removeLayer(self.trailLayer);
            }
          }
        }

        // If zoom changed significantly (more than 2 levels), request reload of normal markers
        // but preserve historical ones and current position
        if (zoomDifference > 2) {
          try {
            self.pushEvent("refresh_markers", {});
          } catch (e) {
            console.debug("Failed to push refresh_markers event:", e);
          }
        }

        self.lastZoom = currentZoom;
        // Save map state and update URL
        if (
          self.map &&
          !self.isDestroyed &&
          self.pushEvent &&
          typeof self.pushEvent === "function"
        ) {
          saveMapState(self.map, self.pushEvent.bind(self));
        }
      }, 300);
    };
    self.map!.on("zoomend", zoomEndHandler);
    self.mapEventHandlers!.set("zoomend", zoomEndHandler);

    // Handle resize
    self.resizeHandler = () => {
      try {
        if (self.map) {
          self.map.invalidateSize();
        }
      } catch (error) {
        console.error("Error invalidating map size on resize:", error);
      }
    };
    window.addEventListener("resize", self.resizeHandler);

    // Add a delayed size check
    self.sizeCheckTimeout = setTimeout(() => {
      if (self.el && self.el.parentNode) {
        const rect = self.el.getBoundingClientRect();
        if (self.map && rect.width > 0 && rect.height > 0) {
          try {
            self.map.invalidateSize();
          } catch (error) {
            console.error("Error re-invalidating map size:", error);
          }
        }
      }
    }, 1000);

    // Queue to hold markers that arrive before map is ready
    self.pendingMarkers = [];

    // LiveView event handlers
    self.setupLiveViewHandlers();

    // Process any pending markers once the map is ready
    self.map.whenReady(() => {
      if (self.pendingMarkers && self.pendingMarkers.length > 0) {
        console.debug(`Processing ${self.pendingMarkers.length} pending markers`);
        self.pendingMarkers.forEach((markerData) => {
          self.addMarker(markerData);
        });
        self.pendingMarkers = [];
      }
    });

    // Set up event delegation for popup navigation links
    self.setupPopupNavigation();

    // Only initialize OMS if clustering is not available or at high zoom levels
    if (typeof window.OverlappingMarkerSpiderfier !== "undefined") {
      self.oms = new window.OverlappingMarkerSpiderfier(self.map, {
        keepSpiderfied: true,
        nearbyDistance: 65,
        circleSpiralSwitchover: 10,
        circleFootSeparation: 80,
        spiralFootSeparation: 50,
        spiralLengthStart: 30,
        spiralLengthFactor: 6,
        legWeight: 2,
        legColors: {
          usual: "#222",
          highlighted: "#f00",
        },
        spiderfyDistanceMultiplier: 6,
      });

      // Add click handler for spiderfied markers
      self.oms.addListener("click", function (marker: Marker) {
        const aprsMarker = marker as APRSMarker;
        if (aprsMarker._markerId) {
          self.userOpenedPopupMarkerId = aprsMarker._markerId;
        }
        if (marker.openPopup) marker.openPopup();
      });

      // Style the spider legs
      self.oms.addListener("spiderfy", function (markers: Marker[]) {
        self.map.closePopup();
      });

      self.oms.addListener("unspiderfy", function (markers: Marker[]) {
        // Markers return to normal positions
      });
    }
  },

  handleFatalError(message: string) {
    const self = this as unknown as LiveViewHookContext;
    console.error("Fatal map error:", message);
    console.error("All errors:", self.errors);

    // Display error message to user
    if (self.el) {
      self.el.innerHTML = `
        <div style="
          display: flex;
          justify-content: center;
          align-items: center;
          height: 100%;
          background: #f8f9fa;
          color: #721c24;
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
          text-align: center;
          padding: 20px;
        ">
          <div>
            <h3 style="margin: 0 0 10px 0;">Map Loading Error</h3>
            <p id="map-error-message" style="margin: 0; font-size: 14px;"></p>
            <p style="margin: 10px 0 0 0; font-size: 12px; color: #856404;">
              Please refresh the page or check the browser console for details.
            </p>
          </div>
        </div>
      `;
      const msgEl = self.el.querySelector('#map-error-message');
      if (msgEl) msgEl.textContent = message;
    }
  },

  setupMobileGestures() {
    const self = this as unknown as LiveViewHookContext;

    // Long press timer
    let longPressTimer: ReturnType<typeof setTimeout> | null = null;
    let startX = 0;
    let startY = 0;
    const longPressDuration = 750; // 750ms for long press
    const movementThreshold = 10; // pixels

    const handleLongPress = (clientX: number, clientY: number) => {
      const latlng = self.map!.containerPointToLatLng(
        L.point(clientX, clientY),
      );

      // Find the nearest marker
      let nearestMarker: APRSMarker | null = null;
      let minDistance = Infinity;

      self.markers.forEach((marker) => {
        const markerLatLng = marker.getLatLng();
        const distance = latlng.distanceTo(markerLatLng);
        if (distance < minDistance && distance < 50) {
          // Within 50 meters
          minDistance = distance;
          nearestMarker = marker;
        }
      });

      if (nearestMarker) {
        const aprsMarker = nearestMarker as APRSMarker;
        if (aprsMarker._markerId) {
          self.userOpenedPopupMarkerId = aprsMarker._markerId;
        }
        nearestMarker.openPopup();
      }
    };

    self.map!.on("touchstart", (e: LeafletTouchEvent) => {
      if (e.originalEvent.touches.length !== 1) return;

      const touch = e.originalEvent.touches[0];
      startX = touch.clientX;
      startY = touch.clientY;

      longPressTimer = setTimeout(() => {
        if (!self.isDestroyed && self.map) {
          handleLongPress(touch.clientX, touch.clientY);
        }
      }, longPressDuration);
    });

    self.map!.on("touchmove", (e: LeafletTouchEvent) => {
      if (!longPressTimer) return;

      const touch = e.originalEvent.touches[0];
      const deltaX = Math.abs(touch.clientX - startX);
      const deltaY = Math.abs(touch.clientY - startY);

      // Cancel long press if moved too much
      if (deltaX > movementThreshold || deltaY > movementThreshold) {
        clearTimeout(longPressTimer);
        longPressTimer = null;
      }
    });

    self.map!.on("touchend", () => {
      if (longPressTimer) {
        clearTimeout(longPressTimer);
        longPressTimer = null;
      }
    });

    self.map!.on("touchcancel", () => {
      if (longPressTimer) {
        clearTimeout(longPressTimer);
        longPressTimer = null;
      }
    });
  },

  setupPopupNavigation() {
    const self = this as unknown as LiveViewHookContext;

    // Store the event handler so we can remove it later
    self.popupNavigationHandler = (e: Event) => {
      const target = e.target;
      if (!(target instanceof Element)) {
        return;
      }

      // Check if clicked element or its parent is a LiveView navigation link
      const navLink = target.closest(".aprs-lv-link");

      if (navLink instanceof HTMLAnchorElement && navLink.href) {
        e.preventDefault();
        e.stopPropagation();

        // Use Phoenix LiveView's built-in navigation
        const liveSocket = getLiveSocket();
        if (liveSocket) {
          try {
            liveSocket.pushHistoryPatch(navLink.href, "push", navLink);
            return;
          } catch (error) {
            console.warn("LiveView navigation failed, falling back to full navigation", error);
          }
        }
        // Fallback to regular navigation if LiveView socket is not available or patching fails
        window.location.href = navLink.href;
      }
    };

    // Use event delegation to handle clicks on popup navigation links
    document.addEventListener("click", self.popupNavigationHandler);
  },

  setupLiveViewHandlers() {
    const self = this as unknown as LiveViewHookContext;
    // Add single marker
    self.handleEvent("add_marker", (data: MarkerData) => {
      // Check if map is ready before adding markers
      if (!self.map || !self.markerLayer) {
        console.warn("Map not ready, queueing marker for later:", data.id);
        // Queue the marker to be added when map is ready
        if (!self.pendingMarkers) {
          self.pendingMarkers = [];
        }
        self.pendingMarkers.push(data);
        return;
      }
      self.addMarker(data);
    });

    // Add multiple markers at once
    self.handleEvent("add_markers", (data: { markers: MarkerData[] }) => {
      if (data.markers && Array.isArray(data.markers)) {
        // Check if map is ready before adding markers
        if (!self.map || !self.markerLayer) {
          console.warn("Map not ready, queueing markers for later");
          // Queue the markers to be added when map is ready
          if (!self.pendingMarkers) {
            self.pendingMarkers = [];
          }
          self.pendingMarkers.push(...data.markers);
          return;
        }
        data.markers.forEach((marker) => self.addMarker(marker));
      }
    });

    // Remove marker
    self.handleEvent("remove_marker", (data: { id: string }) => {
      self.removeMarker(data.id);
    });

    // Remove markers in batch
    self.handleEvent("remove_markers_batch", (data: { ids: string[] }) => {
      data.ids.forEach((id) => self.removeMarker(id));
    });

    // Clear all markers
    self.handleEvent("clear_markers", () => {
      self.clearAllMarkers();
    });

    // Update marker
    self.handleEvent("update_marker", (data: MarkerData) => {
      self.updateMarker(data);
    });

    // Zoom to location
    self.handleEvent(
      "zoom_to_location",
      (data: { lat: number; lng: number; zoom?: number }) => {
        if (!self.map) {
          console.error("Map not initialized, cannot zoom");
          return;
        }

        if (data.lat && data.lng) {
          const lat = parseFloat(data.lat.toString());
          const lng = parseFloat(data.lng.toString());
          const zoom = parseInt(data.zoom?.toString() || "12");

          // Validate coordinates
          if (!isValidCoordinate(lat, lng)) {
            console.error("Invalid coordinates for zoom:", lat, lng);
            return;
          }

          if (isNaN(zoom) || zoom < 1 || zoom > 20) {
            console.error("Invalid zoom level:", zoom);
            return;
          }

          try {
            // Check element dimensions before zoom
            const beforeRect = self.el.getBoundingClientRect();

            // Force map size recalculation before zoom
            self.map.invalidateSize();

            // Use a slight delay to ensure map is ready
            setTimeout(() => {
              if (self.map) {
                // Generate a unique ID for this programmatic move
                const moveId = `move_${Date.now()}_${Math.random()}`;
                self.programmaticMoveId = moveId;

                // Clear any existing timeout
                if (self.programmaticMoveTimeout) {
                  clearTimeout(self.programmaticMoveTimeout);
                }

                // Set a timeout to clear the programmatic move flag
                // This ensures we don't block user interactions indefinitely
                self.programmaticMoveTimeout = setTimeout(() => {
                  if (self.programmaticMoveId === moveId) {
                    self.programmaticMoveId = undefined;
                  }
                }, 1500);

                self.map.setView([lat, lng], zoom, {
                  animate: true,
                  duration: 1,
                });

                // Check element dimensions after zoom
                const dimensionCheckTimeout = setTimeout(() => {
                  // Check if map still exists and not destroyed
                  if (!self.map || self.isDestroyed) {
                    return;
                  }

                  const afterRect = self.el.getBoundingClientRect();

                  if (afterRect.width === 0 || afterRect.height === 0) {
                    console.error("Map element lost dimensions after zoom!");
                    // Try to restore dimensions
                    self.el.style.width = "100vw";
                    self.el.style.height = "100vh";
                    if (self.map) {
                      self.map.invalidateSize();
                    }
                  }
                }, 1000);

                // Store timeout for cleanup
                if (!self.cleanupTimeouts) {
                  self.cleanupTimeouts = [];
                }
                self.cleanupTimeouts.push(dimensionCheckTimeout);
              }
            }, 100);
          } catch (error) {
            console.error("Error during zoom operation:", error);
          }
        } else {
          console.warn("Missing lat/lng data for zoom operation:", data);
        }
      },
    );

    // Handle geolocation requests
    self.handleEvent("request_geolocation", () => {
      if ("geolocation" in navigator) {
        navigator.geolocation.getCurrentPosition(
          (position) => {
            if (
              !self.pushEvent ||
              typeof self.pushEvent !== "function" ||
              self.isDestroyed
            ) {
              return;
            }
            const { latitude, longitude } = position.coords;
            self.pushEvent("set_location", { lat: latitude, lng: longitude });
          },
          (error) => {
            if (
              !self.pushEvent ||
              typeof self.pushEvent !== "function" ||
              self.isDestroyed
            ) {
              return;
            }
            console.warn("Geolocation error:", error.message);
            self.pushEvent("geolocation_error", { error: error.message });
          },
        );
      } else {
        if (
          !self.pushEvent ||
          typeof self.pushEvent !== "function" ||
          self.isDestroyed
        ) {
          return;
        }
        console.warn("Geolocation not available");
        self.pushEvent("geolocation_error", {
          error: "Geolocation not supported",
        });
      }
    });

    // Toggle trails visibility
    self.handleEvent("toggle_trails", (data: { show: boolean }) => {
      if (self.trailManager) {
        self.trailManager.setShowTrails(data.show);
      }
    });

    // Handle trail duration updates from LiveView
    self.handleEvent(
      "update_trail_duration",
      (data: { duration_hours: number }) => {
        if (self.trailManager) {
          self.trailManager.setTrailDuration(data.duration_hours);
        }
      },
    );

    // Handle new packets from LiveView
    self.handleEvent("new_packet", (data: MarkerData) => {
      try {
        // Skip if context is lost
        if (!self || !self.map || self.isDestroyed) {
          console.warn(
            "Map context not ready or destroyed, skipping new packet",
          );
          return;
        }

        // Check if map exists and has the hasLayer method
        if (!self.map.hasLayer) {
          console.warn(
            "Map hasLayer method not available, skipping new packet",
          );
          return;
        }

        // If heat layer is visible, we're in heat map mode - skip individual markers
        if (self.heatLayer && self.map.hasLayer(self.heatLayer)) {
          return;
        }

        // Check if there's already a marker for this callsign
        const incomingCallsign =
          data.callsign_group || data.callsign || data.id;

        if (incomingCallsign) {
          // Find existing live markers for this callsign using reverse index for O(1) lookup
          const markersToConvert: string[] = [];

          // Ensure markerStates exists
          if (!self.markerStates) {
            console.warn("markerStates not initialized, skipping conversion");
            return;
          }

          const existingIds = self.callsignIndex?.get(incomingCallsign);
          if (existingIds) {
            for (const id of existingIds) {
              const state = self.markerStates.get(id);
              if (
                state &&
                (!state.historical || state.is_most_recent_for_callsign) &&
                String(id) !== String(data.id)
              ) {
                markersToConvert.push(String(id));
              }
            }
          }

          // Convert existing live markers to historical dots by updating their icon
          markersToConvert.forEach((id) => {
            if (!self.markers || !self.markerStates) {
              console.warn(
                "markers or markerStates not available during conversion",
              );
              return;
            }
            const existingMarker = self.markers.get(id);
            const existingState = self.markerStates.get(id);

            if (existingMarker && existingState) {
              // Update the state to historical
              existingState.historical = true;
              existingState.is_most_recent_for_callsign = false;

              // Create new historical dot icon
              const historicalIconData = {
                id: String(id),
                lat: existingState.lat,
                lng: existingState.lng,
                callsign: existingState.callsign || incomingCallsign,
                callsign_group:
                  existingState.callsign_group || incomingCallsign,
                symbol_table_id: existingState.symbol_table,
                symbol_code: existingState.symbol_code,
                historical: true,
                is_most_recent_for_callsign: false,
                popup: existingState.popup,
              };

              // Update the marker's icon to show as a dot
              existingMarker.setIcon(self.createMarkerIcon(historicalIconData));

              // Mark as historical
              (existingMarker as APRSMarker)._isHistorical = true;
            }
          });
        }

        // Add the new marker as the most recent for this callsign
        // Check if openPopup is explicitly set to false to prevent interrupting user
        const shouldOpenPopup = data.openPopup !== false;
        self.addMarker({
          ...data,
          historical: false,
          is_most_recent_for_callsign: true,
          callsign_group:
            data.callsign_group || data.callsign || incomingCallsign,
          popup: data.popup,
          openPopup: shouldOpenPopup,
        });
      } catch (error) {
        console.error("Error in new_packet handler:", error);
      }
    });

    // Handle batched new packets from LiveView (performance optimization)
    self.handleEvent(
      "new_packets",
      (payload: {
        packets: MarkerData[];
        convert_to_historical?: string[];
      }) => {
        try {
          if (!self || !self.map || self.isDestroyed) return;
          if (!self.map.hasLayer) return;
          if (self.heatLayer && self.map.hasLayer(self.heatLayer)) return;
          if (!self.markerStates || !self.markers) return;

          const packets = payload.packets;
          if (!packets || packets.length === 0) return;

          // Phase 1: Find markers to convert to historical dots.
          // If the server provides convert_to_historical callsign keys, use
          // those for a targeted lookup instead of scanning all markerStates.
          const serverConvertKeys = payload.convert_to_historical;
          let markersToConvert: string[] = [];

          // Single pass: build new IDs set and callsigns to convert
          const allNewIds = new Set<string>();
          const callsignsToConvert: Set<string> = new Set();
          if (serverConvertKeys && serverConvertKeys.length > 0) {
            for (const cs of serverConvertKeys) callsignsToConvert.add(cs);
            for (const data of packets) {
              if (data.id) allNewIds.add(String(data.id));
            }
          } else {
            for (const data of packets) {
              if (data.id) allNewIds.add(String(data.id));
              const cs = data.callsign_group || data.callsign || data.id;
              if (cs) callsignsToConvert.add(cs);
            }
          }

          // Use callsignIndex for O(1) lookup per callsign instead of scanning all markerStates
          if (self.callsignIndex) {
            for (const cs of callsignsToConvert) {
              const ids = self.callsignIndex.get(cs);
              if (!ids) continue;
              for (const id of ids) {
                if (allNewIds.has(String(id))) continue;
                const state = self.markerStates.get(id);
                if (
                  state &&
                  (!state.historical || state.is_most_recent_for_callsign)
                ) {
                  markersToConvert.push(String(id));
                }
              }
            }
          }

          // Phase 2: Convert all existing live markers to historical dots
          for (const id of markersToConvert) {
            const existingMarker = self.markers.get(id);
            const existingState = self.markerStates.get(id);
            if (existingMarker && existingState) {
              existingState.historical = true;
              existingState.is_most_recent_for_callsign = false;
              const incomingCallsign =
                existingState.callsign_group || existingState.callsign;
              const historicalIconData = {
                id: String(id),
                lat: existingState.lat,
                lng: existingState.lng,
                callsign: existingState.callsign || incomingCallsign,
                callsign_group:
                  existingState.callsign_group || incomingCallsign,
                symbol_table_id: existingState.symbol_table,
                symbol_code: existingState.symbol_code,
                historical: true,
                is_most_recent_for_callsign: false,
                popup: existingState.popup,
              };
              existingMarker.setIcon(self.createMarkerIcon(historicalIconData));
              (existingMarker as APRSMarker)._isHistorical = true;
            }
          }

          // Phase 3: Add all new markers
          for (const data of packets) {
            const shouldOpenPopup = data.openPopup !== false;
            const incomingCallsign =
              data.callsign_group || data.callsign || data.id;
            self.addMarker({
              ...data,
              historical: false,
              is_most_recent_for_callsign: true,
              callsign_group: incomingCallsign,
              popup: data.popup,
              openPopup: shouldOpenPopup,
            });
          }
        } catch (error) {
          console.error("Error in new_packets handler:", error);
        }
      },
    );

    // Handle highlighting the latest packet (open its popup)
    self.currentPopupMarkerId = null;
    self.userOpenedPopupMarkerId = null;
    self.handleEvent("highlight_packet", (data: { id: string }) => {
      if (!data.id || !self.markers || !self.markerStates) return;
      if (self.userOpenedPopupMarkerId) return;

      // Close previous popup if open
      if (
        self.currentPopupMarkerId &&
        self.markers.has(self.currentPopupMarkerId)
      ) {
        const prevMarker = self.markers.get(self.currentPopupMarkerId);
        if (prevMarker && prevMarker.closePopup) prevMarker.closePopup();
      }
      // Try to open popup directly first if marker exists
      const marker = self.markers.get(data.id);
      if (marker && marker.openPopup) {
        marker.openPopup();
      } else {
        // Fallback: re-add marker with openPopup flag if it doesn't exist
        const markerData = self.markerStates.get(data.id);
        if (markerData) {
          self.addMarker({ ...markerData, id: data.id, openPopup: true });
        }
      }
      self.currentPopupMarkerId = data.id;
    });

    // Handle historical packets during replay
    self.handleEvent("historical_packet", (data: MarkerData) => {
      self.addMarker({
        ...data,
        historical: true,
        popup: data.popup,
      });
    });

    // Handle bulk loading of historical packets
    // Server pre-sorts by callsign group with chronological order within each group
    self.handleEvent(
      "add_historical_packets",
      (data: { packets: MarkerData[] }) => {
        if (data.packets && Array.isArray(data.packets)) {
          for (const packet of data.packets) {
            try {
              self.addMarker({
                ...packet,
                historical: true,
                popup: packet.popup,
              });
            } catch (error) {
              console.error("Error adding historical packet:", error, packet);
            }
          }
        }
      },
    );

    // Handle progressive loading of historical packets (batch processing)
    // Server pre-sorts by callsign group with chronological order within each group
    self.handleEvent(
      "add_historical_packets_batch",
      (data: { packets: MarkerData[]; batch: number; is_final: boolean }) => {
        console.debug("Received add_historical_packets_batch event:", {
          packetCount: data.packets?.length || 0,
          batch: data.batch,
          is_final: data.is_final,
        });
        try {
          if (data.packets && Array.isArray(data.packets)) {
            for (const packet of data.packets) {
              try {
                self.addMarker({
                  ...packet,
                  historical: true,
                  popup: packet.popup,
                });
              } catch (error) {
                console.error(
                  "Error adding historical packet:",
                  error,
                  packet,
                );
              }
            }
          }
        } catch (error) {
          console.error("Error processing historical packets batch:", error);
        }
      },
    );

    // Handle refresh markers event
    self.handleEvent("refresh_markers", () => {
      // Remove markers that are outside current bounds
      if (self.map) {
        const bounds = self.map.getBounds();
        self.removeMarkersOutsideBounds(bounds);
      }
    });

    // Handle clearing historical packets
    self.handleEvent("clear_historical_packets", () => {
      // Remove only historical markers (preserve live markers and their trails)
      const markersToRemove: string[] = [];
      self.markers!.forEach((marker: APRSMarker, id: string) => {
        const markerState = self.markerStates!.get(String(id));
        // Only remove markers that are explicitly historical
        if (
          (marker as APRSMarker)._isHistorical ||
          (markerState && markerState.historical)
        ) {
          markersToRemove.push(String(id));
        }
      });

      // Remove historical markers without affecting trails of live markers
      markersToRemove.forEach((id) => {
        const marker = self.markers!.get(id);
        const markerState = self.markerStates!.get(id);

        if (marker) {
          // Remove from map and storage
          self.markerLayer!.removeLayer(marker);
          self.markers!.delete(id);
          self.markerStates!.delete(id);

          // We now NEVER remove trails when clearing historical packets
          // This preserves the historical trail visibility
        }
      });

      // Make sure trail layer is visible and on top
      if (self.trailLayer && self.trailLayer.bringToFront) {
        self.trailLayer.bringToFront();
      }
    });

    // Handle drawing RF path lines when hovering over a station
    self.handleEvent(
      "draw_rf_path",
      (data: {
        station_lat: number;
        station_lng: number;
        path_stations: Array<{ callsign: string; lat: number; lng: number }>;
      }) => {
        if (!self.map || !data.path_stations || data.path_stations.length === 0)
          return;

        // Validate initial station coordinates
        if (!isFinite(data.station_lat) || !isFinite(data.station_lng)) {
          console.warn("Invalid initial station coordinates for RF path:", {
            lat: data.station_lat,
            lng: data.station_lng,
          });
          return;
        }

        // Clear any existing path lines
        if (self.rfPathLines) {
          self.rfPathLines.forEach((line) => self.map!.removeLayer(line));
        }
        self.rfPathLines = [];

        // Draw lines from station to each digipeater/igate in sequence
        let prevLat = data.station_lat;
        let prevLng = data.station_lng;

        data.path_stations.forEach((station, index) => {
          // Validate coordinates before drawing
          if (
            !isFinite(prevLat) ||
            !isFinite(prevLng) ||
            !isFinite(station.lat) ||
            !isFinite(station.lng)
          ) {
            console.warn("Invalid coordinates for RF path:", {
              prevLat,
              prevLng,
              station,
            });
            return;
          }

          // Draw line from previous position to this station
          const line = L.polyline(
            [
              [prevLat, prevLng],
              [station.lat, station.lng],
            ],
            {
              color: "#FF6B6B",
              weight: 3,
              opacity: 0.8,
              dashArray: index === 0 ? undefined : "5, 10", // Solid line for first hop, dashed for subsequent
            },
          );

          line.addTo(self.map!);
          self.rfPathLines.push(line);

          // Add a small circle marker at the digipeater/igate location
          const circle = L.circleMarker([station.lat, station.lng], {
            radius: 6,
            fillColor: "#2563eb",
            color: "#fff",
            weight: 2,
            opacity: 1,
            fillOpacity: 0.8,
          });

          circle.bindTooltip(station.callsign, {
            permanent: false,
            direction: "top",
            offset: [0, -10],
          });

          circle.addTo(self.map!);
          self.rfPathLines.push(circle);

          // Update prev position for next line
          prevLat = station.lat;
          prevLng = station.lng;
        });
      },
    );

    // Handle clearing RF path lines
    self.handleEvent("clear_rf_path", () => {
      self.clearRfPathLines();
    });

    // Resize map when slideover toggles (class changes handled by LiveView JS commands)
    self.mapResizeHandler = () => {
      setTimeout(() => {
        if (self.map) {
          self.map.invalidateSize();
        }
      }, 350);
    };
    self.el.addEventListener("phx:map-resize", self.mapResizeHandler);

    // Handle bounds-based marker filtering
    self.handleEvent(
      "filter_markers_by_bounds",
      (data: { bounds: BoundsData }) => {
        if (data.bounds) {
          // Create Leaflet bounds object from server data
          const bounds = L.latLngBounds(
            [data.bounds.south, data.bounds.west],
            [data.bounds.north, data.bounds.east],
          );
          self.removeMarkersOutsideBounds(bounds);
        }
      },
    );

    // Handle clearing all markers and reloading visible ones
    self.handleEvent("clear_and_reload_markers", () => {
      // This event is just a trigger - the server will handle clearing and adding markers
    });

    // Handle clear_all_markers event
    self.handleEvent("clear_all_markers", () => {
      // Clear trail line if present
      if (self.trailLine) {
        self.map?.removeLayer(self.trailLine);
        self.trailLine = undefined;
      }

      // Modified to preserve historical trails - only removes non-historical markers
      self.clearAllMarkers();

      // Force trail layer to front to ensure visibility
      if (self.trailLayer && self.trailLayer.bringToFront) {
        self.trailLayer.bringToFront();
      }
    });

    // Handle heat map data for low zoom levels
    self.handleEvent("show_heat_map", (data: { heat_points: HeatLatLng[] }) => {
      try {
        if (!self.map || self.isDestroyed) {
          console.warn("Map not ready or destroyed, skipping heat map update");
          return;
        }

        // Clear trail line if present
        if (self.trailLine) {
          self.map.removeLayer(self.trailLine);
          self.trailLine = undefined;
        }

        if (!L.heatLayer) {
          console.error("Leaflet.heat plugin not loaded!");
          return;
        }

        if (!self.heatLayer) {
          try {
            self.heatLayer = L.heatLayer([], {
              radius: 25,
              blur: 15,
              maxZoom: 8,
              gradient: {
                0.4: "blue",
                0.65: "lime",
                0.85: "yellow",
                1.0: "red",
              },
            });
          } catch (error) {
            console.error("Failed to create heat layer:", error);
            return;
          }
        }

        // Convert heat points to format expected by Leaflet.heat.
        // Server pre-normalizes intensity to [0, 1] but validate defensively
        // so malformed rows can't distort the whole heat layer.
        const heatData = data.heat_points
          .filter(
            (point) =>
              isValidCoordinate(point.lat, point.lng) &&
              typeof point.intensity === "number" &&
              isFinite(point.intensity) &&
              point.intensity >= 0 &&
              point.intensity <= 1,
          )
          .map(
            (point) =>
              [point.lat, point.lng, point.intensity] as [
                number,
                number,
                number,
              ],
          );

        // Update heat layer data
        self.heatLayer.setLatLngs(heatData);

        // Show heat layer and hide marker layer
        if (!self.map.hasLayer(self.heatLayer)) {
          self.map.addLayer(self.heatLayer);
        }
        if (self.markerLayer && self.map.hasLayer(self.markerLayer)) {
          self.map.removeLayer(self.markerLayer);
        }
      } catch (error) {
        console.error("Error in show_heat_map handler:", error);
      }
    });

    // Handle switching back to markers
    self.handleEvent("show_markers", () => {
      try {
        if (!self.map || self.isDestroyed) {
          console.warn("Map not ready or destroyed, skipping marker display");
          return;
        }

        // Clear trail line if present
        if (self.trailLine) {
          self.map.removeLayer(self.trailLine);
          self.trailLine = undefined;
        }

        // Hide heat layer and show marker layer
        if (self.heatLayer && self.map.hasLayer(self.heatLayer)) {
          self.map.removeLayer(self.heatLayer);
        }
        if (self.markerLayer && !self.map.hasLayer(self.markerLayer)) {
          self.map.addLayer(self.markerLayer);
        }
      } catch (error) {
        console.error("Error in show_markers handler:", error);
      }
    });

    // Handle trail line display for tracked callsigns at low zoom
    self.handleEvent("show_trail_line", (payload: BaseEventPayload) => {
      const data = payload as { callsign: string; points: Array<{ lat: number; lng: number; timestamp: string }> };

      if (!self.map) {
        console.warn("Map not initialized, cannot show trail line");
        return;
      }

      // Clear existing trail line if present
      if (self.trailLine) {
        self.map.removeLayer(self.trailLine);
        self.trailLine = undefined;
      }

      // Clear heat map if present (switching from heat map to trail line)
      if (self.heatLayer) {
        self.map.removeLayer(self.heatLayer);
        self.heatLayer = undefined;
      }

      // Filter out any invalid coordinates before building the polyline —
      // NaN / Infinity / out-of-range values make Leaflet render junk.
      const validPoints = data.points.filter((point) =>
        isValidCoordinate(point.lat, point.lng),
      );

      // If we have less than 2 valid points, don't draw a line
      if (validPoints.length < 2) {
        console.debug(
          `Not enough valid points for trail line: ${validPoints.length} of ${data.points.length}`,
        );
        return;
      }

      // Unwrap longitudes so a trail that crosses the antimeridian draws as a
      // short hop instead of a polyline that spans the whole world.
      const latlngs = unwrapLongitudes(
        validPoints.map((p) => [p.lat, p.lng] as [number, number]),
      );

      // Access Leaflet from window
      const L = (window as any).L;

      // Create polyline
      self.trailLine = L.polyline(latlngs, {
        color: '#3B82F6',      // blue-500
        weight: 3,
        opacity: 0.8,
        lineCap: 'round',
        lineJoin: 'round',
        interactive: true
      });

      // Add to map
      self.trailLine.addTo(self.map);

      console.debug(`Trail line added for ${data.callsign} with ${validPoints.length} points`);
    });

    // Handle clearing trail line
    self.handleEvent("clear_trail_line", (_payload: BaseEventPayload) => {
      if (!self.map) {
        return;
      }

      // Remove trail line if present
      if (self.trailLine) {
        self.map.removeLayer(self.trailLine);
        self.trailLine = undefined;
        console.debug("Trail line cleared");
      }
    });
  },

  sendBoundsToServer() {
    const self = this as unknown as LiveViewHookContext;
    console.debug(
      "sendBoundsToServer called, map:",
      !!self.map,
      "isDestroyed:",
      self.isDestroyed,
    );
    if (!self.map || self.isDestroyed) return;

    try {
      const bounds = self.map!.getBounds();
      const center = self.map!.getCenter();
      const zoom = self.map!.getZoom();

      // We're no longer removing markers outside bounds during normal panning/zooming
      // to preserve historical positions and the current marker
      // self.removeMarkersOutsideBounds(bounds);

      // Use direct pushEvent call with proper context
      if (self.pushEvent && typeof self.pushEvent === "function") {
        const boundsData = {
          bounds: {
            north: bounds.getNorth(),
            south: bounds.getSouth(),
            east: bounds.getEast(),
            west: bounds.getWest(),
          },
          center: {
            lat: center.lat,
            lng: center.lng,
          },
          zoom: zoom,
        };
        console.debug("Sending bounds_changed event:", boundsData);
        self.pushEvent("bounds_changed", boundsData);
      }
    } catch (error) {
      console.error("Error sending bounds to server:", error);
    }
  },

  addMarker(data: MarkerData & { openPopup?: boolean }) {
    const self = this as unknown as LiveViewHookContext;
    const L = window.L;
    if (
      !data ||
      !data.id ||
      data.lat == null ||
      data.lng == null ||
      typeof data.lat !== "number" ||
      typeof data.lng !== "number"
    ) {
      console.warn("Invalid marker data:", data);
      return;
    }

    // Ensure maps are initialized
    if (!self.markers || !self.markerStates || !self.markerLayer) {
      console.warn("Map data structures not initialized, skipping marker add");
      return;
    }

    // Additional check to ensure map is fully ready
    if (
      !self.map ||
      !self.map.getContainer() ||
      typeof self.map.getZoom !== "function"
    ) {
      console.warn("Map not fully initialized, queueing marker:", data.id);
      if (!self.pendingMarkers) {
        self.pendingMarkers = [];
      }
      self.pendingMarkers.push(data);
      return;
    }

    // Extract coordinates - handle both direct values and nested arrays
    const lat = extractCoordinate(data.lat);
    const lng = extractCoordinate(data.lng);

    // Validate coordinates
    if (!isValidCoordinate(lat, lng)) {
      console.warn("Invalid coordinates for marker:", {
        id: data.id,
        lat,
        lng,
        callsign: data.callsign,
        rawLat: data.lat,
        rawLng: data.lng,
      });
      return;
    }

    // Check if marker already exists with same position and data
    const existingMarker = self.markers.get(data.id);
    const existingState = self.markerStates.get(data.id);

    if (existingMarker && existingState) {
      // Check if marker needs updating
      const currentPos = existingMarker.getLatLng();
      const positionChanged =
        Math.abs(currentPos.lat - lat) > 0.0001 ||
        Math.abs(currentPos.lng - lng) > 0.0001;
      const dataChanged =
        existingState.symbol_table !== data.symbol_table_id ||
        existingState.symbol_code !== data.symbol_code ||
        existingState.popup !== data.popup;

      if (positionChanged && self.trailManager) {
        // Position changed, update trail
        const isHistoricalDot =
          data.historical && !data.is_most_recent_for_callsign;
        const timestamp = parseTimestamp(data.timestamp);
        // Use callsign_group for proper trail grouping
        const trailId = getTrailId(data);

        self.trailManager.addPosition(
          trailId,
          lat,
          lng,
          timestamp,
          isHistoricalDot,
          data.path,
        );
      }

      if (!positionChanged && !dataChanged) {
        // No changes needed, skip update
        // But if openPopup is requested and no user popup is locked, open it
        if (data.openPopup && existingMarker.openPopup && !self.userOpenedPopupMarkerId) {
          existingMarker.openPopup();
        }
        return;
      }
    }

    // Remove existing marker if it exists (without affecting trails since we're just replacing it)
    self.removeMarkerWithoutTrail(data.id);

    // When adding a "most recent" marker, convert any existing "current" markers
    // for the same callsign_group to historical dots. This prevents duplicate labels
    // when multiple senders report the same object (e.g. two stations tracking one radiosonde).
    if (data.is_most_recent_for_callsign && self.callsignIndex) {
      const csGroup = data.callsign_group || data.callsign;
      if (csGroup) {
        const existingIds = self.callsignIndex.get(csGroup);
        if (existingIds) {
          for (const eid of existingIds) {
            if (String(eid) === String(data.id)) continue;
            const eState = self.markerStates.get(eid);
            const eMarker = self.markers.get(eid);
            if (
              eState &&
              eMarker &&
              eState.is_most_recent_for_callsign
            ) {
              eState.historical = true;
              eState.is_most_recent_for_callsign = false;
              eMarker.setIcon(
                self.createMarkerIcon({
                  id: String(eid),
                  lat: eState.lat,
                  lng: eState.lng,
                  callsign: eState.callsign || csGroup,
                  callsign_group: eState.callsign_group || csGroup,
                  symbol_table_id: eState.symbol_table,
                  symbol_code: eState.symbol_code,
                  historical: true,
                  is_most_recent_for_callsign: false,
                  popup: eState.popup,
                }),
              );
              (eMarker as APRSMarker)._isHistorical = true;
            }
          }
        }
      }
    }

    // Create marker - use simple dot for older historical positions, APRS icon for most recent
    const marker = L.marker([lat, lng], {
      icon: self.createMarkerIcon(data),
    });
    (marker as APRSMarker)._markerId = data.id;

    // Add popup if content provided
    if (data.popup) {
      marker.bindPopup(data.popup, { autoPan: false });

      // Handle popup close events - check if hook is still connected
      marker.on("popupclose", () => {
        if (self.userOpenedPopupMarkerId === data.id) {
          self.userOpenedPopupMarkerId = null;
        }
        if (
          !self.isDestroyed &&
          self.pushEvent &&
          typeof self.pushEvent === "function"
        ) {
          try {
            self.pushEvent("popup_closed", {});
          } catch (e) {
            console.debug("Unable to send popup_closed event:", e);
          }
        }
      });
    }

    // Handle marker click
    marker.on("click", (e: any) => {
      // Don't open popup if the marker is managed by OMS and there are nearby markers
      // The OMS will handle the click and open the popup after spiderfying
      if (self.oms && marker._oms && marker._omsData) {
        // Marker is currently spiderfied, allow normal popup
        self.userOpenedPopupMarkerId = data.id;
        if (marker.openPopup) marker.openPopup();
      } else if (!self.oms || !marker._oms) {
        // Normal marker not managed by OMS
        self.userOpenedPopupMarkerId = data.id;
        if (marker.openPopup) marker.openPopup();
      }
      // If marker._oms is true but no _omsData, let OMS handle it (will spiderfy)

      // Bring the clicked marker to front using a counter instead of DOM scan
      if (marker.getElement) {
        const element = marker.getElement();
        if (element) {
          self.markerZIndexCounter = (self.markerZIndexCounter || 1000) + 1;
          element.style.zIndex = self.markerZIndexCounter.toString();
        }
      }

      // Use bound pushEvent function to preserve context
      if (
        self.pushEvent &&
        typeof self.pushEvent === "function" &&
        !self.isDestroyed
      ) {
        safePushEvent(self.pushEvent.bind(self), "marker_clicked", {
          id: data.id,
          callsign: data.callsign,
          lat: lat,
          lng: lng,
        });
      }
    });

    // Handle marker hover for RF path visualization
    if (data.path && data.path.trim() !== "") {
      marker.on("mouseover", () => {
        // Check if LiveView is still connected before sending event
        if (
          self.pushEvent &&
          typeof self.pushEvent === "function" &&
          !self.isDestroyed
        ) {
          try {
            self.pushEvent.call(self, "marker_hover_start", {
              id: data.id,
              callsign: data.callsign,
              path: data.path,
              lat: lat,
              lng: lng,
            });
          } catch (error) {
            console.warn("Failed to send marker_hover_start event:", error);
          }
        } else {
          console.warn("LiveView not connected, cannot send hover event");
        }
      });

      marker.on("mouseout", () => {
        // Check if LiveView is still connected before sending event
        if (
          self.pushEvent &&
          typeof self.pushEvent === "function" &&
          !self.isDestroyed
        ) {
          try {
            self.pushEvent.call(self, "marker_hover_end", {
              id: data.id,
            });
          } catch (error) {
            console.warn("Failed to send marker_hover_end event:", error);
          }
        }
      });
    }

    // Mark historical markers for identification
    if (data.historical) {
      (marker as APRSMarker)._isHistorical = true;
    }

    // Add to map and store reference
    marker.addTo(self.markerLayer);
    self.markers.set(data.id, marker);

    // Make sure historical markers and trails stay visible
    if (data.historical || data.is_most_recent_for_callsign) {
      if (self.trailLayer && self.trailLayer.bringToFront) {
        setTimeout(() => self.trailLayer.bringToFront(), 50);
      }
    }

    // Store marker state for optimization
    self.markerStates.set(data.id, {
      lat: lat,
      lng: lng,
      symbol_table: data.symbol_table_id || "/",
      symbol_code: data.symbol_code || ">",
      popup: data.popup,
      historical: data.historical,
      is_most_recent_for_callsign: data.is_most_recent_for_callsign,
      callsign_group: data.callsign_group || data.callsign,
      callsign: data.callsign,
    });

    // Update callsign reverse index
    const csGroup = data.callsign_group || data.callsign;
    if (csGroup && self.callsignIndex) {
      let ids = self.callsignIndex.get(csGroup);
      if (!ids) {
        ids = new Set<string>();
        self.callsignIndex.set(csGroup, ids);
      }
      ids.add(data.id);
    }

    // Initialize trail for new marker - always add to trail for line drawing
    if (self.trailManager) {
      const isHistoricalDot =
        data.historical && !data.is_most_recent_for_callsign;
      const timestamp = parseTimestamp(data.timestamp);
      // Use callsign_group for proper trail grouping
      const trailId = getTrailId(data);

      self.trailManager.addPosition(
        trailId,
        lat,
        lng,
        timestamp,
        isHistoricalDot,
        data.path,
      );
    }

    // Open popup if requested and no user popup is locked
    if (data.openPopup && marker.openPopup && !self.userOpenedPopupMarkerId) {
      marker.openPopup();
    }

    // Add to OMS for overlapping marker handling (only most recent packets with icons)
    // Fallback: if is_most_recent_for_callsign is undefined, exclude historical markers as before
    const shouldAddToOms =
      data.is_most_recent_for_callsign === true ||
      (data.is_most_recent_for_callsign == null &&
        !(marker as APRSMarker)._isHistorical);
    if (
      self.oms &&
      marker &&
      self.map &&
      !marker._isClusterMarker &&
      shouldAddToOms
    ) {
      self.oms.addMarker(marker);
    }
  },

  removeMarker(id: string) {
    const self = this as unknown as LiveViewHookContext;
    // Ensure id is a string
    const markerId = typeof id === "string" ? id : String(id);

    const marker = self.markers!.get(markerId);
    const markerState = self.markerStates!.get(markerId);

    if (marker) {
      try {
        // Remove marker from appropriate layer with safety checks
        if (
          self.markerClusterGroup &&
          self.markerClusterGroup.hasLayer(marker)
        ) {
          // Check if cluster group is ready before removing
          if (
            self.markerClusterGroup._map &&
            self.markerClusterGroup._topClusterLevel
          ) {
            self.markerClusterGroup.removeLayer(marker);
          } else {
            console.warn(
              "Cluster group not ready, skipping marker removal:",
              markerId,
            );
          }
        } else if (self.markerLayer && self.markerLayer.hasLayer(marker)) {
          self.markerLayer.removeLayer(marker);
        }

        self.markers!.delete(markerId);
        self.markerStates!.delete(markerId);
      } catch (error) {
        console.error("Error removing marker:", markerId, error);
        // Still clean up tracking maps even if layer removal failed
        self.markers!.delete(markerId);
        self.markerStates!.delete(markerId);
      }
    }

    // Update callsign reverse index
    if (markerState && self.callsignIndex) {
      const csGroup = markerState.callsign_group || markerState.callsign;
      if (csGroup) {
        const ids = self.callsignIndex.get(csGroup);
        if (ids) {
          ids.delete(markerId);
          if (ids.size === 0) self.callsignIndex.delete(csGroup);
        }
      }
    }

    // Remove trail - use callsign_group for proper trail identification
    if (self.trailManager) {
      const trailId =
        markerState?.callsign_group || markerState?.callsign || markerId;
      self.trailManager.removeTrail(trailId);
    }

    if (self.oms && marker) {
      try {
        self.oms.removeMarker(marker);
      } catch (e) {
        console.debug("Error removing marker from OMS:", e);
      }
    }
  },

  updateMarker(data: MarkerData) {
    const self = this as unknown as LiveViewHookContext;
    if (!data.id) return;

    const existingMarker = self.markers!.get(data.id);
    if (existingMarker) {
      // Update position if provided
      if (data.lat && data.lng) {
        const lat = parseFloat(data.lat.toString());
        const lng = parseFloat(data.lng.toString());
        if (isValidCoordinate(lat, lng)) {
          const currentPos = existingMarker.getLatLng();
          const positionChanged =
            Math.abs(currentPos.lat - lat) > 0.0001 ||
            Math.abs(currentPos.lng - lng) > 0.0001;

          if (positionChanged) {
            existingMarker.setLatLng([lat, lng]);
            if (self.trailManager) {
              const isHistoricalDot =
                data.historical && !data.is_most_recent_for_callsign;
              const timestamp = parseTimestamp(data.timestamp);
              // Use callsign_group for proper trail grouping
              const trailId = getTrailId(data);
              self.trailManager.addPosition(
                trailId,
                lat,
                lng,
                timestamp,
                isHistoricalDot,
                data.path,
              );
            }
          }
        }
      }

      // Update popup if provided
      if (data.popup) {
        existingMarker.setPopupContent(data.popup);
        // Force popup to refresh by unbinding and rebinding
        existingMarker.unbindPopup();
        existingMarker.bindPopup(data.popup, { autoPan: false });
      }
    } else {
      // Marker doesn't exist, create it
      self.addMarker(data);
    }
  },

  clearAllMarkers() {
    const self = this as unknown as LiveViewHookContext;

    // Clear any RF path lines first
    self.clearRfPathLines();

    // Instead of clearing all markers, only remove non-historical and non-current markers
    const markersToPreserve = new Map();
    const statesToPreserve = new Map();

    // Identify historical markers and most recent markers to preserve
    self.markers!.forEach((marker, id) => {
      const markerState = self.markerStates!.get(String(id));
      const isHistorical =
        (marker as APRSMarker)._isHistorical ||
        (markerState && markerState.historical);
      const isMostRecent =
        markerState && markerState.is_most_recent_for_callsign;

      // Keep historical markers and current position markers
      if (isHistorical || isMostRecent) {
        markersToPreserve.set(String(id), marker);
        if (markerState) {
          statesToPreserve.set(String(id), markerState);
        }
      } else {
        // Remove only non-historical, non-current markers
        self.markerLayer!.removeLayer(marker);
      }
    });

    // Replace the markers and states with just the preserved ones
    self.markers!.clear();
    self.markerStates!.clear();
    if (self.callsignIndex) self.callsignIndex.clear();

    markersToPreserve.forEach((marker, id) => {
      self.markers!.set(id, marker);
    });

    statesToPreserve.forEach((state, id) => {
      self.markerStates!.set(id, state);

      // Rebuild callsign index from preserved markers
      if (self.callsignIndex) {
        const csGroup = state.callsign_group || state.callsign;
        if (csGroup) {
          let ids = self.callsignIndex.get(csGroup);
          if (!ids) {
            ids = new Set<string>();
            self.callsignIndex.set(csGroup, ids);
          }
          ids.add(id);
        }
      }
    });

    // Don't clear trails - keep them visible
    // if (self.trailManager) {
    //   self.trailManager.clearAllTrails();
    // }

    // Make sure trail layer is on top
    if (self.trailLayer && self.trailLayer.bringToFront) {
      setTimeout(() => self.trailLayer.bringToFront(), 100);
    }
  },

  removeMarkersOutsideBounds(bounds: L.LatLngBounds) {
    const self = this as unknown as LiveViewHookContext;
    if (!bounds || !self.markers) return;

    const markersToRemove: string[] = [];

    self.markers!.forEach((marker: L.Marker, id: string) => {
      // Check if this is a historical marker or the most recent marker for a callsign
      const markerState = self.markerStates!.get(String(id));
      const isHistorical =
        (marker as APRSMarker)._isHistorical ||
        (markerState && markerState.historical);
      const isMostRecent =
        markerState && markerState.is_most_recent_for_callsign;

      // Always preserve historical markers and the most recent marker for a callsign
      if (isHistorical || isMostRecent) {
        return; // Skip this marker, don't remove it
      }

      const position = marker.getLatLng();
      const lat = position.lat;
      const lng = position.lng;

      // Check latitude bounds (straightforward)
      const latOutOfBounds = lat < bounds.getSouth() || lat > bounds.getNorth();

      // Check longitude bounds (handle potential wrapping)
      let lngOutOfBounds: boolean;
      if (bounds.getWest() <= bounds.getEast()) {
        // Normal case: bounds don't cross antimeridian
        lngOutOfBounds = lng < bounds.getWest() || lng > bounds.getEast();
      } else {
        // Bounds cross antimeridian (e.g., west=170, east=-170)
        lngOutOfBounds = lng < bounds.getWest() && lng > bounds.getEast();
      }

      // Check if marker is outside the current bounds
      if (latOutOfBounds || lngOutOfBounds) {
        markersToRemove.push(id);
      }
    });

    // Remove out-of-bounds markers without affecting trails
    markersToRemove.forEach((id) => self.removeMarkerWithoutTrail(String(id)));
  },

  clearRfPathLines() {
    const self = this as unknown as LiveViewHookContext;
    if (self.rfPathLines && self.rfPathLines.length > 0) {
      self.rfPathLines.forEach((line) => {
        try {
          if (self.map && self.map.hasLayer(line)) {
            self.map.removeLayer(line);
          }
        } catch (e) {
          console.debug("Error removing RF path line:", e);
        }
      });
      self.rfPathLines = [];
    }
  },

  removeMarkerWithoutTrail(id: string) {
    const self = this as unknown as LiveViewHookContext;
    // Ensure id is a string
    const markerId = typeof id === "string" ? id : String(id);

    const marker = self.markers!.get(markerId);

    if (marker) {
      // Get state before deletion for index cleanup
      const state = self.markerStates!.get(markerId);

      try {
        // Remove marker from appropriate layer with safety checks
        if (
          self.markerClusterGroup &&
          self.markerClusterGroup.hasLayer(marker)
        ) {
          // Check if cluster group is ready before removing
          if (
            self.markerClusterGroup._map &&
            self.markerClusterGroup._topClusterLevel
          ) {
            self.markerClusterGroup.removeLayer(marker);
          } else {
            console.warn(
              "Cluster group not ready, skipping marker removal:",
              markerId,
            );
          }
        } else if (self.markerLayer && self.markerLayer.hasLayer(marker)) {
          self.markerLayer.removeLayer(marker);
        }

        // Always clean up the tracking maps
        self.markers!.delete(markerId);
        self.markerStates!.delete(markerId);
        // Note: We intentionally don't remove trails here
      } catch (error) {
        console.error("Error removing marker:", markerId, error);
        // Still clean up tracking maps even if layer removal failed
        self.markers!.delete(markerId);
        self.markerStates!.delete(markerId);
      }

      // Update callsign reverse index
      if (state && self.callsignIndex) {
        const csGroup = state.callsign_group || state.callsign;
        if (csGroup) {
          const ids = self.callsignIndex.get(csGroup);
          if (ids) {
            ids.delete(markerId);
            if (ids.size === 0) self.callsignIndex.delete(csGroup);
          }
        }
      }
    }
  },

  createMarkerIcon(data: MarkerData): L.DivIcon {
    const self = this as unknown as LiveViewHookContext;
    const L = window.L;
    // If this is the most recent for the callsign, always show the APRS icon
    if (data.is_most_recent_for_callsign) {
      // Remove any historical dot at the same position for this callsign
      // Use callsignIndex for O(1) lookup instead of scanning all markers
      if (self.markers && self.markerStates && self.callsignIndex && data.callsign_group) {
        const ids = self.callsignIndex.get(data.callsign_group);
        if (ids) {
          for (const id of ids) {
            const state = self.markerStates.get(String(id));
            if (
              state &&
              state.historical &&
              Math.abs(state.lat - data.lat) < 0.00001 &&
              Math.abs(state.lng - data.lng) < 0.00001
            ) {
              self.removeMarkerWithoutTrail(String(id));
            }
          }
        }
      }
      // Use server-generated symbol HTML if available
      if (data.symbol_table_id || data.symbol_code) {
        return createDivIcon(createSafeAprsSymbolHtml(data, true), {
          iconSize: [120, 32], // Increased width to accommodate callsign label
          iconAnchor: [16, 16],
        });
      }
    }

    // For historical packets that are not the most recent for their callsign,
    // show a simple dot instead of the full APRS symbol
    if (data.historical && !data.is_most_recent_for_callsign) {
      // Always show a red dot for historical positions
      const iconHtml = `<div style="
        width: 8px;
        height: 8px;
        background-color: #FF6B6B;
        border: 2px solid #FFFFFF;
        border-radius: 50%;
        opacity: 0.8;
        box-shadow: 0 0 2px rgba(0,0,0,0.3);
      " title="Historical position for ${escapeHtml(data.callsign || "")}"></div>`;

      return createDivIcon(iconHtml, {
        className: "historical-dot-marker",
        iconSize: [12, 12],
        iconAnchor: [6, 6],
      });
    }

    // Fallback: Use server-generated symbol HTML if available
    if (data.symbol_table_id || data.symbol_code) {
      return createDivIcon(createSafeAprsSymbolHtml(data, false));
    }

    // Final fallback: Simple dot
    const iconHtml = `<div style="
      width: 8px;
      height: 8px;
      background-color: #2563eb;
      border: 2px solid #FFFFFF;
      border-radius: 50%;
      opacity: 0.8;
      box-shadow: 0 0 2px rgba(0,0,0,0.3);
    " title="APRS Station: ${escapeHtml(data.callsign || "")}"></div>`;

    return createDivIcon(iconHtml, {
      iconSize: [12, 12],
      iconAnchor: [6, 6],
    });
  },

  destroyed() {
    const self = this as unknown as LiveViewHookContext;

    // Mark as destroyed immediately
    self.isDestroyed = true;

    // Disable pushEvent to prevent any events from being sent during cleanup
    const originalPushEvent = self.pushEvent;
    self.pushEvent = () => {}; // No-op function

    // Clear interval timer
    if (self.cleanupInterval !== undefined) {
      clearInterval(self.cleanupInterval);
      self.cleanupInterval = undefined;
    }

    // Clear programmatic move timeout
    if (self.programmaticMoveTimeout !== undefined) {
      clearTimeout(self.programmaticMoveTimeout);
      self.programmaticMoveTimeout = undefined;
    }

    // Clear size check timeout
    if (self.sizeCheckTimeout !== undefined) {
      clearTimeout(self.sizeCheckTimeout);
      self.sizeCheckTimeout = undefined;
    }

    // Clear initialization timeout
    if (self.initializationTimeout !== undefined) {
      clearTimeout(self.initializationTimeout);
      self.initializationTimeout = undefined;
    }

    // Clear any dimension check timeouts
    if (self.cleanupTimeouts !== undefined) {
      self.cleanupTimeouts.forEach((timeout: number) => {
        clearTimeout(timeout);
      });
      self.cleanupTimeouts = [];
    }

    // Clean up trail manager
    if (self.trailManager) {
      self.trailManager.destroy();
    }

    // Remove popup navigation event listener
    if (self.popupNavigationHandler) {
      document.removeEventListener("click", self.popupNavigationHandler);
      self.popupNavigationHandler = undefined;
    }

    // Close any open popups before cleanup
    if (self.map !== undefined) {
      try {
        self.map.closePopup();
      } catch (e) {
        console.debug("Error closing popup during cleanup:", e);
      }
    }

    if (self.boundsTimer !== undefined) {
      clearTimeout(self.boundsTimer);
      self.boundsTimer = undefined;
    }
    if (self.resizeHandler !== undefined) {
      window.removeEventListener("resize", self.resizeHandler);
      self.resizeHandler = undefined;
    }
    if (self.mapResizeHandler !== undefined) {
      self.el.removeEventListener("phx:map-resize", self.mapResizeHandler);
      self.mapResizeHandler = undefined;
    }

    // Remove map event handlers
    if (self.map !== undefined && self.mapEventHandlers !== undefined) {
      self.mapEventHandlers.forEach((handler, event) => {
        try {
          self.map.off(event, handler);
        } catch (e) {
          console.debug(`Error removing ${event} handler:`, e);
        }
      });
      self.mapEventHandlers.clear();
    }

    // Clear any RF path lines
    self.clearRfPathLines();

    // Remove all event listeners from markers before clearing layers
    if (self.markers !== undefined) {
      self.markers.forEach((marker: APRSMarker) => {
        try {
          marker.off(); // Remove all event listeners
          if (marker.getPopup()) {
            marker.unbindPopup(); // Unbind popup to prevent events
          }
        } catch (e) {
          console.debug(`Error cleaning up marker:`, e);
        }
      });
    }

    // Clean up OMS (OverlappingMarkerSpiderfier)
    if (self.oms !== undefined) {
      try {
        self.oms.clearMarkers();
        self.oms.clearListeners("click");
        self.oms.clearListeners("spiderfy");
        self.oms.clearListeners("unspiderfy");
        self.oms = undefined;
      } catch (e) {
        console.debug("Error cleaning up OMS:", e);
      }
    }

    // Clean up heat layer
    if (self.heatLayer !== undefined && self.map !== undefined) {
      try {
        if (self.map.hasLayer(self.heatLayer)) {
          self.map.removeLayer(self.heatLayer);
        }
        self.heatLayer = undefined;
      } catch (e) {
        console.debug("Error removing heat layer:", e);
      }
    }

    if (self.markerLayer !== undefined) {
      self.markerLayer!.clearLayers();
      // If it's a cluster group, remove it from the map
      if (self.markerClusterGroup !== undefined && self.map !== undefined) {
        try {
          self.map.removeLayer(self.markerClusterGroup);
          self.markerClusterGroup = undefined;
        } catch (e) {
          console.debug("Error removing cluster group:", e);
        }
      }
    }
    if (self.markers !== undefined) {
      self.markers!.clear();
    }
    if (self.markerStates !== undefined) {
      self.markerStates!.clear();
    }
    if (self.callsignIndex !== undefined) {
      self.callsignIndex!.clear();
    }
    if (self.map !== undefined) {
      self.map!.remove();
      self.map = undefined;
    }

    // Restore original pushEvent (though it won't be used since we're destroyed)
    self.pushEvent = originalPushEvent;
  },
};

// Helper to extract coordinate from various formats (number, string, or nested array)
function extractCoordinate(value: any): number {
  // Handle null/undefined
  if (value === null || value === undefined) {
    return NaN;
  }

  // Handle arrays (sometimes coordinates come as nested arrays)
  if (Array.isArray(value)) {
    // Recursively extract from first element if it's an array
    return extractCoordinate(value[0]);
  }

  // Handle numbers
  if (typeof value === "number") {
    return value;
  }

  // Handle strings
  if (typeof value === "string") {
    return parseFloat(value);
  }

  return NaN;
}


// Helper to create divIcon with common defaults
function createDivIcon(
  html: string,
  options: Partial<{
    className: string;
    iconSize: [number, number];
    iconAnchor: [number, number];
  }> = {},
) {
  return L.divIcon({
    html,
    className: options.className || "",
    iconSize: options.iconSize || [32, 32],
    iconAnchor: options.iconAnchor || [16, 16],
  });
}

// Build marker markup exclusively from validated APRS fields. Packet-derived
// strings are escaped before entering Leaflet's HTML-based divIcon API.
function createSafeAprsSymbolHtml(
  data: { callsign?: string; symbol_table_id?: string; symbol_code?: string },
  includeLabel: boolean,
): string {
  const rawTable = data.symbol_table_id || "/";
  const isOverlay = /^[A-Z0-9]$/.test(rawTable);
  const table = rawTable === "\\" ? "\\" : rawTable === "]" || isOverlay ? "]" : "/";
  const code = getValidSymbolCode(data.symbol_code, table);
  const tableId = table === "/" ? "0" : table === "\\" ? "1" : "2";
  const index = Math.max(0, Math.min(93, code.charCodeAt(0) - 33));
  const position = `${-(index % 16) * 32}px ${-Math.floor(index / 16) * 32}px`;
  const sprite = `/aprs-symbols/aprs-symbols-128-${tableId}@2x.png`;
  const title = escapeHtml(`${rawTable}${code}`);
  const label = includeLabel
    ? `<span class="aprs-callsign">${escapeHtml(data.callsign || "")}</span>`
    : "";

  return `<div class="aprs-marker-symbol" title="${title}" style="width:32px;height:32px;background-image:url('${sprite}');background-position:${position};background-size:512px 192px;background-repeat:no-repeat;image-rendering:pixelated"></div>${label}`;
}

// Helper to validate and fallback symbol code per aprs.fi logic
function getValidSymbolCode(
  symbolCode: string | undefined,
  symbolTableId: string | undefined,
): string {
  if (!symbolCode || symbolCode.length !== 1) {
    return symbolTableId === "\\" ? "O" : ">";
  }
  const code = symbolCode.charCodeAt(0);
  if (code < 33 || code > 126) {
    return symbolTableId === "\\" ? "O" : ">";
  }
  return symbolCode;
}

export default MapAPRSMap;
