<?php
// Base URL - Calcul intelligent selon l'environnement
$activeAssignedCity = trim((string)($assignedCity ?? ($currentUser['assigned_city'] ?? '')));
$base = '';
if (isset($_SERVER['SCRIPT_NAME'])) {
    $scriptName = (string)($_SERVER['SCRIPT_NAME'] ?? '');
    $sapi = php_sapi_name();

    // Serveur PHP intégré (php -S ... -t public): la racine HTTP correspond à public/
    // => BASE doit être vide, sinon on préfixe à tort /cartographie
    if ($sapi === 'cli-server') {
        $base = '';
    } else {

    // Objectif: BASE doit pointer vers la racine ".../public" (pas vers ".../public/cartographie")
    // Exemple problématique (Apache):
    //   SCRIPT_NAME = /Insuite_backbones/public/cartographie/index.php
    //   => BASE attendu = /Insuite_backbones/public
    // Sinon, on obtient /Insuite_backbones/public/cartographie/cartography/data (404)

        // Si DocumentRoot pointe déjà sur public/, BASE vide
        $docRoot = (string)($_SERVER['DOCUMENT_ROOT'] ?? '');
        if ($docRoot !== '' && preg_match('#(?:^|[/\\\\])public$#', rtrim($docRoot, '/\\'))) {
            $base = '';
        }
        // Si l'URL contient /public/ (à n'importe quel niveau), on force BASE au préfixe jusqu'à /public
        elseif (preg_match('#^(.*?/public)(?:/.*)?$#', $scriptName, $m)) {
            $base = rtrim((string)$m[1], '/');
        }
        // Sinon, base = dossier du script (cas app dans un sous-dossier sans /public)
        else {
            $scriptDir = dirname($scriptName);
            $base = ($scriptDir === '/' || $scriptDir === '\\') ? '' : rtrim($scriptDir, '/\\');
        }
    }
}

// Debug: afficher le BASE dans la console JavaScript
echo "<script>console.log('[PHP] BASE calculé:', " . json_encode($base) . ");</script>";
echo "<script>console.log('[PHP] SCRIPT_NAME:', " . json_encode($_SERVER['SCRIPT_NAME'] ?? 'N/A') . ");</script>";
echo "<script>console.log('[PHP] REQUEST_URI:', " . json_encode($_SERVER['REQUEST_URI'] ?? 'N/A') . ");</script>";
echo "<script>console.log('[PHP] SAPI:', " . json_encode(php_sapi_name()) . ");</script>";
?>


<style>
.cartography-header {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 1.5rem;
    border-radius: 1rem;
    margin-bottom: 1.5rem;
    box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}

.cartography-scope-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    margin-top: 0.75rem;
    padding: 0.45rem 0.85rem;
    border-radius: 999px;
    background: rgba(255,255,255,0.16);
    border: 1px solid rgba(255,255,255,0.22);
    font-size: 0.8rem;
    font-weight: 700;
}

.stats-panel {
    background: white;
    border-radius: 1rem;
    padding: 1rem;
    margin-bottom: 1rem;
    box-shadow: 0 4px 15px rgba(0,0,0,0.05);
    border: 1px solid #e5e7eb;
}

.stat-card {
    text-align: center;
    padding: 0.75rem;
    border-radius: 0.5rem;
    background: #f8fafc;
    border: 1px solid #e2e8f0;
}

.stat-number {
    font-size: 1.5rem;
    font-weight: bold;
    color: #1e293b;
}

.stat-label {
    color: #64748b;
    font-size: 0.875rem;
}

.control-panel {
    background: white;
    border-radius: 1rem;
    padding: 1rem;
    margin-bottom: 1rem;
    box-shadow: 0 4px 15px rgba(0,0,0,0.05);
    border: 1px solid #e5e7eb;
}

.search-container {
    position: relative;
    margin-bottom: 1rem;
}

.search-input {
    width: 100%;
    padding: 0.75rem 2.5rem 0.75rem 1rem;
    border: 1px solid #d1d5db;
    border-radius: 0.5rem;
    font-size: 0.875rem;
}

.search-icon {
    position: absolute;
    right: 0.75rem;
    top: 50%;
    transform: translateY(-50%);
    color: #6b7280;
}

.filter-chips {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}

.chip {
    padding: 0.375rem 0.75rem;
    background: #f3f4f6;
    border: 1px solid #d1d5db;
    border-radius: 1rem;
    font-size: 0.75rem;
    cursor: pointer;
    transition: all 0.2s;
}

.chip.active {
    background: #3b82f6;
    color: white;
    border-color: #3b82f6;
}

.chip:hover {
    background: #e5e7eb;
}

.chip.active:hover {
    background: #2563eb;
}

.map-container {
    position: relative;
    height: 65vh;
    border-radius: 1rem;
    overflow: hidden;
    box-shadow: 0 10px 30px rgba(0,0,0,0.1);
    border: 1px solid #e5e7eb;
}

/* Mode Picture-in-Picture (mini-carte flottante dans la page) */
.map-container.pip-mode {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    width: min(420px, calc(100vw - 2rem));
    height: 280px;
    z-index: 2000;
}

.map-container:fullscreen {
    border-radius: 0;
    height: 100vh;
}

.status-chips {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-top: 0.5rem;
}

.status-chip {
    padding: 0.375rem 0.75rem;
    background: #f3f4f6;
    border: 1px solid #d1d5db;
    border-radius: 1rem;
    font-size: 0.75rem;
    cursor: pointer;
    transition: all 0.2s;
    user-select: none;
}

.status-chip.active {
    background: #3b82f6;
    color: white;
    border-color: #3b82f6;
}

.status-chip:hover {
    background: #e5e7eb;
}

.status-chip.active:hover {
    background: #2563eb;
}

.loading-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(255,255,255,0.9);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    border-radius: 1rem;
}

