<template>
    <div data-testid="tier-address-map">
        <!-- Carte d'ensemble : un marqueur par adresse geolocalisee du Tiers,
             cadree sur l'ensemble (fitBounds). Pin ajustable par drag (M6.6,
             spec § 6.2) : au drag, PATCH direct des coordonnees + geoManual=true
             (RG-6.08), sans passer par le formulaire d'edition. -->
        <div
            v-if="located.length > 0"
            ref="mapEl"
            class="h-96 w-full rounded border border-gray-200"
            data-testid="tier-map"
        />
        <p v-else class="rounded border border-dashed border-gray-300 bg-gray-50 py-8 text-center text-sm text-gray-500">
            {{ t('commercial.geo.map.noLocated') }}
        </p>

        <p v-if="located.length > 0 && editable" class="mt-1 text-xs text-gray-500">
            {{ t('commercial.geo.dragHint') }}
        </p>

        <!-- Adresses sans coordonnees : listees a part (« a geolocaliser »),
             exclues de la carte et du calcul de tournee (RG-6.05). -->
        <div v-if="missing.length > 0" class="mt-6" data-testid="tier-map-missing-list">
            <h3 class="mb-2 flex items-center gap-2 text-sm font-medium text-gray-700">
                {{ t('commercial.geo.map.missingTitle') }}
                <span class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800">
                    {{ missing.length }}
                </span>
            </h3>
            <ul class="flex flex-col gap-2">
                <li
                    v-for="address in missing"
                    :key="address.id"
                    class="rounded border border-gray-200 bg-white px-3 py-2 text-sm"
                    data-testid="tier-map-missing"
                >
                    <span class="font-medium text-gray-800">{{ address.title }}</span>
                    <span v-if="address.typeLabel" class="ml-2 text-xs text-gray-500">{{ address.typeLabel }}</span>
                </li>
            </ul>
        </div>
    </div>
</template>

<script lang="ts">
import type { Map as LeafletMap, Marker } from 'leaflet'

/**
 * Adresse normalisee pour la carte d'ensemble d'un Tiers. La page parente
 * (fiche Client / Fournisseur) construit la liste : elle resout le libelle, le
 * type traduit et l'endpoint PATCH des coordonnees (les modeles d'adresse
 * client/fournisseur different — drapeaux vs enum).
 */
export interface TierMapAddress {
    /** Id serveur de l'adresse. */
    id: number
    /** Latitude WGS84 (chaine decimale) ou null si non geolocalisee. */
    latitude: string | null
    /** Longitude WGS84 (chaine decimale) ou null si non geolocalisee. */
    longitude: string | null
    /** RG-6.08 : pin deja corrige a la main. */
    geoManual: boolean
    /** Libelle principal (rue + code postal ville). */
    title: string
    /** Type d'adresse traduit (Prospect / Livraison / Depart...). */
    typeLabel: string
    /** Endpoint PATCH des coordonnees (ex: /client_addresses/12). */
    patchPath: string
}
</script>

<script setup lang="ts">
const props = withDefaults(defineProps<{
    /** Toutes les adresses du Tiers (geolocalisees ou non). */
    addresses: TierMapAddress[]
    /** Drag du pin actif (PATCH des coordonnees) — exige le droit d'edition. */
    editable?: boolean
}>(), {
    editable: false,
})

const emit = defineEmits<{
    /** Coordonnees d'une adresse mises a jour par drag (PATCH reussi). */
    updated: [value: { id: number, latitude: string, longitude: string }]
}>()

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

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

/** Vrai si l'adresse porte des coordonnees exploitables. */
function hasCoords(address: TierMapAddress): boolean {
    return address.latitude !== null && address.latitude !== ''
        && address.longitude !== null && address.longitude !== ''
}

const located = computed(() => props.addresses.filter(hasCoords))
const missing = computed(() => props.addresses.filter(a => !hasCoords(a)))

