// ECO-PANNEAU.FR - _react/ui/_ui_utils.jsx

// 1. - SYSTÈME DE NOTIFICATIONS GLOBAL (TOASTS) ET LOGS DE FALLBACK
window.pano_showToast = (message, type = 'info', duration = 4000) => {
    let container = document.getElementById('toast-container');
    
    if (!container) {
        container = document.createElement('div');
        container.id = 'toast-container';
        container.className = 'fixed right-4 sm:right-6 flex flex-col items-end gap-3 pointer-events-none transition-all duration-300 ease-out';
        container.style.zIndex = window.pano_getNextModalZIndex ? (window.pano_getNextModalZIndex() + 100) : 999999;
        document.body.appendChild(container);
    } else {
        container.style.zIndex = window.pano_getNextModalZIndex ? (window.pano_getNextModalZIndex() + 100) : 999999;
    }

    let maxOffset = window.innerWidth >= 640 ? 24 : 16;
    const bottomBars = document.querySelectorAll('.bottom-0');
    bottomBars.forEach(bar => {
        const rect = bar.getBoundingClientRect();
        if (rect.bottom >= window.innerHeight - 5 && rect.height > 0) {
            const currentOffset = rect.height + 16;
            if (currentOffset > maxOffset) {
                maxOffset = currentOffset;
            }
        }
    });
    
    container.style.bottom = `${maxOffset}px`;

    const toast = document.createElement('div');
    const bgColor = type === 'success' ? 'bg-emerald-600/95 border-emerald-500 text-white' :
                    (type === 'error' ? 'bg-red-500/95 border-red-400 text-white' : 
                     (type === 'warning' ? 'bg-amber-500/95 border-amber-400 text-white' : 'bg-blue-600/95 border-blue-500 text-white'));
    
    const svgWrap = (path) => `<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">${path}</svg>`;
    const iconSvg = type === 'success' ? svgWrap(window.pano_svgPaths?.CheckCircle || '') :
                    (type === 'error' || type === 'warning' ? svgWrap(window.pano_svgPaths?.AlertTriangle || '') :
                    svgWrap(window.pano_svgPaths?.Info || ''));

    toast.className = `flex items-center gap-3 px-5 py-4 rounded-2xl border shadow-2xl ${bgColor} pointer-events-auto backdrop-blur-md max-w-xs transition-all duration-500 ease-out`;
    
    const iconDiv = document.createElement('div');
    iconDiv.className = 'shrink-0';
    iconDiv.innerHTML = iconSvg;
    
    const msgDiv = document.createElement('div');
    msgDiv.className = 'font-bold text-sm leading-tight break-words';
    msgDiv.textContent = message; 
    
    toast.appendChild(iconDiv);
    toast.appendChild(msgDiv);
    
    toast.style.transform = 'translateY(-100vh)';
    toast.style.opacity = '0';
    
    container.appendChild(toast);
    void toast.offsetWidth;
    toast.style.transform = 'translateY(0)';
    toast.style.opacity = '1';
    
    setTimeout(() => {
        toast.style.transform = 'translateX(120%)';
        setTimeout(() => {
            toast.remove();
            if (container && container.childNodes.length === 0) container.remove();
        }, 500);
    }, duration);
};

window.pano_loggedErrors = window.pano_loggedErrors || new Set();

window.pano_logFallback = (details) => {
    if (window.pano_loggedErrors.has(details)) return; 
    
    window.pano_loggedErrors.add(details);
    setTimeout(() => window.pano_loggedErrors.delete(details), 60000); 

    try {
        const baseUrl = window.pano_CONFIG ? window.pano_CONFIG.apiBaseUrl : '?api=';
        fetch(baseUrl + 'system/log_fallback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ details }) }).catch(() => {});
    } catch(e) {}
};

// 2. - UTILITAIRES ET LOGIQUE MÉTIER

// CORRECTION SÉCURITÉ : Générateur d'ID universel infaillible (Polyfill)
window.pano_generateID = () => {
    if (typeof crypto !== 'undefined' && crypto.randomUUID) {
        try { return crypto.randomUUID(); } catch(e) {}
    }
    // Fallback mathématique si HTTPS ou navigateur restrictif bloque l'API Crypto
    return Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10);
};