.loading-spinner {
    width: 40px;
    height: 40px;
    border: 3px solid #f3f4f6;
    border-top: 3px solid #3b82f6;
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

@keyframes pylonBlink {
    0%, 100% { transform: scale(1); opacity: 0.9; }
    50% { transform: scale(1.15); opacity: 0.45; }
}

.pylon-dot {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    border: 2px solid #fff;
    box-shadow: 0 0 10px rgba(255,255,255,0.35);
}

.pylon-dot.blink {
    animation: pylonBlink 1.05s infinite;
}

.floating-panel {
    position: absolute;
    top: 1rem;
    left: 1rem;
    background: white;
    border-radius: 0.75rem;
    padding: 1rem;
    box-shadow: 0 4px 15px rgba(0,0,0,0.1);
    z-index: 1000;
    min-width: 250px;
    max-height: 300px;
    overflow-y: auto;
}

.btn-modern {
    padding: 0.5rem 1rem;
    border-radius: 0.5rem;
    border: 1px solid #d1d5db;
    background: white;
    font-size: 0.875rem;
    transition: all 0.2s;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.btn-modern:hover {
    background: #f9fafb;
    border-color: #9ca3af;
}

.btn-modern.active {
    background: #3b82f6;
    color: white;
    border-color: #3b82f6;
}

.alert-modern {
    background: #fee2e2;
    border: 1px solid #fca5a5;
    color: #dc2626;
    padding: 0.75rem;
    border-radius: 0.5rem;
    font-size: 0.875rem;
}

@media (max-width: 768px) {
    .cartography-header h2 {
        font-size: 1.25rem;
    }
    
    .floating-panel {
        position: relative;
        top: auto;
        left: auto;
        margin-bottom: 1rem;
        width: 100%;
    }
    
    .map-container {
        height: 50vh;
    }
}
</style>

<div class="cartography-header">
    <div class="d-flex align-items-center justify-content-between">
        <div>
            <h2 class="mb-1">🗺️ Cartographie Avancée</h2>
            <p class="mb-0 opacity-75">Visualisation en temps réel des incidents et techniciens</p>
            <?php if ($activeAssignedCity !== ''): ?>
            <div class="cartography-scope-badge"><i class="fas fa-location-dot"></i>Ville active: <?= htmlspecialchars($activeAssignedCity) ?></div>
            <?php endif; ?>
        </div>
        <div class="text-end">
            <div id="lastUpdate" class="small opacity-75">Mise à jour: --:--</div>
            <div id="statusIndicator" class="small">
                <i class="fas fa-circle text-success"></i> En ligne
            </div>
        </div>
    </div>
</div>

<!-- Panneau de statistiques -->
<div class="stats-panel">
    <div class="row g-3">
        <div class="col-md-3 col-6">
            <div class="stat-card">
                <div id="totalIncidents" class="stat-number">--</div>
                <div class="stat-label">Incidents Total</div>
            </div>
        </div>
        <div class="col-md-3 col-6">
            <div class="stat-card">
                <div id="activeIncidents" class="stat-number">--</div>
                <div class="stat-label">En Cours</div>
            </div>
        </div>
        <div class="col-md-3 col-6">
            <div class="stat-card">
                <div id="activeTechs" class="stat-number">--</div>
                <div class="stat-label">Techniciens Actifs</div>
            </div>
        </div>
        <div class="col-md-3 col-6">
            <div class="stat-card">
                <div id="avgResponseTime" class="stat-number">--</div>
                <div class="stat-label">Temps Moyen</div>
            </div>
        </div>
    </div>
</div>

<!-- Panneau de contrôle -->
<div class="control-panel">
    <div class="row">
        <div class="col-md-6">
            <div class="search-container">
                <input type="text" id="searchInput" class="search-input" placeholder="Rechercher un incident, technicien ou lieu...">
                <i class="fas fa-search search-icon"></i>
            </div>
        </div>
        <div class="col-md-6">
            <div class="d-flex gap-2 flex-wrap">
                <button type="button" id="locateMe" class="btn-modern">
                    <i class="fas fa-location-crosshairs"></i> Localiser
                </button>
                <button type="button" id="toggleTrack" class="btn-modern" data-tracking="off">
                    <i class="fas fa-satellite-dish"></i> Suivi GPS
                </button>
                <button type="button" id="openLiaisons3d" class="btn-modern">
                    <i class="fas fa-tower-cell"></i> Liaisons 3D
                </button>
                <button type="button" id="refreshData" class="btn-modern">
                    <i class="fas fa-sync"></i> Actualiser
                </button>
                <button type="button" id="toggleFullscreen" class="btn-modern">
                    <i class="fas fa-expand"></i> Plein écran
                </button>
                <button type="button" id="togglePip" class="btn-modern" data-pip="off">
                    <i class="fas fa-window-restore"></i> PiP
                </button>
            </div>
        </div>
    </div>
    
    <div class="filter-chips">
        <div class="chip active" data-filter="all">🌍 Tout afficher</div>
        <div class="chip" data-filter="incidents">🔥 Incidents</div>
        <div class="chip" data-filter="technicians">👷 Techniciens</div>
        <div class="chip" data-filter="trajectories">🛤️ Trajectoires</div>
        <div class="chip" data-filter="liaisons">🔌 Liaisons</div>
        <div class="chip" data-filter="high-priority">⚡ Priorité haute</div>
    </div>

    <!-- Filtres avancés: historique trajets + incidents déclarés sur période -->
    <div id="trajectoryFilters" class="mt-2 d-none">
        <div class="row g-2 align-items-end">
            <div class="col-12 col-md-4">
                <label class="form-label small text-muted mb-1">Technicien</label>
                <select id="trajectoryUser" class="form-select">
                    <option value="">Tous</option>
                </select>
            </div>
            <div class="col-6 col-md-3">
                <label class="form-label small text-muted mb-1">Du</label>
                <input id="trajectoryFrom" type="datetime-local" class="form-control">
            </div>
            <div class="col-6 col-md-3">
                <label class="form-label small text-muted mb-1">Au</label>
                <input id="trajectoryTo" type="datetime-local" class="form-control">
            </div>
            <div class="col-12 col-md-2">
                <button type="button" id="applyTrajectoryFilters" class="btn btn-primary w-100">
                    <i class="fas fa-filter me-1"></i>Afficher
                </button>
            </div>
        </div>
        <div class="mt-2 small text-muted" id="trajectoryStats"></div>
    </div>

    <div class="status-chips" aria-label="Vue des statuts incidents">
        <div class="status-chip active" data-incident-status="all">📋 Tous</div>
        <div class="status-chip" data-incident-status="open">🔴 Ouverts (<span id="countOpen">--</span>)</div>
        <div class="status-chip" data-incident-status="in_progress">🚧 En cours (<span id="countInProgress">--</span>)</div>
        <div class="status-chip" data-incident-status="resolved">✅ Résolus (<span id="countResolved">--</span>)</div>
    </div>
</div>

<!-- Conteneur de la carte -->
<div class="map-container">
    <div id="loadingOverlay" class="loading-overlay" style="display: none;">
        <div class="loading-spinner"></div>
    </div>
    <div id="map" style="height: 100%; width: 100%;"></div>
</div>

<!-- Panneau flottant d'informations -->
<div id="infosPanel" class="floating-panel d-none">
    <h6 class="mb-3">📊 Informations Détaillées</h6>
    <div id="panelContent">Sélectionnez un élément sur la carte</div>
</div>

<!-- Modal Liaisons 3D (nécessite Bootstrap JS si dispo, sinon fallback JS) -->
<div class="modal fade" id="liaisons3dModal" tabindex="-1" aria-hidden="true" style="display:none;">
    <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title"><i class="fas fa-tower-cell me-2"></i>Liaisons (pylônes 3D)</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
            </div>
            <div class="modal-body">
                <div class="d-flex flex-wrap gap-2 mb-2 align-items-center">
                    <div class="small text-muted">Couleurs: Ouvert=Rouge (scintillant), En cours=Orange, Traité=Bleu, Clôturé=Vert (scintillant)</div>
                    <button type="button" id="liaisons3dFocusOpen" class="btn btn-outline-danger btn-sm ms-auto">
                        <i class="fas fa-bullseye me-1"></i>Focus incidents ouverts
                    </button>
                </div>
                <div id="liaisons3dCanvasWrap" class="border rounded" style="height:65vh; overflow:hidden; background:#0b1220;"></div>
                <div class="form-text">Astuce: molette=zoom, clic+drag=rotation.</div>
            </div>
        </div>
    </div>
</div>

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
<script src="https://unpkg.com/leaflet-polylinedecorator@1.6.0/dist/leaflet.polylineDecorator.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.5.2/js/all.min.js"></script>
<!-- Three.js est chargé dynamiquement (fallback multi-CDN) au moment d'ouvrir la modale 3D -->

<script>
// Configuration globale
const BASE = '<?= $base ?>';
console.log('[CONFIG] BASE défini à:', BASE);
console.log('[CONFIG] URL complète API incidents:', BASE + '/cartography/data');
console.log('[CONFIG] URL complète API techniciens:', BASE + '/cartography/technicians');

const CONFIG = {
    autoRefresh: true,
    refreshInterval: 15000, // 15 secondes
    maxAge: 1440, // minutes
    defaultCenter: [5.35, -4.02], // Côte d'Ivoire
    defaultZoom: 6,
    clusterDistance: 50
};

console.log('[CONFIG] Configuration:', CONFIG);

// Variables globales
let map, incidentsLayer, techsLayer, trajectoriesLayer, clusterGroup, liaisonsLayer;
let watchId = null;
let refreshTimer = null;
let currentFilters = { filter: 'all' };
let allIncidents = [], allTechnicians = [], allLiaisons = [];
let myLocationMarker = null;
let myAccuracyCircle = null;
let autoRefreshTick = 0;
let pipWindowRef = null;

// Confort d'usage: ne pas casser la vue après zoom/pan utilisateur
let userHasInteractedWithMap = false;
let hasAutoFittedOnce = false;
let autoFitRequested = false;
let hasAutoCenteredOnce = false;

let lastAlertedOpenLiaisonIds = new Set();
let lastLiaisonsFetchAt = 0;
let liaisonsDebugAlreadyShown = false;

function markUserInteraction() {
    userHasInteractedWithMap = true;
}

function shouldAutoFitBounds() {
    // Auto-fit uniquement au premier affichage, ou si l'utilisateur le demande explicitement.
    if (autoFitRequested) return true;
    if (hasAutoFittedOnce) return false;
    if (userHasInteractedWithMap) return false;
    return true;
}

function setAutoTrackingPreference(isOn) {
    try {
        localStorage.setItem('cartography_auto_tracking', isOn ? 'on' : 'off');
    } catch (e) {}
}

function getAutoTrackingPreference() {
    try {
        return localStorage.getItem('cartography_auto_tracking') || 'on';
    } catch (e) {
        return 'on';
    }
}

function getIncidentStatusBucket(incident) {
    const status = (incident?.status || '').toLowerCase();
    const isResolved = status.includes('clôtur') || status.includes('terminé') || status.includes('résolu') || status.includes('fermé');
    if (isResolved) return 'resolved';

    const isInProgress = status.includes('cours') || status.includes('attribué') || status.includes('traitement');
    if (isInProgress) return 'in_progress';

    return 'open';
}

function playAlertBeep() {
    try {
        const AudioCtx = window.AudioContext || window.webkitAudioContext;
        if (!AudioCtx) return;
        const ctx = new AudioCtx();
        const o = ctx.createOscillator();
        const g = ctx.createGain();
        o.type = 'sine';
        o.frequency.value = 880;
        g.gain.value = 0.05;
        o.connect(g);
        g.connect(ctx.destination);
        o.start();
        setTimeout(() => { o.stop(); ctx.close(); }, 250);
    } catch (e) {}
}

function statusBucketToColor(bucket) {
    switch ((bucket || '').toLowerCase()) {
        case 'critical': return '#dc143c';      // Rouge vif (critique)
        case 'open': return '#ff0000';          // Rouge (ouvert)
        case 'in_progress': return '#ff8a00';   // Orange (en cours)
        case 'treated': return '#1e90ff';       // Bleu (traité)
        case 'resolved': return '#22c55e';      // Vert (résolu)
        case 'closed': return '#22c55e';        // Vert (fermé)
        case 'unknown': return '#9ca3af';       // Gris (pas de suivi)
        default: return '#22c55e';
    }
}

function setTrajectoryFiltersVisible(isVisible) {
    const wrap = document.getElementById('trajectoryFilters');
    if (!wrap) return;
    wrap.classList.toggle('d-none', !isVisible);
    if (isVisible) {
        loadTechniciansListIntoSelect();
    }
}

async function loadTechniciansListIntoSelect() {
    const sel = document.getElementById('trajectoryUser');
    if (!sel) return;
    try {
        const res = await fetch(`${BASE}/cartography/technicians-list`, { credentials: 'include' });
        const users = await res.json().catch(() => []);
        const current = sel.value;
        sel.innerHTML = '<option value="">Tous</option>';
        (Array.isArray(users) ? users : []).forEach(u => {
            const opt = document.createElement('option');
            opt.value = String(u.id);
            opt.textContent = u.name;
            sel.appendChild(opt);
        });
        if (current) sel.value = current;
    } catch (e) {
        // ignore
    }
}

function readTrajectoryFilters() {
    const userSel = document.getElementById('trajectoryUser');
    const fromEl = document.getElementById('trajectoryFrom');
    const toEl = document.getElementById('trajectoryTo');
    const user_id = userSel && userSel.value ? userSel.value : '';
    // datetime-local retourne "YYYY-MM-DDTHH:MM"; strtotime() côté PHP sait le parser.
    const from = fromEl && fromEl.value ? fromEl.value : '';
    const to = toEl && toEl.value ? toEl.value : '';
    return { user_id, from, to };
}

async function applyTrajectoryFilters() {
    const statsEl = document.getElementById('trajectoryStats');
    const filters = readTrajectoryFilters();
    await loadTrajectories({ ...filters });

    // Auto-fit sur les trajectoires filtrées (si dispo)
    try {
        const trajs = window.__lastTrajectoriesData || [];
        const allPts = [];
        trajs.forEach(t => {
            (t.points || []).forEach(p => {
                if (p && isFinite(p.lat) && isFinite(p.lng)) allPts.push([p.lat, p.lng]);
            });
        });
        if (map && allPts.length >= 2) {
            autoFitRequested = true;
            map.fitBounds(L.latLngBounds(allPts).pad(0.25));
        }
        if (allPts.length === 0) {
            showError('Aucune trajectoire sur la période (ou GPS non historisé). Activez “Suivi GPS” quelques minutes, puis réessayez.');
        }
    } catch (e) {}

    // Stats incidents déclarés: somme des trajectoires (si backend renvoie stats)
    try {
        let total = 0;
        (window.__lastTrajectoriesData || []).forEach(t => {
            if (t && t.stats && typeof t.stats.incidents_declared === 'number') total += t.stats.incidents_declared;
        });
        if (statsEl) statsEl.textContent = `Incidents déclarés sur la période: ${total}`;
    } catch (e) {
        if (statsEl) statsEl.textContent = '';
    }
}

async function loadLiaisons(options = {}) {
    const debugIfEmpty = options && options.debugIfEmpty === true;
    try {
        // Throttle AGRESSIF: évite de marteler l'API (30s min entre appels)
        // La requête /liaisons peut être lourde (joins + normalisation noms)
        const now = Date.now();
        if (now - lastLiaisonsFetchAt < 30000 && Array.isArray(allLiaisons) && allLiaisons.length > 0) {
            console.log('[LIAISONS] Throttle actif, réutilisation cache (' + Math.round((now - lastLiaisonsFetchAt)/1000) + 's depuis dernier fetch)');
            return;
        }

        const res = await fetch(`${BASE}/cartography/liaisons`, { credentials: 'include' });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const data = await res.json().catch(() => []);
        allLiaisons = Array.isArray(data) ? data : [];
        lastLiaisonsFetchAt = now;

        // Diagnostic si rien ne match
        if (debugIfEmpty && !liaisonsDebugAlreadyShown && allLiaisons.length === 0) {
            try {
                const dbgRes = await fetch(`${BASE}/cartography/liaisons?debug=1`, { credentials: 'include' });
                const dbg = await dbgRes.json().catch(() => null);
                if (dbg && dbg.stats) {
                    console.warn('[LIAISONS][DEBUG]', dbg);
                    const missing = Array.isArray(dbg.missing) ? dbg.missing : [];
                    const sample = missing.slice(0, 3).map(m => {
                        const a = m.site_a || '—';
                        const b = m.site_b || '—';
                        const sa = m.suggest_a?.name ? ` (A≈ ${m.suggest_a.name})` : '';
                        const sb = m.suggest_b?.name ? ` (B≈ ${m.suggest_b.name})` : '';
                        return `${a}${sa} / ${b}${sb}`;
                    });
                    showError(`Aucune liaison géolocalisée. Liaisons=${dbg.stats.liaisons_total}, Locations(avec coords)=${dbg.stats.locations_with_coords}. Exemples: ${sample.join(' | ')}. Ouvrez /cartography/liaisons?debug=1 pour le détail.`);
                    liaisonsDebugAlreadyShown = true;
                }
            } catch (e) {}
        }
    } catch (e) {
        allLiaisons = [];
    }
}

function clearLiaisonsLayer() {
    if (liaisonsLayer) liaisonsLayer.clearLayers();
}

function renderLiaisons2D() {
    if (!liaisonsLayer) return;
    clearLiaisonsLayer();

    const boundsPts = [];
    (allLiaisons || []).forEach(l => {
        if (!l?.a || !l?.b) return;
        const color = statusBucketToColor(l.status_bucket);
        const aLat = Number(l.a.lat);
        const aLng = Number(l.a.lng);
        const bLat = Number(l.b.lat);
        const bLng = Number(l.b.lng);
        if (!isFinite(aLat) || !isFinite(aLng) || !isFinite(bLat) || !isFinite(bLng)) return;
        const a = [aLat, aLng];
        const b = [bLat, bLng];
        boundsPts.push(a, b);

        const line = L.polyline([a, b], { color, weight: 4, opacity: 0.9 }).addTo(liaisonsLayer);
        const shouldBlink = (l.status_bucket === 'critical' || l.status_bucket === 'open' || l.status_bucket === 'closed' || l.status_bucket === 'resolved');
        const iconHtml = `<div class="pylon-dot ${shouldBlink ? 'blink' : ''}" style="background:${color}; box-shadow: 0 0 12px ${color};"></div>`;
        const pylonIcon = L.divIcon({ className: 'pylon-icon', html: iconHtml, iconSize: [18, 18], iconAnchor: [9, 9] });

        const popup = `<div style="min-width:220px">
            <div class="fw-bold">${(l.name || 'Liaison')}</div>
            <div class="small text-muted">${l.site_a} ↔ ${l.site_b}</div>
            <div class="mt-2"><span class="badge" style="background:${color}">${l.status_bucket}</span></div>
        </div>`;

        L.marker(a, { icon: pylonIcon, title: `Site A: ${l.site_a}` }).bindPopup(popup).addTo(liaisonsLayer);
        L.marker(b, { icon: pylonIcon, title: `Site B: ${l.site_b}` }).bindPopup(popup).addTo(liaisonsLayer);
        line.bindPopup(popup);
    });

    if (boundsPts.length >= 2 && map) {
        autoFitRequested = true;
        map.fitBounds(L.latLngBounds(boundsPts).pad(0.25));
    }

    if ((allLiaisons || []).length === 0) {
        showError('Aucune liaison géolocalisée. Vérifiez que “locations” contient bien les coordonnées et que les noms matchent Site A/B.');
    }
}

async function checkOpenLiaisonsAlerts() {
    try {
        await loadLiaisons({ debugIfEmpty: false });
        const openLiaisons = (allLiaisons || []).filter(l => l.status_bucket === 'open');
        const openIds = new Set(openLiaisons.map(l => String(l.id)));
        let hasNew = false;
        openIds.forEach(id => { if (!lastAlertedOpenLiaisonIds.has(id)) hasNew = true; });

        if (hasNew && openLiaisons.length > 0 && map) {
            playAlertBeep();
            const first = openLiaisons[0];
            if (first?.a && first?.b) {
                const bounds = L.latLngBounds([
                    [first.a.lat, first.a.lng],
                    [first.b.lat, first.b.lng]
                ]);
                map.fitBounds(bounds.pad(0.6));
            }
        }
        lastAlertedOpenLiaisonIds = openIds;
    } catch (e) {}
}

function ensureBootstrapModal(id) {
    const el = document.getElementById(id);
    if (!el) return null;
    try {
        if (typeof bootstrap !== 'undefined' && bootstrap.Modal) return new bootstrap.Modal(el);
        if (typeof window.bootstrap !== 'undefined' && window.bootstrap.Modal) return new window.bootstrap.Modal(el);
    } catch (e) {}
    return null;
}

// 3D (Three.js)
let liaisons3d = { scene: null, camera: null, renderer: null, controls: null, anim: null, meshes: [], openFocus: [] };

function loadScriptOnce(url) {
    return new Promise((resolve, reject) => {
        // Déjà chargé ?
        const existing = Array.from(document.scripts || []).find(s => (s.src || '').includes(url));
        if (existing) {
            resolve();
            return;
        }

        const s = document.createElement('script');
        s.src = url;
        s.async = true;
        s.onload = () => resolve();
        s.onerror = () => reject(new Error('failed:' + url));
        document.head.appendChild(s);
    });
}

async function ensureThreeReady() {
    // Charge THREE + OrbitControls LOCALEMENT en priorité (offline-first).
    // Les CDN sont uniquement des fallbacks si les fichiers locaux sont absents.
    const threeCandidates = [
        // PRIORITÉ 1: Fichiers locaux (déjà déployés)
        `${BASE}/assets/vendor/three/three.min.js`,
        // Fallback CDN (si réseau OK)
        'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js',
        'https://unpkg.com/three@0.160.0/build/three.min.js',
        'https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.min.js'
    ];
    const orbitCandidates = [
        // PRIORITÉ 1: Fichier local (wrapper personnalisé déjà déployé)
        `${BASE}/assets/vendor/three/OrbitControls.js`
        // Pas de fallback CDN pour OrbitControls (version classique inexistante en r160)
    ];

    if (!window.THREE) {
        console.log('[3D] Chargement de THREE.js...');
        let loaded = false;
        for (const url of threeCandidates) {
            try {
                console.log('[3D] Tentative:', url);
                await loadScriptOnce(url);
                if (window.THREE) {
                    console.log('[3D] ✓ THREE.js chargé depuis:', url);
                    loaded = true;
                    break;
                }
            } catch (e) {
                console.warn('[3D] ✗ Échec:', url, e.message);
            }
        }
        if (!loaded) {
            console.error('[3D] ✗ Impossible de charger THREE.js depuis aucune source');
            return false;
        }
    } else {
        console.log('[3D] THREE.js déjà chargé');
    }

    if (!(window.THREE && window.THREE.OrbitControls)) {
        console.log('[3D] Chargement de OrbitControls...');
        let loaded = false;
        for (const url of orbitCandidates) {
            try {
                console.log('[3D] Tentative:', url);
                await loadScriptOnce(url);
                if (window.THREE && window.THREE.OrbitControls) {
                    console.log('[3D] ✓ OrbitControls chargé depuis:', url);
                    loaded = true;
                    break;
                }
            } catch (e) {
                console.warn('[3D] ✗ Échec:', url, e.message);
            }
        }
        if (!loaded) {
            console.error('[3D] ✗ Impossible de charger OrbitControls depuis aucune source');
            return false;
        }
    } else {
        console.log('[3D] OrbitControls déjà chargé');
    }

    console.log('[3D] ✓ Moteur 3D prêt !');
    return true;
}

function build3DScene(container) {
    const rect0 = container.getBoundingClientRect ? container.getBoundingClientRect() : { width: container.clientWidth, height: container.clientHeight };
    const width = Math.max(10, Math.round(rect0.width || container.clientWidth || 0));
    const height = Math.max(10, Math.round(rect0.height || container.clientHeight || 0));
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0b1220);

    const camera = new THREE.PerspectiveCamera(55, width / height, 0.1, 2000);
    camera.position.set(0, 40, 70);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(width, height);
    renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));

    container.innerHTML = '';
    container.appendChild(renderer.domElement);

    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.06;

    scene.add(new THREE.AmbientLight(0xffffff, 0.6));
    const dir = new THREE.DirectionalLight(0xffffff, 0.8);
    dir.position.set(30, 50, 20);
    scene.add(dir);
    scene.add(new THREE.GridHelper(140, 20, 0x223044, 0x182235));

    // Resize: important lorsque le canvas est créé juste après l'ouverture d'une modal.
    const resize = () => {
        try {
            const rect = container.getBoundingClientRect ? container.getBoundingClientRect() : { width: container.clientWidth, height: container.clientHeight };
            const w = Math.max(10, Math.round(rect.width || container.clientWidth || 0));
            const h = Math.max(10, Math.round(rect.height || container.clientHeight || 0));
            camera.aspect = w / h;
            camera.updateProjectionMatrix();
            renderer.setSize(w, h);
        } catch (e) {}
    };

    // Double RAF: laisse le temps au layout de se stabiliser.
    requestAnimationFrame(() => requestAnimationFrame(resize));
    window.addEventListener('resize', resize, { passive: true });

    return { scene, camera, renderer, controls, resize };
}

