/**
 * Main JavaScript — Informatika UNTAN 2026
 * OOP modules:
 *   DirectusClient      — API base + URL builders + fetch
 *   FileDownloader      — blob-based force-download with fallback
 *   DocumentFormatter   — pure formatting / mapping / escaping utilities
 *   DocumentCardView    — renders a single card and binds its actions
 *   DocumentRepository  — controller for the repository section
 *   CounterAnimation    — scroll-triggered number animation
 */

/* ===========================================
   DirectusClient
   =========================================== */
class DirectusClient {
    static API_BASE = 'https://api.ifuntanhub.dev';

    static itemsUrl(collection) {
        return `${this.API_BASE}/items/${collection}?limit=-1`;
    }

    static assetUrl(uuid) {
        return `${this.API_BASE}/assets/${uuid}`;
    }

    static viewerUrl(item) {
        const type = DocumentFormatter.fileTypeFromName(item.nama_berkas);
        const params = new URLSearchParams({
            id: item.file,
            name: item.nama_berkas,
            type,
        });
        return `viewer.html?${params.toString()}`;
    }

    static async fetchPublished(collection) {
        const response = await fetch(this.itemsUrl(collection));
        if (!response.ok) throw new Error(`Gagal memuat koleksi ${collection}.`);
        const body = await response.json();
        const items = body.data || [];
        return items.filter(item => item.status === 'published');
    }
}

/* ===========================================
   FileDownloader
   =========================================== */
class FileDownloader {
    static async download(url, filename) {
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error('Gagal mengunduh berkas.');
            const blob = await response.blob();
            const blobUrl = URL.createObjectURL(blob);
            this._click(blobUrl, filename);
            URL.revokeObjectURL(blobUrl);
        } catch (err) {
            console.warn('Unduh via Blob gagal, beralih ke parameter Directus:', err);
            const fallbackUrl = url.includes('?') ? `${url}&download` : `${url}?download`;
            this._click(fallbackUrl, filename);
        }
    }

    static _click(href, filename) {
        const a = document.createElement('a');
        a.style.display = 'none';
        a.href = href;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }
}

/* ===========================================
   DocumentFormatter
   =========================================== */
class DocumentFormatter {
    static MONTHS_ID = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];

    static formatDate(iso) {
        if (!iso) return '';
        const date = new Date(iso);
        if (Number.isNaN(date.getTime())) return '';
        const day = String(date.getDate()).padStart(2, '0');
        const month = this.MONTHS_ID[date.getMonth()];
        return `${day} ${month} ${date.getFullYear()}`;
    }

    /**
     * Map a document's parsed tags to one of the fixed UI categories.
     * Uses the tags array for precise matching instead of raw string search.
     */
    static CATEGORY_RULES = [
        { key: 'skripsi',  match: ['skripsi', 'tugas akhir', 'ta'] },
        { key: 'akademik', match: ['akademik', 'kalender', 'kaldik'] },
        { key: 'praktik',  match: ['praktik', 'praktek', 'kerja praktik', 'kp'] },
    ];

    static categoryFromTags(tags) {
        if (!tags || tags.length === 0) return 'umum';
        const lower = tags.map(t => t.toLowerCase());
        for (const rule of this.CATEGORY_RULES) {
            if (rule.match.some(m => lower.includes(m))) return rule.key;
        }
        return 'umum';
    }

    static fileTypeFromName(name) {
        const lower = (name || '').toLowerCase();
        if (lower.endsWith('.docx') || lower.endsWith('.doc')) return 'docx';
        return 'pdf';
    }

    static downloadFilename(name, type) {
        const ext = `.${type}`;
        return name.toLowerCase().endsWith(ext) ? name : `${name}${ext}`;
    }

    // Escape API-supplied strings before splicing into innerHTML templates.
    static escapeHtml(str) {
        if (str == null) return '';
        return String(str)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
    }

    // Escape values destined for HTML attribute slots (href, etc.).
    static escapeAttr(str) {
        return this.escapeHtml(str);
    }

    /**
     * Parse the `tag` field from the API.
     * Handles two formats:
     *   1. Comma-separated string:  "akademik,panduan,2024"
     *   2. JSON array string:       '["panduan","proposal"]'
     * Returns an array of trimmed, non-empty tag strings.
     */
    static parseTags(raw) {
        if (!raw) return [];
        const str = String(raw).trim();
        if (!str) return [];

        // Attempt JSON parse first (handles '["a","b"]' format).
        if (str.startsWith('[')) {
            try {
                const arr = JSON.parse(str);
                if (Array.isArray(arr)) {
                    return arr.map(t => String(t).trim()).filter(Boolean);
                }
            } catch { /* fall through to comma-split */ }
        }

        // Comma-separated fallback.
        return str.split(',').map(t => t.trim()).filter(Boolean);
    }
}

/* ===========================================
   DocumentCardView
   =========================================== */
class DocumentCardView {
    constructor(item) {
        this.item = item;
        this.title = item.nama_berkas;
        this.tags = DocumentFormatter.parseTags(item.tag);
        this.category = DocumentFormatter.categoryFromTags(this.tags);
        this.fileType = DocumentFormatter.fileTypeFromName(this.title);
        this.assetUrl = DirectusClient.assetUrl(item.file);
        this.viewerUrl = DirectusClient.viewerUrl(item);
        this.downloadName = DocumentFormatter.downloadFilename(this.title, this.fileType);
        this.dateIso = item.tanggal_upload || '';
        this.dateKey = this.dateIso ? this.dateIso.substring(0, 10) : '';
    }

