assets_js_controllers_mobile-calendar-controller.js

import MobileControllerBase from "./mobile-controller-base.js";
import offlineQueueService from "../services/offline-queue-service.js";
import rsvpCacheService from "../services/rsvp-cache-service.js";

/**
 * Mobile Events Controller
 * 
 * Touch-optimized event list for viewing gatherings on mobile devices.
 * Extends MobileControllerBase for offline handling and retry logic.
 * 
 * Features:
 * - Weekly grouped event list
 * - Search and filtering (type, branch, RSVP status)
 * - Quick month navigation
 * - RSVP with offline queue support
 * - Pull-to-refresh support
 */
class MobileCalendarController extends MobileControllerBase {
    static targets = [
        "loading", "error", "errorMessage", 
        "eventList", "emptyState", "emptyMessage", "resultsCount",
        "searchInput", "filterPanel", "filterToggle",
        "typeFilter", "activityFilter", "branchFilter", "rsvpFilter",
        "monthSelect", "yearSelect",
        "rsvpSheet", "rsvpContent",
        "pendingBanner", "pendingCount", "syncBtn"
    ]
    
    static values = {
        year: Number,
        month: Number,
        dataUrl: String,
        rsvpUrl: String,
        unrsvpUrl: String,
        updateRsvpUrl: String
    }

    initialize() {
        super.initialize();
        this.calendarData = null;
        this.filteredEvents = [];
        this.searchDebounce = null;
        this.filters = {
            search: '',
            type: '',
            activity: '',
            branch: '',
            rsvpOnly: false
        };
    }

    /**
     * Called after base class connect
     */
    onConnect() {
        console.log("Mobile Events connected");
        
        // Initialize RSVP cache service
        rsvpCacheService.init().catch(err => {
            console.warn('[Calendar] Failed to init RSVP cache:', err);
        });
        
        // Check for month/year in URL params (for returning from public page)
        this.restoreFromUrlParams();
        
        // Set up swipe handlers for navigation
        this._handleTouchStart = this.bindHandler('touchStart', this.handleTouchStart);
        this._handleTouchEnd = this.bindHandler('touchEnd', this.handleTouchEnd);
        
        this.element.addEventListener('touchstart', this._handleTouchStart, { passive: true });
        this.element.addEventListener('touchend', this._handleTouchEnd, { passive: true });
        
        // Initialize year selector
        this.initYearSelector();
        
        // Set initial month/year in selectors
        this.updateNavigationSelectors();
        
        // Set up pull-to-refresh
        this.setupPullToRefresh();
        
        // Create bottom sheet
        this.createBottomSheet();
        
        // Load initial data
        this.loadCalendarData();
    }

    /**
     * Restore month/year from URL parameters
     */
    restoreFromUrlParams() {
        const urlParams = new URLSearchParams(window.location.search);
        const month = urlParams.get('month');
        const year = urlParams.get('year');
        
        if (month && year) {
            const monthNum = parseInt(month, 10);
            const yearNum = parseInt(year, 10);
            
            if (monthNum >= 1 && monthNum <= 12 && yearNum >= 2000 && yearNum <= 2100) {
                this.monthValue = monthNum;
                this.yearValue = yearNum;
                
                // Clean up URL without reloading
                const newUrl = window.location.pathname;
                window.history.replaceState({}, '', newUrl);
            }
        }
    }

    /**
     * Called after base class disconnect
     */
    onDisconnect() {
        this.element.removeEventListener('touchstart', this._handleTouchStart);
        this.element.removeEventListener('touchend', this._handleTouchEnd);
    }

    /**
     * Initialize year selector with range
     */
    initYearSelector() {
        if (!this.hasYearSelectTarget) return;
        
        const currentYear = new Date().getFullYear();
        const startYear = currentYear - 1;
        const endYear = currentYear + 2;
        
        let html = '';
        for (let year = startYear; year <= endYear; year++) {
            html += `<option value="${year}">${year}</option>`;
        }
        this.yearSelectTarget.innerHTML = html;
    }

    /**
     * Update navigation selectors to current values
     */
    updateNavigationSelectors() {
        if (this.hasMonthSelectTarget) {
            this.monthSelectTarget.value = this.monthValue;
        }
        if (this.hasYearSelectTarget) {
            this.yearSelectTarget.value = this.yearValue;
        }
    }