function latLngToXZ(lat, lng) {
    return { x: lng * 120, z: -lat * 120 };
}

function createPylon(colorHex) {
    const group = new THREE.Group();
    const mat = new THREE.MeshStandardMaterial({
        color: colorHex,
        emissive: colorHex,
        emissiveIntensity: 0.55,
    });
    const base = new THREE.Mesh(new THREE.CylinderGeometry(1.2, 1.4, 2.0, 10), mat);
    base.position.y = 1;
    const tower = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.9, 18.0, 10), mat);
    tower.position.y = 10;
    const head = new THREE.Mesh(new THREE.BoxGeometry(2.2, 1.2, 2.2), mat);
    head.position.y = 19;
    group.add(base, tower, head);
    group.userData.material = mat;
    return group;
}

function renderLiaisons3D(container, liaisons) {
    // Filtrer les liaisons valides (coords présentes et finies)
    const valid = (liaisons || []).filter(l => {
        if (!l || !l.a || !l.b) return false;
        const aLat = Number(l.a.lat), aLng = Number(l.a.lng);
        const bLat = Number(l.b.lat), bLng = Number(l.b.lng);
        return isFinite(aLat) && isFinite(aLng) && isFinite(bLat) && isFinite(bLng);
    });

    if (!valid.length) {
        container.innerHTML = `
            <div class="p-3">
                <div class="fw-bold mb-2">Aucune liaison géolocalisée à afficher</div>
                <div class="text-muted small mb-2">
                    Les pylônes 3D nécessitent des coordonnées pour les deux extrémités (A et B).
                </div>
                <div class="small mb-2">
                    <a href="${BASE}/cartography/liaisons?debug=1" target="_blank" rel="noopener">Ouvrir le diagnostic des liaisons</a>
                </div>
                <div class="text-muted small">
                    Astuce: complétez les coordonnées dans <code>liaisons.site_a_latitude/site_a_longitude</code> et <code>site_b_latitude/site_b_longitude</code>
                    ou assurez-vous que <code>locations.name</code> matche <code>liaisons.site_a_name/site_b_name</code>.
                </div>
            </div>
        `;
        return;
    }

    const built = build3DScene(container);
    liaisons3d.scene = built.scene;
    liaisons3d.camera = built.camera;
    liaisons3d.renderer = built.renderer;
    liaisons3d.controls = built.controls;
    liaisons3d.meshes = [];
    liaisons3d.openFocus = [];

    const pts = [];
    (valid || []).forEach(l => {
        if (l?.a && l?.b) {
            pts.push(latLngToXZ(l.a.lat, l.a.lng));
            pts.push(latLngToXZ(l.b.lat, l.b.lng));
        }
    });
    let cx = 0, cz = 0;
    if (pts.length) {
        cx = pts.reduce((s,p)=>s+p.x,0)/pts.length;
        cz = pts.reduce((s,p)=>s+p.z,0)/pts.length;
    }

    (valid || []).forEach(l => {
        if (!l?.a || !l?.b) return;
        const col = new THREE.Color(statusBucketToColor(l.status_bucket));
        const pA = latLngToXZ(l.a.lat, l.a.lng);
        const pB = latLngToXZ(l.b.lat, l.b.lng);

        const pa = createPylon(col);
        pa.position.set(pA.x - cx, 0, pA.z - cz);
        const pb = createPylon(col);
        pb.position.set(pB.x - cx, 0, pB.z - cz);

        const geom = new THREE.BufferGeometry().setFromPoints([
            new THREE.Vector3(pa.position.x, 18, pa.position.z),
            new THREE.Vector3(pb.position.x, 18, pb.position.z)
        ]);
        const lineMat = new THREE.LineBasicMaterial({ color: col, transparent: true, opacity: 0.9 });
        const line = new THREE.Line(geom, lineMat);
        line.userData.lineMat = lineMat;

        liaisons3d.scene.add(pa);
        liaisons3d.scene.add(pb);
        liaisons3d.scene.add(line);

        const entry = { l, pa, pb, line };
        liaisons3d.meshes.push(entry);
        if (l.status_bucket === 'critical' || l.status_bucket === 'open') liaisons3d.openFocus.push(entry);
    });

    liaisons3d.controls.target.set(0, 8, 0);
    liaisons3d.controls.update();

    const start = performance.now();
    const animate = (t) => {
        const dt = (t - start) / 1000;
        (liaisons3d.meshes || []).forEach(m => {
            const bucket = (m.l?.status_bucket || '').toLowerCase();
            // Scintillement pour Critique/Ouvert (rouge) et Clôturé/Résolu (vert)
            const blink = (bucket === 'critical' || bucket === 'open' || bucket === 'closed' || bucket === 'resolved');
            const pulse = blink ? (0.25 + 0.45 * Math.abs(Math.sin(dt * 3.2))) : 0.22;
            if (m.pa.userData.material) m.pa.userData.material.emissiveIntensity = 0.2 + pulse;
            if (m.pb.userData.material) m.pb.userData.material.emissiveIntensity = 0.2 + pulse;
            if (m.line.userData.lineMat) m.line.userData.lineMat.opacity = 0.55 + 0.35 * pulse;
        });
        liaisons3d.controls && liaisons3d.controls.update();
        liaisons3d.renderer && liaisons3d.renderer.render(liaisons3d.scene, liaisons3d.camera);
        liaisons3d.anim = requestAnimationFrame(animate);
    };
    liaisons3d.anim = requestAnimationFrame(animate);
}