    render() {
        const card = document.createElement('article');
        card.className = 'document-card';
        card.dataset.date = this.dateKey;

        const typeUpper = this.fileType.toUpperCase();
        const typeClass = this.fileType === 'pdf' ? 'doc-type-pdf' : 'doc-type-docx';
        const iconClass = this.fileType === 'pdf' ? 'fas fa-file-pdf' : 'fas fa-file-word';
        const formattedDate = DocumentFormatter.formatDate(this.dateIso);

        const safeTitle = DocumentFormatter.escapeHtml(this.title);
        const safeViewerUrl = DocumentFormatter.escapeAttr(this.viewerUrl);
        const safeAssetUrl = DocumentFormatter.escapeAttr(this.assetUrl);

        // Build tag badges HTML.
        const tagBadgesHtml = this.tags.length
            ? `<div class="doc-tags"><i class="fas fa-tags doc-tags-icon"></i>${this.tags.map(t => `<span class="doc-tag">${DocumentFormatter.escapeHtml(t)}</span>`).join('')}</div>`
            : '';

        card.innerHTML = `
            <div class="doc-type-icon ${typeClass}"><i class="${iconClass}"></i></div>
            <h3 class="doc-title">${safeTitle}</h3>
            <div class="doc-meta">
                <span class="doc-badge">${typeUpper}</span>
                <span class="doc-badge date"><i class="far fa-calendar-alt"></i> ${formattedDate}</span>
            </div>
            ${tagBadgesHtml}
            <div class="doc-actions">
                <a href="${safeViewerUrl}" class="btn-action-primary" target="_blank" rel="noopener"><span data-tp-i18n="card_open">Buka Berkas</span> <i class="fas fa-external-link-alt"></i></a>
                <a href="${safeAssetUrl}" class="btn-action-outline"><span data-tp-i18n="card_download">Unduh Berkas</span> <i class="fas fa-download"></i></a>
            </div>
        `;

        card.querySelector('.btn-action-outline').addEventListener('click', (e) => {
            e.preventDefault();
            FileDownloader.download(this.assetUrl, this.downloadName);
        });

        // Prefetch the asset when the user hovers over the card so the
        // browser cache already has the file by the time the viewer loads.
        let prefetched = false;
        card.addEventListener('mouseenter', () => {
            if (prefetched) return;
            prefetched = true;
            const link = document.createElement('link');
            link.rel = 'prefetch';
            link.as = 'fetch';
            link.crossOrigin = 'anonymous';
            link.href = this.assetUrl;
            document.head.appendChild(link);
        }, { once: true });

        return card;
    }
}

/* ===========================================
   CustomDropdown
   =========================================== */
class CustomDropdown {
    constructor({ container, options, defaultValue, onChange, prefixIcon }) {
        this.container = container;
        this.options = options; // Array of { value, text, i18nKey }
        this.onChange = onChange;
        this.currentValue = defaultValue || options[0]?.value;
        this.prefixIcon = prefixIcon; // optional class name like 'fas fa-sort'
        this.isOpen = false;

        this.elements = {};
        this._build();
        this._bindEvents();
    }

    _build() {
        this.container.innerHTML = '';
        this.container.classList.add('custom-dropdown');

        // Trigger Button
        const trigger = document.createElement('button');
        trigger.className = 'custom-dropdown-trigger';
        trigger.setAttribute('aria-haspopup', 'listbox');
        trigger.setAttribute('aria-expanded', 'false');
        trigger.type = 'button';

        const triggerLeft = document.createElement('div');
        triggerLeft.className = 'trigger-left-content';
        triggerLeft.style.display = 'flex';
        triggerLeft.style.alignItems = 'center';
        triggerLeft.style.gap = '10px';

        if (this.prefixIcon) {
            const icon = document.createElement('i');
            icon.className = this.prefixIcon;
            triggerLeft.appendChild(icon);
        }

        const labelSpan = document.createElement('span');
        labelSpan.className = 'trigger-label';
        
        // Find default text
        const defaultOpt = this.options.find(opt => opt.value === this.currentValue) || this.options[0];
        labelSpan.textContent = defaultOpt ? defaultOpt.text : '';
        if (defaultOpt?.i18nKey) {
            labelSpan.setAttribute('data-tp-i18n', defaultOpt.i18nKey);
        }

        triggerLeft.appendChild(labelSpan);
        trigger.appendChild(triggerLeft);

        const arrowIcon = document.createElement('i');
        arrowIcon.className = 'fas fa-chevron-down trigger-arrow';

        trigger.appendChild(arrowIcon);
        this.container.appendChild(trigger);

        // Menu wrapper
        const menu = document.createElement('div');
        menu.className = 'custom-dropdown-menu';
        menu.setAttribute('role', 'listbox');

        // Populate options
        this.options.forEach(opt => {
            const item = document.createElement('div');
            item.className = 'custom-dropdown-item' + (opt.value === this.currentValue ? ' active' : '');
            item.setAttribute('role', 'option');
            item.setAttribute('data-value', opt.value);
            
            const itemText = document.createElement('span');
            itemText.textContent = opt.text;
            if (opt.i18nKey) {
                itemText.setAttribute('data-tp-i18n', opt.i18nKey);
            }
            
            item.appendChild(itemText);
            menu.appendChild(item);
        });

        this.container.appendChild(menu);

        this.elements = { trigger, labelSpan, menu };
    }

    _bindEvents() {
        // Toggle dropdown on trigger click
        this.elements.trigger.addEventListener('click', (e) => {
            e.stopPropagation();
            this.toggle();
        });

        // Handle item selection
        this.elements.menu.addEventListener('click', (e) => {
            const item = e.target.closest('.custom-dropdown-item');
            if (!item) return;

            const value = item.getAttribute('data-value');
            this.select(value);
            this.close();
        });

        // Close when clicking outside
        document.addEventListener('click', () => {
            this.close();
        });

        // Close on escape key
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') this.close();
        });
    }

    toggle() {
        if (this.isOpen) {
            this.close();
        } else {
            this.open();
        }
    }

    open() {
        // Close other custom dropdowns
        document.querySelectorAll('.custom-dropdown.active').forEach(dropdown => {
            if (dropdown !== this.container) {
                dropdown.classList.remove('active');
                dropdown.querySelector('.custom-dropdown-trigger')?.setAttribute('aria-expanded', 'false');
            }
        });

        this.isOpen = true;
        this.container.classList.add('active');
        this.elements.trigger.setAttribute('aria-expanded', 'true');
    }

    close() {
        this.isOpen = false;
        this.container.classList.remove('active');
        this.elements.trigger.setAttribute('aria-expanded', 'false');
    }

    select(value) {
        if (value === this.currentValue) return;

        this.currentValue = value;
        
        // Update active class in menu items
        this.elements.menu.querySelectorAll('.custom-dropdown-item').forEach(item => {
            const isCurrent = item.getAttribute('data-value') === value;
            item.classList.toggle('active', isCurrent);
        });

        // Update trigger label
        const selectedOpt = this.options.find(opt => opt.value === value);
        if (selectedOpt) {
            this.elements.labelSpan.textContent = selectedOpt.text;
            if (selectedOpt.i18nKey) {
                this.elements.labelSpan.setAttribute('data-tp-i18n', selectedOpt.i18nKey);
            } else {
                this.elements.labelSpan.removeAttribute('data-tp-i18n');
            }
        }

        // Trigger callback
        if (typeof this.onChange === 'function') {
            this.onChange(value);
        }
    }

    setValue(value) {
        this.select(value);
    }

    getValue() {
        return this.currentValue;
    }
}

