<template>
    <!-- Carte Leaflet (exception documentee a @malio/layer-ui : carte interactive,
         type non couvert par la lib — cf. frontend.md § Composants formulaires).
         TODO : migrer si la lib couvre un jour les cartes. -->
    <div class="relative h-full w-full">
        <div ref="mapEl" class="h-full w-full" data-testid="tour-map" />
        <!-- Aide a la selection rectangle (lasso facon Badger Maps). -->
        <div class="pointer-events-none absolute bottom-2 left-2 z-[400] rounded bg-white/90 px-2 py-1 text-xs text-gray-600 shadow">
            {{ t('field_sales.plan.map.lassoHint') }}
        </div>
    </div>
</template>

<script setup lang="ts">
import type { Map as LeafletMap, Marker, Polyline, Rectangle } from 'leaflet'
import type { PlanningStop } from '~/modules/field-sales/composables/useTourPlanning'
import type { VisitableTier } from '~/modules/field-sales/types/tour'

/**
 * Carte interactive de planification de tournee (M6.5, spec § 6.1).
 *
 * - Charge les pins des Tiers geolocalises de la zone visible
 *   (GET /api/visitable_tiers?bbox=...), colores par type (client/fournisseur),
 *   filtrables (types + recherche). Recharge au deplacement/zoom (debounce).
 * - Popup au clic : nom, adresse, bouton « + Ajouter » (emet `add-tier`).
 * - Selection rectangle (Maj + glisser) : ajoute tous les Tiers entoures
 *   (emet `add-tiers`).
 * - Trace la tournee par-dessus : polyline + marqueurs numerotes suivant l'ordre
 *   des etapes geolocalisees.
 *
 * Instances Leaflet hors reactivite Vue (un proxy casse l'API Leaflet).
 */
const props = withDefaults(defineProps<{
    /** Etapes geolocalisees a tracer (polyline numerotee). */
    stops: PlanningStop[]
    /** Types de pins affiches. */
    types: Array<'client' | 'supplier'>
    /** Recherche raison sociale / ville. */
    search: string
    /** Centre initial (defaut : Nantes). */
    center?: [number, number]
}>(), {
    center: () => [47.218, -1.553],
})

const emit = defineEmits<{
    /** Ajout d'un seul Tiers (popup « + Ajouter »). */
    'add-tier': [tier: VisitableTier]
    /** Ajout d'un lot de Tiers (selection rectangle). */
    'add-tiers': [tiers: VisitableTier[]]
}>()

const { t } = useI18n()
const api = useApi()

const mapEl = ref<HTMLElement | null>(null)

// Instances Leaflet (hors reactivite).
let L: typeof import('leaflet') | null = null
let map: LeafletMap | null = null
let pinLayer: Marker[] = []
let pinTiers: Array<{ tier: VisitableTier, marker: Marker }> = []
let routeLine: Polyline | null = null
let stopMarkers: Marker[] = []
let selectionRect: Rectangle | null = null

/** Zoom initial (niveau agglomeration). */
const INITIAL_ZOOM = 12

/** Couleur du pin par type de Tiers. */
const PIN_COLORS: Record<string, string> = {
    client: '#2563eb', // bleu
    supplier: '#16a34a', // vert
}

/** Debounce du rechargement des pins au deplacement de la carte. */
let fetchTimer: ReturnType<typeof setTimeout> | null = null

async function ensureMap(): Promise<void> {
    if (map !== null || mapEl.value === null) {
        return
    }

    const mod = await import('leaflet')
    L = mod.default ?? mod
    await import('leaflet/dist/leaflet.css')

    if (mapEl.value === null) {
        return
    }

    map = L.map(mapEl.value).setView(props.center, INITIAL_ZOOM)
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
        maxZoom: 19,
    }).addTo(map)

    // Selection rectangle a la place du box-zoom natif (Maj + glisser).
    map.boxZoom.disable()
    map.on('mousedown', onMouseDown)

    // Rechargement des pins quand la zone visible change.
    map.on('moveend', scheduleFetch)

    drawRoute()
    await fetchPins()
}

/** bbox de la zone visible au format Leaflet (minLng,minLat,maxLng,maxLat). */
function currentBbox(): string | null {
    if (map === null) {
        return null
    }

    return map.getBounds().toBBoxString()
}

function scheduleFetch(): void {
    if (fetchTimer !== null) {
        clearTimeout(fetchTimer)
    }
    fetchTimer = setTimeout(() => {
        void fetchPins()
    }, 300)
}

/**
 * Charge les pins de la zone visible. `?pagination=false` : la carte affiche
 * TOUS les pins de la bbox (le volume est borne par la zone, pas par la page).
 */
async function fetchPins(): Promise<void> {
    if (map === null || L === null) {
        return
    }
    if (props.types.length === 0) {
        clearPins()
        return
    }

    const bbox = currentBbox()
    if (bbox === null) {
        return
    }

    const query: Record<string, string> = {
        bbox,
        type: props.types.join(','),
        pagination: 'false',
    }
    if (props.search.trim() !== '') {
        query.q = props.search.trim()
    }

    try {
        const response = await api.get<{ member?: VisitableTier[] }>(
            '/visitable_tiers',
            query,
            { headers: { Accept: 'application/ld+json' }, toast: false },
        )
        renderPins(response.member ?? [])
    }
    catch {
        // Echec non bloquant : la carte reste utilisable, les pins ne se mettent
        // simplement pas a jour.
    }
}

function clearPins(): void {
    pinLayer.forEach(m => m.remove())
    pinLayer = []
    pinTiers = []
}