function focusFirstOpenLiaison3D() {
    const first = (liaisons3d.openFocus || [])[0];
    if (!first || !liaisons3d.camera || !liaisons3d.controls) return;
    const mid = new THREE.Vector3().addVectors(first.pa.position, first.pb.position).multiplyScalar(0.5);
    liaisons3d.controls.target.copy(new THREE.Vector3(mid.x, 8, mid.z));
    liaisons3d.camera.position.set(mid.x + 30, 35, mid.z + 30);
    liaisons3d.controls.update();
    playAlertBeep();
}

function stopLiaisons3D() {
    try {
        if (liaisons3d.anim) cancelAnimationFrame(liaisons3d.anim);
    } catch (e) {}
    liaisons3d.anim = null;
    if (liaisons3d.renderer && liaisons3d.renderer.domElement) {
        try { liaisons3d.renderer.dispose(); } catch (e) {}
    }
    liaisons3d.scene = null;
    liaisons3d.camera = null;
    liaisons3d.renderer = null;
    liaisons3d.controls = null;
    liaisons3d.meshes = [];
    liaisons3d.openFocus = [];
}

function updateMyLocationOnMap(lat, lng, accuracy) {
    if (!map || !lat || !lng) return;

    const pos = [lat, lng];
    const acc = (typeof accuracy === 'number' && !isNaN(accuracy)) ? accuracy : null;

    if (!myLocationMarker) {
        myLocationMarker = L.marker(pos, {
            title: 'Ma position',
        }).addTo(map);
    } else {
        myLocationMarker.setLatLng(pos);
    }

    if (acc !== null) {
        if (!myAccuracyCircle) {
            myAccuracyCircle = L.circle(pos, {
                radius: acc,
                color: '#0d6efd',
                weight: 1,
                fillColor: '#0d6efd',
                fillOpacity: 0.12,
            }).addTo(map);
        } else {
            myAccuracyCircle.setLatLng(pos);
            myAccuracyCircle.setRadius(acc);
        }
    }
}

// Initialisation de la carte
function initializeMap() {
    showLoading(true);
    
    map = L.map('map').setView(CONFIG.defaultCenter, CONFIG.defaultZoom);

    // Détecter les interactions utilisateur (évite les recentrages automatiques agaçants)
    try {
        const container = map.getContainer();
        ['wheel', 'mousedown', 'touchstart', 'pointerdown', 'keydown'].forEach(evt => {
            container.addEventListener(evt, () => markUserInteraction(), { passive: true });
        });
        map.on('dragstart', markUserInteraction);
        map.on('zoomstart', markUserInteraction);
    } catch (e) {}
    
    // Tuiles de base avec options multiples
    const baseMaps = {
        "OpenStreetMap": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            maxZoom: 19,
            attribution: '&copy; OpenStreetMap contributors'
        }),
        "Satellite": L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
            maxZoom: 19,
            attribution: '&copy; Esri'
        }),
        "Terrain": L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
            maxZoom: 17,
            attribution: '&copy; OpenTopoMap'
        })
    };
    
    // Ajouter la couche par défaut
    baseMaps["OpenStreetMap"].addTo(map);
    
    // Créer les groupes de couches
    incidentsLayer = L.layerGroup();
    techsLayer = L.layerGroup();
    trajectoriesLayer = L.layerGroup();
    liaisonsLayer = L.layerGroup();
    clusterGroup = L.markerClusterGroup({
        maxClusterRadius: CONFIG.clusterDistance,
        spiderfyOnMaxZoom: true,
        showCoverageOnHover: false
    });
    
    // Contrôle des couches
    const overlays = {
        '🔥 Incidents': incidentsLayer,
        '👷 Techniciens': techsLayer,
        '🛤️ Trajectoires': trajectoriesLayer,
        '🔌 Liaisons': liaisonsLayer,
        '📍 Clusters': clusterGroup
    };
    
    L.control.layers(baseMaps, overlays, { 
        position: 'topright',
        collapsed: false 
    }).addTo(map);
    
    // Ajouter les couches par défaut
    map.addLayer(incidentsLayer);
    map.addLayer(techsLayer);
    map.addLayer(trajectoriesLayer);
    map.addLayer(clusterGroup);
    
    // Échelle
    L.control.scale({ position: 'bottomright' }).addTo(map);
    
    showLoading(false);
}

// Gestion du chargement
function showLoading(show) {
    const overlay = document.getElementById('loadingOverlay');
    if (overlay) {
        overlay.style.display = show ? 'flex' : 'none';
    }
}

// Mise à jour de l'horodatage
function updateLastUpdate() {
    const element = document.getElementById('lastUpdate');
    if (element) {
        element.textContent = `Mise à jour: ${new Date().toLocaleTimeString('fr-FR')}`;
    }
}

// Chargement des incidents avec cache et filtrage
async function loadIncidents() {
    console.log('[INCIDENTS] Chargement des incidents...');
    
    try {
        const res = await fetch(`${BASE}/cartography/data`, {
            credentials: 'include' // Important pour les cookies de session
        });
        
        console.log('[INCIDENTS] Statut réponse:', res.status);
        
        if (!res.ok) {
            const errorText = await res.text();
            console.error('[INCIDENTS] Erreur HTTP:', errorText);
            throw new Error(`HTTP ${res.status}: ${res.statusText}`);
        }
        
        const data = await res.json();
        console.log('[INCIDENTS] Données brutes reçues:', data);
        
        allIncidents = Array.isArray(data) ? data : [];
        
        console.log(`[INCIDENTS] ${allIncidents.length} incidents reçus`, allIncidents);
        
        // Mettre à jour les statistiques
        updateStats();
        
        // Filtrer et afficher
        filterAndDisplayIncidents();
        
        updateLastUpdate();

        // Alertes/zoom liaisons ouvertes: DÉSACTIVÉ (cause surcharge serveur)
        // L'utilisateur peut cliquer manuellement sur "Liaisons" si besoin
        // checkOpenLiaisonsAlerts();
        
    } catch (error) {
        console.error('[INCIDENTS] Erreur complète:', error);
        showError(`Erreur de chargement des incidents: ${error.message}`);
        allIncidents = [];
        updateStats(); // Mettre à jour même en cas d'erreur
    }
}

// Filtrage et affichage des incidents
function filterAndDisplayIncidents() {
    incidentsLayer.clearLayers();
    clusterGroup.clearLayers();

    const incidentViewActive = currentFilters.filter === 'all'
        || currentFilters.filter === 'incidents'
        || currentFilters.filter === 'high-priority';
    
    console.log('[FILTER] Filtrage de', allIncidents.length, 'incidents');
    console.log('[FILTER] Filtre actuel:', currentFilters);
    
    const filtered = allIncidents.filter(incident => {
        // Filtrage par type
        if (currentFilters.filter === 'high-priority') {
            return incident.priority === 'Haute' || incident.priority === 'Critique';
        }

        // Filtrage par statut (vue)
        if (currentFilters.incidentStatus && currentFilters.incidentStatus !== 'all') {
            return getIncidentStatusBucket(incident) === currentFilters.incidentStatus;
        }
        
        // Filtrage par recherche
        if (currentFilters.search) {
            const search = currentFilters.search.toLowerCase();
            return incident.title.toLowerCase().includes(search) ||
                   (incident.location && incident.location.toLowerCase().includes(search)) ||
                   (incident.status && incident.status.toLowerCase().includes(search));
        }
        
        return true;
    });
    
    console.log(`[FILTER] Affichage de ${filtered.length} incidents filtrés`);
    
    const bounds = [];
    
    filtered.forEach(incident => {
        console.log('[MARKER] Traitement incident:', incident);
        
        if (!incident.lat || !incident.lng) {
            console.warn('[MARKER] Incident sans coordonnées:', incident);
            return;
        }
        
        // Icône personnalisée selon la priorité
        const icon = createIncidentIcon(incident);
        
        const marker = L.marker([incident.lat, incident.lng], { icon })
            .bindPopup(createIncidentPopup(incident))
            .on('click', () => showIncidentDetails(incident));
        
        console.log('[MARKER] Marqueur créé à:', incident.lat, incident.lng);
        
        // Ajouter au cluster ou à la couche normale selon la préférence
        if (currentFilters.filter === 'all' || currentFilters.filter === 'incidents') {
            clusterGroup.addLayer(marker);
        } else if (currentFilters.filter === 'high-priority') {
            incidentsLayer.addLayer(marker);
        }
        
        bounds.push([incident.lat, incident.lng]);
    });
    
    // Ajuster la vue sur les incidents
    if (incidentViewActive && bounds.length > 0) {
        console.log('[FILTER] Ajustement de la vue sur', bounds.length, 'incidents');
        if (shouldAutoFitBounds()) {
            const group = L.featureGroup(
                bounds.map(b => L.marker(b))
            );
            map.fitBounds(group.getBounds().pad(0.1));
            hasAutoFittedOnce = true;
            autoFitRequested = false;
        }
    } else {
        console.log('[FILTER] Aucun incident à afficher, vue par défaut');
    }
}

// Création d'icônes personnalisées pour les incidents
function createIncidentIcon(incident) {
    // Déterminer l'icône selon le statut
    let iconHtml = '';
    let iconColor = incident.color || '#3b82f6';
    const status = (incident.status || '').toLowerCase();
    
    if (status.includes('clôtur') || status.includes('terminé') || status.includes('résolu')) {
        // Incident terminé
        iconHtml = '✅';
        iconColor = '#10b981';
    } else if (status.includes('cours') || status.includes('attribué') || status.includes('traitement')) {
        // Incident en cours
        iconHtml = '🚧';
        iconColor = '#f59e0b';
    } else if (status.includes('nouveau') || status.includes('ouvert')) {
        // Incident nouveau
        iconHtml = '🔴';
        iconColor = '#ef4444';
    } else {
        // Par défaut selon la priorité
        switch (incident.priority?.toLowerCase()) {
            case 'critique':
                iconHtml = '🚨';
                iconColor = '#dc2626';
                break;
            case 'haute':
                iconHtml = '⚡';
                iconColor = '#ea580c';
                break;
            case 'moyenne':
                iconHtml = '⚠️';
                iconColor = '#ca8a04';
                break;
            case 'basse':
                iconHtml = '🔧';
                iconColor = '#16a34a';
                break;
            default:
                iconHtml = '📍';
                iconColor = '#3b82f6';
        }
    }
    
    return L.divIcon({
        html: `<div style="
            background: ${iconColor};
            width: 36px;
            height: 36px;
            border-radius: 50% 50% 50% 0;
            transform: rotate(-45deg);
            border: 3px solid white;
            box-shadow: 0 3px 10px rgba(0,0,0,0.3);
            display: flex;
            align-items: center;
            justify-content: center;
        ">
            <span style="
                transform: rotate(45deg);
                font-size: 18px;
                display: block;
                margin-top: -4px;
            ">${iconHtml}</span>
        </div>`,
        className: 'custom-incident-icon',
        iconSize: [36, 36],
        iconAnchor: [18, 36],
        popupAnchor: [0, -36]
    });
}