    /**
     * Set up pull-to-refresh
     */
    setupPullToRefresh() {
        this.pullStartY = 0;
        this.isPulling = false;
        this.pullThreshold = 80;
        
        this.pullIndicator = document.createElement('div');
        this.pullIndicator.className = 'pull-to-refresh-indicator';
        this.pullIndicator.innerHTML = `
            <div class="pull-spinner">
                <i class="bi bi-arrow-down-circle"></i>
            </div>
            <span class="pull-text">Pull to refresh</span>
        `;
        this.element.insertBefore(this.pullIndicator, this.element.firstChild);
    }

    /**
     * Called when connection state changes
     */
    onConnectionStateChanged(isOnline) {
        if (isOnline) {
            // Re-render to show/hide online-only buttons
            if (this.calendarData) {
                this.showEventList();
            } else {
                this.loadCalendarData();
            }
            // Update pending banner (show sync button)
            this.updatePendingBanner();
        } else {
            // Re-render to hide online-only buttons
            if (this.calendarData) {
                this.showEventList();
            }
            // Update pending banner (hide sync button)
            this.updatePendingBanner();
        }
    }

    /**
     * Load calendar data from server
     */
    async loadCalendarData() {
        this.showLoading();
        
        const url = `${this.dataUrlValue}?year=${this.yearValue}&month=${this.monthValue}`;
        
        try {
            const response = await this.fetchWithRetry(url);
            const data = await response.json();
            
            if (data.success) {
                this.calendarData = data.data;
                
                // Cache user's RSVPs when online
                if (navigator.onLine && data.data.events) {
                    rsvpCacheService.cacheUserRsvps(data.data.events).catch(err => {
                        console.warn('[Calendar] Failed to cache RSVPs:', err);
                    });
                }
                
                this.populateFilters();
                this.applyFilters();
                this.showEventList();
                
                // Check for pending RSVPs
                this.updatePendingBanner();
            } else {
                this.showError('Failed to load events');
            }
        } catch (error) {
            console.error('Calendar load error:', error);
            this.showError(this.online ? 'Failed to load events' : 'You\'re offline');
        }
    }

    /**
     * Update the pending RSVPs banner visibility
     */
    async updatePendingBanner() {
        if (!this.hasPendingBannerTarget) return;
        
        try {
            const count = await rsvpCacheService.getPendingCount();
            
            if (count > 0) {
                this.pendingBannerTarget.hidden = false;
                if (this.hasPendingCountTarget) {
                    this.pendingCountTarget.textContent = count;
                }
                // Hide sync button when offline
                if (this.hasSyncBtnTarget) {
                    this.syncBtnTarget.hidden = !navigator.onLine;
                }
            } else {
                this.pendingBannerTarget.hidden = true;
            }
        } catch (error) {
            console.warn('[Calendar] Failed to check pending RSVPs:', error);
            this.pendingBannerTarget.hidden = true;
        }
    }

    /**
     * Sync pending RSVPs to server
     */
    async syncPendingRsvps() {
        if (!navigator.onLine) {
            this.showToast('Cannot sync while offline', 'warning');
            return;
        }
        
        if (this.hasSyncBtnTarget) {
            this.syncBtnTarget.disabled = true;
            this.syncBtnTarget.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
        }
        
        try {
            const result = await rsvpCacheService.syncPendingRsvps();
            
            if (result.success > 0) {
                this.showToast(`Synced ${result.success} RSVP(s)!`, 'success');
            }
            if (result.failed > 0) {
                this.showToast(`${result.failed} RSVP(s) failed to sync`, 'warning');
            }
            
            // Refresh the calendar data
            await this.loadCalendarData();
            
        } catch (error) {
            console.error('Sync failed:', error);
            this.showToast('Failed to sync RSVPs', 'danger');
        } finally {
            if (this.hasSyncBtnTarget) {
                this.syncBtnTarget.disabled = false;
                this.syncBtnTarget.innerHTML = '<i class="bi bi-arrow-repeat"></i> Sync';
            }
            this.updatePendingBanner();
        }
    }

    // ==================== Display States ====================

    showLoading() {
        this.loadingTarget.hidden = false;
        this.errorTarget.hidden = true;
        this.eventListTarget.hidden = true;
        this.emptyStateTarget.hidden = true;
        if (this.hasResultsCountTarget) this.resultsCountTarget.hidden = true;
    }