/* ===========================================
   DateRangePicker (Custom Calendar/Preset Popover)
   =========================================== */
class DateRangePicker {
    constructor({ containerId, onChange, initialStartDate = '', initialEndDate = '' }) {
        this.container = document.getElementById(containerId);
        if (!this.container) return;

        this.trigger = this.container.querySelector('#date-picker-trigger');
        this.popover = this.container.querySelector('#date-picker-popover');
        this.backdrop = this.container.querySelector('#date-picker-backdrop');
        this.label = this.container.querySelector('#date-picker-label');
        
        this.startDisplay = this.container.querySelector('#picker-start-display');
        this.endDisplay = this.container.querySelector('#picker-end-display');
        
        this.prevBtn = this.container.querySelector('#prev-month-btn');
        this.nextBtn = this.container.querySelector('#next-month-btn');
        this.leftMonthLabel = this.container.querySelector('#left-month-label');
        this.rightMonthLabel = this.container.querySelector('#right-month-label');
        
        this.leftMonthName = this.container.querySelector('#left-month-name');
        this.rightMonthName = this.container.querySelector('#right-month-name');
        this.leftYearSelect = this.container.querySelector('#left-year-select');
        this.rightYearSelect = this.container.querySelector('#right-year-select');
        
        this.leftPane = this.container.querySelector('#left-calendar-pane');
        this.rightPane = this.container.querySelector('#right-calendar-pane');
        
        this.leftDaysContainer = this.container.querySelector('#left-days-container');
        this.rightDaysContainer = this.container.querySelector('#right-days-container');
        
        this.cancelBtn = this.container.querySelector('#picker-cancel-btn');
        this.applyBtn = this.container.querySelector('#picker-apply-btn');
        
        this.onChange = onChange;
        
        // Active dates (saved)
        this.startDate = initialStartDate ? new Date(initialStartDate) : null;
        this.endDate = initialEndDate ? new Date(initialEndDate) : null;
        
        // Temporary dates (being edited)
        this.tempStartDate = this.startDate ? new Date(this.startDate) : null;
        this.tempEndDate = this.endDate ? new Date(this.endDate) : null;
        
        // Calendar view month/year
        const today = new Date();
        if (this.endDate) {
            this.currentLeftMonth = this.endDate.getMonth();
            this.currentLeftYear = this.endDate.getFullYear();
        } else {
            this.currentLeftMonth = today.getMonth();
            this.currentLeftYear = today.getFullYear();
        }
        
        this.isOpen = false;
        this._init();
    }

    _init() {
        this._populateYearSelects();
        this._bindEvents();
        this._bindDisplayInputs();
        this._updateTriggerLabel();
        
        // Listen to language changes
        document.addEventListener('languageChanged', () => {
            if (this.isOpen) {
                this._renderCalendars();
            }
            this._updateTriggerLabel();
        });
    }

    _isEnglish() {
        if (typeof window.getLang === 'function') {
            const l = window.getLang();
            if (l === 'id' || l === 'en') return l === 'en';
        }
        try {
            const saved = localStorage.getItem('if_untan_lang');
            if (saved === 'id' || saved === 'en') return saved === 'en';
        } catch (e) {}
        const nav = (navigator.language || 'id').toLowerCase();
        return nav.startsWith('en');
    }

    _clearTime(date) {
        if (!date) return null;
        const d = new Date(date);
        d.setHours(0, 0, 0, 0);
        return d;
    }

    _datesEqual(d1, d2) {
        if (!d1 || !d2) return false;
        return d1.getFullYear() === d2.getFullYear() &&
               d1.getMonth() === d2.getMonth() &&
               d1.getDate() === d2.getDate();
    }

    _getRightMonthYear() {
        let m = this.currentLeftMonth + 1;
        let y = this.currentLeftYear;
        if (m > 11) {
            m = 0;
            y += 1;
        }
        return { month: m, year: y };
    }

    _navigateMonths(direction) {
        this.currentLeftMonth += direction;
        if (this.currentLeftMonth > 11) {
            this.currentLeftMonth = 0;
            this.currentLeftYear += 1;
        } else if (this.currentLeftMonth < 0) {
            this.currentLeftMonth = 11;
            this.currentLeftYear -= 1;
        }
        this._renderCalendars();
    }

