plugins_Waivers_assets_js_controllers_waiver-upload-wizard-controller.js

import { Controller } from "@hotwired/stimulus"
import * as pdfjsLib from 'pdfjs-dist'

// Set up PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
    'pdfjs-dist/build/pdf.worker.min.mjs',
    import.meta.url
).toString()

/**
 * Waiver Upload Wizard Controller
 *
 * Gathering-level workflow:
 * Step 1: Select waiver type
 * Step 2: Upload pages or attest not needed
 * Step 3: Review & submit
 */
class WaiverUploadWizardController extends Controller {
    static targets = [
        "step",
        "stepIndicator",
        "prevButton",
        "nextButton",
        "submitButton",
        "submitButtonText",
        "waiverTypeOption",
        "waiverTypeSelect",
        "pagesPreview",
        "fileInput",
        "reviewWaiverType",
        "reviewPageCount",
        "reviewPagesList",
        "notesField",
        "progressBar",
        "uploadSection",
        "attestSection",
        "attestReasonList",
        "attestNotes",
        "reviewUploadSection",
        "reviewAttestSection",
        "reviewAttestReason",
        "reviewAttestNotes",
        "reviewAttestNotesSection",
        "modeToggle",
        "step3Lead"
    ]

    static values = {
        currentStep: { type: Number, default: 1 },
        totalSteps: { type: Number, default: 3 },
        gatheringId: Number,
        gatheringPublicId: String,
        maxFileSize: Number,           // Maximum single file size in bytes
        totalMaxSize: Number,          // Maximum total upload size in bytes
        preSelectedWaiverTypeId: Number, // Pre-selected waiver type ID from URL
        attestUrl: String,             // URL for attestation endpoint
        gatheringViewUrl: String,      // URL for gathering view page
        mobileSelectUrl: String        // URL for mobile select gathering page
    }

    connect() {
        this.uploadedPages = []
        this.selectedWaiverType = null
        this.notes = ""
        this.isAttestMode = false
        this.attestReason = null
        this.attestNotes = ""

        if (this.hasPreSelectedWaiverTypeIdValue) {
            setTimeout(() => {
                const waiverTypeRadio = document.querySelector(
                    `input[name="waiver_type"][value="${this.preSelectedWaiverTypeIdValue}"]`
                )

                if (waiverTypeRadio) {
                    if (this.isWaiverTypeAttested(this.preSelectedWaiverTypeIdValue)) {
                        this.showError('This waiver type has been attested as not needed for this gathering.')
                        waiverTypeRadio.checked = false
                        return
                    }
                    waiverTypeRadio.checked = true
                    this.selectedWaiverType = {
                        id: this.preSelectedWaiverTypeIdValue,
                        name: waiverTypeRadio.dataset.name
                    }
                    this.checkAttestationAvailability()
                }
            }, 50)
        }

        this.showStep(1)
    }

    // Step Navigation
    nextStep() {
        if (this.validateCurrentStep()) {
            if (this.currentStepValue < this.totalStepsValue) {
                this.currentStepValue++
                this.showStep(this.currentStepValue)
            }
        }
    }

    prevStep() {
        if (this.currentStepValue > 1) {
            this.currentStepValue--
            this.showStep(this.currentStepValue)
        }
    }

    goToStep(event) {
        const step = parseInt(event.currentTarget.dataset.step)
        if (step < this.currentStepValue) {
            this.currentStepValue = step
            this.showStep(this.currentStepValue)
        }
    }

    showStep(stepNumber) {
        // Hide all steps
        this.stepTargets.forEach(step => {
            step.classList.add('d-none')
        })

        // Show current step
        const currentStep = this.stepTargets.find(step =>
            parseInt(step.dataset.stepNumber) === stepNumber
        )
        if (currentStep) {
            currentStep.classList.remove('d-none')
        }

        // Update step indicators
        this.updateStepIndicators(stepNumber)

        // Update navigation buttons
        this.updateNavigationButtons(stepNumber)

        // Update progress bar
        this.updateProgressBar(stepNumber)

        // Perform step-specific actions
        this.onStepChange(stepNumber)
    }

    updateStepIndicators(currentStep) {
        this.stepIndicatorTargets.forEach(indicator => {
            const step = parseInt(indicator.dataset.step)
            indicator.classList.remove('active', 'completed')

            if (step === currentStep) {
                indicator.classList.add('active')
            } else if (step < currentStep) {
                indicator.classList.add('completed')
            }
        })
    }

