<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import { useMeshStore } from '@/stores/mesh'
import type { NodePosition } from '@/stores/mesh'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'

const mesh = useMeshStore()

const mapContainer = ref<HTMLElement | null>(null)
let map: L.Map | null = null
const markersLayer = ref<L.LayerGroup | null>(null)
const linesLayer = ref<L.LayerGroup | null>(null)

// Whether we have any position data to show
const hasPositions = computed(() => mesh.nodePositions.size > 0)

// Location sharing state
const sharingLocation = ref(false)
const locationSource = ref<'browser' | 'device'>('browser')
const locationError = ref('')
const hasDeviceGps = computed(() => mesh.deadmanStatus?.has_gps ?? false)
let geoWatchId: number | null = null

function toggleLocationSharing() {
  if (sharingLocation.value) {
    stopSharing()
  } else {
    startSharing()
  }
}

function switchSource(source: 'browser' | 'device') {
  locationSource.value = source
  if (sharingLocation.value) {
    stopSharing()
    startSharing()
  }
}

function startSharing() {
  locationError.value = ''

  if (locationSource.value === 'browser') {
    if (!navigator.geolocation) {
      locationError.value = 'Geolocation not supported'
      return
    }
    geoWatchId = navigator.geolocation.watchPosition(
      (pos) => {
        mesh.updateSelfPosition(pos.coords.latitude, pos.coords.longitude, 'This Node')
        sharingLocation.value = true
        locationError.value = ''
      },
      (err) => {
        locationError.value = err.code === 1 ? 'Location permission denied' : err.message
        sharingLocation.value = false
      },
      { enableHighAccuracy: true, timeout: 15000, maximumAge: 30000 },
    )
    sharingLocation.value = true
  } else {
    // Device GPS — position data comes from deadman/mesh GPS module
    sharingLocation.value = true
  }
}

function stopSharing() {
  if (geoWatchId !== null) {
    navigator.geolocation.clearWatch(geoWatchId)
    geoWatchId = null
  }
  sharingLocation.value = false
  mesh.nodePositions.delete(-1)
}

function createMarkerIcon(type: 'self' | 'online' | 'offline'): L.DivIcon {
  const colorMap = {
    self: { bg: '#fb923c', border: '#f59e0b', shadow: 'rgba(251,146,60,0.5)' },
    online: { bg: '#4ade80', border: '#22c55e', shadow: 'rgba(74,222,128,0.4)' },
    offline: { bg: '#6b7280', border: '#4b5563', shadow: 'rgba(107,114,128,0.3)' },
  }
  const c = colorMap[type]
  const size = type === 'self' ? 16 : 12
  const pulse = type === 'self'
    ? `<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:${size + 12}px;height:${size + 12}px;border-radius:50%;background:${c.shadow};animation:mesh-map-pulse 2s infinite;"></div>`
    : ''

  return L.divIcon({
    className: 'mesh-map-marker-wrapper',
    iconSize: [size + 12, size + 12],
    iconAnchor: [(size + 12) / 2, (size + 12) / 2],
    popupAnchor: [0, -(size / 2 + 6)],
    html: `
      ${pulse}
      <div style="
        width:${size}px;height:${size}px;
        border-radius:50%;
        background:${c.bg};
        border:2px solid ${c.border};
        box-shadow:0 0 8px ${c.shadow};
        position:absolute;top:50%;left:50%;
        transform:translate(-50%,-50%);
        z-index:2;
      "></div>
    `,
  })
}

function getSignalBars(rssi: number | null): string {
  if (rssi === null) return 'Unknown'
  if (rssi >= -70) return 'Strong'
  if (rssi >= -90) return 'Good'
  if (rssi >= -110) return 'Weak'
  return 'Very Weak'
}

function formatLastHeard(timestamp: string): string {
  const now = Date.now()
  const then = new Date(timestamp).getTime()
  const diffSecs = Math.floor((now - then) / 1000)
  if (diffSecs < 60) return 'Just now'
  if (diffSecs < 3600) return `${Math.floor(diffSecs / 60)}m ago`
  if (diffSecs < 86400) return `${Math.floor(diffSecs / 3600)}h ago`
  return `${Math.floor(diffSecs / 86400)}d ago`
}

