<template>
    <div>
        <div class="flight-map-wrapper">
            <PlaneLoader v-if="!mapReady" class="map-loader" />
            <div ref="mapContainer" class="map-container" :class="{ 'map-hidden': !mapReady }" />
            <button @click="exportMapBasic()" class="export-btn" title="Download as Image">
                <span class="mdi mdi-download" />
            </button>
            <button class="projection-btn" @click="toggleProjection" :title="isGlobe ? 'Switch to flat map' : 'Switch to globe'">
                <span class="mdi" :class="isGlobe ? 'mdi-map' : 'mdi-earth'"/>
            </button>
            <div v-if="!flights.length" class="empty-state">
                <span class="mdi mdi-earth-off" />
                <p>No flight data available</p>
            </div>
            <div v-if="showLegend" class="map-legend" :class="{ 'map-legend--open': legendOpen }">
                <button class="map-legend__toggle" @click="toggleLegend">
                    <span class="mdi mdi-format-list-bulleted" />
                    <span class="map-legend__toggle-label">Legend</span>
                    <span class="mdi" :class="legendOpen ? 'mdi-chevron-down' : 'mdi-chevron-up'" />
                </button>
                <div class="map-legend__body">
                    <div class="map-legend__row" v-for="item in legendItems" :key="item.label">
                        <svg width="28" height="10" class="map-legend__swatch">
                            <line
                                x1="2" y1="5" x2="26" y2="5"
                                :stroke="item.color"
                                :stroke-dasharray="item.dashed ? '4 3' : undefined"
                                stroke-width="2"
                                stroke-linecap="round"
                            />
                        </svg>
                        <span class="map-legend__label">{{ item.label }}</span>
                    </div>
                </div>
            </div>
        </div>
        <small class="attribution" v-if="compact" v-html="cartoAttribution" />
    </div>
</template>

<script lang="ts">
import {defineComponent, ref, onMounted, onBeforeUnmount, watch, nextTick, PropType, computed} from 'vue'
import maplibregl from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
import { Flight, Airport, SharedProps } from '@/Types/types'
import PlaneLoader from '@/Components/FlightsGoneBy/PlaneLoader.vue'
import type { Feature, FeatureCollection, LineString, Point } from 'geojson'
import {useUpdateSetting} from "@/Composables/useUpdateSetting";
import {usePage} from "@inertiajs/vue3";


type LngLat = [number, number]

interface RouteFlightBucket {
    historical: Flight[]
    future:     Flight[]
}

interface RouteFeatureProperties {
    color?:    string
    routeKey:  string
    depId:     number
    arrId:     number
}

interface AirportFeatureProperties {
    id: number
}

interface RoutesGeoJSON {
    historical: FeatureCollection<LineString, RouteFeatureProperties>
    future:     FeatureCollection<LineString, RouteFeatureProperties>
}



const cartoAttribution = computed(() => `&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>`)
// ── Constants ─────────────────────────────────────────────────────────────────

const PULSE_PHASES  = 6
const PULSE_PERIOD  = 2200
const TOUCH_RADIUS  = 20

const ROUTE_COLORS: [number, string][] = [
    [5, '#f97316'],
    [3, '#eab308'],
    [2, '#22c55e'],
    [1, '#a150d5'],
]

// ── Helpers ───────────────────────────────────────────────────────────────────

function routeKey(a: { id: number }, b: { id: number }): string {
    return [a.id, b.id].sort().join('-')
}

function routeColor(flightCount: number): string {
    return ROUTE_COLORS.find(([min]) => flightCount >= min)![1]
}

function isTouchDevice(): boolean {
    return window.matchMedia('(pointer: coarse)').matches
}