    updateNavigationButtons(stepNumber) {
        // Previous button
        if (this.hasPrevButtonTarget) {
            if (stepNumber === 1) {
                this.prevButtonTarget.classList.add('d-none')
            } else {
                this.prevButtonTarget.classList.remove('d-none')
            }
        }

        // Next button
        if (this.hasNextButtonTarget) {
            if (stepNumber === this.totalStepsValue) {
                this.nextButtonTarget.classList.add('d-none')
            } else {
                this.nextButtonTarget.classList.remove('d-none')

                // Update button text based on step
                if (stepNumber === 3) {
                    this.nextButtonTarget.innerHTML = '<i class="bi bi-arrow-right"></i> Review'
                } else {
                    this.nextButtonTarget.innerHTML = '<i class="bi bi-arrow-right"></i> Next'
                }
            }
        }

        // Submit button
        if (this.hasSubmitButtonTarget) {
            if (stepNumber === this.totalStepsValue) {
                this.submitButtonTarget.classList.remove('d-none')
                // Update submit button text based on mode
                if (this.hasSubmitButtonTextTarget) {
                    this.submitButtonTextTarget.textContent = this.isAttestMode ? 'Submit Attestation' : 'Submit Waivers'
                }
            } else {
                this.submitButtonTarget.classList.add('d-none')
            }
        }
    }

    updateProgressBar(stepNumber) {
        if (this.hasProgressBarTarget) {
            const progress = (stepNumber / this.totalStepsValue) * 100
            this.progressBarTarget.style.width = `${progress}%`
            this.progressBarTarget.setAttribute('aria-valuenow', progress)
        }
    }

    onStepChange(stepNumber) {
        switch (stepNumber) {
            case 2:
                this.checkAttestationAvailability()
                break
            case 3:
                this.updateReviewSection()
                break
        }
    }

    // Check if attestation is available for selected waiver type
    checkAttestationAvailability() {
        if (!this.selectedWaiverType) {
            return
        }

        // Find the waiver type to get exemption reasons
        const waiverTypeRadio = document.querySelector(
            `input[name="waiver_type"][value="${this.selectedWaiverType.id}"]`
        )

        let exemptionReasons = []
        if (waiverTypeRadio && waiverTypeRadio.dataset.exemptionReasons) {
            try {
                exemptionReasons = JSON.parse(waiverTypeRadio.dataset.exemptionReasons)
            } catch (e) {
                console.error('Failed to parse exemption reasons:', e)
            }
        }

        // Show/hide mode toggle and update lead text based on exemption reasons availability
        if (exemptionReasons.length > 0) {
            // Has exemption reasons - show toggle
            if (this.hasModeToggleTarget) {
                this.modeToggleTarget.classList.remove('d-none')
            }
            if (this.hasStep3LeadTarget) {
                this.step3LeadTarget.textContent = 'Add one or more pages to your waiver document, or attest that a waiver is not needed'
            }
        } else {
            // No exemption reasons - hide toggle, force upload mode
            if (this.hasModeToggleTarget) {
                this.modeToggleTarget.classList.add('d-none')
            }
            if (this.hasStep3LeadTarget) {
                this.step3LeadTarget.textContent = 'Add one or more pages to your waiver document'
            }
            // Force upload mode
            this.isAttestMode = false
            const uploadRadio = document.getElementById('mode-upload')
            if (uploadRadio) {
                uploadRadio.checked = true
            }
            if (this.hasUploadSectionTarget && this.hasAttestSectionTarget) {
                this.uploadSectionTarget.classList.remove('d-none')
                this.attestSectionTarget.classList.add('d-none')
            }
        }

        // Populate attestation reasons if available
        if (exemptionReasons.length > 0) {
            this.populateAttestationReasons()
        }
    }

    // Step 3: Mode Toggle (Upload vs Attest)
    setModeUpload(event) {
        this.isAttestMode = false
        if (this.hasUploadSectionTarget && this.hasAttestSectionTarget) {
            this.uploadSectionTarget.classList.remove('d-none')
            this.attestSectionTarget.classList.add('d-none')
        }
    }

    setModeAttest(event) {
        // Verify exemption reasons are available before allowing switch
        if (!this.selectedWaiverType) return

        const waiverTypeRadio = document.querySelector(
            `input[name="waiver_type"][value="${this.selectedWaiverType.id}"]`
        )

        let exemptionReasons = []
        if (waiverTypeRadio && waiverTypeRadio.dataset.exemptionReasons) {
            try {
                exemptionReasons = JSON.parse(waiverTypeRadio.dataset.exemptionReasons)
            } catch (e) {
                console.error('Failed to parse exemption reasons:', e)
            }
        }

        if (exemptionReasons.length === 0) {
            // No exemption reasons - prevent switching to attest mode
            this.showError('Attestation is not available for this waiver type.')
            const uploadRadio = document.getElementById('mode-upload')
            if (uploadRadio) {
                uploadRadio.checked = true
            }
            return
        }

        this.isAttestMode = true
        if (this.hasUploadSectionTarget && this.hasAttestSectionTarget) {
            this.uploadSectionTarget.classList.add('d-none')
            this.attestSectionTarget.classList.remove('d-none')
        }
        this.populateAttestationReasons()
    }