window.pano_formatPlural = (count, singular, plural) => {
    return count > 1 ? `${count} ${plural}` : `${count} ${singular}`;
};

window.pano_normalizeString = (str) => {
    if (str === null || str === undefined) return '';
    return String(str)
        .normalize("NFD")
        .replace(/[\u0300-\u036f]/g, "")
        .toLowerCase()
        .trim();
};

window.pano_apiFetch = async (endpoint, options = {}) => {
    const { method = 'POST', body, setLoading, successMessage, errorMessage = "Erreur réseau ou serveur", silent = false } = options;
    if (setLoading) setLoading(true);

    try {
        const fetchOptions = { method, headers: {} };
        if (silent) fetchOptions.headers['x-silent'] = '1';
        
        // CORRECTION SÉCURITÉ : Interdiction stricte de forcer un body sur GET/HEAD
        if ((body || window.pano_CURRENT_DELEGATE_TOKEN) && method !== 'GET' && method !== 'HEAD') {
            let cleanBody = {};
            if (body) {
                cleanBody = JSON.parse(JSON.stringify(body), (key, value) => {
                    if (['imageUrl', 'maitreOuvrageLogoUrl', 'logoUrl', 'architecteLogoUrl'].includes(key)) return undefined; 
                    if (typeof value === 'string' && value.includes('data:image/')) return value.replace(/(src|href)=["']data:image\/[^;]+;base64,[^"']+["']/gi, '$1="#"');
                    return value;
                });
            }
            if (window.pano_CURRENT_DELEGATE_TOKEN && method === 'POST') cleanBody.delegate_token = window.pano_CURRENT_DELEGATE_TOKEN;
            fetchOptions.body = JSON.stringify(cleanBody);
            
            // CORRECTION RÉSEAU 1 : Injection du header MIME approprié
            fetchOptions.headers['Content-Type'] = 'application/json';
        }
        
        const baseUrl = window.pano_CONFIG ? window.pano_CONFIG.apiBaseUrl : '?api=';
        const fullEndpoint = baseUrl + endpoint;
        const targetUrl = silent ? (fullEndpoint + (fullEndpoint.includes('?') ? '&' : '?') + 'silent=1') : fullEndpoint;
        
        const res = await fetch(targetUrl, fetchOptions);
        const d = await res.json();
        
        if (d.status === 'success') {
            if (successMessage && window.pano_showToast && !silent) window.pano_showToast(successMessage, "success");
            return d;
        } else {
            const isGlobalBlock = res.status === 423 || (res.status === 429 && d.data && d.data.is_blocked);
            if (!isGlobalBlock && window.pano_showToast && !silent) {
                window.pano_showToast(d.message || errorMessage, "error");
            }
            return null;
        }
    } catch (e) {
        if (window.pano_showToast && !silent) window.pano_showToast(errorMessage, "error");
        return null;
    } finally {
        if (setLoading) setLoading(false);
    }
};

// UTILITAIRE GLOBAL ZÉRO-DETTE : Wrapper pour protéger les requêtes asynchrones
window.pano_useSafeFetch = () => {
    const isMounted = React.useRef(true);
    
    React.useEffect(() => {
        isMounted.current = true;
        return () => { isMounted.current = false; };
    }, []);

    const safeFetch = async (endpoint, options = {}) => {
        const { setLoading, ...restOptions } = options;
        
        // On enveloppe silencieusement le setLoading
        const wrappedSetLoading = setLoading 
            ? (val) => { if (isMounted.current) setLoading(val); } 
            : undefined;

        const res = await window.pano_apiFetch(endpoint, {
            ...restOptions,
            setLoading: wrappedSetLoading
        });

        return res;
    };

    return { isMounted, safeFetch };
};

window.pano_formatDate = (dateStr, fallback = '-') => {
    if (!dateStr) return fallback;
    const d = new Date(String(dateStr).replace(' ', 'T'));
    return isNaN(d.getTime()) ? fallback : d.toLocaleDateString('fr-FR');
};