    _renderCalendars() {
        const isEn = this._isEnglish();
        const monthNames = isEn ? 
          ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] :
          ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];

        const dayHeaders = isEn ?
          ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] :
          ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'];

        if (this.leftMonthName) {
            this.leftMonthName.textContent = monthNames[this.currentLeftMonth];
        }
        if (this.leftYearSelect) {
            this.leftYearSelect.value = this.currentLeftYear;
        }
        
        const rightMY = this._getRightMonthYear();
        if (this.rightMonthName) {
            this.rightMonthName.textContent = monthNames[rightMY.month];
        }
        if (this.rightYearSelect) {
            this.rightYearSelect.value = rightMY.year;
        }
        
        const drawHeaders = (pane) => {
            if (!pane) return;
            const dayHeadersEl = pane.querySelector('.day-headers');
            if (!dayHeadersEl) return;
            dayHeadersEl.innerHTML = '';
            dayHeaders.forEach(day => {
                const span = document.createElement('span');
                span.textContent = day;
                dayHeadersEl.appendChild(span);
            });
        };
        
        drawHeaders(this.leftPane);
        drawHeaders(this.rightPane);
        
        if (this.leftDaysContainer) {
            this._populateDaysContainer(this.leftDaysContainer, this.currentLeftMonth, this.currentLeftYear);
        }
        if (this.rightDaysContainer) {
            this._populateDaysContainer(this.rightDaysContainer, rightMY.month, rightMY.year);
        }
        
        this._updateDisplayInputs();
        this._updatePresetSelectionVisuals();
    }

    _populateDaysContainer(container, month, year) {
        container.innerHTML = '';
        
        const firstDay = new Date(year, month, 1);
        const numDays = new Date(year, month + 1, 0).getDate();
        
        let offset = (firstDay.getDay() + 6) % 7;
        
        for (let i = 0; i < offset; i++) {
            const cell = document.createElement('div');
            cell.className = 'day-cell empty';
            container.appendChild(cell);
        }
        
        const today = new Date();
        today.setHours(23, 59, 59, 999);
        
        for (let day = 1; day <= numDays; day++) {
            const cell = document.createElement('div');
            cell.className = 'day-cell';
            cell.textContent = day;
            
            const cellDate = new Date(year, month, day);
            cellDate.setHours(0, 0, 0, 0);
            
            if (cellDate > today) {
                cell.classList.add('disabled');
            } else {
                if (this.tempStartDate && this.tempEndDate) {
                    const start = this._clearTime(this.tempStartDate);
                    const end = this._clearTime(this.tempEndDate);
                    
                    if (this._datesEqual(cellDate, start)) {
                        cell.classList.add('range-start');
                    }
                    if (this._datesEqual(cellDate, end)) {
                        cell.classList.add('range-end');
                    }
                    if (cellDate > start && cellDate < end) {
                        cell.classList.add('range-between');
                    }
                } else if (this.tempStartDate && this._datesEqual(cellDate, this._clearTime(this.tempStartDate))) {
                    cell.classList.add('range-start');
                    cell.classList.add('range-end');
                }
                
                cell.addEventListener('click', () => {
                    this._handleDayClick(cellDate);
                });
            }
            
            container.appendChild(cell);
        }
    }

    _handleDayClick(date) {
        if (!this.tempStartDate || (this.tempStartDate && this.tempEndDate)) {
            this.tempStartDate = new Date(date);
            this.tempEndDate = null;
        } else {
            if (date < this.tempStartDate) {
                this.tempStartDate = new Date(date);
                this.tempEndDate = null;
            } else {
                this.tempEndDate = new Date(date);
            }
        }
        this._renderCalendars();
    }

    _formatDateDisplay(date) {
        if (!date) return '';
        const d = new Date(date);
        const dd = String(d.getDate()).padStart(2, '0');
        const mm = String(d.getMonth() + 1).padStart(2, '0');
        const yyyy = d.getFullYear();
        return `${dd}/${mm}/${yyyy}`;
    }

    _updateDisplayInputs() {
        if (this.startDisplay) {
            this.startDisplay.value = this._formatDateDisplay(this.tempStartDate);
        }
        if (this.endDisplay) {
            this.endDisplay.value = this._formatDateDisplay(this.tempEndDate);
        }
    }

    _calculatePreset(presetName) {
        const today = new Date();
        today.setHours(0, 0, 0, 0);
        
        let start = null;
        let end = null;
        
        switch (presetName) {
            case 'today':
                start = new Date(today);
                end = new Date(today);
                break;
            case 'yesterday':
                start = new Date(today);
                start.setDate(today.getDate() - 1);
                end = new Date(start);
                break;
            case 'last7':
                end = new Date(today);
                start = new Date(today);
                start.setDate(today.getDate() - 6);
                break;
            case 'last14':
                end = new Date(today);
                start = new Date(today);
                start.setDate(today.getDate() - 13);
                break;
            case 'last30':
                end = new Date(today);
                start = new Date(today);
                start.setDate(today.getDate() - 29);
                break;
            case 'thisWeek':
                end = new Date(today);
                start = new Date(today);
                const day = today.getDay();
                const diff = today.getDate() - day + (day === 0 ? -6 : 1);
                start.setDate(diff);
                break;
            case 'lastWeek':
                const temp = new Date(today);
                const tempDay = temp.getDay();
                const tempDiff = temp.getDate() - tempDay + (tempDay === 0 ? -6 : 1);
                start = new Date(temp);
                start.setDate(tempDiff - 7);
                end = new Date(temp);
                end.setDate(tempDiff - 1);
                break;
            case 'thisMonth':
                start = new Date(today.getFullYear(), today.getMonth(), 1);
                end = new Date(today);
                break;
            case 'lastMonth':
                start = new Date(today.getFullYear(), today.getMonth() - 1, 1);
                end = new Date(today.getFullYear(), today.getMonth(), 0);
                break;
            case 'all':
            default:
                start = null;
                end = null;
                break;
        }
        return { start, end };
    }

    _handlePresetClick(presetName) {
        const { start, end } = this._calculatePreset(presetName);
        this.tempStartDate = start;
        this.tempEndDate = end;
        
        const today = new Date();
        if (this.tempEndDate) {
            this.currentLeftMonth = this.tempEndDate.getMonth();
            this.currentLeftYear = this.tempEndDate.getFullYear();
        } else {
            this.currentLeftMonth = today.getMonth();
            this.currentLeftYear = today.getFullYear();
        }
        this._renderCalendars();
    }

    _updatePresetSelectionVisuals() {
        this.container.querySelectorAll('.preset-btn').forEach(btn => {
            btn.classList.remove('active');
        });
        
        if (!this.tempStartDate && !this.tempEndDate) {
            this.container.querySelector('[data-preset="all"]')?.classList.add('active');
            return;
        }
        
        if (this.tempStartDate && this.tempEndDate) {
            const activePreset = ['today', 'yesterday', 'last7', 'last14', 'last30', 'thisWeek', 'lastWeek', 'thisMonth', 'lastMonth'].find(preset => {
                const { start, end } = this._calculatePreset(preset);
                return start && end &&
                       this._datesEqual(this.tempStartDate, start) &&
                       this._datesEqual(this.tempEndDate, end);
            });
            
            if (activePreset) {
                this.container.querySelector(`[data-preset="${activePreset}"]`)?.classList.add('active');
            }
        }
    }

    _updateTriggerLabel() {
        if (!this.startDate && !this.endDate) {
            if (this.label) {
                this.label.textContent = window.tpT?.('filter_all_time') || 'Semua Waktu';
                this.label.setAttribute('data-tp-i18n', 'filter_all_time');
            }
        } else {
            if (this.label) {
                this.label.removeAttribute('data-tp-i18n');
                const startFormatted = this._formatDateDisplay(this.startDate);
                const endFormatted = this._formatDateDisplay(this.endDate);
                if (this._datesEqual(this.startDate, this.endDate)) {
                    this.label.textContent = startFormatted;
                } else {
                    this.label.textContent = `${startFormatted} - ${endFormatted}`;
                }
            }
        }
    }

    _bindEvents() {
        this.trigger.addEventListener('click', (e) => {
            e.stopPropagation();
            this.toggle();
        });
        
        this.popover.addEventListener('click', (e) => {
            e.stopPropagation();
        });
        
        if (this.prevBtn) {
            this.prevBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                this._navigateMonths(-1);
            });
        }
        if (this.nextBtn) {
            this.nextBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                this._navigateMonths(1);
            });
        }
        
        const presetsList = this.container.querySelector('.presets-list');
        if (presetsList) {
            presetsList.addEventListener('click', (e) => {
                const btn = e.target.closest('.preset-btn');
                if (!btn) return;
                e.stopPropagation();
                const preset = btn.getAttribute('data-preset');
                this._handlePresetClick(preset);
            });
        }
        
        if (this.cancelBtn) {
            this.cancelBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                this._handleCancel();
            });
        }
        
        if (this.applyBtn) {
            this.applyBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                this._handleApply();
            });
        }
        
        if (this.backdrop) {
            this.backdrop.addEventListener('click', (e) => {
                e.stopPropagation();
                this._handleCancel();
            });
        }
        
        document.addEventListener('click', () => {
            if (this.isOpen) {
                this._handleCancel();
            }
        });
        
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && this.isOpen) {
                this._handleCancel();
            }
        });
    }

    _bindDisplayInputs() {
        const parseDate = (val) => {
            const m = val.trim().match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{4})$/);
            if (!m) return null;
            const d = parseInt(m[1], 10);
            const mon = parseInt(m[2], 10) - 1;
            const y = parseInt(m[3], 10);
            
            if (mon < 0 || mon > 11 || d < 1 || d > 31 || y < 1000 || y > 9999) return null;
            
            const testDate = new Date(y, mon, d);
            if (testDate.getFullYear() !== y || testDate.getMonth() !== mon || testDate.getDate() !== d) {
                return null;
            }
            return testDate;
        };

        const setupInput = (input, isStart) => {
            if (!input) return;
            
            input.addEventListener('keydown', (e) => {
                const CTRL_KEYS = new Set(['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter']);
                if (!/^\d$/.test(e.key) && e.key !== '/' && e.key !== '-' && e.key !== '.' && !CTRL_KEYS.has(e.key)) {
                    e.preventDefault();
                }
            });

            input.addEventListener('input', (e) => {
                if (e.inputType && e.inputType.startsWith('delete')) {
                    return;
                }
                
                let val = input.value.replace(/[^\d]/g, '');
                let formatted = val;
                if (val.length > 2) {
                    formatted = val.substring(0, 2) + '/' + val.substring(2);
                }
                if (val.length > 4) {
                    formatted = val.substring(0, 2) + '/' + val.substring(2, 4) + '/' + val.substring(4, 8);
                }
                input.value = formatted;

                const parsed = parseDate(formatted);
                if (parsed) {
                    const today = new Date();
                    today.setHours(23, 59, 59, 999);
                    if (parsed <= today) {
                        if (isStart) {
                            this.tempStartDate = parsed;
                            if (this.tempEndDate && this.tempEndDate < this.tempStartDate) {
                                this.tempEndDate = new Date(this.tempStartDate);
                            }
                        } else {
                            this.tempEndDate = parsed;
                            if (this.tempStartDate && this.tempStartDate > this.tempEndDate) {
                                this.tempStartDate = new Date(this.tempEndDate);
                            }
                        }
                        
                        this.currentLeftMonth = parsed.getMonth();
                        this.currentLeftYear = parsed.getFullYear();
                        
                        const rightMY = this._getRightMonthYear();
                        const isEn = this._isEnglish();
                        const monthNames = isEn ? 
                          ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] :
                          ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
                        if (this.leftMonthName) this.leftMonthName.textContent = monthNames[this.currentLeftMonth];
                        if (this.leftYearSelect) this.leftYearSelect.value = this.currentLeftYear;
                        if (this.rightMonthName) this.rightMonthName.textContent = monthNames[rightMY.month];
                        if (this.rightYearSelect) this.rightYearSelect.value = rightMY.year;
                        if (this.leftDaysContainer) this._populateDaysContainer(this.leftDaysContainer, this.currentLeftMonth, this.currentLeftYear);
                        if (this.rightDaysContainer) this._populateDaysContainer(this.rightDaysContainer, rightMY.month, rightMY.year);
                        this._updatePresetSelectionVisuals();
                    }
                }
            });
            
            input.addEventListener('blur', () => {
                const parsed = parseDate(input.value);
                if (!parsed) {
                    if (isStart) {
                        input.value = this._formatDateDisplay(this.tempStartDate);
                    } else {
                        input.value = this._formatDateDisplay(this.tempEndDate);
                    }
                }
            });
        };

        setupInput(this.startDisplay, true);
        setupInput(this.endDisplay, false);
    }

    _populateYearSelects() {
        if (!this.leftYearSelect || !this.rightYearSelect) return;
        
        this.leftYearSelect.innerHTML = '';
        this.rightYearSelect.innerHTML = '';
        
        const startYear = 2020;
        const endYear = new Date().getFullYear() + 4;
        
        for (let y = startYear; y <= endYear; y++) {
            const optLeft = document.createElement('option');
            optLeft.value = y;
            optLeft.textContent = y;
            this.leftYearSelect.appendChild(optLeft);
            
            const optRight = document.createElement('option');
            optRight.value = y;
            optRight.textContent = y;
            this.rightYearSelect.appendChild(optRight);
        }
        
        this.leftYearSelect.addEventListener('change', () => {
            this._handleYearChange(parseInt(this.leftYearSelect.value, 10), true);
        });
        
        this.rightYearSelect.addEventListener('change', () => {
            this._handleYearChange(parseInt(this.rightYearSelect.value, 10), false);
        });
    }

    _handleYearChange(year, isLeft) {
        if (isLeft) {
            this.currentLeftYear = year;
        } else {
            const rightMY = this._getRightMonthYear();
            const rightMonth = rightMY.month;
            if (rightMonth === 0) {
                this.currentLeftYear = year - 1;
            } else {
                this.currentLeftYear = year;
            }
        }
        this._renderCalendars();
    }

    toggle() {
        if (this.isOpen) {
            this.close();
        } else {
            this.open();
        }
    }

    open() {
        document.querySelectorAll('.custom-dropdown.active').forEach(dd => {
            dd.classList.remove('active');
            dd.querySelector('.custom-dropdown-trigger')?.setAttribute('aria-expanded', 'false');
        });
        
        this.isOpen = true;
        this.container.classList.add('active');
        this.trigger.setAttribute('aria-expanded', 'true');
        
        this.tempStartDate = this.startDate ? new Date(this.startDate) : null;
        this.tempEndDate = this.endDate ? new Date(this.endDate) : null;
        
        const today = new Date();
        if (this.endDate) {
            this.currentLeftMonth = this.endDate.getMonth();
            this.currentLeftYear = this.endDate.getFullYear();
        } else {
            this.currentLeftMonth = today.getMonth();
            this.currentLeftYear = today.getFullYear();
        }
        
        this._renderCalendars();
        
        if (window.innerWidth <= 768) {
            document.body.classList.add('date-picker-open');
        }
    }

    close() {
        this.isOpen = false;
        this.container.classList.remove('active');
        this.trigger.setAttribute('aria-expanded', 'false');
        document.body.classList.remove('date-picker-open');
    }

    _handleCancel() {
        this.tempStartDate = this.startDate ? new Date(this.startDate) : null;
        this.tempEndDate = this.endDate ? new Date(this.endDate) : null;
        this.close();
    }

    _handleApply() {
        this.startDate = this.tempStartDate;
        this.endDate = this.tempEndDate;
        
        this._updateTriggerLabel();
        this.close();
        
        const startStr = this._getDateString(this.startDate);
        const endStr = this._getDateString(this.endDate);
        if (this.onChange) {
            this.onChange(startStr, endStr);
        }
    }
    
    _getDateString(date) {
        if (!date) return '';
        const y = date.getFullYear();
        const m = String(date.getMonth() + 1).padStart(2, '0');
        const d = String(date.getDate()).padStart(2, '0');
        return `${y}-${m}-${d}`;
    }
}