    populateAttestationReasons() {
        if (!this.hasAttestReasonListTarget || !this.selectedWaiverType) return

        // Find the waiver type to get exemption reasons
        const waiverTypeRadio = document.querySelector(
            `input[name="waiver_type"][value="${this.selectedWaiverType.id}"]`
        )

        let exemptionReasons = []
        if (waiverTypeRadio && waiverTypeRadio.dataset.exemptionReasons) {
            try {
                exemptionReasons = JSON.parse(waiverTypeRadio.dataset.exemptionReasons)
            } catch (e) {
                console.error('Failed to parse exemption reasons:', e)
            }
        }

        if (exemptionReasons.length === 0) {
            this.attestReasonListTarget.innerHTML = `
                <div class="alert alert-warning">
                    <i class="bi bi-exclamation-triangle"></i>
                    No exemption reasons have been configured for this waiver type.
                </div>
            `
            return
        }

        // Build radio buttons
        let html = '<div class="list-group">'
        let hasMatchingSelection = false
        exemptionReasons.forEach((reason, index) => {
            const id = `attest_reason_${index}`
            const isSelected = this.attestReason === reason
            if (isSelected) {
                hasMatchingSelection = true
            }
            html += `
                <label class="list-group-item list-group-item-action">
                    <input class="form-check-input me-2" type="radio" name="attest_reason" 
                           id="${id}" value="${this.escapeHtml(reason)}"
                           data-action="change->waiver-upload-wizard#selectAttestReason"
                           ${isSelected ? 'checked' : ''}>
                    ${this.escapeHtml(reason)}
                </label>
            `
        })
        html += '</div>'

        this.attestReasonListTarget.innerHTML = html

        // Clear attestReason if the previously selected reason is not available for the current waiver type
        if (!hasMatchingSelection) {
            this.attestReason = null
        }
    }

    selectAttestReason(event) {
        this.attestReason = event.currentTarget.value
    }

    selectWaiverType(event) {
        const option = event.currentTarget
        if (option.disabled || option.dataset.attested === '1') {
            option.checked = false
            this.showError('This waiver type has been attested as not needed for this gathering.')
            return
        }
        this.selectedWaiverType = {
            id: parseInt(option.value),
            name: option.dataset.name
        }
        this.checkAttestationAvailability()
    }

    escapeHtml(text) {
        const div = document.createElement('div')
        div.textContent = text
        return div.innerHTML
    }

    isWaiverTypeAttested(waiverTypeId) {
        const waiverTypeRadio = document.querySelector(
            `input[name="waiver_type"][value="${waiverTypeId}"]`
        )
        if (!waiverTypeRadio) {
            return false
        }
        return waiverTypeRadio.disabled || waiverTypeRadio.dataset.attested === '1'
    }

    // Step 3: Add Pages
    triggerFileInput() {
        if (this.hasFileInputTarget) {
            this.fileInputTarget.click()
        }
    }

    handleFileSelect(event) {
        const files = Array.from(event.target.files)

        // Get max file size (use configured value or fallback to 5MB)
        const maxFileSize = this.hasMaxFileSizeValue ? this.maxFileSizeValue : (5 * 1024 * 1024)
        const totalMaxSize = this.hasTotalMaxSizeValue ? this.totalMaxSizeValue : maxFileSize

        // Calculate current total size
        const currentTotalSize = this.uploadedPages.reduce((sum, page) => sum + page.size, 0)

        files.forEach(file => {
            // Validate file type
            if (!this.isValidFile(file)) {
                this.showError(`Invalid file type: ${file.name}. Please upload images (JPEG, PNG, GIF, BMP, WEBP) or PDF files.`)
                return
            }

            // Validate individual file size
            if (file.size > maxFileSize) {
                const maxFormatted = this.formatBytes(maxFileSize)
                const fileFormatted = this.formatBytes(file.size)
                this.showError(`File too large: ${file.name} (${fileFormatted}). Maximum size per file is ${maxFormatted}.`)
                return
            }

            // Check if adding this file would exceed total size limit
            const newTotalSize = currentTotalSize + file.size
            if (newTotalSize > totalMaxSize) {
                const totalFormatted = this.formatBytes(newTotalSize)
                const maxFormatted = this.formatBytes(totalMaxSize)
                const currentFormatted = this.formatBytes(currentTotalSize)
                const fileFormatted = this.formatBytes(file.size)

                this.showError(
                    `Cannot add ${file.name} (${fileFormatted}). ` +
                    `Current total: ${currentFormatted}. ` +
                    `Adding this file would exceed the maximum total upload size of ${maxFormatted} ` +
                    `(would be ${totalFormatted}).`
                )
                return
            }

            // Add to uploaded pages
            this.addPage(file)
        })

        // Clear input so same file can be selected again
        event.target.value = ''

        // Show total size info if we have files
        if (this.uploadedPages.length > 0) {
            this.updateTotalSizeDisplay()
        }
    }