window.pano_getPanelAccessRights = (allPanels = [], clientUid, clientEmailHash) => {
    const isMe = (uid) => uid === clientUid || uid === clientEmailHash;
    
    const pendingInvites = allPanels.filter(p => p.client_uid !== clientUid && p.collaborators?.some(c => isMe(c.uid) && c.status === 'pending'));
    const ownedPanels = allPanels.filter(p => p.client_uid === clientUid);
    const acceptedCollabPanels = allPanels.filter(p => p.client_uid !== clientUid && p.collaborators?.some(c => isMe(c.uid) && c.status === 'accepted'));
    const visiblePanels = [...ownedPanels, ...acceptedCollabPanels];

    return { pendingInvites, ownedPanels, acceptedCollabPanels, visiblePanels };
};

window.pano_computeClientUnread = (interactions = [], myClientData = {}, visiblePanels = [], pendingInvites = []) => {
    let unread = 0;
    const expectedFullName = myClientData.full_name || 'Utilisateur';
    const expectedCompany = myClientData.name || 'Société';
    const expectedAuthorName = `${expectedFullName} (${expectedCompany})`;
    const isMe = (uid) => uid === myClientData.id || uid === myClientData.email_hash;
    
    interactions.forEach(m => {
        if (m.resolved) return;

        const isMyOwnMessage = (m.authorType === 'Client' && (
            m.author === 'Client' || 
            m.author === myClientData.email || 
            m.author === myClientData.full_name || 
            m.author === myClientData.name ||
            m.author === expectedAuthorName
        ));

        if (m.panneauId.startsWith('SUPPORT_')) {
            if (m.authorType !== 'Client') unread++;
        } 
        else if (m.panneauId.startsWith('GROUP_')) {
            const pId = m.panneauId.replace('GROUP_', '');
            const hasAccess = visiblePanels.some(p => p.id === pId) || pendingInvites.some(p => p.id === pId);
            if (hasAccess && !isMyOwnMessage && m.authorType !== 'Systeme' && m.authorType !== 'System') unread++; 
        } 
        else {
            if (m.authorType !== 'Client' && m.authorType !== 'Admin' && m.authorType !== 'Systeme' && m.authorType !== 'System') {
                const panneauObj = visiblePanels.find(p => p.id === m.panneauId);
                if (panneauObj) {
                    const isOwner = panneauObj.client_uid === myClientData.id;
                    const myCollab = !isOwner ? panneauObj.collaborators?.find(c => isMe(c.uid)) : null;
                    const hasRiverainAccess = isOwner || (myCollab && myCollab.rights?.chat_access !== 'none');
                    if (hasRiverainAccess) unread++;
                }
            }
        }
    });
    return unread;
};