function renderPins(tiers: VisitableTier[]): void {
    if (map === null || L === null) {
        return
    }
    clearPins()

    for (const tier of tiers) {
        const marker = L.marker([tier.latitude, tier.longitude], {
            icon: pinIcon(PIN_COLORS[tier.tierType] ?? '#6b7280'),
        }).addTo(map)

        marker.bindPopup(popupHtml(tier))
        marker.on('popupopen', () => bindPopupButton(tier))

        pinLayer.push(marker)
        pinTiers.push({ tier, marker })
    }
}

/** divIcon SVG inline colore (evite les assets PNG Leaflet casses par Vite). */
function pinIcon(color: string) {
    return L!.divIcon({
        className: '',
        html: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="26" height="38" fill="${color}" stroke="#ffffff" stroke-width="1"><path d="M12 0C7 0 3 4 3 9c0 6.6 9 15 9 15s9-8.4 9-15c0-5-4-9-9-9zm0 12.5A3.5 3.5 0 1 1 12 5.5a3.5 3.5 0 0 1 0 7z"/></svg>`,
        iconSize: [26, 38],
        iconAnchor: [13, 38],
        popupAnchor: [0, -34],
    })
}

/** Contenu HTML du popup (le bouton est cable a l'ouverture, cf. bindPopupButton). */
function popupHtml(tier: VisitableTier): string {
    const name = escapeHtml(tier.displayName)
    const address = escapeHtml(tier.address)

    return `<div class="text-sm">
        <div class="font-semibold">${name}</div>
        <div class="text-gray-600">${address}</div>
        <button type="button" data-add-tier class="mt-2 rounded bg-blue-600 px-2 py-1 text-xs font-medium text-white">${t('field_sales.plan.map.add')}</button>
    </div>`
}

/** Cable le bouton « + Ajouter » du popup ouvert vers l'emit `add-tier`. */
function bindPopupButton(tier: VisitableTier): void {
    const el = map?.getContainer().querySelector('[data-add-tier]')
    el?.addEventListener('click', () => {
        emit('add-tier', tier)
        map?.closePopup()
    }, { once: true })
}

// ── Selection rectangle (lasso) ─────────────────────────────────────────────
let selectStart: import('leaflet').LatLng | null = null

function onMouseDown(e: import('leaflet').LeafletMouseEvent): void {
    if (map === null || L === null || !e.originalEvent.shiftKey) {
        return
    }
    // Empeche le drag de la carte pendant la selection.
    map.dragging.disable()
    selectStart = e.latlng
    selectionRect = L.rectangle(L.latLngBounds(e.latlng, e.latlng), {
        color: '#2563eb',
        weight: 1,
        fillOpacity: 0.1,
    }).addTo(map)

    map.on('mousemove', onMouseMove)
    map.on('mouseup', onMouseUp)
}

function onMouseMove(e: import('leaflet').LeafletMouseEvent): void {
    if (selectStart === null || selectionRect === null || L === null) {
        return
    }
    selectionRect.setBounds(L.latLngBounds(selectStart, e.latlng))
}

function onMouseUp(): void {
    if (map === null) {
        return
    }
    const bounds = selectionRect?.getBounds() ?? null
    cleanupSelection()

    if (bounds === null) {
        return
    }
    const selected = pinTiers
        .filter(({ marker }) => bounds.contains(marker.getLatLng()))
        .map(({ tier }) => tier)

    if (selected.length > 0) {
        emit('add-tiers', selected)
    }
}

function cleanupSelection(): void {
    selectionRect?.remove()
    selectionRect = null
    selectStart = null
    map?.off('mousemove', onMouseMove)
    map?.off('mouseup', onMouseUp)
    map?.dragging.enable()
}

// ── Trace de la tournee ──────────────────────────────────────────────────────
function drawRoute(): void {
    if (map === null || L === null) {
        return
    }
    routeLine?.remove()
    routeLine = null
    stopMarkers.forEach(m => m.remove())
    stopMarkers = []

    const located = props.stops.filter(s => s.latitude != null && s.longitude != null)
    const points = located.map(s => [s.latitude as number, s.longitude as number] as [number, number])

    if (points.length >= 2) {
        routeLine = L.polyline(points, { color: '#1e40af', weight: 3, opacity: 0.7 }).addTo(map)
    }

    located.forEach((stop, index) => {
        const marker = L!.marker([stop.latitude as number, stop.longitude as number], {
            icon: numberedIcon(index + 1),
            zIndexOffset: 1000,
        }).addTo(map!)
        marker.bindTooltip(stop.label, { direction: 'top' })
        stopMarkers.push(marker)
    })
}

/** Pastille numerotee pour une etape de la tournee. */
function numberedIcon(n: number) {
    return L!.divIcon({
        className: '',
        html: `<div style="display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:9999px;background:#1e40af;color:#fff;font-size:12px;font-weight:700;border:2px solid #fff;box-shadow:0 1px 2px rgba(0,0,0,.4)">${n}</div>`,
        iconSize: [24, 24],
        iconAnchor: [12, 12],
    })
}

function escapeHtml(value: string): string {
    return value
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
}

// Recharge les pins quand les filtres changent.
watch(() => [props.types, props.search], scheduleFetch, { deep: true })

// Redessine le trace quand les etapes changent.
watch(() => props.stops, drawRoute, { deep: true })

onMounted(ensureMap)

onBeforeUnmount(() => {
    if (fetchTimer !== null) {
        clearTimeout(fetchTimer)
    }
    map?.remove()
    map = null
    L = null
    pinLayer = []
    pinTiers = []
    stopMarkers = []
    routeLine = null
    selectionRect = null
})

defineExpose({
    /** Recentre la carte sur une cible (ex: depuis le panneau). */
    panTo(target: { latitude: number, longitude: number }) {
        map?.panTo([target.latitude, target.longitude])
    },
})
</script>