    isValidFile(file) {
        const validTypes = [
            'image/jpeg',
            'image/jpg',
            'image/png',
            'image/gif',
            'image/bmp',
            'image/webp',
            'image/x-ms-bmp', // Alternative MIME type for BMP
            'image/x-windows-bmp', // Another BMP variant
            'application/pdf' // PDF files
        ]
        return validTypes.includes(file.type) || file.name.toLowerCase().endsWith('.pdf')
    }

    addPage(file) {
        const pageNumber = this.uploadedPages.length + 1
        const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')
        const reader = new FileReader()

        reader.onload = async (e) => {
            const page = {
                file: file,
                dataUrl: e.target.result,
                number: pageNumber,
                name: file.name,
                size: file.size,
                isPdf: isPdf,
                pdfPageCount: 0,
                thumbnailUrl: null
            }

            // Generate thumbnail for PDFs
            if (isPdf) {
                try {
                    const pdfData = new Uint8Array(e.target.result.split(',')[1] ? 
                        atob(e.target.result.split(',')[1]).split('').map(c => c.charCodeAt(0)) :
                        [])
                    
                    if (pdfData.length > 0) {
                        const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise
                        page.pdfPageCount = pdf.numPages
                        
                        // Render first page as thumbnail
                        const pdfPage = await pdf.getPage(1)
                        const scale = 0.5
                        const viewport = pdfPage.getViewport({ scale })
                        
                        const canvas = document.createElement('canvas')
                        const context = canvas.getContext('2d')
                        canvas.width = viewport.width
                        canvas.height = viewport.height
                        
                        await pdfPage.render({ canvasContext: context, viewport }).promise
                        page.thumbnailUrl = canvas.toDataURL('image/png')
                    }
                } catch (err) {
                    console.warn('Could not generate PDF thumbnail:', err)
                }
            }

            this.uploadedPages.push(page)
            this.renderPages()
        }

        reader.readAsDataURL(file)
    }

    removePage(event) {
        const index = parseInt(event.currentTarget.dataset.index)
        this.uploadedPages.splice(index, 1)

        // Renumber pages
        this.uploadedPages.forEach((page, idx) => {
            page.number = idx + 1
        })

        this.renderPages()

        // Update total size display after removal
        if (this.uploadedPages.length > 0) {
            this.updateTotalSizeDisplay()
        }
    }