/* ===========================================
   DocumentRepository
   =========================================== */
class DocumentRepository {
    static PAGE_SIZE = 6;

    constructor() {
        this.gridContainer = document.getElementById('document-grid');
        this.filterWrapper = document.getElementById('tag-filter-wrapper');
        this.mobileFilterWrapper = document.getElementById('mobile-tag-filter-wrapper');
        this.searchInput = document.getElementById('doc-search');
        this.sortFilterWrapper = document.getElementById('sort-filter-wrapper');

        // Hero stat values (filled from real API data after fetch).
        this.statActiveEl = document.getElementById('stat-active-value');
        this.statUpdatesEl = document.getElementById('stat-updates-value');

        this.cards = [];
        this.paginationContainer = null;
        this.state = {
            filter: 'all',   // 'all' or a specific tag string
            search: '',
            startDate: '',
            endDate: '',
            sort: 'newest',
            page: 1,
        };
    }


    async init() {
        if (!this.gridContainer) return;
        this._bindControls();
        this._initPagination();
        try {
            const items = await DirectusClient.fetchPublished('berkas');
            this._renderCards(items);
            this._buildCategoryFilters(items);
            this._renderStats(items);
            this._applySort();
            this._applyFilters();
        } catch (err) {
            console.error(err);
            this._renderError();
            this._renderStatsError();
        }
    }