    showError(message) {
        this.loadingTarget.hidden = true;
        this.errorTarget.hidden = false;
        this.eventListTarget.hidden = true;
        this.errorMessageTarget.textContent = message;
    }

    showEventList() {
        this.loadingTarget.hidden = true;
        this.errorTarget.hidden = true;
        
        if (this.filteredEvents.length === 0) {
            this.eventListTarget.hidden = true;
            this.emptyStateTarget.hidden = false;
            if (this.hasResultsCountTarget) this.resultsCountTarget.hidden = true;
        } else {
            this.eventListTarget.hidden = false;
            this.emptyStateTarget.hidden = true;
            this.renderEventList();
        }
    }

    reload() {
        this.loadCalendarData();
    }

    // ==================== Navigation ====================

    previousMonth() {
        if (this.monthValue === 1) {
            this.monthValue = 12;
            this.yearValue--;
        } else {
            this.monthValue--;
        }
        this.updateNavigationSelectors();
        this.loadCalendarData();
    }

    nextMonth() {
        if (this.monthValue === 12) {
            this.monthValue = 1;
            this.yearValue++;
        } else {
            this.monthValue++;
        }
        this.updateNavigationSelectors();
        this.loadCalendarData();
    }

    goToToday() {
        const today = new Date();
        this.yearValue = today.getFullYear();
        this.monthValue = today.getMonth() + 1;
        this.updateNavigationSelectors();
        this.loadCalendarData();
    }

    jumpToMonth() {
        this.monthValue = parseInt(this.monthSelectTarget.value);
        this.yearValue = parseInt(this.yearSelectTarget.value);
        this.loadCalendarData();
    }

    // ==================== Filtering ====================

    /**
     * Populate filter dropdowns from event data
     */
    populateFilters() {
        if (!this.calendarData?.events) return;
        
        // Collect unique types, activities, and branches
        const types = new Map();
        const activities = new Map();
        const branches = new Map();
        
        this.calendarData.events.forEach(event => {
            if (event.type?.name) {
                types.set(event.type.name, event.type);
            }
            if (event.branch) {
                branches.set(event.branch, event.branch);
            }
            // Collect activity types from activities array
            if (event.activities && Array.isArray(event.activities)) {
                event.activities.forEach(activity => {
                    if (activity?.name) {
                        activities.set(activity.name, activity);
                    }
                });
            }
        });
        
        // Populate type filter
        if (this.hasTypeFilterTarget) {
            let html = '<option value="">All Types</option>';
            types.forEach((type, name) => {
                html += `<option value="${this.escapeHtml(name)}">${this.escapeHtml(name)}</option>`;
            });
            this.typeFilterTarget.innerHTML = html;
            this.typeFilterTarget.value = this.filters.type;
        }
        
        // Populate activity filter
        if (this.hasActivityFilterTarget) {
            let html = '<option value="">All Activities</option>';
            // Sort activities alphabetically
            const sortedActivities = Array.from(activities.keys()).sort();
            sortedActivities.forEach(name => {
                html += `<option value="${this.escapeHtml(name)}">${this.escapeHtml(name)}</option>`;
            });
            this.activityFilterTarget.innerHTML = html;
            this.activityFilterTarget.value = this.filters.activity;
        }
        
        // Populate branch filter
        if (this.hasBranchFilterTarget) {
            let html = '<option value="">All Branches</option>';
            branches.forEach((branch) => {
                html += `<option value="${this.escapeHtml(branch)}">${this.escapeHtml(branch)}</option>`;
            });
            this.branchFilterTarget.innerHTML = html;
            this.branchFilterTarget.value = this.filters.branch;
        }
    }

    /**
     * Toggle filter panel visibility
     */
    toggleFilters() {
        this.filterPanelTarget.hidden = !this.filterPanelTarget.hidden;
        
        // Update filter toggle button appearance
        if (this.hasFilterToggleTarget) {
            const hasActiveFilters = this.filters.type || this.filters.branch || this.filters.rsvpOnly;
            this.filterToggleTarget.classList.toggle('filter-active', hasActiveFilters);
        }
    }