    renderPages() {
        if (!this.hasPagesPreviewTarget) return

        if (this.uploadedPages.length === 0) {
            this.pagesPreviewTarget.innerHTML = `
                <div class="text-center text-muted py-5">
                    <i class="bi bi-file-earmark-image" style="font-size: 3rem;"></i>
                    <p class="mt-3">No pages added yet</p>
                    <p class="small">Click "Add Page" to select images or PDFs</p>
                </div>
            `
            return
        }

        const html = this.uploadedPages.map((page, index) => {
            // Generate preview: PDF thumbnail, PDF icon fallback, or image
            let previewHtml
            if (page.isPdf) {
                if (page.thumbnailUrl) {
                    // Show rendered PDF thumbnail with page count badge
                    const pageCountBadge = page.pdfPageCount > 1 
                        ? `<span class="badge bg-danger position-absolute top-0 end-0 m-1">${page.pdfPageCount} pages</span>` 
                        : ''
                    previewHtml = `<div class="position-relative">
                        <img src="${page.thumbnailUrl}" 
                             class="img-thumbnail" 
                             style="width: 100px; height: 130px; object-fit: contain; background: #f8f9fa;"
                             alt="PDF Preview">
                        ${pageCountBadge}
                    </div>`
                } else {
                    // Fallback to PDF icon
                    previewHtml = `<div class="d-flex align-items-center justify-content-center bg-light border rounded" 
                            style="width: 100px; height: 130px;">
                         <div class="text-center">
                           <i class="bi bi-file-earmark-pdf text-danger" style="font-size: 2.5rem;"></i>
                           <div class="small text-muted mt-1">PDF</div>
                         </div>
                       </div>`
                }
            } else {
                previewHtml = `<img src="${page.dataUrl}" 
                        class="img-thumbnail" 
                        style="width: 100px; height: 130px; object-fit: cover;"
                        alt="Page ${page.number}">`
            }

            // Show page count info for multi-page PDFs
            const pageInfo = page.isPdf && page.pdfPageCount > 1 
                ? `<small class="text-info">${page.pdfPageCount} pages</small><br>` 
                : ''

            return `
            <div class="col-md-4 mb-3">
                <div class="card">
                    <div class="card-body p-2">
                        <div class="d-flex align-items-start">
                            <div class="flex-shrink-0">
                                ${previewHtml}
                            </div>
                            <div class="flex-grow-1 ms-3">
                                <div class="d-flex justify-content-between align-items-start">
                                    <div>
                                        <strong>Page ${page.number}</strong><br>
                                        ${pageInfo}<small class="text-muted">${page.name}</small><br>
                                        <small class="text-muted">${this.formatFileSize(page.size)}</small>
                                    </div>
                                    <button type="button" 
                                            class="btn btn-sm btn-outline-danger"
                                            data-index="${index}"
                                            data-action="click->waiver-upload-wizard#removePage">
                                        <i class="bi bi-trash"></i>
                                    </button>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        `}).join('')

        this.pagesPreviewTarget.innerHTML = html
    }

    formatFileSize(bytes) {
        if (bytes < 1024) return bytes + ' B'
        if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
        return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
    }

    // Step 3: Review
    updateReviewSection() {
        // Waiver Type
        if (this.hasReviewWaiverTypeTarget && this.selectedWaiverType) {
            this.reviewWaiverTypeTarget.textContent = this.selectedWaiverType.name
        }

        // Show/hide sections based on mode
        if (this.isAttestMode) {
            // Attestation Mode
            if (this.hasReviewUploadSectionTarget) {
                this.reviewUploadSectionTarget.classList.add('d-none')
            }
            if (this.hasReviewAttestSectionTarget) {
                this.reviewAttestSectionTarget.classList.remove('d-none')
            }

            // Display attestation reason
            if (this.hasReviewAttestReasonTarget) {
                this.reviewAttestReasonTarget.textContent = this.attestReason || 'Not selected'
            }

            // Get notes from attest section
            if (this.hasAttestNotesTarget) {
                this.attestNotes = this.attestNotesTarget.value
            }

            // Display attestation notes if provided
            if (this.hasReviewAttestNotesTarget && this.hasReviewAttestNotesSectionTarget) {
                if (this.attestNotes && this.attestNotes.trim().length > 0) {
                    this.reviewAttestNotesTarget.textContent = this.attestNotes
                    this.reviewAttestNotesSectionTarget.classList.remove('d-none')
                } else {
                    this.reviewAttestNotesSectionTarget.classList.add('d-none')
                }
            }
        } else {
            // Upload Mode
            if (this.hasReviewUploadSectionTarget) {
                this.reviewUploadSectionTarget.classList.remove('d-none')
            }
            if (this.hasReviewAttestSectionTarget) {
                this.reviewAttestSectionTarget.classList.add('d-none')
            }

            // Page Count - calculate total pages including multi-page PDFs
            const totalPages = this.uploadedPages.reduce((sum, page) => {
                return sum + (page.pdfPageCount > 1 ? page.pdfPageCount : 1)
            }, 0)
            
            if (this.hasReviewPageCountTarget) {
                if (totalPages !== this.uploadedPages.length) {
                    this.reviewPageCountTarget.textContent = `${this.uploadedPages.length} files (${totalPages} total pages)`
                } else {
                    this.reviewPageCountTarget.textContent = this.uploadedPages.length
                }
            }

            // Pages List
            if (this.hasReviewPagesListTarget) {
                const html = this.uploadedPages.map(page => {
                    // Use thumbnail for PDFs, dataUrl for images
                    const imgSrc = page.isPdf && page.thumbnailUrl ? page.thumbnailUrl : page.dataUrl
                    const imgStyle = page.isPdf 
                        ? 'height: 200px; object-fit: contain; background: #f8f9fa;' 
                        : 'height: 200px; object-fit: cover;'
                    
                    // Show page count badge for multi-page PDFs
                    const pageCountBadge = page.isPdf && page.pdfPageCount > 1 
                        ? `<span class="badge bg-danger position-absolute top-0 end-0 m-2">${page.pdfPageCount} pages</span>` 
                        : ''
                    
                    // PDF icon overlay if no thumbnail available
                    const pdfFallback = page.isPdf && !page.thumbnailUrl
                        ? `<div class="d-flex align-items-center justify-content-center" style="height: 200px; background: #f8f9fa;">
                             <div class="text-center">
                               <i class="bi bi-file-earmark-pdf text-danger" style="font-size: 4rem;"></i>
                               <div class="text-muted">PDF Document</div>
                               ${page.pdfPageCount > 1 ? `<div class="text-info">${page.pdfPageCount} pages</div>` : ''}
                             </div>
                           </div>`
                        : `<img src="${imgSrc}" class="card-img-top" style="${imgStyle}" alt="Page ${page.number}">`
                    
                    return `
                    <div class="col-md-3 mb-3">
                        <div class="card position-relative">
                            ${pdfFallback}
                            ${pageCountBadge}
                            <div class="card-body p-2 text-center">
                                <small>${page.isPdf ? 'PDF' : 'Page'} ${page.number}</small>
                            </div>
                        </div>
                    </div>
                `}).join('')
                this.reviewPagesListTarget.innerHTML = html
            }

            // Get notes if available
            if (this.hasNotesFieldTarget) {
                this.notes = this.notesFieldTarget.value
            }
        }
    }