    _initPagination() {
        const pag = document.createElement('div');
        pag.id = 'doc-pagination';
        pag.className = 'doc-pagination';
        this.gridContainer.insertAdjacentElement('afterend', pag);
        this.paginationContainer = pag;
    }

    // Days within which an upload counts as a "recent update".
    static RECENT_DAYS = 30;

    // Populate the hero stats from real data:
    //   Dokumen Aktif      → number of published documents
    //   Pembaruan Terbaru  → uploads within the last RECENT_DAYS days
    _renderStats(items) {
        const active = items.length;

        const windowMs = DocumentRepository.RECENT_DAYS * 24 * 60 * 60 * 1000;
        const now = Date.now();
        const recent = items.filter(item => {
            if (!item.tanggal_upload) return false;
            const t = new Date(item.tanggal_upload).getTime();
            return !Number.isNaN(t) && now - t <= windowMs;
        }).length;

        CounterAnimation.animateValue(this.statActiveEl, active);
        CounterAnimation.animateValue(this.statUpdatesEl, recent);
    }

    _renderStatsError() {
        if (this.statActiveEl) this.statActiveEl.textContent = '—';
        if (this.statUpdatesEl) this.statUpdatesEl.textContent = '—';
    }
    /**
     * Fixed category definitions for the sidebar filter.
     * Each has a key, an i18n key for bilingual labels, and a fallback label.
     */
    static CATEGORIES = [
        { key: 'all',      i18n: 'filter_all',      label: 'Semua Berkas' },
        { key: 'skripsi',  i18n: 'filter_skripsi',  label: 'Panduan Skripsi' },
        { key: 'akademik', i18n: 'filter_akademik', label: 'Kalender Akademik' },
        { key: 'praktik',  i18n: 'filter_praktik',  label: 'Kerja Praktik' },
        { key: 'umum',     i18n: 'filter_umum',     label: 'Formulir Umum' },
    ];

    /**
     * Build category filter controls:
     *  – Desktop: sidebar buttons (hidden on mobile via CSS)
     *  – Mobile:  a <select> dropdown (hidden on desktop via CSS)
     * Both stay in sync through a shared _setFilter() helper.
     */
    _buildCategoryFilters(items) {
        if (!this.filterWrapper) return;

        // Clear skeleton placeholder.
        this.filterWrapper.innerHTML = '';

        // ── Desktop buttons ──
        const btnGroup = document.createElement('div');
        btnGroup.className = 'filter-btn-group';

        DocumentRepository.CATEGORIES.forEach(({ key, i18n, label }) => {
            const isActive = key === 'all';
            const btn = document.createElement('button');
            btn.className = 'doc-filter-btn' + (isActive ? ' active' : '');
            btn.dataset.filter = key;
            btn.textContent = (window.tpT?.(i18n)) || label;
            btn.setAttribute('data-tp-i18n', i18n);

            btn.addEventListener('click', () => this._setFilter(key));
            btnGroup.appendChild(btn);
        });

        this.filterWrapper.appendChild(btnGroup);

        // ── Mobile Custom UI Dropdown ──
        if (this.mobileFilterWrapper) {
            const dropdownOptions = DocumentRepository.CATEGORIES.map(({ key, i18n, label }) => ({
                value: key,
                i18nKey: i18n,
                text: (window.tpT?.(i18n)) || label
            }));

            this._mobileDropdown = new CustomDropdown({
                container: this.mobileFilterWrapper,
                options: dropdownOptions,
                defaultValue: this.state.filter,
                onChange: (value) => this._setFilter(value)
            });
        }

        // Store refs for syncing.
        this._filterBtnGroup = btnGroup;

        // Re-translate if i18n is active.
        window.applyTp?.(this.filterWrapper);
    }