function greatCirclePoints(from: LngLat, to: LngLat, steps?: number): LngLat[] {
    const dist = Math.sqrt((to[0] - from[0]) ** 2 + (to[1] - from[1]) ** 2)
    const s    = steps ?? (dist > 60 ? 64 : dist > 20 ? 32 : 16)

    const toRad = (d: number) => d * Math.PI / 180
    const toDeg = (r: number) => r * 180 / Math.PI

    const lat1 = toRad(from[1]), lng1 = toRad(from[0])
    const lat2 = toRad(to[1]),   lng2 = toRad(to[0])

    const angularDist = 2 * Math.asin(Math.sqrt(
        Math.sin((lat2 - lat1) / 2) ** 2 +
        Math.cos(lat1) * Math.cos(lat2) * Math.sin((lng2 - lng1) / 2) ** 2
    ))

    if (angularDist === 0) return [from, to]

    const points: LngLat[] = []
    for (let i = 0; i <= s; i++) {
        const f = i / s
        const A = Math.sin((1 - f) * angularDist) / Math.sin(angularDist)
        const B = Math.sin(f * angularDist) / Math.sin(angularDist)
        const x = A * Math.cos(lat1) * Math.cos(lng1) + B * Math.cos(lat2) * Math.cos(lng2)
        const y = A * Math.cos(lat1) * Math.sin(lng1) + B * Math.cos(lat2) * Math.sin(lng2)
        const z = A * Math.sin(lat1) + B * Math.sin(lat2)
        points.push([toDeg(Math.atan2(y, x)), toDeg(Math.atan2(z, Math.sqrt(x * x + y * y)))])
    }

    // Unwrap antimeridian crossings
    for (let i = 1; i < points.length; i++) {
        const diff = points[i][0] - points[i - 1][0]
        if (diff >  180) points[i][0] -= 360
        if (diff < -180) points[i][0] += 360
    }

    return points
}

// ── Popup HTML ────────────────────────────────────────────────────────────────

function airportPopupHTML(airport: Airport): string {
    const row = (label: string, value: string) =>
        `<div class="ap-row"><span class="ap-label">${label}</span><span class="ap-value">${value}</span></div>`

    const elevation = airport.elevation_ft !== null
        ? row('Elevation', `${airport.elevation_ft.toLocaleString()} ft`) : ''
    const city    = airport.municipality
        ? row('City', airport.municipality) : ''
    const country = airport.region?.country
        ? row('Country', `${airport.region.country.name} <span class="fi fi-${airport.region.country.code.toLowerCase()}"></span>`) : ''
    const badges  = [airport.iata_code, airport.icao_code]
        .filter(Boolean)
        .map(code => `<span class="ap-badge">${code}</span>`)
        .join('')

    return `
        <div class="ap-tooltip glass">
            <div class="ap-header">
                <div class="ap-name">${airport.name}</div>
                <div class="ap-badges">${badges}</div>
            </div>
            <div class="ap-divider"></div>
            <div class="ap-rows">
                ${city}${country}${elevation}
                ${row('Timezone',    `<span class="ap-mono">${airport.timezone}</span>`)}
                ${row('Coordinates', `<span class="ap-mono ap-muted">${airport.latitude_deg.toFixed(4)}, ${airport.longitude_deg.toFixed(4)}</span>`)}
            </div>
        </div>`
}

function routePopupHTML(historical: Flight[], future: Flight[]): string {
    interface AirlineEntry { html: string; count: number }
    interface DirectionGroup {
        label: string
        airlines: Map<string, AirlineEntry>
        users: Map<number, { name: string }>
    }

    const groupByDirection = (flights: Flight[]): DirectionGroup[] => {
        const groups = new Map<string, DirectionGroup>()

        flights.forEach(flight => {
            const key   = `${flight.departure_airport.id}-${flight.arrival_airport.id}`
            const label = `${flight.departure_airport.municipality} to ${flight.arrival_airport.municipality}`

            if (!groups.has(key)) groups.set(key, { label, airlines: new Map(), users: new Map() })

            const group = groups.get(key)!

            if (flight.user) {
                group.users.set(flight.user.id, { name: flight.user.name })
            }

            if (!flight.airline) return

            const { iata_code, logo_url, name } = flight.airline
            const airlineKey = iata_code ?? ''
            const airlines   = group.airlines

            if (airlines.has(airlineKey)) {
                airlines.get(airlineKey)!.count++
            } else {
                airlines.set(airlineKey, {
                    html: `<span style="display:inline-flex;align-items:center;gap:6px;">
                        <img src="${logo_url}" width="24" height="24" alt="${iata_code}" style="flex-shrink:0;" />
                        ${name}
                    </span>`,
                    count: 1,
                })
            }
        })

        return [...groups.values()]
    }

    const renderUsers = (users: Map<number, { name: string }>): string => {
        if (!users.size) return ''

        const badges = [...users.values()]
            .map(({ name }) => `<span class="rp-user-badge">${name}</span>`)
            .join('')

        return `<div class="rp-users">${badges}</div>`
    }

    const renderSection = (title: string, flights: Flight[]): string => {
        if (!flights.length) return ''

        const rows = groupByDirection(flights).map(({ label, airlines, users }) => {
            const airlineLines = [...airlines.values()]
                .map(({ html, count }) => count > 1
                    ? `<span style="display:inline-flex;align-items:center;gap:4px;">${html}<span style="color:#556677">(x${count})</span></span>`
                    : html)
                .join('<br/>')

            return `
                <div class="rp-direction">
                    <div class="rp-route">${label}</div>
                    <div class="rp-airlines">${airlineLines || '—'}</div>
                    ${renderUsers(users)}
                </div>`
        }).join('')

        return `<div class="rp-section"><div class="rp-section-title">${title}</div>${rows}</div>`
    }

    const divider = historical.length && future.length ? '<div class="rp-divider"></div>' : ''

    return `
        <div class="rp-tooltip glass">
            ${renderSection('Flown', historical)}
            ${divider}
            ${renderSection('Upcoming', future)}
        </div>`
}