    validateWaiverType() {
        if (!this.selectedWaiverType) {
            this.showError("Please select a waiver type")
            return false
        }
        if (this.isWaiverTypeAttested(this.selectedWaiverType.id)) {
            this.showError("This waiver type has been attested as not needed for this gathering.")
            return false
        }
        return true
    }

    validateUploadOrAttest() {
        if (this.isAttestMode) {
            if (!this.attestReason) {
                this.showError("Please select a reason for the exemption")
                return false
            }
            return true
        }

        if (this.uploadedPages.length === 0) {
            this.showError("Please add at least one page")
            return false
        }

        return true
    }

    validateReview() {
        return this.validateWaiverType() && this.validateUploadOrAttest()
    }

    // Form Submission
    async submitForm(event) {
        event.preventDefault()

        if (!this.validateReview()) {
            return
        }

        // Disable submit button and show processing page immediately
        if (this.hasSubmitButtonTarget) {
            this.submitButtonTarget.disabled = true
        }
        this.showProcessingStep()

        // Track when we started (for minimum 2-second display)
        const startTime = Date.now()

        try {
            if (this.isAttestMode) {
                // Submit attestation
                await this.submitAttestation(startTime)
            } else {
                // Submit waiver upload
                await this.submitWaiverUpload(startTime)
            }
        } catch (error) {
            console.error('Submission error:', error)
            this.showError('An error occurred during submission. Please try again.')
            if (this.hasSubmitButtonTarget) {
                this.submitButtonTarget.disabled = false
            }
            if (this.hasSubmitButtonTextTarget) {
                this.submitButtonTextTarget.textContent = this.isAttestMode ? 'Submit Attestation' : 'Submit Waivers'
            }
        }
    }