    /**
     * Shared filter setter — keeps buttons + dropdown in sync.
     */
    _setFilter(key) {
        this.state.filter = key;
        this.state.page = 1;

        // Sync buttons.
        if (this._filterBtnGroup) {
            this._filterBtnGroup.querySelectorAll('.doc-filter-btn')
                .forEach(b => b.classList.toggle('active', b.dataset.filter === key));
        }

        // Sync custom dropdown.
        if (this._mobileDropdown && this._mobileDropdown.getValue() !== key) {
            this._mobileDropdown.setValue(key);
        }

        this._applyFilters();
    }

    _bindControls() {
        this.searchInput?.addEventListener('input', (e) => {
            this.state.search = e.target.value.toLowerCase().trim();
            this.state.page = 1;
            this._applyFilters();
        });

        // Initialize Custom Date Range Picker
        const datePickerGroup = document.getElementById('date-range-control-group');
        if (datePickerGroup) {
            this.dateRangePicker = new DateRangePicker({
                containerId: 'date-range-control-group',
                onChange: (startDate, endDate) => {
                    this.state.startDate = startDate;
                    this.state.endDate = endDate;
                    this.state.page = 1;
                    this._applyFilters();
                }
            });
            window.applyTp?.(datePickerGroup);
        }

        if (this.sortFilterWrapper) {
            const sortOptions = [
                { value: 'newest', i18nKey: 'sort_newest', text: (window.tpT?.('sort_newest')) || 'Terbaru' },
                { value: 'oldest', i18nKey: 'sort_oldest', text: (window.tpT?.('sort_oldest')) || 'Terlama' },
                { value: 'az',     i18nKey: 'sort_az',     text: (window.tpT?.('sort_az'))     || 'Nama A-Z' }
            ];

            this._sortDropdown = new CustomDropdown({
                container: this.sortFilterWrapper,
                options: sortOptions,
                defaultValue: this.state.sort,
                prefixIcon: 'fas fa-sort-amount-down',
                onChange: (value) => {
                    this.state.sort = value;
                    this.state.page = 1;
                    this._applySort();
                    this._applyFilters();
                }
            });

            window.applyTp?.(this.sortFilterWrapper);
        }
    }

    _renderCards(items) {
        this.gridContainer.innerHTML = '';

        if (items.length === 0) {
            this._renderState('repo-empty', 'fas fa-folder-open', '#cbd5e1', '#64748b',
                'state_empty', 'Belum ada dokumen yang tersedia di repositori.');
            return;
        }

        this.cards = items.map(item => {
            const view = new DocumentCardView(item);
            const element = view.render();
            this.gridContainer.appendChild(element);
            return { view, element };
        });

        // Translate the freshly-rendered card labels (Buka/Unduh Berkas).
        window.applyTp?.(this.gridContainer);
    }

    _renderError() {
        this._renderState('repo-error', 'fas fa-exclamation-triangle', '#f87171', '#ef4444',
            'state_error', 'Gagal memuat berkas. Silakan periksa koneksi internet Anda atau coba lagi nanti.');
    }

    // Build the empty/error placeholder via safe DOM APIs (no innerHTML).
    // `i18nKey` lets the Tapops i18n layer re-translate the message on toggle.
    _renderState(className, iconClass, iconColor, textColor, i18nKey, message) {
        this.cards = [];
        this.gridContainer.innerHTML = '';

        const wrapper = document.createElement('div');
        wrapper.className = className;
        Object.assign(wrapper.style, {
            gridColumn: '1 / -1',
            textAlign: 'center',
            padding: '60px 20px',
            color: textColor,
            fontWeight: '700',
            fontSize: '1rem',
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            gap: '15px',
        });

        const icon = document.createElement('i');
        icon.className = iconClass;
        Object.assign(icon.style, { fontSize: '3rem', color: iconColor });

        const span = document.createElement('span');
        if (i18nKey) span.setAttribute('data-tp-i18n', i18nKey);
        span.textContent = (window.tpT?.(i18nKey)) || message;

        wrapper.appendChild(icon);
        wrapper.appendChild(span);
        this.gridContainer.appendChild(wrapper);
    }

    _applyFilters() {
        const matched = this.cards.filter(({ view }) => this._cardMatches(view));
        const total = matched.length;

        // Handle zero-results state when cards exist but none match the filter.
        this._toggleNoResultsState(total === 0 && this.cards.length > 0);

        if (total === 0) {
            this.cards.forEach(({ element }) => { element.style.display = 'none'; });
            this._renderPagination(0, 0);
            return;
        }

        const totalPages = Math.ceil(total / DocumentRepository.PAGE_SIZE);

        // Clamp page to valid range after filter changes.
        this.state.page = Math.min(Math.max(this.state.page, 1), totalPages);

        const start = (this.state.page - 1) * DocumentRepository.PAGE_SIZE;
        const pageSet = new Set(
            matched.slice(start, start + DocumentRepository.PAGE_SIZE).map(c => c.element)
        );

        this.cards.forEach(({ element }) => {
            const show = pageSet.has(element);
            element.style.display = show ? 'flex' : 'none';
            if (show) {
                setTimeout(() => {
                    element.style.opacity = '1';
                    element.style.transform = 'translateY(0)';
                }, 10);
            } else {
                element.style.opacity = '0';
                element.style.transform = 'translateY(10px)';
            }
        });

        this._renderPagination(totalPages, total);
    }

    _toggleNoResultsState(show) {
        const existing = this.gridContainer.querySelector('.repo-no-results');
        if (show && !existing) {
            const wrapper = document.createElement('div');
            wrapper.className = 'repo-no-results';
            Object.assign(wrapper.style, {
                gridColumn: '1 / -1',
                textAlign: 'center',
                padding: '60px 20px',
                color: '#64748b',
                fontWeight: '700',
                fontSize: '1rem',
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                gap: '15px',
            });
            const icon = document.createElement('i');
            icon.className = 'fas fa-search';
            Object.assign(icon.style, { fontSize: '3rem', color: '#cbd5e1' });
            const span = document.createElement('span');
            span.setAttribute('data-tp-i18n', 'state_no_results');
            span.textContent = (window.tpT?.('state_no_results')) || 'Tidak ada berkas yang cocok dengan filter saat ini.';
            wrapper.appendChild(icon);
            wrapper.appendChild(span);
            this.gridContainer.appendChild(wrapper);
        } else if (!show && existing) {
            existing.remove();
        }
    }