// ── Component ─────────────────────────────────────────────────────────────────

export default defineComponent({
    name: 'FlightMap',
    components: { PlaneLoader },

    props: {
        flights: {
            type:    Array as PropType<Flight[]>,
            default: (): Flight[] => [],
        },
        showLegend: {
            type: Boolean,
            default: true,
        },
        compact: {
            type: Boolean,
            default: false,
        }

    },

    setup(props) {
        const mapContainer = ref<HTMLDivElement | null>(null)
        const mapReady     = ref(false)
        const { updateSetting } = useUpdateSetting()
        const page = usePage<SharedProps>().props


        let map:        maplibregl.Map    | null = null
        let popup:      maplibregl.Popup  | null = null
        let pulseFrame: number            | null = null

        const airportById   = new Map<number, Airport>()
        const routeFlights  = new Map<string, RouteFlightBucket>()
        const arcCache      = new Map<string, LngLat[]>()

        let selectedAirportId: number | null = null

        const legendOpen = ref(page.auth?.user?.resolved_settings?.show_map_legend ?? true)

        function toggleLegend() {
            legendOpen.value = !legendOpen.value
            updateSetting('show_map_legend', legendOpen.value).catch(() => {})
        }

        const isGlobe = ref(false)

        const toggleProjection = (): void => {
            if (!map?.isStyleLoaded()) return
            isGlobe.value = !isGlobe.value
            map.setProjection({ type: isGlobe.value ? 'globe' : 'mercator' })
        }


        const legendItems = [
            { color: '#f97316', label: '5+ flights' },
            { color: '#eab308', label: '3–4 flights' },
            { color: '#22c55e', label: '2 flights' },
            { color: '#a150d5', label: '1 flight' },
            { color: '#ffffff', label: 'Upcoming', dashed: true },
        ]


        // ── Arc cache ─────────────────────────────────────────────────────────

        const exportMap = (targetWidth = 7680, targetHeight = 4320): void => {
            if (!map) return

            const container      = map.getContainer()
            const originalWidth  = container.offsetWidth
            const originalHeight = container.offsetHeight
            const currentCenter  = map.getCenter()
            const currentZoom    = map.getZoom()

            container.style.width  = `${targetWidth}px`
            container.style.height = `${targetHeight}px`
            map.resize()
            map.jumpTo({ center: [100, 20], zoom: 2 })// low zoom, centered roughly on your data

            setTimeout(() => {
                map!.triggerRepaint()
                requestAnimationFrame(() => {
                    requestAnimationFrame(() => {
                        const dataURL = map!.getCanvas().toDataURL('image/jpeg', 0.95)
                        const link    = document.createElement('a')
                        link.href     = dataURL
                        link.download = 'flight-map.jpg'
                        link.click()

                        container.style.width  = `${originalWidth}px`
                        container.style.height = `${originalHeight}px`
                        map!.resize()
                        map!.jumpTo({ center: currentCenter, zoom: currentZoom })
                    })
                })
            }, 10000)
        }

        const exportMapBasic = (): void => {
            if (!map) return
            const canvas  = map.getCanvas()
            const link    = document.createElement('a')
            link.href     = canvas.toDataURL('image/jpeg', 0.95)
            link.download = 'flight-map.jpg'
            link.click()
        }

        const getArc = (flight: Flight): LngLat[] => {
            const key = routeKey(flight.departure_airport, flight.arrival_airport)
            if (!arcCache.has(key)) {
                arcCache.set(key, greatCirclePoints(
                    [flight.departure_airport.longitude_deg, flight.departure_airport.latitude_deg],
                    [flight.arrival_airport.longitude_deg,   flight.arrival_airport.latitude_deg],
                ))
            }
            return arcCache.get(key)!
        }

        // ── Popup ─────────────────────────────────────────────────────────────

        const showPopup = (lngLat: maplibregl.LngLatLike, html: string) =>
            popup!.setLngLat(lngLat).setHTML(html).addTo(map!)

        // ── Filter ────────────────────────────────────────────────────────────

        const applyFilter = (): void => {
            if (!map?.isStyleLoaded()) return
            const id = selectedAirportId

            const routeFilter: maplibregl.FilterSpecification | null = id
                ? ['any', ['==', ['get', 'depId'], id], ['==', ['get', 'arrId'], id]]
                : null

            for (const layerId of ['routes-line', 'routes-hit', 'routes-future-line', 'routes-future-hit']) {
                map.setFilter(layerId, routeFilter)
            }

            // Build the set of airports that should remain visible
            let visibleAirportIds: Set<number> | null = null
            if (id) {
                visibleAirportIds = new Set([id])
                routeFlights.forEach((bucket, key) => {
                    const allFlights = [...bucket.historical, ...bucket.future]
                    if (allFlights.some(f =>
                        f.departure_airport.id === id || f.arrival_airport.id === id
                    )) {
                        allFlights.forEach(f => {
                            visibleAirportIds!.add(f.departure_airport.id)
                            visibleAirportIds!.add(f.arrival_airport.id)
                        })
                    }
                })
            }

            const airportFilter: maplibregl.FilterSpecification | null = visibleAirportIds
                ? ['in', ['get', 'id'], ['literal', [...visibleAirportIds]]]
                : null

            for (let i = 0; i < PULSE_PHASES; i++) {
                map.setFilter(`airports-dot-${i}`,   airportFilter)
                map.setFilter(`airports-pulse-${i}`, airportFilter)
            }
        }

        // ── GeoJSON builders ──────────────────────────────────────────────────

        const buildRouteFlights = (): void => {
            routeFlights.clear()
            const now = new Date()

            props.flights.forEach(flight => {
                const key    = routeKey(flight.departure_airport, flight.arrival_airport)
                const bucket = routeFlights.get(key) ?? { historical: [], future: [] }
                bucket[new Date(flight.departure_date) > now ? 'future' : 'historical'].push(flight)
                routeFlights.set(key, bucket)
            })
        }

        const buildRoutesGeoJSON = (): RoutesGeoJSON => {
            buildRouteFlights()
            const now = new Date()

            // Count historical flights per route for colour coding
            const flightCountByRoute = new Map<string, number>()
            props.flights
                .filter(f => new Date(f.departure_date) <= now)
                .forEach(f => {
                    const key = routeKey(f.departure_airport, f.arrival_airport)
                    flightCountByRoute.set(key, (flightCountByRoute.get(key) ?? 0) + 1)
                })

            const makeFeature = (
                flight: Flight,
                extraProps: Partial<RouteFeatureProperties> = {}
            ): Feature<LineString, RouteFeatureProperties> => ({
                type: 'Feature',
                properties: {
                    routeKey: routeKey(flight.departure_airport, flight.arrival_airport),
                    depId:    flight.departure_airport.id,
                    arrId:    flight.arrival_airport.id,
                    ...extraProps,
                },
                geometry: { type: 'LineString', coordinates: getArc(flight) },
            })

            // One feature per unique route, deduped by key
            const historicalFeatures = [...new Map(
                props.flights
                    .filter(f => new Date(f.departure_date) <= now)
                    .map(f => [
                        routeKey(f.departure_airport, f.arrival_airport),
                        makeFeature(f, { color: routeColor(flightCountByRoute.get(routeKey(f.departure_airport, f.arrival_airport)) ?? 1) }),
                    ])
            ).values()]

            const historicalRouteKeys = new Set(flightCountByRoute.keys())
            const futureFeatures = [...new Map(
                props.flights
                    .filter(f => new Date(f.departure_date) > now)
                    .filter(f => !historicalRouteKeys.has(routeKey(f.departure_airport, f.arrival_airport)))
                    .map(f => [routeKey(f.departure_airport, f.arrival_airport), makeFeature(f)])
            ).values()]

            const toCollection = (features: Feature<LineString, RouteFeatureProperties>[]) =>
                ({ type: 'FeatureCollection' as const, features })

            return {
                historical: toCollection(historicalFeatures),
                future:     toCollection(futureFeatures),
            }
        }

        const buildAirportsGeoJSON = (): FeatureCollection<Point, AirportFeatureProperties>[] => {
            const uniqueAirports = new Map<number, Airport>()
            props.flights.forEach(({ departure_airport: dep, arrival_airport: arr }) => {
                uniqueAirports.set(dep.id, dep)
                uniqueAirports.set(arr.id, arr)
            })

            // Spread airports across pulse phase buckets for staggered animation
            const buckets: Airport[][] = Array.from({ length: PULSE_PHASES }, () => [])
            uniqueAirports.forEach(airport => buckets[airport.id % PULSE_PHASES].push(airport))

            return buckets.map(airports => ({
                type: 'FeatureCollection',
                features: airports.map(airport => ({
                    type:       'Feature',
                    properties: { id: airport.id },
                    geometry:   { type: 'Point', coordinates: [airport.longitude_deg, airport.latitude_deg] },
                })),
            }))
        }

        // ── Map layers ────────────────────────────────────────────────────────

        const addLayers = (): void => {
            const { historical, future } = buildRoutesGeoJSON()
            const isTouch = isTouchDevice()

            // Routes
            map!.addSource('routes', { type: 'geojson', data: historical })
            map!.addLayer({
                id: 'routes-line', type: 'line', source: 'routes',
                paint:  { 'line-color': ['get', 'color'], 'line-opacity': 0.75, 'line-width': 1.8 },
                layout: { 'line-join': 'round', 'line-cap': 'round' },
            })
            map!.addLayer({
                id: 'routes-hit', type: 'line', source: 'routes',
                paint: { 'line-color': 'transparent', 'line-width': 12 },
            })

            // Future routes
            map!.addSource('routes-future', { type: 'geojson', data: future })
            map!.addLayer({
                id: 'routes-future-line', type: 'line', source: 'routes-future',
                paint:  { 'line-color': '#ffffff', 'line-opacity': 0.5, 'line-width': 1.5, 'line-dasharray': [3, 3] },
                layout: { 'line-join': 'round', 'line-cap': 'round' },
            })
            map!.addLayer({
                id: 'routes-future-hit', type: 'line', source: 'routes-future',
                paint: { 'line-color': 'transparent', 'line-width': 12 },
            })

            // Airport dots + pulse rings (one source per phase bucket)
            buildAirportsGeoJSON().forEach((data, i) => {
                map!.addSource(`airports-${i}`, { type: 'geojson', data })
                map!.addLayer({
                    id: `airports-pulse-${i}`, type: 'circle', source: `airports-${i}`,
                    paint: {
                        'circle-radius': 8, 'circle-color': 'transparent',
                        'circle-stroke-width': 1.5, 'circle-stroke-color': 'rgba(77,166,255,0.6)',
                        'circle-stroke-opacity': 0.6,
                    },
                })
                map!.addLayer({
                    id: `airports-dot-${i}`, type: 'circle', source: `airports-${i}`,
                    paint: {
                        'circle-radius': 5, 'circle-color': '#4da6ff',
                        'circle-stroke-width': 1.5, 'circle-stroke-color': 'rgba(255,255,255,0.9)',
                    },
                })
            })

            popup = new maplibregl.Popup({
                closeButton: isTouch,
                closeOnClick: false,
                className: 'ap-popup',
                maxWidth: 'none',
                offset: 12,
            })

            const airportLayerIds = Array.from({ length: PULSE_PHASES }, (_, i) => `airports-dot-${i}`)

            // ── Hover events (desktop only) ───────────────────────────────────

            if (!isTouch) {
                for (let i = 0; i < PULSE_PHASES; i++) {
                    map!.on('mouseenter', `airports-dot-${i}`, e => {
                        map!.getCanvas().style.cursor = 'pointer'
                        const airport = airportById.get(Number(e.features![0].properties.id))
                        const geom    = e.features![0].geometry
                        if (airport && geom.type === 'Point') showPopup(geom.coordinates as LngLat, airportPopupHTML(airport))
                    })
                    map!.on('mouseleave', `airports-dot-${i}`, () => {
                        map!.getCanvas().style.cursor = ''
                        popup!.remove()
                    })
                }

                for (const layerId of ['routes-hit', 'routes-future-hit']) {
                    map!.on('mouseenter', layerId, e => {
                        map!.getCanvas().style.cursor = 'pointer'
                        const bucket = routeFlights.get(e.features![0].properties.routeKey as string)
                        if (bucket) showPopup(e.lngLat, routePopupHTML(bucket.historical, bucket.future))
                    })
                    map!.on('mouseleave', layerId, () => {
                        map!.getCanvas().style.cursor = ''
                        popup!.remove()
                    })
                }
            }

            // ── Click handler ─────────────────────────────────────────────────

            map!.on('click', e => {
                // Widen hit area on touch
                const airportQuery = isTouch
                    ? [[e.point.x - TOUCH_RADIUS, e.point.y - TOUCH_RADIUS], [e.point.x + TOUCH_RADIUS, e.point.y + TOUCH_RADIUS]] as [maplibregl.PointLike, maplibregl.PointLike]
                    : e.point as maplibregl.PointLike

                const clickedAirport = map!.queryRenderedFeatures(airportQuery, { layers: airportLayerIds })[0]

                if (clickedAirport) {
                    const id = Number(clickedAirport.properties.id)
                    selectedAirportId = selectedAirportId === id ? null : id
                    applyFilter()

                    if (isTouch) {
                        const airport = airportById.get(id)
                        const geom    = clickedAirport.geometry
                        if (airport && geom.type === 'Point') showPopup(geom.coordinates as LngLat, airportPopupHTML(airport))
                    }
                    return
                }

                const clickedRoute = map!.queryRenderedFeatures(e.point, { layers: ['routes-hit', 'routes-future-hit'] })[0]

                if (clickedRoute) {
                    const bucket = routeFlights.get(clickedRoute.properties.routeKey as string)
                    if (bucket) showPopup(e.lngLat, routePopupHTML(bucket.historical, bucket.future))
                    return
                }

                // Clicked empty space — deselect
                selectedAirportId = null
                applyFilter()
                popup!.remove()
            })

            // ── Pulse animation ───────────────────────────────────────────────

            const animate = (): void => {
                const now = Date.now()
                for (let i = 0; i < PULSE_PHASES; i++) {
                    const progress = ((now + (i / PULSE_PHASES) * PULSE_PERIOD) % PULSE_PERIOD) / PULSE_PERIOD
                    map!.setPaintProperty(`airports-pulse-${i}`, 'circle-radius', 5 + progress * 13)
                    if (!selectedAirportId) {
                        map!.setPaintProperty(`airports-pulse-${i}`, 'circle-stroke-opacity', 0.7 * (1 - progress))
                    }
                }
                pulseFrame = requestAnimationFrame(animate)
            }
            animate()
        }

        // ── Fit bounds ────────────────────────────────────────────────────────

        const fitBounds = (): void => {
            if (!props.flights.length) return
            const padding = props.compact ? 25 : 60

            const lngs = props.flights.flatMap(f => [f.departure_airport.longitude_deg, f.arrival_airport.longitude_deg])
            const lats = props.flights.flatMap(f => [f.departure_airport.latitude_deg,  f.arrival_airport.latitude_deg])

            const minLat = Math.min(...lats)
            const maxLat = Math.max(...lats)

            // Compare normal span vs dateline-crossing span to pick the tighter fit
            const spanNormal = Math.max(...lngs) - Math.min(...lngs)
            const lngs360    = lngs.map(lng => lng < 0 ? lng + 360 : lng)
            const span360    = Math.max(...lngs360) - Math.min(...lngs360)

            let minLng: number, maxLng: number
            if (span360 < spanNormal) {
                const min360 = Math.min(...lngs360)
                const max360 = Math.max(...lngs360)
                minLng = min360 > 180 ? min360 - 360 : min360
                maxLng = max360 > 180 ? max360 - 360 : max360
            } else {
                minLng = Math.min(...lngs)
                maxLng = Math.max(...lngs)
            }
            map!.fitBounds([[minLng, minLat], [maxLng, maxLat]], { padding, duration: 0 })
        }

        // ── Map init ──────────────────────────────────────────────────────────

        const initMap = (): void => {
            nextTick(() => {
                map = new maplibregl.Map({
                    container:           mapContainer.value!,
                    cooperativeGestures: true,
                    attributionControl:  props.compact ? false : {compact: true},
                    center:              [0, 20],
                    zoom:                2,
                    renderWorldCopies:   true,
                    canvasContextAttributes: { preserveDrawingBuffer: true },
                    style: {
                        version: 8,
                        sources: {
                            'carto-dark': {
                                type:     'raster',
                                tileSize: 256,
                                maxzoom:  19,
                                attribution: cartoAttribution.value,
                                tiles: [
                                    'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
                                    'https://b.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
                                    'https://c.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
                                    'https://d.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png',
                                ],
                            },
                        },
                        layers: [{ id: 'carto-dark', type: 'raster', source: 'carto-dark' }],
                    },
                })

                map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-right')
                map.addControl(new maplibregl.FullscreenControl(), 'top-right')

                map.on('style.load', () => {
                    map?.setProjection({ type: 'mercator' })
                })

                map.on('load', () => {
                    addLayers()
                    fitBounds()
                    mapReady.value = true
                })
            })
        }


        // ── Data updates ──────────────────────────────────────────────────────

        const updateData = (): void => {
            if (!map?.isStyleLoaded()) return

            arcCache.clear()
            airportById.clear()
            props.flights.forEach(({ departure_airport: dep, arrival_airport: arr }) => {
                airportById.set(dep.id, dep)
                airportById.set(arr.id, arr)
            })

            const { historical, future } = buildRoutesGeoJSON()
            ;(map.getSource('routes')        as maplibregl.GeoJSONSource)?.setData(historical)
            ;(map.getSource('routes-future') as maplibregl.GeoJSONSource)?.setData(future)
            buildAirportsGeoJSON().forEach((data, i) =>
                (map!.getSource(`airports-${i}`) as maplibregl.GeoJSONSource)?.setData(data)
            )

            fitBounds()
        }

        // ── Lifecycle ─────────────────────────────────────────────────────────

        onMounted(() => {
            props.flights.forEach(({ departure_airport: dep, arrival_airport: arr }) => {
                airportById.set(dep.id, dep)
                airportById.set(arr.id, arr)
            })
            initMap()
        })

        watch(() => props.flights, updateData)

        onBeforeUnmount(() => {
            if (pulseFrame !== null) cancelAnimationFrame(pulseFrame)
            if (map) { map.remove(); map = null }
        })

        return { mapContainer, mapReady, exportMapBasic, legendOpen, legendItems, isGlobe, toggleProjection, toggleLegend, cartoAttribution }
    },
})