// Création du contenu popup pour incident
function createIncidentPopup(incident) {
    // Déterminer le badge selon le statut
    let badgeClass = 'bg-secondary';
    const status = (incident.status || '').toLowerCase();
    
    if (status.includes('clôtur') || status.includes('terminé') || status.includes('résolu')) {
        badgeClass = 'bg-success';
    } else if (status.includes('cours') || status.includes('attribué') || status.includes('traitement')) {
        badgeClass = 'bg-warning';
    } else if (status.includes('nouveau') || status.includes('ouvert')) {
        badgeClass = 'bg-danger';
    }
    
    // Badge pour la priorité
    let priorityBadge = 'bg-info';
    switch (incident.priority?.toLowerCase()) {
        case 'critique':
            priorityBadge = 'bg-danger';
            break;
        case 'haute':
            priorityBadge = 'bg-warning';
            break;
        case 'moyenne':
            priorityBadge = 'bg-primary';
            break;
        case 'basse':
            priorityBadge = 'bg-success';
            break;
    }
    
    // Section technicien
    let technicianHtml = '';
    if (incident.technician) {
        technicianHtml = `
            <div class="mb-2">
                <small class="text-muted d-block mb-1"><strong><i class="bi bi-person-fill"></i> Technicien</strong></small>
                <span class="badge bg-info px-2 py-1">${incident.technician}</span>
            </div>`;
    }
    
    // Section images
    let imagesHtml = '';
    if (incident.images && incident.images.length > 0) {
        imagesHtml = `
            <div class="mb-2">
                <small class="text-muted d-block mb-1"><strong><i class="bi bi-images"></i> Images (${incident.images.length})</strong></small>
                <div class="d-flex gap-1 flex-wrap" style="max-height: 80px; overflow-y: auto;">
                    ${incident.images.slice(0, 3).map(img => `
                        <img src="/Insuite_backbones/public/storage/${img.path}" 
                             class="rounded" 
                             style="width: 60px; height: 60px; object-fit: cover; cursor: pointer;"
                             onclick="window.open(this.src, '_blank')"
                             title="${img.comment || 'Voir l\'image'}">
                    `).join('')}
                    ${incident.images.length > 3 ? `<div class="badge bg-secondary align-self-center">+${incident.images.length - 3}</div>` : ''}
                </div>
            </div>`;
    }
    
    // Section commentaires
    let commentsHtml = '';
    if (incident.comments && incident.comments.length > 0) {
        const latestComment = incident.comments[0];
        commentsHtml = `
            <div class="mb-2">
                <small class="text-muted d-block mb-1"><strong><i class="bi bi-chat-dots-fill"></i> Dernier commentaire</strong></small>
                <div class="p-2 bg-light rounded small">
                    <div class="text-muted" style="font-size: 0.75rem;">${latestComment.author} - ${new Date(latestComment.date).toLocaleDateString('fr-FR')}</div>
                    <div>${latestComment.text.substring(0, 100)}${latestComment.text.length > 100 ? '...' : ''}</div>
                </div>
                ${incident.comments.length > 1 ? `<small class="text-muted">${incident.comments.length} commentaire(s) total</small>` : ''}
            </div>`;
    }
    
    return `
        <div class="card border-0 shadow-sm" style="min-width: 300px; max-width: 400px;">
            <div class="card-header bg-primary text-white py-2">
                <h6 class="mb-0 fw-bold">
                    <i class="bi bi-exclamation-triangle-fill me-2"></i>
                    ${incident.title}
                </h6>
            </div>
            <div class="card-body p-3">
                <div class="mb-2">
                    <small class="text-muted d-block mb-1"><strong>Statut</strong></small>
                    <span class="badge ${badgeClass} px-2 py-1">${incident.status}</span>
                </div>
                <div class="mb-2">
                    <small class="text-muted d-block mb-1"><strong>Priorité</strong></small>
                    <span class="badge ${priorityBadge} px-2 py-1">${incident.priority}</span>
                </div>
                ${technicianHtml}
                <div class="mb-2">
                    <small class="text-muted d-block mb-1"><strong>Lieu</strong></small>
                    <span class="text-dark">${incident.location || 'Non spécifié'}</span>
                </div>
                ${imagesHtml}
                ${commentsHtml}
                <div class="mb-0">
                    <small class="text-muted d-block mb-1"><strong>Coordonnées GPS</strong></small>
                    <code class="small">${incident.lat.toFixed(6)}, ${incident.lng.toFixed(6)}</code>
                </div>
            </div>
            <div class="card-footer bg-white border-0 pt-0 pb-2">
                <a href="/Insuite_backbones/public/incidents/treatment?id=${incident.id}#tab-intervention" 
                   class="btn btn-sm btn-primary w-100" 
                   target="_blank">
                    <i class="bi bi-box-arrow-up-right me-1"></i>
                    Voir les détails complets
                </a>
            </div>
        </div>
    `;
}

// Chargement des techniciens
async function loadTechnicians() {
    console.log('[TECHNICIANS] Chargement des techniciens...');
    
    try {
        const res = await fetch(`${BASE}/cartography/technicians?max_age=${CONFIG.maxAge}`, {
            credentials: 'include'
        });
        
        console.log('[TECHNICIANS] Statut réponse:', res.status);
        
        if (!res.ok) {
            const errorText = await res.text();
            console.log('[TECHNICIANS] Réponse:', errorText);
            // Ne pas throw ici, car les techniciens ne sont pas critiques
            allTechnicians = [];
            return;
        }
        
        const data = await res.json();
        console.log('[TECHNICIANS] Données reçues:', data);
        
        allTechnicians = Array.isArray(data) ? data : [];
        
        console.log(`[TECHNICIANS] ${allTechnicians.length} techniciens reçus`);
        
        filterAndDisplayTechnicians();
        updateStats(); // Mettre à jour les stats aussi
        
    } catch (error) {
        console.error('[TECHNICIANS] Erreur:', error);
        allTechnicians = [];
    }
}

// Filtrage et affichage des techniciens
function filterAndDisplayTechnicians() {
    techsLayer.clearLayers();
    const bounds = [];
    
    allTechnicians.forEach(tech => {
        if (!tech.lat || !tech.lng) return;
        
        const isActive = isTechnicianActive(tech);
        const acc = (tech.accuracy !== null && tech.accuracy !== undefined) ? Number(tech.accuracy) : null;
        const isApprox = (acc !== null && !isNaN(acc) && acc > 30);
        // Couleur: actif+précis (vert), actif+approx (orange), inactif (gris)
        const color = !isActive ? '#9ca3af' : (isApprox ? '#f59e0b' : '#10b981');
        
        const icon = L.divIcon({
            html: `<div style="background: ${color}; border: 2px solid white; border-radius: 50%; width: 25px; height: 25px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.3);">👷</div>`,
            className: 'custom-tech-icon',
            iconSize: [25, 25],
            iconAnchor: [12, 12]
        });
        
        const marker = L.marker([tech.lat, tech.lng], { icon })
            .bindPopup(createTechnicianPopup(tech));
        
        techsLayer.addLayer(marker);
        bounds.push([tech.lat, tech.lng]);

        // Cercle de précision (zone) si disponible et raisonnable
        if (acc !== null && !isNaN(acc) && acc > 0) {
            const radius = Math.min(acc, 200); // éviter des cercles énormes
            const circle = L.circle([tech.lat, tech.lng], {
                radius,
                color: isApprox ? '#f59e0b' : '#10b981',
                weight: 1,
                fillColor: isApprox ? '#f59e0b' : '#10b981',
                fillOpacity: isApprox ? 0.08 : 0.12,
                interactive: false,
            });
            techsLayer.addLayer(circle);
        }
    });

    const technicianViewActive = currentFilters.filter === 'all' || currentFilters.filter === 'technicians';
    if (technicianViewActive && bounds.length > 0 && shouldAutoFitBounds()) {
        const group = L.featureGroup(bounds.map((point) => L.marker(point)));
        map.fitBounds(group.getBounds().pad(0.18));
        hasAutoFittedOnce = true;
        autoFitRequested = false;
    }
}

function getTechnicianActiveWindowSeconds(tech) {
    const value = Number(tech?.active_window_seconds);
    return Number.isFinite(value) && value > 0 ? value : 600;
}

function isTechnicianActive(tech) {
    if (tech?.is_active === true) return true;

    const ageSeconds = Number(tech?.age_seconds);
    if (!Number.isFinite(ageSeconds) || ageSeconds < 0) {
        return false;
    }

    return ageSeconds <= getTechnicianActiveWindowSeconds(tech);
}

// Création du contenu popup pour technicien
function createTechnicianPopup(tech) {
    const lastUpdate = tech.updated_at ? new Date(tech.updated_at).toLocaleString('fr-FR') : 'Inconnue';
    const ageMinutes = tech.age_seconds ? Math.round(tech.age_seconds / 60) : null;
    const acc = (tech.accuracy !== null && tech.accuracy !== undefined) ? Number(tech.accuracy) : null;
    const isApprox = (acc !== null && !isNaN(acc) && acc > 30);
    
    return `
        <div style="min-width: 180px;">
            <h6 style="margin-bottom: 8px; color: #1e293b;">👷 ${tech.name}</h6>
            <div style="font-size: 0.875rem;">
                <div><strong>Rôle:</strong> ${tech.role || 'Technicien'}</div>
                <div><strong>Dernière position:</strong> ${lastUpdate}</div>
                ${ageMinutes !== null ? `<div><strong>Il y a:</strong> ${ageMinutes} min</div>` : ''}
                ${tech.accuracy ? `<div><strong>Précision:</strong> ±${Math.round(tech.accuracy)}m</div>` : ''}
                ${isApprox ? `<div style="margin-top:6px; padding:6px 8px; border-radius:6px; background: rgba(245,158,11,.12); color:#92400e;"><strong>Localisation approximative</strong><br><small>Activez “Position précise” + GPS pour améliorer.</small></div>` : ''}
                ${tech.speed ? `<div><strong>Vitesse:</strong> ${tech.speed} m/s</div>` : ''}
            </div>
        </div>
    `;
}