    async submitAttestation(startTime) {
        const attestUrl = this.hasAttestUrlValue ? this.attestUrlValue : '/waivers/gathering-waivers/attest'

        const response = await fetch(attestUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Requested-With': 'XMLHttpRequest',
                'X-CSRF-Token': this.getCsrfToken()
            },
            body: JSON.stringify({
                gathering_id: this.gatheringIdValue,
                waiver_type_id: this.selectedWaiverType.id,
                reason: this.attestReason,
                notes: this.attestNotes
            })
        })

        try {
            const data = await response.json()

            if (response.ok && data.success) {
                // Attestation succeeded
                const elapsed = Date.now() - startTime
                const remainingTime = Math.max(0, 2000 - elapsed)

                setTimeout(() => {
                    if (data.redirectUrl) {
                        window.location.href = data.redirectUrl
                    } else {
                        // Fallback redirect using gatheringViewUrl or construct from public_id
                        const fallbackUrl = this.hasGatheringViewUrlValue
                            ? this.gatheringViewUrlValue
                            : `/gatherings/view/${this.gatheringPublicIdValue}?tab=gathering-waivers`
                        window.location.href = fallbackUrl
                    }
                }, remainingTime)
            } else {
                // Attestation failed
                this.showError(data.message || 'Attestation failed. Please try again.')
                if (this.hasSubmitButtonTarget) {
                    this.submitButtonTarget.disabled = false
                }
                if (this.hasSubmitButtonTextTarget) {
                    this.submitButtonTextTarget.textContent = 'Submit Attestation'
                }
            }
        } catch (error) {
            this.showError('Network error. Please try again.')
            if (this.hasSubmitButtonTarget) {
                this.submitButtonTarget.disabled = false
            }
            if (this.hasSubmitButtonTextTarget) {
                this.submitButtonTextTarget.textContent = 'Submit Attestation'
            }
        }
    }

    async submitWaiverUpload(startTime) {
        const formData = new FormData()

        // Add gathering ID
        formData.append('gathering_id', this.gatheringIdValue)

        // Add waiver type
        formData.append('waiver_type_id', this.selectedWaiverType.id)

        // Add notes
        formData.append('notes', this.notes)

        // Add all page files
        this.uploadedPages.forEach((page, index) => {
            formData.append('waiver_images[]', page.file)
        })

        // If any PDF has a client-generated thumbnail, send the first one
        const pdfWithThumbnail = this.uploadedPages.find(p => p.isPdf && p.thumbnailUrl)
        if (pdfWithThumbnail) {
            formData.append('client_thumbnail', pdfWithThumbnail.thumbnailUrl)
        }

        // Get CSRF token and add to form data (CakePHP expects it in the body)
        const csrfToken = this.getCsrfToken()
        if (csrfToken) {
            formData.append('_csrfToken', csrfToken)
        }

        // Submit via fetch
        const response = await fetch(window.location.href, {
            method: 'POST',
            body: formData,
            headers: {
                'X-Requested-With': 'XMLHttpRequest'
            }
        })

        if (response.ok) {
            // Parse JSON response
            const data = await response.json().catch((err) => {
                console.error('Failed to parse JSON response:', err)
                return {}
            })

            console.log('Upload response data:', data)

            // Calculate how long to wait (minimum 2 seconds total)
            const elapsed = Date.now() - startTime
            const remainingTime = Math.max(0, 2000 - elapsed)

            // Wait for remaining time, then redirect
            setTimeout(() => {
                if (data.redirectUrl) {
                    console.log('Redirecting to:', data.redirectUrl)
                    window.location.href = data.redirectUrl
                } else {
                    // Fallback redirect using gatheringViewUrl or construct from gathering ID
                    const fallbackUrl = this.hasGatheringViewUrlValue
                        ? this.gatheringViewUrlValue
                        : `/gatherings/view/${this.gatheringIdValue}`
                    window.location.href = fallbackUrl
                }
            }, remainingTime)

        } else {
            const data = await response.json().catch(() => ({}))
            this.showError(data.message || 'Upload failed. Please try again.')
            if (this.hasSubmitButtonTarget) {
                this.submitButtonTarget.disabled = false
            }
            if (this.hasSubmitButtonTextTarget) {
                this.submitButtonTextTarget.textContent = 'Submit Waivers'
            }
        }
    }

    showProcessingStep() {
        // Hide all regular steps
        this.stepTargets.forEach(step => step.classList.add('d-none'))

        // Show processing message based on mode
        let processingHtml
        if (this.isAttestMode) {
            processingHtml = `
                <div class="text-center py-5">
                    <div class="mb-4">
                        <div class="spinner-border text-primary" role="status" style="width: 5rem; height: 5rem;">
                            <span class="visually-hidden">Processing...</span>
                        </div>
                    </div>
                    <h2 class="mb-3">Processing Your Attestation</h2>
                    <p class="lead text-muted mb-4">
                        Please wait while we record your attestation...
                    </p>
                    <div class="alert alert-info d-inline-block">
                        <i class="bi bi-shield-check"></i>
                        Attesting that a waiver is not needed for this gathering
                    </div>
                </div>
            `
        } else {
            processingHtml = `
                <div class="text-center py-5">
                    <div class="mb-4">
                        <div class="spinner-border text-primary" role="status" style="width: 5rem; height: 5rem;">
                            <span class="visually-hidden">Uploading...</span>
                        </div>
                    </div>
                    <h2 class="mb-3">Processing Your Waiver</h2>
                    <p class="lead text-muted mb-4">
                        Please wait while we upload and process your waiver...
                    </p>
                    <div class="alert alert-info d-inline-block">
                        <i class="bi bi-info-circle"></i>
                        Uploading ${this.uploadedPages.length} page(s)
                    </div>
                </div>
            `
        }

        const container = this.element.querySelector('.wizard-container') || this.element
        container.innerHTML = processingHtml
    }

    // Validation
    validateCurrentStep() {
        switch (this.currentStepValue) {
            case 1: return this.validateWaiverType()
            case 2: return this.validateUploadOrAttest()
            case 3: return this.validateReview()
            default: return true
        }
    }

    // Error Handling
    showError(message) {
        // Check if we're showing the processing screen (wizard container innerHTML was replaced)
        const container = this.element.querySelector('.wizard-container') || this.element
        const isProcessing = container.querySelector('h2') &&
            (container.querySelector('h2').textContent.includes('Processing Your Attestation') ||
                container.querySelector('h2').textContent.includes('Processing Your Waiver'))

        if (isProcessing) {
            // We're in the processing screen, so we can't show a toast
            // Check if we're in mobile mode by checking the URL
            const isMobile = window.location.pathname.includes('mobile-upload')

            if (isMobile) {
                // Redirect to mobile card using mobileSelectUrl if available, otherwise construct URL
                const mobileUrl = this.hasMobileSelectUrlValue
                    ? `${this.mobileSelectUrlValue}?error=${encodeURIComponent(message)}`
                    : `/waivers/gathering-waivers/mobile-select-gathering?error=${encodeURIComponent(message)}`
                window.location.href = mobileUrl
            } else {
                // Desktop mode - redirect back to the gathering with a flash message
                const gatheringPublicId = this.gatheringPublicIdValue
                const desktopUrl = this.hasGatheringViewUrlValue
                    ? `${this.gatheringViewUrlValue}&error=${encodeURIComponent(message)}`
                    : `/gatherings/view/${gatheringPublicId}?tab=gathering-waivers&error=${encodeURIComponent(message)}`
                window.location.href = desktopUrl
            }
            return
        }

        // Create toast notification
        const toastHtml = `
            <div class="toast align-items-center text-white bg-danger border-0" role="alert">
                <div class="d-flex">
                    <div class="toast-body">
                        <i class="bi bi-exclamation-triangle me-2"></i>${message}
                    </div>
                    <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
                </div>
            </div>
        `

        // Add to toast container or create one
        let toastContainer = document.querySelector('.toast-container')
        if (!toastContainer) {
            toastContainer = document.createElement('div')
            toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3'
            document.body.appendChild(toastContainer)
        }

        toastContainer.insertAdjacentHTML('beforeend', toastHtml)
        const toastElement = toastContainer.lastElementChild
        const toast = new bootstrap.Toast(toastElement)
        toast.show()

        // Remove after hidden
        toastElement.addEventListener('hidden.bs.toast', () => {
            toastElement.remove()
        })
    }

    getCsrfToken() {
        // Try to get from meta tag first (CakePHP default)
        const metaTag = document.querySelector('meta[name="csrf-token"]')
            || document.querySelector('meta[name="csrfToken"]')
        if (metaTag) {
            return metaTag.content
        }

        // Try to get from cookie as fallback
        const match = document.cookie.match(/csrfToken=([^;]+)/)
        if (match) {
            return match[1]
        }

        // Try to get from hidden input in any form
        const hiddenInput = document.querySelector('input[name="_csrfToken"]')
        if (hiddenInput) {
            return hiddenInput.value
        }

        console.error('CSRF token not found')
        return ''
    }

    /**
     * Format bytes to human-readable string
     * 
     * @param {number} bytes - Size in bytes
     * @param {number} decimals - Number of decimal places
     * @returns {string} Formatted size string
     */
    formatBytes(bytes, decimals = 2) {
        if (bytes === 0) return '0 Bytes'

        const k = 1024
        const dm = decimals < 0 ? 0 : decimals
        const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']

        const i = Math.floor(Math.log(bytes) / Math.log(k))

        return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
    }

    /**
     * Update the display to show current total size
     */
    updateTotalSizeDisplay() {
        const totalSize = this.uploadedPages.reduce((sum, page) => sum + page.size, 0)
        const totalFormatted = this.formatBytes(totalSize)

        // If we're getting close to the limit, show a warning
        if (this.hasTotalMaxSizeValue) {
            const percentUsed = (totalSize / this.totalMaxSizeValue) * 100

            if (percentUsed > 80 && percentUsed <= 100) {
                // Show warning when using 80-100% of limit
                const remaining = this.totalMaxSizeValue - totalSize
                const remainingFormatted = this.formatBytes(remaining)
                const maxFormatted = this.formatBytes(this.totalMaxSizeValue)

                console.warn(
                    `Upload size warning: ${totalFormatted} of ${maxFormatted} used. ` +
                    `${remainingFormatted} remaining.`
                )
            }
        }
    }
}

// Register controller
if (!window.Controllers) {
    window.Controllers = {}
}
window.Controllers["waiver-upload-wizard"] = WaiverUploadWizardController

export default WaiverUploadWizardController