</script>

<style scoped>

.export-btn {
    position:         absolute;
    bottom:           12px;
    right:            12px;
    z-index:          10;
    background:       rgba(10,14,22,0.85);
    border:           1px solid rgba(255,255,255,0.08);
    color:            #a0b4c8;
    width:            30px;
    height:           30px;
    border-radius:    4px;
    cursor:           pointer;
    display:          flex;
    align-items:      center;
    justify-content:  center;
    font-size:        16px;
    transition:       color 0.15s, background 0.15s;
}
.export-btn:hover {
    background: rgba(77,166,255,0.15);
    color:      #4da6ff;
}

.flight-map-wrapper {
    position:       relative;
    width:          100%;
    aspect-ratio:   16 / 9;
    max-height:     70vh;
    display:        flex;
    align-items:    center;
    justify-content: center;
}

@media (max-width: 768px) {
    .flight-map-wrapper {
        aspect-ratio: 7 / 10;
        max-height:   none;
    }
}

small.attribution{
    font-size: 0.7em;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--muted);
    gap: 2px;
}

small.attribution * {
    color: var(--muted)
}

.map-container { position: absolute; inset: 0; }
.map-loader    { z-index: 10; }
.map-hidden    { visibility: hidden; }

.empty-state {
    position:        absolute;
    inset:           0;
    display:         flex;
    flex-direction:  column;
    align-items:     center;
    justify-content: center;
    color:           #3a4a58;
    gap:             12px;
    z-index:         900;
    pointer-events:  none;
}
.empty-state .mdi { font-size: 48px; }
.empty-state p    { font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase; margin: 0; }
</style>