// Mise à jour des statistiques
function updateStats() {
    console.log('[STATS] Mise à jour des statistiques');
    console.log('[STATS] Incidents:', allIncidents.length);
    console.log('[STATS] Techniciens:', allTechnicians.length);
    
    const activeIncidents = allIncidents.filter(i => {
        const status = (i.status || '').toLowerCase();
        return status !== 'clôturé' && status !== 'résolu' && status !== 'fermé';
    }).length;

    const openCount = allIncidents.filter(i => getIncidentStatusBucket(i) === 'open').length;
    const inProgressCount = allIncidents.filter(i => getIncidentStatusBucket(i) === 'in_progress').length;
    const resolvedCount = allIncidents.filter(i => getIncidentStatusBucket(i) === 'resolved').length;
    
    const activeTechs = allTechnicians.filter((t) => isTechnicianActive(t)).length;
    
    console.log('[STATS] Incidents actifs:', activeIncidents);
    console.log('[STATS] Techniciens actifs:', activeTechs);
    
    // Mise à jour de l'interface
    const totalEl = document.getElementById('totalIncidents');
    const activeEl = document.getElementById('activeIncidents');
    const techsEl = document.getElementById('activeTechs');
    const avgEl = document.getElementById('avgResponseTime');
    
    if (totalEl) totalEl.textContent = allIncidents.length;
    if (activeEl) activeEl.textContent = activeIncidents;
    if (techsEl) techsEl.textContent = activeTechs;

    const countOpenEl = document.getElementById('countOpen');
    const countInProgressEl = document.getElementById('countInProgress');
    const countResolvedEl = document.getElementById('countResolved');
    if (countOpenEl) countOpenEl.textContent = openCount;
    if (countInProgressEl) countInProgressEl.textContent = inProgressCount;
    if (countResolvedEl) countResolvedEl.textContent = resolvedCount;
    
    // Calculer temps de réponse moyen réel basé sur les dates
    let avgTime = '--';
    if (allIncidents.length > 0) {
        // Filtrer les incidents résolus avec dates
        const resolvedWithDates = allIncidents.filter(inc => {
            const status = (inc.status || '').toLowerCase();
            const isResolved = status.includes('clôtur') || status.includes('terminé') || status.includes('résolu');
            return isResolved && inc.created_at && inc.resolved_at;
        });
        
        if (resolvedWithDates.length > 0) {
            // Calculer le temps moyen réel
            let totalMinutes = 0;
            resolvedWithDates.forEach(inc => {
                const created = new Date(inc.created_at);
                const resolved = new Date(inc.resolved_at);
                const diffMs = resolved - created;
                const diffMinutes = Math.floor(diffMs / 1000 / 60);
                totalMinutes += diffMinutes;
            });
            
            const avgMinutes = Math.round(totalMinutes / resolvedWithDates.length);
            
            if (avgMinutes >= 1440) { // Plus d'un jour
                const days = Math.floor(avgMinutes / 1440);
                const hours = Math.floor((avgMinutes % 1440) / 60);
                avgTime = hours > 0 ? `${days}j ${hours}h` : `${days}j`;
            } else if (avgMinutes >= 60) {
                const hours = Math.floor(avgMinutes / 60);
                const mins = avgMinutes % 60;
                avgTime = mins > 0 ? `${hours}h${mins}` : `${hours}h`;
            } else {
                avgTime = `${avgMinutes}min`;
            }
        } else {
            // Aucun incident résolu avec dates, afficher N/A
            avgTime = 'N/A';
        }
    }
    if (avgEl) avgEl.textContent = avgTime;
    
    console.log('[STATS] Statistiques mises à jour dans l\'interface');
}

// Gestion des événements et interactions
function setupEventHandlers() {
    // Recherche en temps réel
    const searchInput = document.getElementById('searchInput');
    if (searchInput) {
        let searchTimeout;
        searchInput.addEventListener('input', (e) => {
            clearTimeout(searchTimeout);
            searchTimeout = setTimeout(() => {
                currentFilters.search = e.target.value.trim();
                filterAndDisplayIncidents();
            }, 300);
        });
    }
    
    // Filtres par chips
    document.querySelectorAll('.chip').forEach(chip => {
        chip.addEventListener('click', () => {
            // Retirer l'état actif des autres chips
            document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
            chip.classList.add('active');
            
            // Appliquer le filtre
            const filter = chip.getAttribute('data-filter');
            currentFilters.filter = filter;

            // Changement de vue = autoriser un auto-fit ponctuel (sans casser ensuite)
            autoFitRequested = (filter === 'all' || filter === 'incidents' || filter === 'high-priority' || filter === 'technicians');
            
            // Gérer la visibilité des couches
            switch (filter) {
                case 'incidents':
                    setTrajectoryFiltersVisible(false);
                    map.addLayer(incidentsLayer);
                    map.addLayer(clusterGroup);
                    map.removeLayer(techsLayer);
                    // Trajectoires: on laisse masqué en vue incidents pour limiter la pollution visuelle
                    map.removeLayer(trajectoriesLayer);
                    map.removeLayer(liaisonsLayer);
                    break;
                case 'technicians':
                    setTrajectoryFiltersVisible(false);
                    map.addLayer(techsLayer);
                    map.addLayer(trajectoriesLayer);
                    map.removeLayer(incidentsLayer);
                    map.removeLayer(clusterGroup);
                    map.removeLayer(liaisonsLayer);
                    // Assurer l'affichage immédiat (au lieu d'attendre le prochain auto-refresh)
                    filterAndDisplayTechnicians();
                    loadTechnicians();
                    loadTrajectories(24);
                    break;
                case 'trajectories':
                    setTrajectoryFiltersVisible(true);
                    map.addLayer(trajectoriesLayer);
                    map.removeLayer(incidentsLayer);
                    map.removeLayer(clusterGroup);
                    map.removeLayer(techsLayer);
                    map.removeLayer(liaisonsLayer);
                    applyTrajectoryFilters();
                    break;
                case 'liaisons':
                    setTrajectoryFiltersVisible(false);
                    map.addLayer(liaisonsLayer);
                    map.removeLayer(incidentsLayer);
                    map.removeLayer(clusterGroup);
                    map.removeLayer(techsLayer);
                    map.removeLayer(trajectoriesLayer);
                    loadLiaisons({ debugIfEmpty: true }).then(() => {
                        renderLiaisons2D();
                    });
                    break;
                case 'all':
                default:
                    setTrajectoryFiltersVisible(false);
                    map.addLayer(incidentsLayer);
                    map.addLayer(clusterGroup);
                    map.addLayer(techsLayer);
                    map.addLayer(trajectoriesLayer);
                    map.removeLayer(liaisonsLayer);
                    clearLiaisonsLayer();
                    // Assurer l'affichage immédiat
                    filterAndDisplayTechnicians();
                    loadTechnicians();
                    loadTrajectories(24);
                    break;
            }
            
            filterAndDisplayIncidents();
        });
    });

    // Filtres trajectoires (historique)
    const applyTrajBtn = document.getElementById('applyTrajectoryFilters');
    if (applyTrajBtn) {
        applyTrajBtn.addEventListener('click', () => {
            applyTrajectoryFilters();
        });
    }

    // Vue par statut incidents
    document.querySelectorAll('.status-chip').forEach(chip => {
        chip.addEventListener('click', () => {
            document.querySelectorAll('.status-chip').forEach(c => c.classList.remove('active'));
            chip.classList.add('active');
            currentFilters.incidentStatus = chip.getAttribute('data-incident-status') || 'all';
            autoFitRequested = true;
            filterAndDisplayIncidents();
        });
    });
    
    // Bouton localisation
    const locateBtn = document.getElementById('locateMe');
    if (locateBtn) {
        locateBtn.addEventListener('click', handleGeolocation);
    }
    
    // Bouton suivi GPS
    const trackBtn = document.getElementById('toggleTrack');
    if (trackBtn) {
        trackBtn.addEventListener('click', toggleTracking);
    }
    
    // Bouton actualiser
    const refreshBtn = document.getElementById('refreshData');
    if (refreshBtn) {
        refreshBtn.addEventListener('click', () => {
            showLoading(true);
            autoFitRequested = true;
            Promise.all([loadIncidents(), loadTechnicians()])
                .finally(() => showLoading(false));
        });
    }

    // Liaisons 3D
    const open3dBtn = document.getElementById('openLiaisons3d');
    const focusOpenBtn = document.getElementById('liaisons3dFocusOpen');
    if (open3dBtn) {
        open3dBtn.addEventListener('click', async () => {
            await loadLiaisons({ debugIfEmpty: true });

            const container = document.getElementById('liaisons3dCanvasWrap');
            let doRender = null;
            if (container) {
                container.innerHTML = '<div class="p-3 text-muted">Chargement du moteur 3D…</div>';
                const ok = await ensureThreeReady();
                doRender = () => {
                    try { stopLiaisons3D(); } catch (e) {}
                    renderLiaisons3D(container, allLiaisons);
                    // Resize post-render: utile si bootstrap applique encore des transitions.
                    setTimeout(() => {
                        try {
                            if (liaisons3d && liaisons3d.renderer && liaisons3d.camera) {
                                const rect = container.getBoundingClientRect ? container.getBoundingClientRect() : { width: container.clientWidth, height: container.clientHeight };
                                const w = Math.max(10, Math.round(rect.width || container.clientWidth || 0));
                                const h = Math.max(10, Math.round(rect.height || container.clientHeight || 0));
                                liaisons3d.camera.aspect = w / h;
                                liaisons3d.camera.updateProjectionMatrix();
                                liaisons3d.renderer.setSize(w, h);
                            }
                        } catch (e) {}
                    }, 200);
                };

                if (!ok) {
                    container.innerHTML = `
                        <div class="p-3">
                            <div class="text-danger fw-bold mb-1">⚠️ Moteur 3D indisponible</div>
                            <div class="text-muted small">
                                Impossible de charger Three.js (ni local ni CDN).<br>
                                <strong>Vérification requise :</strong>
                                <ul class="mb-0 mt-2">
                                    <li>Les fichiers locaux existent-ils ?
                                        <ul>
                                            <li><code>${BASE}/assets/vendor/three/three.min.js</code> (669 Ko)</li>
                                            <li><code>${BASE}/assets/vendor/three/OrbitControls.js</code> (11 Ko)</li>
                                        </ul>
                                    </li>
                                    <li>Ouvrez la Console du navigateur (F12) pour voir les erreurs de chargement</li>
                                    <li>Si les fichiers sont présents, videz le cache du navigateur (Ctrl+Shift+Del)</li>
                                </ul>
                                <div class="mt-2 p-2 bg-light rounded">
                                    <small><strong>Note :</strong> Les fichiers locaux sont prioritaires. Les CDN externes (jsDelivr/unpkg) ne sont utilisés qu'en dernier recours.</small>
                                </div>
                            </div>
                        </div>
                    `;
                }
            }

            const modal = ensureBootstrapModal('liaisons3dModal');
            const el = document.getElementById('liaisons3dModal');
            if (modal) {
                // IMPORTANT: rendre après affichage de la modal sinon canvas minuscule (container hidden).
                const onShown = () => {
                    el?.removeEventListener('shown.bs.modal', onShown);
                    try { if (typeof doRender === 'function') doRender(); } catch (e) {}
                };
                el?.addEventListener('shown.bs.modal', onShown);
                modal.show();
            } else if (el) {
                // fallback minimal
                el.style.display = 'block';
                el.classList.add('show');
                el.setAttribute('aria-modal', 'true');

                // Fallback: attendre un tick layout puis rendre.
                requestAnimationFrame(() => requestAnimationFrame(() => {
                    try { if (typeof doRender === 'function') doRender(); } catch (e) {}
                }));
            }
        });
    }
    if (focusOpenBtn) {
        focusOpenBtn.addEventListener('click', () => focusFirstOpenLiaison3D());
    }

    // Cleanup à la fermeture modal
    const modalEl = document.getElementById('liaisons3dModal');
    if (modalEl) {
        modalEl.addEventListener('hidden.bs.modal', () => {
            stopLiaisons3D();
        });
        const closeBtn = modalEl.querySelector('.btn-close');
        if (closeBtn) {
            closeBtn.addEventListener('click', () => {
                // fallback si pas de Bootstrap
                if (!ensureBootstrapModal('liaisons3dModal')) {
                    stopLiaisons3D();
                    modalEl.classList.remove('show');
                    modalEl.style.display = 'none';
                    modalEl.removeAttribute('aria-modal');
                }
            });
        }
    }

    // Plein écran
    const fullscreenBtn = document.getElementById('toggleFullscreen');
    if (fullscreenBtn) {
        fullscreenBtn.addEventListener('click', toggleFullscreen);
    }

    // PiP
    const pipBtn = document.getElementById('togglePip');
    if (pipBtn) {
        pipBtn.addEventListener('click', togglePipMode);
    }

    // Sync état plein écran (sortie via ESC)
    document.addEventListener('fullscreenchange', () => {
        updateFullscreenButtonState();
        setTimeout(() => {
            if (map) map.invalidateSize(true);
        }, 250);
    });
}