window.pano_uploadFile = async (file, type, onProgress, isPublic = false, isPrivate = false, clientUid = null, panneauId = null) => {
    let fileToSend = file;
    let previewBlobs = {};
    let realTotalPages = 1;
    let hasLocalProcessing = false;
    
    try {
        if (type === 'image' && file.type.startsWith('image/')) {
            if (onProgress) onProgress("Optimisation de l'image", 10);
            const img = new Image();
            const imgUrl = URL.createObjectURL(file);
            await new Promise((resolve, reject) => {
                img.onload = resolve; img.onerror = () => reject(new Error("Image illisible.")); img.src = imgUrl;
            });
            
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            let width = img.width, height = img.height;
            const MAX_SIZE = 1920; 
            const targetScale = MAX_SIZE / Math.max(width, height);
            
            if (targetScale < 1) {
                width = Math.round(width * targetScale); 
                height = Math.round(height * targetScale);
            }
            
            canvas.width = width; canvas.height = height;
            ctx.drawImage(img, 0, 0, width, height);
            URL.revokeObjectURL(imgUrl);
            
            const blob = await new Promise((resolve, reject) => {
                canvas.toBlob(b => { 
                    if (b) resolve(b); 
                    else canvas.toBlob(b2 => b2 ? resolve(b2) : reject(new Error("Echec toBlob")), 'image/jpeg', 0.85); 
                }, 'image/webp', 0.85);
            });
            
            const ext = blob.type === 'image/jpeg' ? '.jpg' : '.webp';
            const safeName = file.name || 'image_collee';
            fileToSend = new File([blob], safeName.replace(/\.[^/.]+$/, "") + ext, { type: blob.type });

            if (onProgress) onProgress("Génération de la miniature", 20);
            const thumbScale = 400 / Math.max(img.width, img.height);
            if (thumbScale < 1) {
                const canvasThumb = document.createElement('canvas');
                canvasThumb.width = Math.round(img.width * thumbScale);
                canvasThumb.height = Math.round(img.height * thumbScale);
                const ctxThumb = canvasThumb.getContext('2d');
                ctxThumb.drawImage(img, 0, 0, canvasThumb.width, canvasThumb.height);
                
                let thumbDataUrl = canvasThumb.toDataURL('image/webp', 0.25);
                if (!thumbDataUrl || thumbDataUrl === 'data:,') thumbDataUrl = canvasThumb.toDataURL('image/jpeg', 0.25);
                previewBlobs['preview_thumb'] = thumbDataUrl;
                canvasThumb.width = 0; canvasThumb.height = 0;
            } else {
                let thumbDataUrl = canvas.toDataURL('image/webp', 0.25);
                if (!thumbDataUrl || thumbDataUrl === 'data:,') thumbDataUrl = canvas.toDataURL('image/jpeg', 0.25);
                previewBlobs['preview_thumb'] = thumbDataUrl;
            }

            canvas.width = 0; canvas.height = 0;
            hasLocalProcessing = true;
            if (onProgress) onProgress("Préparation de l'envoi", 30);
        }
        else if (type === 'pdf' && file.type === 'application/pdf') {
            if (onProgress) onProgress("Analyse du PDF", 5);
            let limitPdfPages = 20;
            try {
                const res = await fetch((window.pano_CONFIG ? window.pano_CONFIG.apiBaseUrl : '?api=') + 'sync&limit=1&silent=1');
                const d = await res.json();
                if (d?.data?.settings?.limit_pdf_pages) limitPdfPages = parseInt(d.data.settings.limit_pdf_pages, 10);
            } catch(e) {}

            if (!window.pano_pdfjsLib) {
                await new Promise((resolve, reject) => {
                    const assetsUrl = (window.pano_CONFIG && window.pano_CONFIG.assetsUrl) ? window.pano_CONFIG.assetsUrl : '_assets/';
                    const script = document.createElement('script'); script.src = assetsUrl + 'pdfjs/pdf.min.js';
                    script.onload = () => { window.pano_pdfjsLib.GlobalWorkerOptions.workerSrc = assetsUrl + 'pdfjs/pdf.worker.min.js'; resolve(); };
                    script.onerror = () => {
                        const cdnScript = document.createElement('script'); 
                        cdnScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
                        cdnScript.crossOrigin = 'anonymous';
                        
                        cdnScript.onload = () => { 
                            window.pano_pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; 
                            resolve(); 
                        };
                        cdnScript.onerror = () => reject(new Error("Impossible de charger PDF.js.")); 
                        document.head.appendChild(cdnScript);
                    };
                    document.head.appendChild(script);
                });
            }
            if (window.pano_pdfjsLib) {
                if (onProgress) onProgress("Extraction des pages", 10);
                const arrayBuffer = await file.arrayBuffer();
                const loadingTask = window.pano_pdfjsLib.getDocument({ data: arrayBuffer });
                const pdf = await loadingTask.promise;
                
                realTotalPages = pdf.numPages; 
                const pagesToExtract = Math.min(realTotalPages, limitPdfPages); 

                for (let i = 1; i <= pagesToExtract; i++) {
                    if (onProgress) onProgress(`Rendu page ${i}/${pagesToExtract}`, 10 + Math.round((i/pagesToExtract)*20));
                    const page = await pdf.getPage(i);
                    
                    if (i === 1) {
                        const vpBaseThumb = page.getViewport({ scale: 1.0 });
                        const viewportThumb = page.getViewport({ scale: 400 / Math.max(vpBaseThumb.width, vpBaseThumb.height) });
                        const canvasThumb = document.createElement('canvas'); canvasThumb.width = viewportThumb.width; canvasThumb.height = viewportThumb.height;
                        await page.render({ canvasContext: canvasThumb.getContext('2d'), viewport: viewportThumb }).promise;
                        
                        let thumbDataUrl = canvasThumb.toDataURL('image/webp', 0.25);
                        if (!thumbDataUrl || thumbDataUrl === 'data:,') thumbDataUrl = canvasThumb.toDataURL('image/jpeg', 0.25);
                        previewBlobs['preview_thumb'] = thumbDataUrl;
                        canvasThumb.width = 0; canvasThumb.height = 0;
                    }
                    
                    const vpBase = page.getViewport({ scale: 1.0 });
                    const scaleW = 896 / vpBase.width;
                    const scaleH = 1280 / vpBase.height;
                    const targetScale = Math.min(scaleW, scaleH);
                    
                    const viewport = page.getViewport({ scale: targetScale });
                    const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height;
                    await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
                    
                    let dataUrl = canvas.toDataURL('image/webp', 0.50);
                    if (!dataUrl || dataUrl === 'data:,') dataUrl = canvas.toDataURL('image/jpeg', 0.50);
                    previewBlobs[`preview_b64_${i}`] = dataUrl;
                    
                    canvas.width = 0; canvas.height = 0; page.cleanup();
                }
                pdf.destroy(); hasLocalProcessing = true;
            }
        }
    } catch (e) {
        if (window.pano_showToast) window.pano_showToast("Aperçu local indisponible. Envoi du fichier brut en cours.", "info");
        hasLocalProcessing = false; fileToSend = file; previewBlobs = {}; realTotalPages = 1;
    }

    return new Promise((resolve, reject) => {
        const formData = new FormData();
        formData.append('file', fileToSend); formData.append('type', type);
        if (window.pano_CURRENT_DELEGATE_TOKEN) formData.append('delegate_token', window.pano_CURRENT_DELEGATE_TOKEN);
        if (isPublic) formData.append('isPublic', 'true');
        if (isPrivate) formData.append('is_private', 'true');
        if (clientUid) formData.append('client_uid', clientUid);
        if (panneauId) formData.append('panneau_id', panneauId);
        
        if (Object.keys(previewBlobs).length > 0) {
            for (const [key, b64] of Object.entries(previewBlobs)) {
                formData.append(key, b64);
            }
            formData.append('numPages', realTotalPages); 
        }

        const xhr = new XMLHttpRequest();
        const baseUrl = window.pano_CONFIG ? window.pano_CONFIG.apiBaseUrl : '?api=';

        xhr.open('POST', baseUrl + 'file/upload', true);
        xhr.upload.onprogress = (e) => {
            if (e.lengthComputable && onProgress) {
                const base = hasLocalProcessing ? 30 : 0;
                const multiplier = hasLocalProcessing ? 70 : 100;
                onProgress(`Transfert`, Math.round(base + ((e.loaded / e.total) * multiplier)));
            }
        };

        xhr.onload = () => {
            window.dispatchEvent(new CustomEvent('api_call_end'));
            if (xhr.status === 401) {
                window.dispatchEvent(new CustomEvent('session_expired'));
            }
            if (xhr.status >= 200 && xhr.status < 300) {
                try {
                    const res = JSON.parse(xhr.responseText);
                    if (res.status === 'success') {
                        if (onProgress) onProgress("Terminé", 100);
                        const ret = new String(res.data.id);
                        ret.id = res.data.id; ret.numPages = res.data.numPages || 1;
                        resolve(ret);
                    } else reject(new Error(res.message || 'Erreur upload serveur'));
                } catch (err) { reject(new Error('Réponse invalide du serveur')); }
            } else reject(new Error('Erreur HTTP ' + xhr.status));
        };
        xhr.onerror = () => {
            window.dispatchEvent(new CustomEvent('api_call_end'));
            reject(new Error("Erreur réseau (déconnexion ou blocage)"));
        };
        
        window.dispatchEvent(new CustomEvent('api_call_start'));
        xhr.send(formData);
    });
};