    _renderPagination(totalPages, total) {
        if (!this.paginationContainer) return;
        this.paginationContainer.innerHTML = '';
        if (total === 0 || totalPages <= 1) return;

        const current = this.state.page;
        const start = (current - 1) * DocumentRepository.PAGE_SIZE + 1;
        const end = Math.min(current * DocumentRepository.PAGE_SIZE, total);

        const info = document.createElement('span');
        info.className = 'pagination-info';
        info.textContent = `${start}–${end} dari ${total} berkas`;
        this.paginationContainer.appendChild(info);

        const nav = document.createElement('nav');
        nav.className = 'pagination-nav';
        nav.setAttribute('aria-label', 'Navigasi halaman dokumen');

        const prev = document.createElement('button');
        prev.className = 'pagination-btn' + (current <= 1 ? ' disabled' : '');
        prev.disabled = current <= 1;
        prev.setAttribute('aria-label', 'Halaman sebelumnya');
        const prevIcon = document.createElement('i');
        prevIcon.className = 'fas fa-chevron-left';
        prev.appendChild(prevIcon);
        prev.addEventListener('click', () => {
            if (this.state.page > 1) { this.state.page--; this._applyFilters(); this._scrollToGrid(); }
        });
        nav.appendChild(prev);

        this._paginationRange(current, totalPages).forEach(p => {
            if (p === '...') {
                const el = document.createElement('span');
                el.className = 'pagination-ellipsis';
                el.setAttribute('aria-hidden', 'true');
                el.textContent = '…';
                nav.appendChild(el);
            } else {
                const btn = document.createElement('button');
                btn.className = 'pagination-btn pagination-number' + (p === current ? ' active' : '');
                btn.textContent = p;
                btn.setAttribute('aria-label', `Halaman ${p}`);
                if (p === current) btn.setAttribute('aria-current', 'page');
                btn.addEventListener('click', () => {
                    this.state.page = p; this._applyFilters(); this._scrollToGrid();
                });
                nav.appendChild(btn);
            }
        });

        const next = document.createElement('button');
        next.className = 'pagination-btn' + (current >= totalPages ? ' disabled' : '');
        next.disabled = current >= totalPages;
        next.setAttribute('aria-label', 'Halaman berikutnya');
        const nextIcon = document.createElement('i');
        nextIcon.className = 'fas fa-chevron-right';
        next.appendChild(nextIcon);
        next.addEventListener('click', () => {
            if (this.state.page < totalPages) { this.state.page++; this._applyFilters(); this._scrollToGrid(); }
        });
        nav.appendChild(next);

        this.paginationContainer.appendChild(nav);
    }

    // Returns page numbers and '...' ellipsis placeholders for the given range.
    _paginationRange(current, total) {
        if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);

        const pages = [1];
        const lo = Math.max(2, current - 1);
        const hi = Math.min(total - 1, current + 1);

        if (lo > 2) pages.push('...');
        for (let i = lo; i <= hi; i++) pages.push(i);
        if (hi < total - 1) pages.push('...');
        pages.push(total);

        return pages;
    }

    _scrollToGrid() {
        document.getElementById('repository')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }

    _cardMatches(view) {
        const { filter, search } = this.state;
        const matchesCategory = filter === 'all' || view.category === filter;
        const matchesTitle = view.title.toLowerCase().includes(search);
        const matchesTags = search
            ? view.tags.some(t => t.toLowerCase().includes(search))
            : false;
        const matchesSearch = matchesTitle || matchesTags;
        return matchesCategory && matchesSearch && this._dateMatches(view.dateKey);
    }

    _dateMatches(dateKey) {
        const { startDate, endDate } = this.state;
        if (!dateKey) return !startDate && !endDate;
        const cardDate = new Date(dateKey);
        if (startDate && cardDate < new Date(startDate)) return false;
        if (endDate) {
            const end = new Date(endDate);
            end.setHours(23, 59, 59, 999);
            if (cardDate > end) return false;
        }
        return true;
    }

    _applySort() {
        const { sort } = this.state;
        this.cards.sort((a, b) => {
            if (sort === 'newest') return b.view.dateKey.localeCompare(a.view.dateKey);
            if (sort === 'oldest') return a.view.dateKey.localeCompare(b.view.dateKey);
            if (sort === 'az') return a.view.title.toLowerCase().localeCompare(b.view.title.toLowerCase());
            return 0;
        });
        this.cards.forEach(({ element }) => this.gridContainer.appendChild(element));
    }
}

/* ===========================================
   CounterAnimation
   =========================================== */
class CounterAnimation {
    static DURATION_MS = 2000;

    init() {
        const counters = document.querySelectorAll('.counter-value[data-target]');
        if (counters.length === 0) return;

        if ('IntersectionObserver' in window) {
            const observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        this._animate(entry.target);
                        observer.unobserve(entry.target);
                    }
                });
            }, { threshold: 0.2 });
            counters.forEach(c => observer.observe(c));
        } else {
            counters.forEach(c => this._animate(c));
        }
    }

    _animate(el) {
        const target = Number.parseInt((el.dataset.target || '0').replace(/[^0-9]/g, ''), 10);
        if (Number.isNaN(target)) return;
        CounterAnimation.animateValue(el, target, el.dataset.suffix || '');
    }

    // Reusable easing tween from 0 → target. Callable for stats whose
    // value is only known after an async fetch (no data-target needed).
    static animateValue(el, target, suffix = '') {
        if (!el || Number.isNaN(target)) return;
        const startTime = performance.now();

        const tick = (now) => {
            const progress = Math.min((now - startTime) / CounterAnimation.DURATION_MS, 1);
            const eased = 1 - Math.pow(1 - progress, 3);
            el.textContent = Math.floor(eased * target) + suffix;
            if (progress < 1) requestAnimationFrame(tick);
            else el.textContent = target + suffix;
        };
        requestAnimationFrame(tick);
    }
}

/* ===========================================
   App entry
   =========================================== */
document.addEventListener('DOMContentLoaded', () => {
    new DocumentRepository().init();
});
