/**
 * PLATAFORMA GANADERA — App principal
 * Inicialización, sidebar, UI global, utilidades
 */

$(document).ready(function() {
    App.init();
});

const App = {
    /**
     * Inicialización
     */
    init() {
        this.initServiceWorker();
        this.initInstallPrompt();
        this.initSidebar();
        this.initSearch();
        this.initNotifications();
        this.initNotificationPermission();
        this.initTooltips();
        this.initOnlineStatus();
        this.highlightActiveNav();
    },

    // ─── SERVICE WORKER ─────────────────────────────────────

    async initServiceWorker() {
        if (!('serviceWorker' in navigator)) return;

        // Force unregister old SWs and clear ALL stale caches on every load
        try {
            const currentVersion = 'v3.6';
            const cacheKeys = await caches.keys();
            const staleCaches = cacheKeys.filter(k => !k.includes(currentVersion));
            await Promise.all(staleCaches.map(k => caches.delete(k)));
            if (staleCaches.length) console.log('[App] Cleared stale caches:', staleCaches);
        } catch(e) { /* ignore */ }

        try {
            // Force browser to re-fetch sw.js by adding updateViaCache and version param
            const reg = await navigator.serviceWorker.register('/sw.js?v=3.9.9', { updateViaCache: 'none' });
            // Force update check on every page load
            reg.update().catch(() => {});
            console.log('[App] Service Worker registrado', reg.scope);

            // Check for updates
            reg.addEventListener('updatefound', () => {
                const newWorker = reg.installing;
                if (!newWorker) return;

                newWorker.addEventListener('statechange', () => {
                    if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
                        // New version available
                        this.showUpdateBanner();
                    }
                });
            });
        } catch (err) {
            console.warn('[App] SW registration failed:', err);
        }

        // When the new worker takes control, reload
        navigator.serviceWorker.addEventListener('controllerchange', () => {
            if (this._reloading) return;
            this._reloading = true;
            window.location.reload();
        });
    },

    showUpdateBanner() {
        const $banner = $(`
            <div class="pwa-update-banner">
                <div class="pwa-update-content">
                    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
                    <span>Nueva versión disponible</span>
                </div>
                <button class="pwa-update-btn" id="pwaUpdateBtn">Actualizar</button>
            </div>
        `);
        $('body').append($banner);
        setTimeout(() => $banner.addClass('show'), 50);

        $('#pwaUpdateBtn').on('click', () => {
            navigator.serviceWorker.ready.then(reg => {
                if (reg.waiting) {
                    reg.waiting.postMessage({ type: 'SKIP_WAITING' });
                }
            });
        });
    },

    // ─── PWA INSTALL PROMPT ─────────────────────────────────

    _deferredInstallPrompt: null,

    initInstallPrompt() {
        // Intercept the browser's install prompt
        window.addEventListener('beforeinstallprompt', (e) => {
            e.preventDefault();
            this._deferredInstallPrompt = e;

            // Only show if not dismissed recently
            const dismissed = localStorage.getItem('pwa-install-dismissed');
            if (dismissed) {
                const dismissedAt = parseInt(dismissed, 10);
                // Don't show again for 7 days
                if (Date.now() - dismissedAt < 7 * 24 * 60 * 60 * 1000) return;
            }

            this.showInstallBanner();
        });

        // Track successful install
        window.addEventListener('appinstalled', () => {
            this._deferredInstallPrompt = null;
            $('.pwa-install-banner').remove();
            Toast.success('¡App instalada correctamente!');
            localStorage.removeItem('pwa-install-dismissed');
        });
    },

    showInstallBanner() {
        // Don't show if already in standalone mode
        if (window.matchMedia('(display-mode: standalone)').matches) return;
        if (window.navigator.standalone) return; // iOS

        const $banner = $(`
            <div class="pwa-install-banner">
                <div class="pwa-install-icon">
                    <svg width="24" height="24" viewBox="0 0 34 34" fill="none">
                        <rect width="34" height="34" rx="9" fill="url(#lgInstall)"/>
                        <path d="M9 23 C11.5 16 13.5 13 17 13 C20.5 13 22.5 16 25 23" stroke="#fff" stroke-width="2.2" stroke-linecap="round" fill="none"/>
                        <circle cx="17" cy="9.5" r="2.2" fill="#D4A03C"/>
                        <defs><linearGradient id="lgInstall" x1="0" y1="0" x2="34" y2="34"><stop stop-color="#2D6A4F"/><stop offset="1" stop-color="#1B4332"/></linearGradient></defs>
                    </svg>
                </div>
                <div class="pwa-install-text">
                    <strong>Instalar Ganadera</strong>
                    <small>Acceso rápido desde tu pantalla de inicio</small>
                </div>
                <div class="pwa-install-actions">
                    <button class="pwa-install-dismiss" id="pwaInstallDismiss">Ahora no</button>
                    <button class="pwa-install-accept" id="pwaInstallAccept">Instalar</button>
                </div>
            </div>
        `);

        $('body').append($banner);
        setTimeout(() => $banner.addClass('show'), 100);

        $('#pwaInstallAccept').on('click', async () => {
            if (!this._deferredInstallPrompt) return;
            this._deferredInstallPrompt.prompt();
            const result = await this._deferredInstallPrompt.userChoice;
            if (result.outcome === 'accepted') {
                console.log('[PWA] Install accepted');
            }
            this._deferredInstallPrompt = null;
            $banner.removeClass('show');
            setTimeout(() => $banner.remove(), 400);
        });

        $('#pwaInstallDismiss').on('click', () => {
            localStorage.setItem('pwa-install-dismissed', Date.now().toString());
            $banner.removeClass('show');
            setTimeout(() => $banner.remove(), 400);
        });
    },

    // ─── NOTIFICATION PERMISSION ────────────────────────────

    /**
     * Trigger PWA install from anywhere (settings page, etc.)
     * Returns: 'accepted', 'dismissed', 'unavailable', or 'standalone'
     */
    async triggerInstall() {
        if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone) {
            return 'standalone';
        }
        if (!this._deferredInstallPrompt) {
            return 'unavailable';
        }
        this._deferredInstallPrompt.prompt();
        const result = await this._deferredInstallPrompt.userChoice;
        this._deferredInstallPrompt = null;
        return result.outcome;
    },

    /**
     * Check if PWA install is available
     */
    canInstallPWA() {
        if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone) {
            return 'installed';
        }
        return this._deferredInstallPrompt ? 'available' : 'unavailable';
    },

    initNotificationPermission() {
        if (!('Notification' in window)) return;
        if (Notification.permission === 'granted') return; // Already granted
        if (Notification.permission === 'denied') return;  // Can't ask again

        // Check if user has already been asked recently
        const asked = localStorage.getItem('notification-ask-dismissed');
        if (asked) {
            const askedAt = parseInt(asked, 10);
            if (Date.now() - askedAt < 3 * 24 * 60 * 60 * 1000) return; // 3 days
        }

        // Show after a delay so user has context
        setTimeout(() => this.showNotificationPrompt(), 5000);
    },

    showNotificationPrompt() {
        const $prompt = $(`
            <div class="pwa-notif-prompt">
                <div class="pwa-notif-content">
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
                        <path d="M13.73 21a2 2 0 0 1-3.46 0"/>
                    </svg>
                    <span>¿Activar notificaciones para vacunas, tareas y alertas?</span>
                </div>
                <div class="pwa-notif-actions">
                    <button class="pwa-notif-no" id="notifNo">No</button>
                    <button class="pwa-notif-yes" id="notifYes">Activar</button>
                </div>
            </div>
        `);

        $('body').append($prompt);
        setTimeout(() => $prompt.addClass('show'), 50);

        $('#notifYes').on('click', async () => {
            $prompt.removeClass('show');
            setTimeout(() => $prompt.remove(), 400);
            const granted = await this.requestPushPermission();
            if (granted) {
                Toast.success('Notificaciones activadas');
            }
        });

        $('#notifNo').on('click', () => {
            localStorage.setItem('notification-ask-dismissed', Date.now().toString());
            $prompt.removeClass('show');
            setTimeout(() => $prompt.remove(), 400);
        });
    },

    // ─── SIDEBAR ────────────────────────────────────────────

    initSidebar() {
        const $sidebar = $('#appSidebar');
        const $overlay = $('.sidebar-overlay');
        const $toggle = $('.btn-menu-toggle');
        const $collapseBtn = $('#sidebarCollapseBtn');

        // Add tooltip data attributes for collapsed state
        $sidebar.find('.nav-item').each(function() {
            const label = $(this).find('span').text().trim();
            if (label) $(this).attr('data-tooltip', label);
        });

        // Mobile: hamburger opens overlay sidebar
        $toggle.on('click', () => {
            $sidebar.toggleClass('open');
            $overlay.toggleClass('active');
        });

        $overlay.on('click', () => {
            $sidebar.removeClass('open');
            $overlay.removeClass('active');
        });

        // Close sidebar on mobile on navigation
        $('.nav-item').on('click', function() {
            if ($(window).width() < 768) {
                $sidebar.removeClass('open');
                $overlay.removeClass('active');
            }
        });

        // Desktop: collapse toggle (icon-only mode)
        const savedState = localStorage.getItem('sidebar-collapsed');
        if (savedState === 'true' && $(window).width() >= 768) {
            $sidebar.addClass('collapsed');
        }

        $collapseBtn.on('click', () => {
            $sidebar.toggleClass('collapsed');
            localStorage.setItem('sidebar-collapsed', $sidebar.hasClass('collapsed'));
            // Trigger resize event for charts/layouts that depend on content width
            setTimeout(() => $(window).trigger('resize'), 350);
        });

        // On resize, handle state transitions
        $(window).on('resize', () => {
            if ($(window).width() < 768) {
                $sidebar.removeClass('collapsed');
                $sidebar.removeClass('open');
                $overlay.removeClass('active');
            }
        });
    },

    // ─── NAV ACTIVE ─────────────────────────────────────────

    highlightActiveNav() {
        const path = window.location.pathname.replace(window.APP_BASE || '', '');
        $('.nav-item').each(function() {
            const href = $(this).attr('href') || $(this).data('href');
            if (href === path || (href !== '/' && path.startsWith(href))) {
                $(this).addClass('active');
            }
        });
    },

    // ─── SEARCH ─────────────────────────────────────────────

    initSearch() {
        let searchTimeout;
        const $input = $('.search-bar input');
        const $results = $('.search-results');
        const resultsEl = $results[0];

        $input.on('input', function() {
            clearTimeout(searchTimeout);
            const query = $(this).val().trim();

            if (query.length < 2) {
                if (resultsEl) resultsEl.style.display = 'none';
                return;
            }

            searchTimeout = setTimeout(async () => {
                const result = await API.get(`/search?search=${encodeURIComponent(query)}`);
                if (result.success) {
                    App.renderSearchResults(result.data, $results);
                }
            }, 300);
        });

        // Cerrar al hacer clic fuera — vanilla JS to avoid jQuery CSS recursion
        document.addEventListener('click', function(e) {
            if (!e.target.closest('.search-bar') && resultsEl) {
                resultsEl.style.display = 'none';
            }
        });
    },

    renderSearchResults(data, $container) {
        const el = $container[0];
        if (!data.length) {
            $container.html('<div class="search-empty">Sin resultados</div>');
            if (el) el.style.display = '';
            return;
        }

        let html = '';
        data.forEach(item => {
            html += `
                <a class="search-result-item" href="${(window.APP_BASE || '') + item.url}">
                    <span class="search-result-type">${item.type}</span>
                    <span class="search-result-title">${item.title}</span>
                    <span class="search-result-sub">${item.subtitle || ''}</span>
                </a>`;
        });
        $container.html(html);
        if (el) el.style.display = '';
    },

    // ─── NOTIFICATIONS ──────────────────────────────────────

    _lastNotifCheck: null,
    _notifPollInterval: null,

    async initNotifications() {
        // Generate notifications on first load (debounced — only once per 5 min)
        this.triggerNotificationGeneration();

        // Load initial count
        await this.refreshNotificationCount();

        // Poll every 30 seconds for new notifications
        this._lastNotifCheck = new Date().toISOString().replace('T', ' ').substr(0, 19);
        this._notifPollInterval = setInterval(() => this.pollNewNotifications(), 30000);

        // Setup notification dropdown
        this.initNotifDropdown();
    },

    initNotifDropdown() {
        const wrapper = document.getElementById('notifDropdownWrapper');
        const dropdown = document.getElementById('notifDropdown');
        const btn = document.getElementById('btnNotifications');
        if (!wrapper || !dropdown || !btn) return;

        // Toggle dropdown
        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            const isOpen = dropdown.style.display !== 'none';
            if (isOpen) {
                dropdown.style.display = 'none';
            } else {
                dropdown.style.display = '';
                this.loadDropdownNotifications();
            }
        });

        // Close on outside click
        document.addEventListener('click', (e) => {
            if (!e.target.closest('#notifDropdownWrapper')) {
                dropdown.style.display = 'none';
            }
        });

        // Mark all read
        const btnMarkAll = document.getElementById('btnMarkAllRead');
        if (btnMarkAll) {
            btnMarkAll.addEventListener('click', async (e) => {
                e.stopPropagation();
                try {
                    await API.put('/notifications/read-all', {});
                    this.updateNotificationBadge(0);
                    this.loadDropdownNotifications();
                    Toast.success('Notificaciones marcadas como leídas');
                } catch(err) { /* silent */ }
            });
        }
    },

    async loadDropdownNotifications() {
        const body = document.getElementById('notifDropdownBody');
        if (!body) return;

        body.innerHTML = '<div style="padding:24px;text-align:center;color:var(--text-muted);font-size:0.8rem">Cargando...</div>';

        try {
            const res = await API.get('/notifications?per_page=15&filter=unread');
            const notifications = res.data || [];
            
            // If no unread, try all recent
            if (!notifications.length) {
                const res2 = await API.get('/notifications?per_page=10');
                const allNotifs = res2.data || [];
                this.renderDropdownNotifications(allNotifs, body);
            } else {
                this.renderDropdownNotifications(notifications, body);
            }
        } catch(e) {
            body.innerHTML = '<div class="notif-dropdown-empty"><span>Error al cargar notificaciones</span></div>';
        }
    },

    renderDropdownNotifications(notifications, container) {
        if (!notifications.length) {
            container.innerHTML = `
                <div class="notif-dropdown-empty">
                    <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="opacity:0.3">
                        <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
                        <path d="M13.73 21a2 2 0 0 1-3.46 0"/>
                    </svg>
                    <span>Sin notificaciones nuevas</span>
                </div>`;
            return;
        }

        const priorityIcons = { critical: '🔴', high: '🟠', medium: '🔵', low: 'ℹ️' };
        const typeIcons = {
            vaccine_due: '💉', vaccine_overdue: '⚠️💉', task_overdue: '📋',
            task_assigned: '📌', low_stock: '📦', birth_registered: '🐣',
            health_alert: '🏥', animal_no_weight: '⚖️', mortality_alert: '☠️',
            quarantine_active: '🔒', pregnancy_due: '🤰', general: '🔔'
        };

        let html = '';
        notifications.forEach(n => {
            const icon = typeIcons[n.type] || priorityIcons[n.priority] || '🔔';
            const isUnread = !n.is_read || n.is_read === '0' || n.is_read === 0;
            const time = App.timeAgo(n.created_at);
            
            html += `
                <div class="notif-item ${isUnread ? 'unread' : ''}" data-notif-id="${n.id}" data-url="${n.action_url || ''}">
                    <div class="notif-item-icon ${n.priority || 'medium'}">${icon}</div>
                    <div class="notif-item-body">
                        <div class="notif-item-title">${this.escapeHtml(n.title)}</div>
                        <div class="notif-item-msg">${this.escapeHtml(n.message || '')}</div>
                        <div class="notif-item-time">${time}</div>
                    </div>
                </div>`;
        });
        container.innerHTML = html;

        // Bind click handlers
        container.querySelectorAll('.notif-item').forEach(item => {
            item.addEventListener('click', async () => {
                const id = item.dataset.notifId;
                const url = item.dataset.url;
                // Mark as read
                try { await API.put(`/notifications/${id}/read`, {}); } catch(e) {}
                item.classList.remove('unread');
                this.refreshNotificationCount();
                // Navigate if action URL
                if (url) window.location.href = url;
            });
        });
    },

    async triggerNotificationGeneration() {
        if (!API.isAuthenticated()) return;
        const lastGen = localStorage.getItem('notif-last-gen');
        const now = Date.now();
        // Only generate every 5 minutes max
        if (lastGen && (now - parseInt(lastGen)) < 300000) return;
        localStorage.setItem('notif-last-gen', now.toString());
        try {
            await API.post('/notifications/generate', {});
        } catch(e) { /* silent */ }
    },

    async refreshNotificationCount() {
        if (!API.isAuthenticated()) return;
        try {
            const res = await API.get('/notifications/unread-count');
            if (res.success) {
                this.updateNotificationBadge(res.data?.unread || 0);
            }
        } catch(e) { /* silent */ }
    },

    async pollNewNotifications() {
        if (!API.isAuthenticated()) return;

        try {
            const since = this._lastNotifCheck || new Date(Date.now() - 60000).toISOString().replace('T', ' ').substr(0, 19);
            const res = await API.get(`/notifications/recent?since=${encodeURIComponent(since)}`);
            if (!res.success) return;

            const data = res.data;
            // Update badge
            this.updateNotificationBadge(data.unread || 0);

            // Show toast for new notifications
            const notifs = data.notifications || [];
            if (notifs.length > 0) {
                notifs.slice(0, 3).forEach(n => {
                    this.showNotificationToast(n);
                });
                // Send browser push if permission granted
                if ('Notification' in window && Notification.permission === 'granted' && notifs.length > 0) {
                    const n = notifs[0];
                    try {
                        new Notification(n.title, {
                            body: n.message,
                            icon: '/assets/img/icons/icon-192x192.png',
                            tag: 'notif-' + n.id,
                        });
                    } catch(e) {}
                }
            }

            this._lastNotifCheck = new Date().toISOString().replace('T', ' ').substr(0, 19);
        } catch(e) { /* silent */ }
    },

    updateNotificationBadge(count) {
        const badge = document.querySelector('.notification-badge');
        const dot = document.querySelector('.header-btn .badge-dot');
        const countEl = document.getElementById('notifCount');
        if (count > 0) {
            if (badge) { badge.textContent = count > 99 ? '99+' : count; badge.style.display = ''; }
            if (dot) dot.style.display = '';
            if (countEl) { countEl.textContent = count > 99 ? '99+' : count; countEl.style.display = ''; }
        } else {
            if (badge) badge.style.display = 'none';
            if (dot) dot.style.display = 'none';
            if (countEl) countEl.style.display = 'none';
        }
    },

    showNotificationToast(notif) {
        Toast.init();
        const priorityClass = {
            critical: 'danger',
            high: 'warning',
            medium: 'info',
            low: 'info',
        }[notif.priority] || 'info';

        const icon = { critical: '🔴', high: '🟠', medium: '🔵', low: 'ℹ️' }[notif.priority] || '🔔';

        const $toast = $(`
            <div class="toast toast-${priorityClass} toast-notification" data-notif-id="${notif.id}">
                <span class="toast-icon">${icon}</span>
                <div class="toast-notif-body">
                    <strong class="toast-notif-title">${this.escapeHtml(notif.title)}</strong>
                    <span class="toast-message">${this.escapeHtml(notif.message).substring(0, 100)}</span>
                </div>
                <button class="toast-close" onclick="$(this).closest('.toast').remove()">×</button>
            </div>
        `);

        if (notif.action_url) {
            $toast.css('cursor', 'pointer').on('click', function(e) {
                if ($(e.target).hasClass('toast-close')) return;
                window.location.href = notif.action_url;
            });
        }

        Toast.container.append($toast);
        setTimeout(() => {
            $toast.css({ opacity: 0, transform: 'translateX(100px)' });
            setTimeout(() => $toast.remove(), 300);
        }, 8000);
    },

    escapeHtml(text) {
        if (!text) return '';
        const div = document.createElement('div');
        div.appendChild(document.createTextNode(text));
        return div.innerHTML;
    },

    // ─── PUSH NOTIFICATIONS ─────────────────────────────────

    async requestPushPermission() {
        if (!('Notification' in window)) return false;

        const permission = await Notification.requestPermission();
        if (permission === 'granted') {
            // Suscribirse a push
            const reg = await navigator.serviceWorker.ready;
            const sub = await reg.pushManager.subscribe({
                userVisibleOnly: true,
                applicationServerKey: this.urlBase64ToUint8Array(window.VAPID_PUBLIC_KEY || ''),
            });

            // Enviar suscripción al servidor
            try {
                await API.post('/notifications/subscribe', {
                    endpoint: sub.endpoint,
                    keys: {
                        p256dh: btoa(String.fromCharCode(...new Uint8Array(sub.getKey('p256dh')))),
                        auth: btoa(String.fromCharCode(...new Uint8Array(sub.getKey('auth')))),
                    },
                });
            } catch (e) {
                // Endpoint may not exist yet — silently ignore
            }

            return true;
        }
        return false;
    },

    urlBase64ToUint8Array(base64String) {
        const padding = '='.repeat((4 - base64String.length % 4) % 4);
        const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
        const rawData = window.atob(base64);
        return new Uint8Array([...rawData].map(char => char.charCodeAt(0)));
    },

    // ─── TOOLTIPS ───────────────────────────────────────────

    initTooltips() {
        // Tooltips simples con data-tooltip (excluye sidebar — usa CSS-only tooltips)
        let tooltipActive = false;
        $(document).on('mouseenter', '[data-tooltip]', function() {
            // Sidebar nav items use CSS ::after tooltips, skip JS tooltip
            if ($(this).closest('.app-sidebar').length) return;
            if (tooltipActive) return;
            tooltipActive = true;

            const text = this.getAttribute('data-tooltip');
            const tip = document.createElement('div');
            tip.className = 'tooltip-popup';
            tip.textContent = text;
            document.body.appendChild(tip);

            const rect = this.getBoundingClientRect();
            const tipH = tip.offsetHeight || 30;
            const tipW = tip.offsetWidth || 80;
            tip.style.top = (rect.top - tipH - 8) + 'px';
            tip.style.left = (rect.left + (rect.width / 2) - (tipW / 2)) + 'px';
            tip.style.pointerEvents = 'none';
        }).on('mouseleave', '[data-tooltip]', function() {
            if ($(this).closest('.app-sidebar').length) return;
            document.querySelectorAll('.tooltip-popup').forEach(el => el.remove());
            tooltipActive = false;
        });
    },

    // ─── ONLINE/OFFLINE STATUS ──────────────────────────────

    initOnlineStatus() {
        window.addEventListener('online', () => {
            Toast.success('Conexión restablecida');
            // Trigger sync
            if ('serviceWorker' in navigator) {
                navigator.serviceWorker.ready.then(reg => {
                    reg.sync.register('sync-pending-data').catch(() => {});
                });
            }
        });

        window.addEventListener('offline', () => {
            Toast.warning('Sin conexión. Los datos se guardarán localmente.');
        });
    },

    // ─── COUNTER ANIMATION ──────────────────────────────────

    animateCounter(element, target, duration = 1000, suffix = '', prefix = '') {
        const start = 0;
        const startTime = performance.now();

        function step(currentTime) {
            const elapsed = currentTime - startTime;
            const progress = Math.min(elapsed / duration, 1);
            const eased = 1 - Math.pow(1 - progress, 3); // easeOutCubic
            const current = Math.round(start + (target - start) * eased);

            element.textContent = prefix + current.toLocaleString('es-MX') + suffix;

            if (progress < 1) {
                requestAnimationFrame(step);
            }
        }

        requestAnimationFrame(step);
    },

    // ─── UTILITY HELPERS ────────────────────────────────────

    debounce(fn, delay = 300) {
        let timer;
        return function(...args) {
            clearTimeout(timer);
            timer = setTimeout(() => fn.apply(this, args), delay);
        };
    },

    // ─── PAGINATION ─────────────────────────────────────────

    /**
     * Render pagination controls.
     * @param {string} selector  — jQuery selector for the container
     * @param {object} meta      — { total, per_page, current_page, last_page, from, to }
     * @param {function} onPage  — callback(pageNumber) when user clicks a page
     */
    renderPagination(selector, meta, onPage) {
        const $c = $(selector);
        $c.empty();
        if (!meta) return;

        const page = meta.current_page || meta.page || 1;
        const totalPages = meta.last_page || meta.total_pages || 1;
        const total = meta.total || 0;

        if (totalPages <= 1) return;

        $c.append(`<span class="al-pg-info">Pág. ${page} de ${totalPages} (${total})</span>`);
        $c.append(`<button class="al-pg-btn" ${page <= 1 ? 'disabled' : ''} data-page="${page - 1}">‹ Ant</button>`);

        const pages = this._paginationRange(page, totalPages);
        let last = 0;
        pages.forEach(i => {
            if (i - last > 1) $c.append('<span class="al-pg-dots">…</span>');
            $c.append(`<button class="al-pg-btn${i === page ? ' active' : ''}" data-page="${i}">${i}</button>`);
            last = i;
        });

        $c.append(`<button class="al-pg-btn" ${page >= totalPages ? 'disabled' : ''} data-page="${page + 1}">Sig ›</button>`);

        $c.off('click', '.al-pg-btn').on('click', '.al-pg-btn', function() {
            const p = parseInt($(this).data('page'));
            if (p >= 1 && p <= totalPages && p !== page && onPage) {
                onPage(p);
            }
        });
    },

    _paginationRange(current, total) {
        if (total <= 9) return Array.from({ length: total }, (_, i) => i + 1);
        const pages = new Set([1, 2, total - 1, total]);
        for (let i = current - 2; i <= current + 2; i++) {
            if (i > 0 && i <= total) pages.add(i);
        }
        return [...pages].sort((a, b) => a - b);
    },

    // ─── FORMAT HELPERS ─────────────────────────────────────

    formatDate(dateStr) {
        if (!dateStr) return '—';
        // Handle both "YYYY-MM-DD" and "YYYY-MM-DD HH:MM:SS" formats
        const clean = String(dateStr).trim().split(' ')[0]; // take date part only
        const d = new Date(clean + 'T00:00:00');
        if (isNaN(d.getTime())) return '—';
        return d.toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' });
    },

    formatDateTime(dateStr) {
        if (!dateStr) return '—';
        // Normalize "YYYY-MM-DD HH:MM:SS" → ISO format
        const d = new Date(String(dateStr).trim().replace(' ', 'T'));
        if (isNaN(d.getTime())) return '—';
        return d.toLocaleDateString('es-MX', {
            day: '2-digit', month: 'short', year: 'numeric',
            hour: '2-digit', minute: '2-digit',
        });
    },

    formatMoney(amount) {
        return new Intl.NumberFormat('es-MX', {
            style: 'currency', currency: 'MXN'
        }).format(amount || 0);
    },

    formatWeight(kg) {
        if (!kg) return '—';
        return `${parseFloat(kg).toLocaleString('es-MX', { maximumFractionDigits: 2 })} kg`;
    },

    timeAgo(dateStr) {
        if (!dateStr) return '—';
        const date = new Date(String(dateStr).trim().replace(' ', 'T'));
        if (isNaN(date.getTime())) return '—';
        const now = new Date();
        const seconds = Math.floor((now - date) / 1000);

        if (seconds < 60) return 'Hace un momento';
        if (seconds < 3600) return `Hace ${Math.floor(seconds / 60)} min`;
        if (seconds < 86400) return `Hace ${Math.floor(seconds / 3600)}h`;
        if (seconds < 604800) return `Hace ${Math.floor(seconds / 86400)}d`;
        return this.formatDate(dateStr);
    },
};