<style>
/* Popup chrome */
.ap-popup .maplibregl-popup-content {
    background: transparent !important;
    border: none; border-radius: 0; padding: 0;
    box-shadow: none; backdrop-filter: none;
    color: #c8cdd8; width: max-content;
}
.ap-popup .maplibregl-popup-tip          { border-top-color: rgba(10,14,22,0.95); }
.ap-popup .maplibregl-popup-close-button {
    color: #556677; font-size: 18px; padding: 4px 8px;
    right: 4px; top: 4px; line-height: 1;
    background: transparent; border: none;
}
.ap-popup .maplibregl-popup-close-button:hover { color: #c8cdd8; }

/* Airport tooltip */
.ap-tooltip { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
.ap-header  { display: flex; flex-direction: column; gap: 6px; }
.ap-name    { font-size: 1.2em; color: #e0e6f0; line-height: 1.3; white-space: nowrap; }
.ap-badges  { display: flex; gap: 4px; flex-wrap: wrap; }
.ap-badge   {
    font-family: 'Share Tech Mono', monospace;
    font-size: 0.65rem; letter-spacing: 0.1em;
    background: rgba(77,166,255,0.12); border: 1px solid rgba(77,166,255,0.25);
    color: #4da6ff; border-radius: 3px; padding: 1px 6px;
}
.ap-divider { height: 1px; background: rgba(255,255,255,0.08); }
.ap-rows    { display: flex; flex-direction: column; gap: 4px; }
.ap-row     { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.ap-label   {
    font-family: 'Share Tech Mono', monospace;
    font-size: 0.6rem; letter-spacing: 0.15em;
    color: #445566; text-transform: uppercase; flex-shrink: 0;
}
.ap-value      { font-size: 0.78rem; color: #c8cdd8; text-align: right; }
.ap-mono       { font-family: 'Share Tech Mono', monospace; font-size: 0.7rem; letter-spacing: 0.03em; }
.ap-muted      { font-family: 'Share Tech Mono', monospace; font-size: 0.68rem; color: #556677; letter-spacing: 0.03em; }
.ap-value .fi  { display: inline-block; vertical-align: middle; }

/* Route tooltip */
.rp-tooltip       { padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; min-width: 160px; }
.rp-section       { display: flex; flex-direction: column; gap: 6px; }
.rp-section-title {
    font-family: 'Share Tech Mono', monospace;
    font-size: 0.65rem; letter-spacing: 0.12em;
    text-transform: uppercase; color: #c8cdd8; margin-bottom: 2px;
}
.rp-direction { display: flex; flex-direction: column; gap: 2px; }
.rp-route     { font-size: 0.82rem; color: #e0e6f0; }
.rp-airlines  { font-size: 0.75rem; color: #778899; }
.rp-divider   { height: 1px; background: rgba(255,255,255,0.08); margin: 2px 0; }

.rp-users {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 4px;
}
.rp-user-badge {
    font-size: 0.7rem;
    color: #c8cdd8;
    background: rgba(77,166,255,0.12);
    border: 1px solid rgba(77,166,255,0.25);
    border-radius: 999px;
    padding: 1px 8px;
    white-space: nowrap;
}

/* Map controls */
.maplibregl-ctrl-group                { background: rgba(10,14,22,0.85) !important; border: 1px solid rgba(255,255,255,0.08) !important; }
.maplibregl-ctrl-group button         { color: #a0b4c8 !important; }
.maplibregl-ctrl-group button:hover   { background: rgba(77,166,255,0.15) !important; color: #4da6ff !important; }
.maplibregl-ctrl-attrib               { background: rgba(10,14,22,0.7) !important; color: #666 !important; }
.maplibregl-ctrl-attrib a             { color: #666 !important; }

.map-legend {
    position:      absolute;
    bottom:        12px;
    left:          12px;
    z-index:       10;
    background:    rgba(10,14,22,0.85);
    border:        1px solid rgba(255,255,255,0.08);
    border-radius: 6px;
    overflow:      hidden;
    min-width:     148px;
}

.map-legend__toggle {
    display:         flex;
    align-items:     center;
    gap:             6px;
    width:           100%;
    padding:         6px 10px;
    background:      transparent;
    border:          none;
    color:           #a0b4c8;
    font-size:       12px;
    cursor:          pointer;
    transition:      color 0.15s;
    white-space:     nowrap;
}
.map-legend__toggle:hover { color: #4da6ff; }

.map-legend__toggle-label { flex: 1; text-align: left; }

.map-legend__body {
    display:        flex;
    flex-direction: column;
    gap:            6px;
    padding:        0 10px 0;
    max-height:     0;
    overflow:       hidden;
    transition:     max-height 0.2s ease, padding 0.2s ease;
}
.map-legend--open .map-legend__body {
    max-height: 200px;
    padding: 2px 10px 10px;
}

.map-legend__row {
    display:     flex;
    align-items: center;
    gap:         8px;
}

.map-legend__swatch { flex-shrink: 0; }

.map-legend__label {
    font-family: 'Share Tech Mono', monospace;
    font-size:   11px;
    letter-spacing: 0.06em;
    color:       #c8cdd8;
}

.projection-btn {
    position:         absolute;
    bottom:           48px;
    right:            12px;
    z-index:          10;
    background:       rgba(10,14,22,0.85);
    border:           1px solid rgba(255,255,255,0.08);
    color:            #a0b4c8;
    width:            30px;
    height:           30px;
    border-radius:    4px;
    cursor:           pointer;
    display:          flex;
    align-items:      center;
    justify-content:  center;
    font-size:        16px;
    transition:       color 0.15s, background 0.15s;
}
.projection-btn:hover        { background: rgba(77,166,255,0.15); color: #4da6ff; }
.projection-btn.globe-active { color: #4da6ff; border-color: rgba(77,166,255,0.35); }
.maplibregl-ctrl-bottom-right {
    left:      50%;
    right:     auto;
    transform: translateX(-50%);
}
</style>