    /**
     * Apply current filters
     */
    applyFilters() {
        if (!this.calendarData?.events) {
            this.filteredEvents = [];
            return;
        }
        
        // Update filter values from inputs
        if (this.hasSearchInputTarget) {
            this.filters.search = this.searchInputTarget.value.toLowerCase().trim();
        }
        if (this.hasTypeFilterTarget) {
            this.filters.type = this.typeFilterTarget.value;
        }
        if (this.hasActivityFilterTarget) {
            this.filters.activity = this.activityFilterTarget.value;
        }
        if (this.hasBranchFilterTarget) {
            this.filters.branch = this.branchFilterTarget.value;
        }
        if (this.hasRsvpFilterTarget) {
            this.filters.rsvpOnly = this.rsvpFilterTarget.checked;
        }
        
        // Filter events
        this.filteredEvents = this.calendarData.events.filter(event => {
            // Search filter
            if (this.filters.search) {
                const searchText = `${event.name} ${event.location || ''} ${event.branch || ''}`.toLowerCase();
                if (!searchText.includes(this.filters.search)) {
                    return false;
                }
            }
            
            // Type filter
            if (this.filters.type && event.type?.name !== this.filters.type) {
                return false;
            }
            
            // Activity filter - check if event has the selected activity
            if (this.filters.activity) {
                const hasActivity = event.activities?.some(
                    activity => activity?.name === this.filters.activity
                );
                if (!hasActivity) {
                    return false;
                }
            }
            
            // Branch filter
            if (this.filters.branch && event.branch !== this.filters.branch) {
                return false;
            }
            
            // RSVP filter
            if (this.filters.rsvpOnly && !event.user_attending) {
                return false;
            }
            
            return true;
        });
        
        // Update filter indicator
        if (this.hasFilterToggleTarget) {
            const hasActiveFilters = this.filters.type || this.filters.activity || this.filters.branch || this.filters.rsvpOnly;
            this.filterToggleTarget.classList.toggle('filter-active', hasActiveFilters);
        }
        
        // Update results count
        this.updateResultsCount();
        
        // Re-render
        this.showEventList();
    }

    /**
     * Handle search input with debounce
     */
    handleSearch() {
        clearTimeout(this.searchDebounce);
        this.searchDebounce = setTimeout(() => {
            this.applyFilters();
        }, 300);
    }

    /**
     * Clear all filters
     */
    clearFilters() {
        if (this.hasSearchInputTarget) this.searchInputTarget.value = '';
        if (this.hasTypeFilterTarget) this.typeFilterTarget.value = '';
        if (this.hasActivityFilterTarget) this.activityFilterTarget.value = '';
        if (this.hasBranchFilterTarget) this.branchFilterTarget.value = '';
        if (this.hasRsvpFilterTarget) this.rsvpFilterTarget.checked = false;
        
        this.filters = { search: '', type: '', activity: '', branch: '', rsvpOnly: false };
        this.applyFilters();
    }

    /**
     * Update results count display
     */
    updateResultsCount() {
        if (!this.hasResultsCountTarget) return;
        
        const total = this.calendarData?.events?.length || 0;
        const filtered = this.filteredEvents.length;
        
        if (total !== filtered) {
            this.resultsCountTarget.innerHTML = `<small class="text-muted">Showing ${filtered} of ${total} events</small>`;
            this.resultsCountTarget.hidden = false;
        } else {
            this.resultsCountTarget.innerHTML = `<small class="text-muted">${total} events</small>`;
            this.resultsCountTarget.hidden = false;
        }
    }

    // ==================== Rendering ====================

    /**
     * Render the event list grouped by week
     */
    renderEventList() {
        if (this.filteredEvents.length === 0) return;
        
        // Group events by week
        const weeks = this.groupEventsByWeek(this.filteredEvents);
        
        let html = '';
        weeks.forEach((weekEvents, weekLabel) => {
            html += `
                <div class="mobile-week-section mb-3">
                    <div class="mobile-week-header">
                        <span>${weekLabel}</span>
                        <span class="mobile-week-count">${weekEvents.length} event${weekEvents.length !== 1 ? 's' : ''}</span>
                    </div>
                    ${this.renderWeekEvents(weekEvents)}
                </div>
            `;
        });
        
        this.eventListTarget.innerHTML = html;
    }