function truncatePubkey(pubkey: string | null): string {
  if (!pubkey) return 'No pubkey'
  if (pubkey.length <= 16) return pubkey
  return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`
}

function buildPopupContent(
  name: string,
  pubkey: string | null,
  rssi: number | null,
  lastHeard: string,
  hops: number,
  isSelf: boolean,
): string {
  const signal = getSignalBars(rssi)
  const heard = formatLastHeard(lastHeard)
  const truncPk = truncatePubkey(pubkey)
  const selfBadge = isSelf
    ? '<span style="display:inline-block;background:rgba(251,146,60,0.2);color:#fb923c;font-size:0.65rem;padding:1px 6px;border-radius:4px;margin-left:6px;font-weight:600;">THIS NODE</span>'
    : ''

  return `
    <div style="font-family:'Avenir Next',sans-serif;min-width:160px;">
      <div style="font-weight:600;font-size:0.9rem;color:#fff;margin-bottom:4px;">
        ${name}${selfBadge}
      </div>
      <div style="font-size:0.72rem;color:rgba(255,255,255,0.5);font-family:monospace;margin-bottom:8px;word-break:break-all;">
        ${truncPk}
      </div>
      <div style="display:flex;flex-direction:column;gap:3px;font-size:0.78rem;">
        ${!isSelf ? `<div style="color:rgba(255,255,255,0.7);">Signal: <span style="color:${rssi !== null && rssi >= -90 ? '#4ade80' : '#fbbf24'};">${signal}</span>${rssi !== null ? ` (${rssi} dBm)` : ''}</div>` : ''}
        ${!isSelf ? `<div style="color:rgba(255,255,255,0.7);">Hops: <span style="color:rgba(255,255,255,0.9);">${hops}</span></div>` : ''}
        <div style="color:rgba(255,255,255,0.7);">Last heard: <span style="color:rgba(255,255,255,0.9);">${heard}</span></div>
      </div>
    </div>
  `
}

function initMap() {
  if (!mapContainer.value || map) return

  const el = mapContainer.value
  const rect = el.getBoundingClientRect()

  // If container has no height yet, retry
  if (rect.height < 10) {
    setTimeout(initMap, 150)
    return
  }

  map = L.map(el, {
    zoomControl: true,
    attributionControl: true,
    center: [30, 0],
    zoom: 3,
  })

  L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/">CARTO</a>',
    subdomains: 'abcd',
    maxZoom: 19,
    detectRetina: true,
  }).addTo(map)

  markersLayer.value = L.layerGroup().addTo(map)
  linesLayer.value = L.layerGroup().addTo(map)

  // Give Leaflet a frame to measure, then invalidate
  requestAnimationFrame(() => {
    if (map) {
      try { map.invalidateSize() } catch { /* destroyed */ }
    }
  })

  // Style attribution for dark theme
  const attrib = el.querySelector('.leaflet-control-attribution')
  if (attrib instanceof HTMLElement) {
    attrib.style.background = 'rgba(0,0,0,0.6)'
    attrib.style.color = 'rgba(255,255,255,0.4)'
    attrib.style.fontSize = '0.65rem'
  }

  updateMarkers()
}

function updateMarkers() {
  if (!map || !markersLayer.value || !linesLayer.value) return

  markersLayer.value.clearLayers()
  linesLayer.value.clearLayers()

  const positions = mesh.nodePositions
  if (positions.size === 0) return

  const bounds: L.LatLngExpression[] = []
  const selfPos = positions.get(-1)

  // Find which contact_ids are in the peers list (for online status)
  const peerMap = new Map(mesh.peers.map(p => [p.contact_id, p]))

  positions.forEach((pos: NodePosition, contactId: number) => {
    const isSelf = contactId === -1
    const peer = peerMap.get(contactId)
    const isOnline = isSelf || !!peer

    const marker = L.marker([pos.lat, pos.lng], {
      icon: createMarkerIcon(isSelf ? 'self' : isOnline ? 'online' : 'offline'),
    })

    const name = isSelf
      ? (mesh.status?.self_advert_name ?? 'This Node')
      : (peer?.advert_name ?? pos.label ?? `Node ${contactId}`)
    const pubkey = isSelf ? null : (peer?.pubkey_hex ?? null)
    const rssi = peer?.rssi ?? null
    const lastHeard = isSelf ? new Date().toISOString() : (peer?.last_heard ?? pos.timestamp)
    const hops = peer?.hops ?? 0

    marker.bindPopup(buildPopupContent(name, pubkey, rssi, lastHeard, hops, isSelf), {
      className: 'mesh-map-popup',
      closeButton: true,
      maxWidth: 250,
    })

    markersLayer.value!.addLayer(marker)
    bounds.push([pos.lat, pos.lng])

    // Draw dashed line from self to each connected peer
    if (!isSelf && selfPos) {
      const line = L.polyline(
        [[selfPos.lat, selfPos.lng], [pos.lat, pos.lng]],
        {
          color: isOnline ? 'rgba(74,222,128,0.4)' : 'rgba(107,114,128,0.3)',
          weight: 1.5,
          dashArray: '6, 8',
          opacity: 0.7,
        },
      )
      linesLayer.value!.addLayer(line)
    }
  })

  // Fit map to show all markers
  if (bounds.length > 1) {
    map.fitBounds(L.latLngBounds(bounds), { padding: [40, 40], maxZoom: 14 })
  } else if (bounds.length === 1 && bounds[0]) {
    map.setView(bounds[0], 12)
  }
}

function handleResize() {
  if (map) {
    map.invalidateSize()
  }
}

// Watch for changes in node positions and peers
watch(
  () => [mesh.nodePositions.size, mesh.peers.length],
  () => {
    updateMarkers()
  },
)

let resizeObserver: ResizeObserver | null = null

onMounted(() => {
  window.addEventListener('resize', handleResize)

  if (mapContainer.value) {
    resizeObserver = new ResizeObserver((entries) => {
      const entry = entries[0]
      if (!entry) return
      const { height } = entry.contentRect
      if (!map && height > 10) {
        initMap()
      } else if (map) {
        try { map.invalidateSize() } catch { /* destroyed */ }
      }
    })
    resizeObserver.observe(mapContainer.value)
  }

  // Fallback init
  setTimeout(initMap, 300)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
  stopSharing()
  if (resizeObserver) {
    resizeObserver.disconnect()
    resizeObserver = null
  }
  if (map) {
    map.remove()
    map = null
  }
  markersLayer.value = null
  linesLayer.value = null
})
</script>

<template>
  <div class="mesh-map-outer">
    <div ref="mapContainer" class="mesh-map-inner"></div>

    <!-- Floating hint when no positions -->
    <div v-if="!hasPositions && !sharingLocation" class="mesh-map-hint">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
        <circle cx="12" cy="10" r="3" />
      </svg>
      <span>Enable location sharing to see nodes on the map</span>
    </div>

    <!-- Location sharing overlay -->
    <div class="mesh-map-location-bar">
      <div class="mesh-map-location-row">
        <svg class="mesh-map-location-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
          <circle cx="12" cy="10" r="3" />
        </svg>
        <span class="mesh-map-location-label">Share Location</span>
        <button
          class="mesh-map-toggle"
          :class="{ active: sharingLocation }"
          role="switch"
          :aria-checked="sharingLocation"
          @click="toggleLocationSharing"
        >
          <span class="mesh-map-toggle-knob" />
        </button>
      </div>

      <!-- Source selector (visible when sharing and device GPS available) -->
      <div v-if="sharingLocation && hasDeviceGps" class="mesh-map-source-row">
        <button
          class="mesh-map-source-btn"
          :class="{ active: locationSource === 'browser' }"
          @click="switchSource('browser')"
        >This Machine</button>
        <button
          class="mesh-map-source-btn"
          :class="{ active: locationSource === 'device' }"
          @click="switchSource('device')"
        >Mesh Radio GPS</button>
      </div>

      <div v-if="locationError" class="mesh-map-location-error">{{ locationError }}</div>
    </div>
  </div>
</template>

<style>
/* Must be unscoped — Leaflet creates DOM nodes dynamically */

/* CRITICAL: Override Tailwind's img { max-width: 100% } which breaks Leaflet tiles */
.mesh-map-inner img {
  max-width: none !important;
  max-height: none !important;
}

.mesh-map-outer {
  width: 100%;
  height: 100%;
  min-height: 420px;
  position: relative;
  overflow: hidden;
}

.mesh-map-inner {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: #1a1a2e;
}

/* Leaflet adds .leaflet-container to the element itself (not a child) */
.mesh-map-inner.leaflet-container {
  background: #1a1a2e;
}

.mesh-map-hint {
  position: absolute;
  bottom: 64px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 400;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 8px;
  color: rgba(255, 255, 255, 0.6);
  font-size: 0.78rem;
  white-space: nowrap;
  pointer-events: none;
}

/* ─── Location sharing overlay ─── */
.mesh-map-location-bar {
  position: absolute;
  bottom: 8px;
  left: 8px;
  right: 8px;
  z-index: 500;
  background: rgba(0, 0, 0, 0.75);
  backdrop-filter: blur(16px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 10px;
  padding: 8px 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.mesh-map-location-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.mesh-map-location-icon {
  color: rgba(255, 255, 255, 0.5);
  flex-shrink: 0;
}

.mesh-map-location-label {
  font-size: 0.78rem;
  color: rgba(255, 255, 255, 0.7);
  font-weight: 500;
  flex: 1;
}

/* Toggle switch */
.mesh-map-toggle {
  width: 36px;
  height: 20px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(255, 255, 255, 0.1);
  cursor: pointer;
  position: relative;
  padding: 0;
  flex-shrink: 0;
  transition: all 0.25s ease;
}

.mesh-map-toggle.active {
  background: rgba(251, 146, 60, 0.35);
  border-color: rgba(251, 146, 60, 0.5);
}

.mesh-map-toggle-knob {
  display: block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.6);
  position: absolute;
  top: 2px;
  left: 2px;
  transition: all 0.25s ease;
}

.mesh-map-toggle.active .mesh-map-toggle-knob {
  left: 18px;
  background: #fb923c;
}

/* Source selector */
.mesh-map-source-row {
  display: flex;
  gap: 4px;
  background: rgba(0, 0, 0, 0.3);
  border-radius: 6px;
  padding: 2px;
}

.mesh-map-source-btn {
  flex: 1;
  padding: 4px 8px;
  font-size: 0.7rem;
  font-weight: 500;
  color: rgba(255, 255, 255, 0.5);
  background: transparent;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.mesh-map-source-btn:hover {
  color: rgba(255, 255, 255, 0.7);
}

.mesh-map-source-btn.active {
  color: #fff;
  background: rgba(255, 255, 255, 0.1);
}

.mesh-map-location-error {
  font-size: 0.7rem;
  color: #ef4444;
}
</style>

<style>
/* Global styles for Leaflet popup theming - must not be scoped */
.mesh-map-popup .leaflet-popup-content-wrapper {
  background: rgba(0, 0, 0, 0.85);
  backdrop-filter: blur(16px);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 10px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
  color: #fff;
}

.mesh-map-popup .leaflet-popup-tip {
  background: rgba(0, 0, 0, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.15);
}

.mesh-map-popup .leaflet-popup-close-button {
  color: rgba(255, 255, 255, 0.5);
  font-size: 18px;
  padding: 4px 8px;
}

.mesh-map-popup .leaflet-popup-close-button:hover {
  color: rgba(255, 255, 255, 0.9);
}

/* Marker wrapper reset */
.mesh-map-marker-wrapper {
  background: none !important;
  border: none !important;
}

/* Pulse animation for self marker */
@keyframes mesh-map-pulse {
  0% { transform: translate(-50%, -50%) scale(1); opacity: 0.6; }
  50% { transform: translate(-50%, -50%) scale(1.8); opacity: 0; }
  100% { transform: translate(-50%, -50%) scale(1); opacity: 0; }
}

/* Dark theme for Leaflet zoom controls */
.mesh-map-inner .leaflet-control-zoom a {
  background: rgba(0, 0, 0, 0.7) !important;
  color: rgba(255, 255, 255, 0.8) !important;
  border-color: rgba(255, 255, 255, 0.1) !important;
}

.mesh-map-inner .leaflet-control-zoom a:hover {
  background: rgba(0, 0, 0, 0.85) !important;
  color: #fff !important;
}
</style>