function updateFullscreenButtonState() {
    const btn = document.getElementById('toggleFullscreen');
    if (!btn) return;
    const isFs = !!document.fullscreenElement;
    btn.classList.toggle('active', isFs);
    btn.innerHTML = isFs ? '<i class="fas fa-compress"></i> Quitter plein écran' : '<i class="fas fa-expand"></i> Plein écran';
}

function toggleFullscreen() {
    const container = document.querySelector('.map-container');
    if (!container) return;

    if (!document.fullscreenElement) {
        if (container.requestFullscreen) {
            container.requestFullscreen().catch(() => {});
        }
    } else {
        if (document.exitFullscreen) {
            document.exitFullscreen().catch(() => {});
        }
    }
}

function togglePipMode() {
    const btn = document.getElementById('togglePip');
    if (!btn) return;

    const isOn = btn.getAttribute('data-pip') === 'on';
    const nextOn = !isOn;

    if (nextOn) {
        // Important: pas de trailing slash, certains serveurs traitent /supervision/ comme un dossier
        // et peuvent renvoyer 403 si aucun index n'est présent.
        const base = (BASE || '').replace(/\/$/, '');
        const url = `${base}/cartography/supervision?views=4`;
        const features = 'popup=yes,width=1200,height=800,resizable=yes,scrollbars=yes';
        pipWindowRef = window.open(url, 'insuite_supervision_live', features);
        if (pipWindowRef) {
            try { pipWindowRef.focus(); } catch (e) {}
        }
    } else {
        if (pipWindowRef && !pipWindowRef.closed) {
            try { pipWindowRef.close(); } catch (e) {}
        }
        pipWindowRef = null;
    }

    btn.setAttribute('data-pip', nextOn ? 'on' : 'off');
    btn.classList.toggle('active', nextOn);
    btn.innerHTML = nextOn ? '<i class="fas fa-window-minimize"></i> Supervision ouverte' : '<i class="fas fa-window-restore"></i> PiP';
}

// Géolocalisation améliorée
function ensureGeoAllowedContext() {
    // La géolocalisation navigateur requiert un contexte sécurisé (HTTPS) sauf localhost.
    try {
        const isLocal = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
        const isSecure = !!window.isSecureContext || location.protocol === 'https:' || isLocal;
        if (!isSecure) {
            showError('La géolocalisation nécessite HTTPS (ou localhost).');
            return false;
        }
    } catch (e) {}
    return true;
}

function handleGeolocation(opts = {}) {
    const btn = document.getElementById('locateMe');
    if (!navigator.geolocation) {
        showError('Géolocalisation non supportée par ce navigateur');
        return;
    }

    if (!ensureGeoAllowedContext()) return;

    const silent = !!opts.silent;
    const shouldCenter = (opts.center !== undefined) ? !!opts.center : true;

    if (btn && !silent) {
        btn.disabled = true;
        btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Localisation...';
    }
    
    const options = {
        enableHighAccuracy: true,
        maximumAge: 0, // éviter les positions "cachées" / anciennes
        timeout: 15000
    };
    
    navigator.geolocation.getCurrentPosition(
        (position) => {
            const { latitude, longitude, accuracy } = position.coords;

            // Mettre à jour l'affichage "Moi" (marker + cercle d'accuracy)
            updateMyLocationOnMap(latitude, longitude, accuracy);

            // Centrage seulement si demandé et si l'utilisateur n'a pas déjà manipulé la carte
            if (shouldCenter && !userHasInteractedWithMap) {
                const zoom = (accuracy && accuracy <= 20) ? 19 : (accuracy && accuracy <= 50 ? 18 : 16);
                map.setView([latitude, longitude], zoom);
            }

            // Envoyer la position si le suivi est actif
            if (watchId !== null) {
                sendLocationUpdate(position);
            }

            if (btn && !silent) {
                btn.disabled = false;
                btn.innerHTML = '<i class="fas fa-location-crosshairs"></i> Localiser';
            }
        },
        (error) => {
            let message = 'Impossible d\'obtenir votre position';
            switch (error.code) {
                case error.PERMISSION_DENIED:
                    message = 'Permission de géolocalisation refusée';
                    break;
                case error.POSITION_UNAVAILABLE:
                    message = 'Position indisponible';
                    break;
                case error.TIMEOUT:
                    message = 'Délai d\'attente dépassé';
                    break;
            }
            showError(message);
            
            if (btn && !silent) {
                btn.disabled = false;
                btn.innerHTML = '<i class="fas fa-location-crosshairs"></i> Localiser';
            }
        },
        options
    );
}

// Suivi GPS amélioré
function toggleTracking(opts = {}) {
    const btn = document.getElementById('toggleTrack');
    const isTracking = btn.getAttribute('data-tracking') === 'on';
    const silent = !!opts.silent;
    
    if (!navigator.geolocation) {
        showError('Géolocalisation non supportée');
        return;
    }

    if (!ensureGeoAllowedContext()) return;
    
    if (!isTracking) {
        // Phase 1: Calibration initiale pour obtenir le meilleur fix possible
        const statusIndicator = document.getElementById('statusIndicator');
        if (statusIndicator && !silent) {
            statusIndicator.innerHTML = '<i class="fas fa-spinner fa-spin text-info"></i> Calibration GPS...';
        }

        if (!silent) {
            btn.disabled = true;
            btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Calibration...';
        }
        
        // Options ultra-précises
        const options = {
            enableHighAccuracy: true,  // Active GPS matériel (pas WiFi/IP)
            maximumAge: 0,              // Force acquisition fraîche
            timeout: 15000              // Laisser le temps au GPS de fixer (smartphones)
        };
        
        let lastAccuracy = null;
        let positionBuffer = [];  // Buffer pour moyenner les positions
        const bufferSize = 3;     // Nombre de positions à moyenner
        
        const startWatch = () => {
            // Phase 2: suivi continu
            watchId = navigator.geolocation.watchPosition(
                (position) => {
                    const accuracy = position.coords.accuracy;

                    // Toujours mettre à jour l'affichage local
                    updateMyLocationOnMap(position.coords.latitude, position.coords.longitude, accuracy);

                    console.log('[GPS] Position:', {
                        lat: position.coords.latitude.toFixed(6),
                        lng: position.coords.longitude.toFixed(6),
                        accuracy: accuracy.toFixed(1) + 'm',
                        altitude: position.coords.altitude ? position.coords.altitude.toFixed(1) + 'm' : 'N/A',
                        timestamp: new Date(position.timestamp).toLocaleTimeString()
                    });

                    // Seuil de précision STRICT: rejeter si > 20m (ne pas envoyer au serveur)
                    if (accuracy > 20) {
                        console.warn('[GPS] ⚠️ Précision insuffisante (' + accuracy.toFixed(1) + 'm > 20m), ignoré');
                        if (statusIndicator && !silent) {
                            statusIndicator.innerHTML = '<i class="fas fa-circle text-warning"></i> Calibration GPS... (±' + accuracy.toFixed(0) + 'm)';
                        }
                        return;
                    }

                    positionBuffer.push({
                        lat: position.coords.latitude,
                        lng: position.coords.longitude,
                        accuracy: accuracy,
                        timestamp: position.timestamp
                    });

                    if (positionBuffer.length > bufferSize) {
                        positionBuffer.shift();
                    }

                    if (positionBuffer.length >= bufferSize) {
                        let sumLat = 0, sumLng = 0, sumWeight = 0;
                        positionBuffer.forEach(pos => {
                            const weight = 1 / pos.accuracy;
                            sumLat += pos.lat * weight;
                            sumLng += pos.lng * weight;
                            sumWeight += weight;
                        });

                        const avgLat = sumLat / sumWeight;
                        const avgLng = sumLng / sumWeight;
                        const avgAccuracy = positionBuffer.reduce((sum, p) => sum + p.accuracy, 0) / positionBuffer.length;

                        const avgPosition = {
                            coords: {
                                latitude: avgLat,
                                longitude: avgLng,
                                accuracy: avgAccuracy,
                                heading: position.coords.heading,
                                speed: position.coords.speed
                            },
                            timestamp: position.timestamp
                        };

                        sendLocationUpdate(avgPosition);
                    }

                    if (lastAccuracy === null || accuracy < lastAccuracy) {
                        console.log('[GPS] 📈 Amélioration: ' + accuracy.toFixed(1) + 'm');
                    }
                    lastAccuracy = accuracy;

                    if (statusIndicator && !silent) {
                        const quality = accuracy < 10 ? 'Excellent' : accuracy < 15 ? 'Très bon' : 'Bon';
                        const color = accuracy < 10 ? 'success' : accuracy < 15 ? 'primary' : 'info';
                        statusIndicator.innerHTML = '<i class="fas fa-circle text-' + color + '"></i> GPS ' + quality + ' (±' + accuracy.toFixed(1) + 'm)';
                    }
                },
                (error) => {
                    if (error.code === error.TIMEOUT) {
                        console.warn('[GPS] Timeout de lecture, nouvelle tentative en cours');
                    } else {
                        console.error('[GPS] Erreur:', error);
                    }
                    let errorMsg = 'GPS: ';
                    switch(error.code) {
                        case error.PERMISSION_DENIED:
                            errorMsg += 'Permission refusée';
                            break;
                        case error.POSITION_UNAVAILABLE:
                            errorMsg += 'Signal indisponible';
                            break;
                        case error.TIMEOUT:
                            errorMsg += 'Timeout - réessayez';
                            break;
                        default:
                            errorMsg += error.message;
                    }

                    if (!silent && error.code !== error.TIMEOUT) {
                        showError(errorMsg);
                    }

                    if (error.code === error.PERMISSION_DENIED) {
                        if (watchId !== null) {
                            navigator.geolocation.clearWatch(watchId);
                            watchId = null;
                            btn.setAttribute('data-tracking', 'off');
                            btn.classList.remove('active');
                            btn.disabled = false;
                            btn.innerHTML = '<i class="fas fa-satellite-dish"></i> Suivi GPS';
                            setAutoTrackingPreference(false);
                        }
                    }
                },
                options
            );

            // UI active
            btn.disabled = false;
            btn.setAttribute('data-tracking', 'on');
            btn.classList.add('active');
            btn.innerHTML = '<i class="fas fa-satellite-dish"></i> Suivi actif';
            setAutoTrackingPreference(true);
        };

        // Calibration initiale avec getCurrentPosition (souvent plus précis au démarrage)
        navigator.geolocation.getCurrentPosition(
            (initialPos) => {
                console.log('[GPS] Calibration initiale:', {
                    accuracy: initialPos.coords.accuracy.toFixed(1) + 'm',
                    lat: initialPos.coords.latitude,
                    lng: initialPos.coords.longitude
                });

                // Démarrer le suivi continu après calibration
                startWatch();
            },
            (error) => {
                if (error.code === error.TIMEOUT) {
                    console.warn('[GPS] Calibration expirée, bascule sur le suivi continu');
                } else {
                    console.error('[GPS] Échec calibration:', error);
                }
                let msg = 'Calibration GPS: ';
                switch(error.code) {
                    case error.PERMISSION_DENIED:
                        msg += 'permission refusée';
                        break;
                    case error.POSITION_UNAVAILABLE:
                        msg += 'signal indisponible';
                        break;
                    case error.TIMEOUT:
                        msg += 'délai dépassé';
                        break;
                    default:
                        msg += error.message;
                }

                // Fallback: même si la calibration échoue, on tente watchPosition
                if (!silent) {
                    showError(msg + ' (on tente le suivi en continu)');
                }

                startWatch();
            },
            options
        );

    } else {
        // Arrêter le suivi
        if (watchId !== null) {
            navigator.geolocation.clearWatch(watchId);
            watchId = null;
        }
        
        btn.setAttribute('data-tracking', 'off');
        btn.classList.remove('active');
        btn.innerHTML = '<i class="fas fa-satellite-dish"></i> Suivi GPS';
        setAutoTrackingPreference(false);
        
        const statusIndicator = document.getElementById('statusIndicator');
        if (statusIndicator && !silent) {
            statusIndicator.innerHTML = '<i class="fas fa-circle text-success"></i> En ligne';
        }
    }
}