    /**
     * Group events by week
     */
    groupEventsByWeek(events) {
        const weeks = new Map();
        const today = new Date();
        today.setHours(0, 0, 0, 0);
        
        // Sort events by date
        const sortedEvents = [...events].sort((a, b) => {
            return new Date(a.start_date) - new Date(b.start_date);
        });
        
        sortedEvents.forEach(event => {
            const eventDate = new Date(event.start_date + 'T00:00:00');
            const weekStart = this.getWeekStart(eventDate);
            const weekEnd = new Date(weekStart);
            weekEnd.setDate(weekEnd.getDate() + 6);
            
            // Format week label
            const startMonth = weekStart.toLocaleDateString('en-US', { month: 'short' });
            const endMonth = weekEnd.toLocaleDateString('en-US', { month: 'short' });
            const startDay = weekStart.getDate();
            const endDay = weekEnd.getDate();
            
            let weekLabel;
            if (startMonth === endMonth) {
                weekLabel = `${startMonth} ${startDay} - ${endDay}`;
            } else {
                weekLabel = `${startMonth} ${startDay} - ${endMonth} ${endDay}`;
            }
            
            // Check if this week contains today
            if (today >= weekStart && today <= weekEnd) {
                weekLabel = `This Week (${weekLabel})`;
            }
            
            if (!weeks.has(weekLabel)) {
                weeks.set(weekLabel, []);
            }
            weeks.get(weekLabel).push(event);
        });
        
        return weeks;
    }

    /**
     * Get the start of the week (Sunday)
     */
    getWeekStart(date) {
        const d = new Date(date);
        const day = d.getDay();
        d.setDate(d.getDate() - day);
        return d;
    }

    /**
     * Render events for a week
     */
    renderWeekEvents(events) {
        const today = new Date();
        today.setHours(0, 0, 0, 0);
        const isOnline = navigator.onLine;
        
        return events.map(event => {
            const eventDate = new Date(event.start_date + 'T00:00:00');
            const eventEnd = event.end_date ? new Date(event.end_date + 'T23:59:59') : eventDate;
            const isPast = eventEnd < today;
            const isToday = eventDate.getTime() === today.getTime();
            
            const cardClasses = ['mobile-event-card'];
            if (event.is_cancelled) cardClasses.push('cancelled');
            if (event.user_attending) cardClasses.push('attending');
            
            const typeStyle = event.type?.color 
                ? `background-color: ${event.type.color}; color: white;` 
                : 'background-color: var(--bs-secondary); color: white;';
            
            // Show RSVP button for future events; when offline only show for new RSVPs (not edit)
            const canRsvpOffline = !event.user_attending;
            const showRsvpButton = !event.is_cancelled && !isPast && (isOnline || canRsvpOffline);
            
            // For edit mode when attending - only show when online
            const showEditButton = !event.is_cancelled && !isPast && event.user_attending && isOnline;
            
            const rsvpBtnClass = event.user_attending 
                ? 'btn btn-outline-success mobile-event-rsvp-btn' 
                : 'btn btn-success mobile-event-rsvp-btn';
            const rsvpBtnText = event.user_attending ? 'Edit' : 'RSVP';
            
            // Format date
            const dateStr = eventDate.toLocaleDateString('en-US', { 
                weekday: 'short', 
                month: 'short', 
                day: 'numeric' 
            });
            
            // Render activities
            const activitiesHtml = this.renderActivities(event.activities);
            
            // Only show View Details when online
            const showViewDetails = event.public_page_enabled && isOnline;
            
            return `
                <div class="${cardClasses.join(' ')}">
                    ${event.is_cancelled ? `
                        <div class="mobile-event-cancelled-banner">
                            <i class="bi bi-x-circle-fill me-2"></i>CANCELLED
                        </div>
                    ` : ''}
                    <div class="mobile-event-header">
                        <div class="mobile-event-info">
                            ${event.type ? `<span class="mobile-event-type-badge mb-1" style="${typeStyle}">${this.escapeHtml(event.type.name)}</span>` : ''}
                            <a href="/gatherings/view/${event.public_id}" 
                               class="mobile-event-name ${event.is_cancelled ? 'cancelled' : ''} text-decoration-none">
                                ${this.escapeHtml(event.name)}
                            </a>
                            <div class="mobile-event-meta">
                                <span><i class="bi bi-calendar3"></i> ${dateStr}${isToday ? ' <strong>(Today)</strong>' : ''}</span>
                                ${event.start_time ? `<span><i class="bi bi-clock"></i> ${this.formatTime(event.start_time)}</span>` : ''}
                            </div>
                            <div class="mobile-event-meta">
                                ${event.branch ? `<span><i class="bi bi-building"></i> ${this.escapeHtml(event.branch)}</span>` : ''}
                                ${event.location ? `<span><i class="bi bi-geo-alt"></i> ${this.escapeHtml(event.location)}</span>` : ''}
                            </div>
                            ${activitiesHtml}
                            ${showViewDetails ? `
                                <a href="/gatherings/public-landing/${event.public_id}?from=mobile&month=${this.monthValue}&year=${this.yearValue}" 
                                   class="btn btn-sm btn-outline-secondary mt-2">
                                    <i class="bi bi-info-circle me-1"></i>View Details
                                </a>
                            ` : ''}
                        </div>
                        <div class="mobile-event-actions">
                            ${showEditButton ? `
                                <button type="button" 
                                        class="${rsvpBtnClass}"
                                        data-event-id="${event.id}"
                                        data-action="click->mobile-calendar#showRsvpSheet">
                                    Edit
                                </button>
                            ` : ''}
                            ${(showRsvpButton && !event.user_attending) ? `
                                <button type="button" 
                                        class="${rsvpBtnClass}"
                                        data-event-id="${event.id}"
                                        data-action="click->mobile-calendar#handleRsvpClick">
                                    RSVP
                                </button>
                            ` : ''}
                            ${event.user_attending ? '<i class="bi bi-check-circle-fill text-success"></i>' : ''}
                        </div>
                    </div>
                </div>
            `;
        }).join('');
    }