// 3. - LE ROUTEUR (STACK NAVIGATOR) SINGLETON (ZÉRO FUITE MÉMOIRE)
if (typeof window !== 'undefined' && !window._panoHistoryPatched) {
    window._panoHistoryPatched = true;
    window.pano_current_depth = 0;

    if (!window.history.state || typeof window.history.state.depth !== 'number') {
        const initialState = window.history.state || {};
        initialState.depth = window.pano_current_depth;
        window.history.replaceState(initialState, '', window.location.href);
    } else {
        window.pano_current_depth = window.history.state.depth;
    }

    const originalPushState = window.history.pushState;
    window.history.pushState = function(state, title, url) {
        window.pano_current_depth += 1; 
        const newState = state || {};
        newState.depth = window.pano_current_depth;
        return originalPushState.apply(this, [newState, title, url]);
    };

    const originalReplaceState = window.history.replaceState;
    window.history.replaceState = function(state, title, url) {
        const newState = state || {};
        newState.depth = window.pano_current_depth; 
        return originalReplaceState.apply(this, [newState, title, url]);
    };
}

if (typeof window !== 'undefined' && !window._panoRouterSingleton) {
    window._panoRouterSingleton = true;
    window._panoRouterSubscribers = new Set();
    window._panoRouterMaxLevel = 0;

    const handlePopState = (e) => {
        const state = e.state;
        if (!state || !state.panoStack) return;

        const incomingLevel = state.level || 0;

        if (incomingLevel > window._panoRouterMaxLevel) {
            window.history.back();
        } else {
            window._panoRouterMaxLevel = incomingLevel;
            if (window.pano_internal_pushes > 0) window.pano_internal_pushes--;
            window._panoRouterSubscribers.forEach(fn => fn({ ...state }));
        }
    };

    const handleSync = () => {
        const state = window.history.state;
        if (state && state.panoStack) {
            window._panoRouterMaxLevel = state.level;
            window._panoRouterSubscribers.forEach(fn => fn({ ...state }));
        }
    };

    window.addEventListener('popstate', handlePopState);
    window.addEventListener('pano_stack_sync', handleSync);
}