function autoStartGeoInBackground() {
    // Auto-activation demandée: lancer géoloc + suivi sans clic utilisateur.
    // NOTE: selon navigateur, une popup de permission peut apparaître.
    if (!navigator.geolocation) return;
    if (getAutoTrackingPreference() === 'off') return;

    // Ne pas recentrer automatiquement (casse la lecture) ; juste activer le fix + suivi.
    if (!hasAutoCenteredOnce) {
        handleGeolocation({ silent: true, center: false });
        hasAutoCenteredOnce = true;
    }

    const trackBtn = document.getElementById('toggleTrack');
    if (trackBtn && trackBtn.getAttribute('data-tracking') !== 'on') {
        toggleTracking({ silent: true });
    }
}

// Envoi de mise à jour de position
async function sendLocationUpdate(position) {
    const { latitude, longitude, accuracy, heading, speed } = position.coords;
    
    const formData = new FormData();
    formData.append('lat', latitude);
    formData.append('lng', longitude);
    if (accuracy) formData.append('accuracy', accuracy);
    if (heading && !isNaN(heading)) formData.append('heading', heading);
    if (speed && !isNaN(speed)) formData.append('speed', speed);
    
    try {
        // Préférer l'endpoint cartography (historisation + tables dédiées)
        let response = await fetch(`${BASE}/cartography/update-location`, {
            method: 'POST',
            body: formData
        });

        // Fallback: ancien endpoint (si route cartography absente)
        if (!response.ok) {
            response = await fetch(`${BASE}/technicians/update-location`, {
                method: 'POST',
                body: formData
            });
        }
        
        const result = await response.json();
        
        if (response.ok && !result.error) {
            console.log('[GPS] Position envoyée avec succès');
            // Recharger les techniciens pour voir la mise à jour
            setTimeout(loadTechnicians, 1000);
        } else {
            console.error('[GPS] Erreur serveur:', result);
        }
        
    } catch (error) {
        console.error('[GPS] Erreur envoi:', error);
    }
}

// Chargement des trajectoires amélioré
async function loadTrajectories(hoursOrFilters = 24) {
    try {
        let url = '';
        if (typeof hoursOrFilters === 'object' && hoursOrFilters !== null) {
            const qs = new URLSearchParams();
            if (hoursOrFilters.user_id) qs.set('user_id', String(hoursOrFilters.user_id));
            if (hoursOrFilters.from) qs.set('from', String(hoursOrFilters.from));
            if (hoursOrFilters.to) qs.set('to', String(hoursOrFilters.to));
            url = `${BASE}/cartography/trajectories?${qs.toString()}`;
        } else {
            const hours = hoursOrFilters;
            const safeHours = (typeof hours === 'number' && isFinite(hours) && hours > 0) ? Math.min(Math.floor(hours), 72) : 24;
            url = `${BASE}/cartography/trajectories?hours=${safeHours}`;
        }

        const res = await fetch(url, { credentials: 'include' });
        
        if (!res.ok) {
            throw new Error(`HTTP ${res.status}`);
        }
        
        const trajectories = await res.json();
        window.__lastTrajectoriesData = Array.isArray(trajectories) ? trajectories : [];
        trajectoriesLayer.clearLayers();
        
        const colors = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899'];
        
        (Array.isArray(trajectories) ? trajectories : []).forEach((traj, idx) => {
            if (!traj.points || traj.points.length < 2) return;
            
            const color = colors[idx % colors.length];
            const latlngs = traj.points.map(p => [p.lat, p.lng]);
            
            // Ligne de trajectoire avec style amélioré
            const polyline = L.polyline(latlngs, {
                color: color,
                weight: 4,
                opacity: 0.8,
                smoothFactor: 2,
                className: 'trajectory-line'
            });
            
            // Animation de la ligne (effet pointillé mobile)
            const animatedLine = L.polyline(latlngs, {
                color: color,
                weight: 6,
                opacity: 0.3,
                dashArray: '10, 10',
                className: 'trajectory-animation'
            });
            
            polyline.addTo(trajectoriesLayer);
            animatedLine.addTo(trajectoriesLayer);
            
            // Marqueurs de début et fin
            const startIcon = L.divIcon({
                html: `<div style="background: white; border: 2px solid ${color}; border-radius: 50%; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 12px;">🚀</div>`,
                iconSize: [20, 20],
                iconAnchor: [10, 10]
            });
            
            const endIcon = L.divIcon({
                html: `<div style="background: ${color}; border: 2px solid white; border-radius: 50%; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 12px;">🏁</div>`,
                iconSize: [20, 20],
                iconAnchor: [10, 10]
            });
            
            L.marker([traj.points[0].lat, traj.points[0].lng], { icon: startIcon })
                .bindTooltip('Début du trajet')
                .addTo(trajectoriesLayer);
                
            L.marker([traj.points[traj.points.length - 1].lat, traj.points[traj.points.length - 1].lng], { icon: endIcon })
                .bindTooltip('Fin du trajet')
                .addTo(trajectoriesLayer);
            
            // Popup informatif
            const distance = calculateTrajectoryDistance(traj.points);
            const duration = new Date(traj.points[traj.points.length - 1].time) - new Date(traj.points[0].time);
            const durationHours = Math.round(duration / (1000 * 60 * 60) * 10) / 10;
            
            polyline.bindPopup(`
                <div>
                    <h6>🛤️ ${traj.name}</h6>
                    <div><strong>Distance:</strong> ${distance.toFixed(1)} km</div>
                    <div><strong>Durée:</strong> ${durationHours}h</div>
                    <div><strong>Points:</strong> ${traj.points.length}</div>
                    ${traj.stats && typeof traj.stats.incidents_declared === 'number' ? `<div><strong>Incidents déclarés:</strong> ${traj.stats.incidents_declared}</div>` : ''}
                </div>
            `);
        });
        
    } catch (error) {
        console.error('[TRAJECTORIES] Erreur:', error);
    }
}

// Calcul de distance pour trajectoires
function calculateTrajectoryDistance(points) {
    let totalDistance = 0;
    for (let i = 1; i < points.length; i++) {
        const prev = points[i - 1];
        const curr = points[i];
        totalDistance += haversineDistance(prev.lat, prev.lng, curr.lat, curr.lng);
    }
    return totalDistance / 1000; // Convertir en kilomètres
}

// Formule de Haversine
function haversineDistance(lat1, lon1, lat2, lon2) {
    const R = 6371000; // Rayon de la Terre en mètres
    const dLat = (lat2 - lat1) * Math.PI / 180;
    const dLon = (lon2 - lon1) * Math.PI / 180;
    const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
              Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
              Math.sin(dLon/2) * Math.sin(dLon/2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    return R * c;
}

// Affichage d'erreurs amélioré
function showError(message) {
    // Créer un toast d'erreur temporaire
    const toast = document.createElement('div');
    toast.className = 'alert-modern';
    toast.style.cssText = `
        position: fixed;
        top: 20px;
        right: 20px;
        z-index: 2000;
        min-width: 300px;
        animation: slideIn 0.3s ease;
    `;
    toast.innerHTML = `
        <div style="display: flex; justify-content: space-between; align-items: center;">
            <span>⚠️ ${message}</span>
            <button onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; color: inherit; font-size: 1.2em; cursor: pointer;">&times;</button>
        </div>
    `;
    
    document.body.appendChild(toast);
    
    // Supprimer automatiquement après 5 secondes
    setTimeout(() => {
        if (toast.parentElement) {
            toast.remove();
        }
    }, 5000);
}

// Affichage des détails d'incident
function showIncidentDetails(incidentId) {
    const incident = allIncidents.find(i => i.id == incidentId);
    if (!incident) return;
    
    const panel = document.getElementById('infosPanel');
    const content = document.getElementById('panelContent');
    
    if (panel && content) {
        content.innerHTML = `
            <h6>🔥 ${incident.title}</h6>
            <div class="small">
                <div><strong>ID:</strong> ${incident.id}</div>
                <div><strong>Statut:</strong> <span style="color: ${incident.color}">${incident.status}</span></div>
                <div><strong>Priorité:</strong> ${incident.priority}</div>
                <div><strong>Lieu:</strong> ${incident.location || 'Non spécifié'}</div>
                <div><strong>Coordonnées:</strong> ${incident.lat.toFixed(6)}, ${incident.lng.toFixed(6)}</div>
            </div>
            <div style="margin-top: 10px;">
                <button onclick="map.setView([${incident.lat}, ${incident.lng}], 18)" class="btn-modern" style="width: 100%; font-size: 0.75rem;">
                    <i class="fas fa-crosshairs"></i> Centrer sur la carte
                </button>
            </div>
        `;
        
        panel.classList.remove('d-none');
        
        // Fermeture automatique après 10 secondes
        setTimeout(() => {
            panel.classList.add('d-none');
        }, 10000);
    }
}

// Timer de rafraîchissement automatique
function startAutoRefresh() {
    if (CONFIG.autoRefresh && refreshTimer === null) {
        refreshTimer = setInterval(() => {
            console.log('[AUTO] Rafraîchissement automatique...');
            autoRefreshTick++;
            loadTechnicians(); // Recharger en priorité les techniciens (temps réel)

            // Rafraîchir les incidents moins souvent (pour suivre ouvert/en cours/résolu)
            if (autoRefreshTick % 4 === 0) {
                loadIncidents();
            }

            // Rafraîchir les trajectoires si la couche est affichée (temps réel)
            // On garde une fenêtre courte (2h) pour que ce soit lisible et performant.
            if (map && trajectoriesLayer && map.hasLayer(trajectoriesLayer)) {
                if (autoRefreshTick % 2 === 0) {
                    if ((currentFilters.filter || '') === 'trajectories') {
                        loadTrajectories({ ...readTrajectoryFilters() });
                    } else {
                        loadTrajectories(24);
                    }
                }
            }
        }, CONFIG.refreshInterval);
    }
}

function stopAutoRefresh() {
    if (refreshTimer) {
        clearInterval(refreshTimer);
        refreshTimer = null;
    }
}

// Initialisation complète
document.addEventListener('DOMContentLoaded', () => {
    console.log('[INIT] Initialisation de la cartographie avancée...');
    
    // Initialiser la carte
    initializeMap();
    
    // Configurer les événements
    setupEventHandlers();
    
    // Charger les données initiales
    showLoading(true);
    Promise.all([loadIncidents(), loadTechnicians()])
        .then(() => {
            console.log('[INIT] Données initiales chargées');
            // Trajectoires visibles par défaut (parcours récent)
            loadTrajectories(24);
            showLoading(false);
        })
        .catch((error) => {
            console.error('[INIT] Erreur lors du chargement initial:', error);
            showLoading(false);
        });
    
    // Démarrer le rafraîchissement automatique
    startAutoRefresh();

    // Auto-activation en arrière-plan (géoloc + suivi) + anti-recentrage
    // L'utilisateur peut couper le suivi via le bouton (préférence mémorisée).
    setTimeout(() => {
        autoStartGeoInBackground();
    }, 700);
    
    console.log('[INIT] Cartographie avancée prête !');
});

// Nettoyage lors de la fermeture
window.addEventListener('beforeunload', () => {
    stopAutoRefresh();
    if (watchId !== null) {
        navigator.geolocation.clearWatch(watchId);
    }
});
</script>