    /**
     * Render activities list for an event
     */
    renderActivities(activities) {
        if (!activities || activities.length === 0) return '';
        
        const activityNames = activities.map(a => this.escapeHtml(a.name)).join(', ');
        
        return `
            <div class="mobile-event-activities">
                <i class="bi bi-list-check"></i>
                <span>${activityNames}</span>
            </div>
        `;
    }

    /**
     * Format time for display
     */
    formatTime(time) {
        if (!time) return '';
        const [hours, minutes] = time.split(':');
        const hour = parseInt(hours);
        const ampm = hour >= 12 ? 'PM' : 'AM';
        const hour12 = hour % 12 || 12;
        return `${hour12}:${minutes} ${ampm}`;
    }

    /**
     * Escape HTML entities
     */
    escapeHtml(text) {
        if (!text) return '';
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    // ==================== Touch Handlers ====================

    handleTouchStart(event) {
        this.touchStartX = event.touches[0].clientX;
        this.touchStartY = event.touches[0].clientY;
        
        if (window.scrollY === 0) {
            this.isPulling = true;
            this.pullStartY = event.touches[0].clientY;
        }
    }

    handleTouchEnd(event) {
        // Pull to refresh
        if (this.isPulling && this.pullIndicator) {
            const touchEndY = event.changedTouches[0].clientY;
            const pullDistance = touchEndY - this.pullStartY;
            
            if (pullDistance >= this.pullThreshold) {
                this.pullIndicator.querySelector('.pull-text').textContent = 'Refreshing...';
                this.pullIndicator.querySelector('.pull-spinner i').className = 'bi bi-arrow-clockwise spin';
                this.loadCalendarData().then(() => this.resetPullIndicator());
            } else {
                this.resetPullIndicator();
            }
            this.isPulling = false;
        }
        
        // Swipe navigation
        if (!this.touchStartX || !this.touchStartY) return;
        
        const touchEndX = event.changedTouches[0].clientX;
        const diffX = this.touchStartX - touchEndX;
        const diffY = this.touchStartY - event.changedTouches[0].clientY;
        
        if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
            if (diffX > 0) {
                this.nextMonth();
            } else {
                this.previousMonth();
            }
        }
        
        this.touchStartX = 0;
        this.touchStartY = 0;
    }

    resetPullIndicator() {
        if (!this.pullIndicator) return;
        this.pullIndicator.style.height = '0';
        this.pullIndicator.style.opacity = '0';
        this.pullIndicator.classList.remove('ready');
        this.pullIndicator.querySelector('.pull-text').textContent = 'Pull to refresh';
        this.pullIndicator.querySelector('.pull-spinner i').className = 'bi bi-arrow-down-circle';
    }

    // ==================== RSVP Methods ====================