// ─── TOAST NOTIFICATIONS ────────────────────────────────────

const Toast = {
    container: null,

    init() {
        if (!this.container) {
            this.container = $('<div class="toast-container">').appendTo('body');
        }
    },

    show(message, type = 'info', duration = 4000) {
        this.init();

        const icon = { success: '✓', warning: '⚠', danger: '✕', info: 'ℹ' }[type] || 'ℹ';
        const $toast = $(`
            <div class="toast toast-${type}">
                <span class="toast-icon">${icon}</span>
                <span class="toast-message">${message}</span>
                <button class="toast-close" onclick="$(this).parent().remove()">×</button>
            </div>
        `);

        this.container.append($toast);

        if (duration > 0) {
            setTimeout(() => {
                $toast.css({ opacity: 0, transform: 'translateX(100px)' });
                setTimeout(() => $toast.remove(), 300);
            }, duration);
        }
    },

    success(msg) { this.show(msg, 'success'); },
    warning(msg) { this.show(msg, 'warning'); },
    danger(msg)  { this.show(msg, 'danger');  },
    error(msg)   { this.show(msg, 'danger');  },
    info(msg)    { this.show(msg, 'info');    },
};

// ─── MODAL HELPER ───────────────────────────────────────────

const Modal = {
    /**
     * Create and show a dynamic modal (generated from JS)
     */
    show(options) {
        const { title, body, footer, size = 'md', onClose } = options;

        const sizeClass = size === 'lg' ? 'max-width: 800px' : size === 'sm' ? 'max-width: 400px' : '';

        const $overlay = $(`
            <div class="modal-overlay active">
                <div class="modal" style="${sizeClass}">
                    <div class="modal-header">
                        <h3 class="modal-title">${title}</h3>
                        <button class="btn-icon btn-outline modal-close">×</button>
                    </div>
                    <div class="modal-body">${body}</div>
                    ${footer ? `<div class="modal-footer">${footer}</div>` : ''}
                </div>
            </div>
        `);

        $('body').append($overlay);

        $overlay.find('.modal-close').on('click', () => {
            this.close($overlay);
            if (onClose) onClose();
        });

        $overlay.on('click', function(e) {
            if ($(e.target).hasClass('modal-overlay')) {
                Modal.close($overlay);
                if (onClose) onClose();
            }
        });

        return $overlay;
    },

    /**
     * Open an inline HTML modal by its DOM id
     */
    open(id) {
        const $el = $('#' + id);
        if (!$el.length) return;

        // Support both patterns:
        // Pattern A: <div class="modal" id="X"> (outer is .modal)
        // Pattern B: <div class="modal-overlay" id="X"> (outer is .modal-overlay)
        $el.addClass('active');

        // Prevent body scroll while modal is open
        $('body').css('overflow', 'hidden');
    },

    /**
     * Close a modal — handles jQuery objects, string IDs, and the fallback
     */
    close(ref) {
        if (!ref) {
            // Close all dynamic overlays
            $('.modal-overlay.active').removeClass('active');
            $('.modal-overlay').not('[id]').remove();
            $('body').css('overflow', '');
            return;
        }

        // If ref is a string (ID), hide the inline modal
        if (typeof ref === 'string') {
            const $el = $('#' + ref);
            $el.removeClass('active');
            $('body').css('overflow', '');
            return;
        }

        // If ref is a jQuery object (dynamic modal), remove it
        if (ref instanceof $ || ref.jquery) {
            ref.remove();
            $('body').css('overflow', '');
            return;
        }
    },

    confirm(title, message) {
        return new Promise((resolve) => {
            const $modal = this.show({
                title,
                body: `<p>${message}</p>`,
                footer: `
                    <button class="btn btn-outline modal-cancel">Cancelar</button>
                    <button class="btn btn-primary modal-confirm">Confirmar</button>
                `,
            });

            $modal.find('.modal-cancel').on('click', () => {
                this.close($modal);
                resolve(false);
            });

            $modal.find('.modal-confirm').on('click', () => {
                this.close($modal);
                resolve(true);
            });
        });
    },
};