window.pano_useUrlModal = () => {
    const { useState, useEffect, useCallback, useRef } = React;
    
    const getParams = () => {
        const p = new URLSearchParams(window.location.search);
        return {
            modal: p.get('modal'),
            dialog: p.get('dialog'),
            chat_id: p.get('chat_id'),
            targetId: p.get('targetId'),
            dialogId: p.get('dialogId'),
            alert: p.get('alert'),
            tab: p.get('tab') || 'dashboard'
        };
    };

    const [appState, setAppState] = useState(() => {
        if (typeof window !== 'undefined' && window.history.state && window.history.state.panoStack) {
            window._panoRouterMaxLevel = window.history.state.level;
            return window.history.state;
        }
        
        const p = getParams();
        const stack = [];

        if (p.modal) stack.push({ type: 'modal', name: p.modal, targetId: p.targetId });
        if (p.dialog) stack.push({ type: 'dialog', name: p.dialog, targetId: p.dialogId });
        if (p.chat_id) stack.push({ type: 'chat', name: 'chat', targetId: p.chat_id });
        if (p.alert) stack.push({ type: 'alert', name: p.alert, targetId: null });

        const st = { panoStack: stack, level: stack.length, currentTab: p.tab, depth: window.pano_current_depth || 0 };
        window._panoRouterMaxLevel = stack.length;
        return st;
    });

    const isInit = useRef(false);

    useEffect(() => {
        window._panoRouterSubscribers.add(setAppState);

        if (!isInit.current && typeof window !== 'undefined') {
            isInit.current = true;
            window.pano_internal_pushes = window.pano_internal_pushes || 0; 
            
            const state = window.history.state || {};
            if (!state.panoStack) {
                const cleanUrl = new URL(window.location.href);
                ['modal', 'targetId', 'dialog', 'dialogId', 'chat_id', 'alert'].forEach(k => cleanUrl.searchParams.delete(k));
                
                const initSt = { ...state, panoStack: appState.panoStack, level: appState.level, currentTab: appState.currentTab, depth: window.pano_current_depth || 0 };
                window.history.replaceState(initSt, '', cleanUrl);
                window._panoRouterMaxLevel = appState.level;
                setAppState(initSt);
            }
        }

        return () => {
            window._panoRouterSubscribers.delete(setAppState);
        };
    }, []);

    const broadcastSync = () => {
        window.dispatchEvent(new Event('pano_stack_sync'));
    };

    const buildUrl = (tab, type, name, targetId) => {
        let qs = `tab=${tab}`;
        if (type === 'modal') { qs += `&modal=${name}`; if (targetId) qs += `&targetId=${targetId}`; }
        else if (type === 'dialog') { qs += `&dialog=${name}`; if (targetId) qs += `&dialogId=${targetId}`; }
        else if (type === 'chat') { qs += `&chat_id=${targetId}`; }
        else if (type === 'alert') { qs += `&alert=${name}`; }
        return `?${qs}`;
    };

    const pushLayer = useCallback((type, name, targetId = null, silent = true) => {
        const prev = window.history.state || { panoStack: [], level: 0, currentTab: 'dashboard' };
        const currentStack = prev.panoStack || [];
        
        const newStack = currentStack.filter(l => l.type !== type);
        newStack.push({ type, name, targetId });
        
        const newLevel = newStack.length;
        const currentTab = prev.currentTab || 'dashboard';
        const newState = { ...prev, panoStack: newStack, level: newLevel, currentTab };
        
        const url = silent ? window.location.href : buildUrl(currentTab, type, name, targetId);
        
        window.history.pushState(newState, '', url);
        window.pano_internal_pushes = (window.pano_internal_pushes || 0) + 1;
        window._panoRouterMaxLevel = newLevel;
        
        setAppState(newState);
        broadcastSync();
    }, []);

    const replaceCurrentLayer = useCallback((type, name, targetId = null, silent = true) => {
        const prev = window.history.state || { panoStack: [], level: 0, currentTab: 'dashboard' };
        const currentStack = prev.panoStack || [];
        
        if (currentStack.length === 0) {
            const newStack = [{ type, name, targetId }];
            const currentTab = prev.currentTab || 'dashboard';
            const newState = { ...prev, panoStack: newStack, level: 1, currentTab };
            const url = silent ? window.location.href : buildUrl(currentTab, type, name, targetId);
            window.history.pushState(newState, '', url);
            window.pano_internal_pushes = (window.pano_internal_pushes || 0) + 1;
            window._panoRouterMaxLevel = 1;
            setAppState(newState);
            broadcastSync();
            return;
        }
        
        const newStack = [...currentStack.slice(0, -1), { type, name, targetId }];
        const currentTab = prev.currentTab || 'dashboard';
        const newState = { ...prev, panoStack: newStack, level: prev.level || newStack.length, currentTab };
        
        const url = silent ? window.location.href : buildUrl(currentTab, type, name, targetId);
        
        window.history.replaceState(newState, '', url);
        
        setAppState(newState);
        broadcastSync();
    }, []);

    const closeCurrentLayer = useCallback(() => {
        const prev = window.history.state || { panoStack: [], level: 0, currentTab: 'dashboard' };
        const currentStack = prev.panoStack || [];
        
        if (currentStack.length === 0) return;

        if (window.history.length > 1 && window.pano_internal_pushes > 0) {
            window.pano_internal_pushes--;
            window.history.back();
        } else {
            const newStack = currentStack.slice(0, -1);
            const newLevel = newStack.length;
            const currentTab = prev.currentTab || 'dashboard';
            const newState = { ...prev, panoStack: newStack, level: newLevel, currentTab };
            
            let url = `?tab=${currentTab}`;
            if (newStack.length > 0) {
                const top = newStack[newStack.length - 1];
                url = buildUrl(currentTab, top.type, top.name, top.targetId);
            }
            
            window.history.replaceState(newState, '', url);
            window._panoRouterMaxLevel = newLevel;
            
            setAppState(newState);
            broadcastSync();
        }
    }, []);

    const changeTab = useCallback((tabId) => {
        const newState = { panoStack: [], level: 0, currentTab: tabId, depth: window.pano_current_depth || 0 };
        window.history.pushState(newState, '', `?tab=${tabId}`);
        window.pano_internal_pushes = 0; 
        window._panoRouterMaxLevel = 0;
        
        setAppState(newState);
        broadcastSync();
    }, []);

    const openModal = useCallback((name, id = null, silent = true) => pushLayer('modal', name, id, silent), [pushLayer]);
    const openDialog = useCallback((name, id = null, silent = true) => pushLayer('dialog', name, id, silent), [pushLayer]);
    const openChat = useCallback((chatId, silent = true) => pushLayer('chat', 'chat', chatId, silent), [pushLayer]);
    const setAlert = useCallback((name, silent = true) => pushLayer('alert', name, null, silent), [pushLayer]);

    const getFromStack = useCallback((type) => (appState.panoStack || []).find(l => l.type === type), [appState.panoStack]);

    return { 
        activeModal: getFromStack('modal')?.name || null,
        activeDialog: getFromStack('dialog')?.name || null,
        activeChat: getFromStack('chat')?.targetId || null,
        targetId: getFromStack('modal')?.targetId || getFromStack('dialog')?.targetId || null,
        dialogId: getFromStack('dialog')?.targetId || null,
        alertParam: getFromStack('alert')?.name || null,
        currentTab: appState.currentTab,
        
        openModal,
        openDialog,
        openChat,
        setAlert,
        
        replaceCurrentLayer,
        closeCurrentLayer,
        changeTab
    };
};