    /**
     * Create RSVP modal container
     */
    createBottomSheet() {
        // Create a Bootstrap modal container for RSVP
        if (document.getElementById('mobileRsvpModal')) return;
        
        const modal = document.createElement('div');
        modal.className = 'modal fade';
        modal.id = 'mobileRsvpModal';
        modal.tabIndex = -1;
        modal.setAttribute('aria-labelledby', 'mobileRsvpModalLabel');
        modal.setAttribute('aria-hidden', 'true');
        modal.innerHTML = `
            <div class="modal-dialog modal-dialog-centered">
                <div class="modal-content" data-mobile-calendar-target="rsvpContent">
                    <div class="modal-body text-center py-5">
                        <div class="spinner-border text-primary"></div>
                        <p class="mt-2 text-muted">Loading...</p>
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(modal);
        
        // Listen for modal hidden to refresh data
        modal.addEventListener('hidden.bs.modal', () => {
            this.loadCalendarData();
        });
    }

    /**
     * Handle RSVP button click - online uses modal, offline queues RSVP
     */
    async handleRsvpClick(event) {
        event.preventDefault();
        event.stopPropagation();
        
        const button = event.currentTarget;
        const eventId = button.dataset.eventId;
        const eventData = this.filteredEvents.find(e => e.id == eventId) 
            || this.calendarData?.events?.find(e => e.id == eventId);
        
        if (!eventData) return;
        
        if (navigator.onLine) {
            // When online, show the modal for full RSVP options
            this.showRsvpSheet(event);
        } else {
            // When offline, queue a simple RSVP
            await this.queueOfflineRsvp(eventData, button);
        }
    }

    /**
     * Queue an RSVP for sync when back online
     */
    async queueOfflineRsvp(eventData, button) {
        try {
            // Disable button and show loading
            const originalHtml = button.innerHTML;
            button.disabled = true;
            button.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
            
            await rsvpCacheService.queueOfflineRsvp({
                gathering_id: eventData.id,
                gathering_name: eventData.name,
                share_with_kingdom: false,
                share_with_hosting_group: false,
                share_with_crown: false,
                public_note: ''
            });
            
            // Update local state to show as attending
            eventData.user_attending = true;
            
            // Update button to show success
            button.innerHTML = '<i class="bi bi-check"></i> Queued';
            button.classList.remove('btn-success');
            button.classList.add('btn-outline-success');
            
            this.showToast('RSVP queued - will sync when online', 'info');
            
            // Re-render after short delay
            setTimeout(() => {
                this.showEventList();
            }, 1000);
            
        } catch (error) {
            console.error('Failed to queue offline RSVP:', error);
            this.showToast('Failed to queue RSVP', 'danger');
            button.disabled = false;
        }
    }

    /**
     * Show RSVP modal by loading attendance modal content from server
     */
    async showRsvpSheet(event) {
        event.preventDefault();
        event.stopPropagation();
        
        const button = event.currentTarget;
        const eventId = button.dataset.eventId;
        const eventData = this.filteredEvents.find(e => e.id == eventId) 
            || this.calendarData?.events?.find(e => e.id == eventId);
        
        if (!eventData) return;
        
        this.currentRsvpEvent = eventData;
        
        // Show modal with loading state
        const modalEl = document.getElementById('mobileRsvpModal');
        if (!modalEl) return;
        
        const modalContent = modalEl.querySelector('.modal-content');
        modalContent.innerHTML = `
            <div class="modal-body text-center py-5">
                <div class="spinner-border text-primary"></div>
                <p class="mt-2 text-muted">Loading...</p>
            </div>
        `;
        
        // Show the modal
        const modal = new bootstrap.Modal(modalEl);
        modal.show();
        
        // Load attendance modal content from server
        try {
            let url = `/gatherings/attendance-modal/${eventData.id}`;
            if (eventData.attendance_id) {
                url += `?attendance_id=${eventData.attendance_id}`;
            }
            
            const response = await this.fetchWithRetry(url);
            const html = await response.text();
            
            modalContent.innerHTML = html;
            
            // Handle form submission via AJAX instead of regular submit
            this.setupModalFormHandlers(modalContent);
            
        } catch (error) {
            console.error('Failed to load attendance modal:', error);
            modalContent.innerHTML = `
                <div class="modal-header">
                    <h5 class="modal-title">Error</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <div class="alert alert-danger">
                        Failed to load attendance form. Please try again.
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
                </div>
            `;
        }
    }

    /**
     * Setup form handlers for the attendance modal
     */
    setupModalFormHandlers(modalContent) {
        const mainForm = modalContent.querySelector('#attendanceModalForm');
        const deleteForm = modalContent.querySelector('[id^="deleteAttendanceForm_"]');
        
        if (mainForm) {
            mainForm.addEventListener('submit', async (e) => {
                e.preventDefault();
                await this.handleAttendanceFormSubmit(mainForm);
            });
        }
        
        if (deleteForm) {
            deleteForm.addEventListener('submit', async (e) => {
                e.preventDefault();
                await this.handleDeleteAttendance(deleteForm);
            });
        }
    }

    /**
     * Handle attendance form submission
     */
    async handleAttendanceFormSubmit(form) {
        const submitBtn = form.closest('.modal-content').querySelector('button[type="submit"][form]');
        if (submitBtn) {
            submitBtn.disabled = true;
            submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Saving...';
        }
        
        const formData = new FormData(form);
        
        try {
            const response = await fetch(form.action, {
                method: 'POST',
                body: formData,
                headers: {
                    'X-Requested-With': 'XMLHttpRequest'
                }
            });
            
            if (response.ok || response.redirected) {
                // Update RSVP cache with new data
                if (this.currentRsvpEvent) {
                    rsvpCacheService.updateCachedRsvp(this.currentRsvpEvent.id, {
                        share_with_kingdom: formData.get('share_with_kingdom') === '1',
                        share_with_hosting_group: formData.get('share_with_hosting_group') === '1',
                        share_with_crown: formData.get('share_with_crown') === '1',
                        public_note: formData.get('public_note') || ''
                    }).catch(err => console.warn('[Calendar] Failed to update RSVP cache:', err));
                }
                
                this.showToast('Attendance saved!', 'success');
                bootstrap.Modal.getInstance(document.getElementById('mobileRsvpModal'))?.hide();
            } else {
                throw new Error('Form submission failed');
            }
        } catch (error) {
            this.showToast('Failed to save attendance', 'danger');
            if (submitBtn) {
                submitBtn.disabled = false;
                submitBtn.textContent = 'Register';
            }
        }
    }

    /**
     * Handle delete attendance
     */
    async handleDeleteAttendance(form) {
        try {
            const response = await fetch(form.action, {
                method: 'POST',
                body: new FormData(form),
                headers: {
                    'X-Requested-With': 'XMLHttpRequest'
                }
            });
            
            if (response.ok || response.redirected) {
                // Remove from RSVP cache
                if (this.currentRsvpEvent) {
                    rsvpCacheService.removeCachedRsvp(this.currentRsvpEvent.id)
                        .catch(err => console.warn('[Calendar] Failed to remove from RSVP cache:', err));
                }
                
                this.showToast('Attendance removed!', 'success');
                bootstrap.Modal.getInstance(document.getElementById('mobileRsvpModal'))?.hide();
            } else {
                throw new Error('Delete failed');
            }
        } catch (error) {
            this.showToast('Failed to remove attendance', 'danger');
        }
    }

    // Legacy methods kept for compatibility (not used with new modal)
    openBottomSheet() {}
    closeBottomSheet() {
        bootstrap.Modal.getInstance(document.getElementById('mobileRsvpModal'))?.hide();
    }

    // ==================== Utilities ====================

    getCsrfToken() {
        const meta = document.querySelector('meta[name="csrf-token"]') 
            || document.querySelector('meta[name="csrfToken"]');
        return meta?.getAttribute('content') || '';
    }

    showToast(message, type = 'info') {
        const toast = document.createElement('div');
        toast.className = `alert alert-${type} position-fixed bottom-0 start-50 translate-middle-x mb-3`;
        toast.style.cssText = 'z-index: 9999; opacity: 0; transform: translateX(-50%) translateY(20px); transition: all 0.3s;';
        
        const icon = type === 'success' ? 'check-circle' : type === 'danger' ? 'exclamation-circle' : 'info-circle';
        toast.innerHTML = `<i class="bi bi-${icon} me-2"></i>${message}`;
        document.body.appendChild(toast);
        
        requestAnimationFrame(() => {
            toast.style.opacity = '1';
            toast.style.transform = 'translateX(-50%) translateY(0)';
        });
        
        setTimeout(() => {
            toast.style.opacity = '0';
            toast.style.transform = 'translateX(-50%) translateY(20px)';
            setTimeout(() => toast.remove(), 300);
        }, 3000);
    }
}

// Register controller
if (!window.Controllers) {
    window.Controllers = {};
}
window.Controllers["mobile-calendar"] = MobileCalendarController;

export default MobileCalendarController;