// Instances Leaflet (hors reactivite Vue : un proxy casse l'API Leaflet).
let L: typeof import('leaflet') | null = null
let map: LeafletMap | null = null
let markers: Marker[] = []

/** Zoom max applique par fitBounds (evite un zoom excessif sur un seul pin). */
const MAX_FIT_ZOOM = 16

/** Monte la carte Leaflet (import dynamique : chargee seulement si besoin). */
async function ensureMap(): Promise<void> {
    if (map !== null || mapEl.value === null || located.value.length === 0) {
        return
    }

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

    // Le conteneur peut avoir disparu pendant le chargement async (v-if).
    if (mapEl.value === null) {
        return
    }

    map = L.map(mapEl.value, { scrollWheelZoom: false })
    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)

    renderMarkers()
}

/** Pin SVG inline (evite les assets PNG Leaflet casses par Vite). */
function pinIcon() {
    return L!.divIcon({
        className: '',
        html: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="28" height="40" fill="#2563eb" stroke="#1e40af" stroke-width="0.5"><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: [28, 40],
        iconAnchor: [14, 40],
        popupAnchor: [0, -36],
    })
}

/** Contenu HTML du popup : libelle + type de l'adresse. */
function popupHtml(address: TierMapAddress): string {
    const title = escapeHtml(address.title)
    const type = address.typeLabel ? `<div class="text-gray-600">${escapeHtml(address.typeLabel)}</div>` : ''
    return `<div class="text-sm"><div class="font-semibold">${title}</div>${type}</div>`
}

/** (Re)pose un marqueur par adresse geolocalisee et cadre la carte dessus. */
function renderMarkers(): void {
    if (map === null || L === null) {
        return
    }

    markers.forEach(m => m.remove())
    markers = []

    const points: [number, number][] = []
    for (const address of located.value) {
        const position: [number, number] = [Number(address.latitude), Number(address.longitude)]
        const marker = L.marker(position, { icon: pinIcon(), draggable: props.editable }).addTo(map)
        marker.bindPopup(popupHtml(address))
        if (props.editable) {
            marker.on('dragend', () => onMarkerDragEnd(address, marker))
        }
        markers.push(marker)
        points.push(position)
    }

    // Cadre sur l'ensemble des marqueurs (fitBounds), borne pour un pin isole.
    if (points.length > 0) {
        map.fitBounds(L.latLngBounds(points), { padding: [40, 40], maxZoom: MAX_FIT_ZOOM })
    }
}

/**
 * Drag d'un pin -> PATCH direct des coordonnees + geoManual=true (RG-6.08).
 * Contrairement au formulaire d'edition (persistance differee au submit), la
 * carte d'ensemble enregistre immediatement le nouveau positionnement.
 */
async function onMarkerDragEnd(address: TierMapAddress, marker: Marker): Promise<void> {
    const position = marker.getLatLng()
    const latitude = position.lat.toFixed(7)
    const longitude = position.lng.toFixed(7)
    try {
        await api.patch(address.patchPath, { latitude, longitude, geoManual: true }, { toast: false })
        address.geoManual = true
        address.latitude = latitude
        address.longitude = longitude
        emit('updated', { id: address.id, latitude, longitude })
    }
    catch {
        // Echec d'enregistrement : on remet le pin a sa derniere position connue.
        marker.setLatLng([Number(address.latitude), Number(address.longitude)])
    }
}

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

// (Re)monte ou rafraichit la carte quand la liste des adresses geolocalisees
// change (chargement async de la fiche, ajout de coordonnees).
watch(located, async () => {
    if (located.value.length === 0) {
        return
    }
    if (map === null) {
        await nextTick()
        await ensureMap()
        return
    }
    renderMarkers()
}, { deep: true })

onMounted(ensureMap)

onBeforeUnmount(() => {
    map?.remove()
    map = null
    L = null
    markers = []
})
</script>