// 4. - MOTEUR DE RECHERCHE ET PAGINATION
window.pano_useSearchAndPagination = (initialData, filterFn, initialCount = 50) => {
    const { useState, useMemo, useEffect, useRef } = React;
    const [searchQuery, setSearchQuery] = useState("");
    const [visibleCount, setVisibleCount] = useState(initialCount);

    // CORRECTION PERF 2 : Pattern useRef pour casser la boucle de dépendance du useMemo
    const filterFnRef = useRef(filterFn);
    useEffect(() => {
        filterFnRef.current = filterFn;
    }, [filterFn]);

    useEffect(() => {
        setVisibleCount(initialCount);
    }, [searchQuery, initialCount]);

    const filteredData = useMemo(() => {
        if (!searchQuery.trim() || typeof filterFnRef.current !== 'function') return initialData || [];
        const q = window.pano_normalizeString(searchQuery);
        return (initialData || []).filter(item => filterFnRef.current(item, q));
    }, [initialData, searchQuery]); // <-- filterFn n'est plus dans les dépendances, fin des re-rendus infinis

    const displayedData = filteredData.slice(0, visibleCount);

    return {
        searchQuery,
        setSearchQuery,
        visibleCount,
        setVisibleCount,
        filteredData,
        displayedData
    };
};

// 5. - REGISTRES CENTRALISÉS
window.pano_getIcons = () => {
    const fallback = () => null;
    return new Proxy({}, {
        get: (target, prop) => {
            if (typeof window['pano_' + prop] !== 'undefined') return window['pano_' + prop];
            window.pano_logFallback(`Icône manquante : ${prop}`);
            return fallback;
        }
    });
};

window.pano_getComponents = () => {
    const FallbackNull = () => null;
    
    const FallbackTextLogo = ({ className = "", colorEco = "text-slate-800" }) => {
        const isHome = window.location.pathname === '/' && (!window.location.search || window.location.search === '?tab=accueil' || window.location.search === '');
        const content = (
            <span className={`whitespace-nowrap font-black tracking-tighter ${className}`}>
                <span className={colorEco}>eco</span><span className="text-emerald-600">-panneau</span><span className="text-slate-400 font-bold">.fr</span>
            </span>
        );
        return isHome ? content : <a href="https://eco-panneau.fr" className="hover:opacity-80 transition block">{content}</a>;
    };
    
    const FallbackCardGrid = ({children, className = ""}) => (
        <div className={`grid gap-4 ${className}`}>{children}</div>
    );
    
    const FallbackDataCard = ({children, className = "", onClick, variant}) => (
        <div onClick={onClick} className={`bg-white rounded-3xl p-5 shadow-sm border flex flex-col gap-4 ${className} ${variant === 'warning' ? 'border-amber-300 bg-amber-50/50' : (variant === 'danger' ? 'border-red-300 bg-red-50/50' : (variant === 'info' ? 'border-blue-300 bg-blue-50/50' : 'border-slate-200'))}`}>
            {children}
        </div>
    );

    const fallbacks = {
        TextLogo: FallbackTextLogo,
        CardGrid: FallbackCardGrid,
        DataCard: FallbackDataCard,
        AdminPanelCard: FallbackNull,
        AdminLogisticsCard: FallbackNull,
        AdminMessageCard: FallbackNull
    };

    return new Proxy({}, {
        get: (target, prop) => {
            if (typeof window['pano_' + prop] !== 'undefined') return window['pano_' + prop];
            window.pano_logFallback(`Composant manquant : ${prop}`);
            return fallbacks[prop] || FallbackNull;
        }
    });
};

/* EOF ========== [_react/ui/_ui_utils.jsx] */