assets_js_controllers_permission-import-controller.js

import { Controller } from "@hotwired/stimulus"

/**
 * Permission Import Controller
 * 
 * Handles permission policy import workflow with preview modal showing
 * what policies will be added and removed during the sync operation.
 * 
 * Features:
 * - File upload handling for JSON import files
 * - Preview analysis showing additions and removals
 * - Confirmation modal before applying changes
 * - Progress feedback during import
 * 
 * @class PermissionImport
 * @extends Controller
 * 
 * HTML Structure Example:
 * ```html
 * <div data-controller="permission-import"
 *      data-permission-import-preview-url-value="/permissions/preview-import"
 *      data-permission-import-import-url-value="/permissions/import-policies">
 *   <input type="file" data-permission-import-target="fileInput" data-action="change->permission-import#handleFileSelect">
 *   <button data-action="click->permission-import#triggerFileSelect">Import</button>
 *   
 *   <!-- Modal for preview -->
 *   <div class="modal" data-permission-import-target="modal">
 *     <div data-permission-import-target="modalContent"></div>
 *     <button data-action="click->permission-import#confirmImport">Confirm</button>
 *     <button data-action="click->permission-import#cancelImport">Cancel</button>
 *   </div>
 * </div>
 * ```
 */
class PermissionImport extends Controller {
    static targets = ["fileInput", "modal", "modalContent", "addList", "removeList", "confirmBtn", "summary", "loadingOverlay"]
    static values = {
        previewUrl: String,
        importUrl: String,
        buttonContainer: String,  // Selector for external button container
    }

    /** @type {string|null} Base64 encoded import data for final submission */
    importData = null

    /** @type {HTMLInputElement|null} External file input reference */
    externalFileInput = null

    /** @type {HTMLElement|null} External import button reference */
    externalImportButton = null

    /** @type {Function|null} Bound handleFileSelect reference for cleanup */
    boundHandleFileSelect = null

    /** @type {Function|null} Bound triggerFileSelect reference for cleanup */
    boundTriggerFileSelect = null

    /**
     * Initialize controller
     */
    connect() {
        this.importData = null

        // If buttons are in an external container, wire them up
        if (this.hasButtonContainerValue && this.buttonContainerValue) {
            const container = document.querySelector(this.buttonContainerValue)
            if (container) {
                // Find the file input in the external container
                this.externalFileInput = container.querySelector('input[type="file"]')
                if (this.externalFileInput) {
                    this.boundHandleFileSelect = this.handleFileSelect.bind(this)
                    this.externalFileInput.addEventListener('change', this.boundHandleFileSelect)
                }

                // Find the import button and wire it up
                const importBtn = container.querySelector('[data-action*="triggerFileSelect"]')
                if (importBtn) {
                    this.externalImportButton = importBtn
                    this.boundTriggerFileSelect = this.triggerFileSelect.bind(this)
                    importBtn.addEventListener('click', this.boundTriggerFileSelect)
                }
            }
        }
    }

    /**
     * Trigger file input click
     * Called when the import button is clicked
     */
    triggerFileSelect(event) {
        event.preventDefault()
        // Use external file input if available, otherwise use target
        const fileInput = this.externalFileInput || (this.hasFileInputTarget ? this.fileInputTarget : null)
        if (fileInput) {
            fileInput.click()
        }
    }

    /**
     * Handle file selection
     * Validates file type and initiates preview request
     * 
     * @param {Event} event - Change event from file input
     */
    handleFileSelect(event) {
        const file = event.target.files[0]
        if (!file) return

        // Validate file type
        if (!file.name.endsWith('.json')) {
            alert('Please select a JSON file.')
            event.target.value = ''
            return
        }

        this.showLoadingOverlay()
        this.previewImport(file)
    }

    /**
     * Get the file input element (either external or target)
     * @returns {HTMLInputElement|null}
     */
    getFileInput() {
        return this.externalFileInput || (this.hasFileInputTarget ? this.fileInputTarget : null)
    }

    /**
     * Reset the file input value
     */
    resetFileInput() {
        const fileInput = this.getFileInput()
        if (fileInput) {
            fileInput.value = ''
        }
    }

    /**
     * Send file to server for preview analysis
     * 
     * @param {File} file - The uploaded JSON file
     */
    async previewImport(file) {
        const formData = new FormData()
        formData.append('import_file', file)

        try {
            const response = await fetch(this.previewUrlValue, {
                method: 'POST',
                headers: {
                    'X-CSRF-Token': document.querySelector("meta[name='csrf-token']").content,
                },
                body: formData,
            })

            const data = await response.json()

            if (!response.ok || data.error) {
                this.hideLoadingOverlay()
                alert(data.error || 'Failed to preview import file.')
                this.resetFileInput()
                return
            }

            // Store import data for final submission
            this.importData = data.import_data

            // Display preview modal
            this.displayPreview(data.changes)
            this.hideLoadingOverlay()
            this.showModal()

        } catch (error) {
            console.error('Preview error:', error)
            this.hideLoadingOverlay()
            alert('An error occurred while analyzing the import file.')
            this.resetFileInput()
        }
    }

    /**
     * Display preview information in modal
     * 
     * @param {Object} changes - The changes object from preview response
     */
    displayPreview(changes) {
        // Update summary
        if (this.hasSummaryTarget) {
            let summaryContent = `
                <div class="alert alert-info">
                    <strong>Import Summary</strong>
                    ${changes.source_permission ? `<br><small class="text-muted">Source: ${this.escapeHtml(changes.source_permission)}</small>` : ''}
                    <ul class="mb-0 mt-2">
                        <li><span class="badge bg-success">${changes.summary.total_add}</span> policies will be added</li>
                        <li><span class="badge bg-danger">${changes.summary.total_remove}</span> policies will be removed</li>
                    </ul>
                </div>
            `
            this.summaryTarget.innerHTML = summaryContent
        }

        // Display policies to add
        if (this.hasAddListTarget) {
            if (changes.policies_to_add.length > 0) {
                let addHtml = '<h6 class="text-success"><i class="bi bi-plus-circle me-1"></i>Policies to Add:</h6>'
                addHtml += '<div class="list-group list-group-flush" style="max-height: 200px; overflow-y: auto;">'
                changes.policies_to_add.forEach(policy => {
                    addHtml += `
                        <div class="list-group-item list-group-item-success py-1 px-2">
                            <small><code>${this.formatPolicyName(policy.policy_class)}::${this.escapeHtml(policy.policy_method)}</code></small>
                        </div>
                    `
                })
                addHtml += '</div>'
                this.addListTarget.innerHTML = addHtml
            } else {
                this.addListTarget.innerHTML = '<p class="text-muted small">No policies to add.</p>'
            }
        }

        // Display policies to remove
        if (this.hasRemoveListTarget) {
            if (changes.policies_to_remove.length > 0) {
                let removeHtml = '<h6 class="text-danger"><i class="bi bi-dash-circle me-1"></i>Policies to Remove:</h6>'
                removeHtml += '<div class="list-group list-group-flush" style="max-height: 200px; overflow-y: auto;">'
                changes.policies_to_remove.forEach(policy => {
                    removeHtml += `
                        <div class="list-group-item list-group-item-danger py-1 px-2">
                            <small><code>${this.formatPolicyName(policy.policy_class)}::${this.escapeHtml(policy.policy_method)}</code></small>
                        </div>
                    `
                })
                removeHtml += '</div>'
                this.removeListTarget.innerHTML = removeHtml
            } else {
                this.removeListTarget.innerHTML = '<p class="text-muted small">No policies to remove.</p>'
            }
        }

        // Enable/disable confirm button based on changes
        if (this.hasConfirmBtnTarget) {
            const hasChanges = changes.summary.total_add > 0 || changes.summary.total_remove > 0
            this.confirmBtnTarget.disabled = !hasChanges
            if (!hasChanges) {
                this.summaryTarget.innerHTML = `
                    <div class="alert alert-success">
                        <i class="bi bi-check-circle me-2"></i><strong>No changes needed!</strong>
                        The current permission policies match the import file exactly.
                    </div>
                `
            }
        }
    }

    /**
     * Format policy class name for display
     * Extracts just the class name from full namespace
     * 
     * @param {string} policyClass - Full policy class name
     * @returns {string} Formatted class name
     */
    formatPolicyName(policyClass) {
        const parts = policyClass.split('\\')
        return parts[parts.length - 1]
    }

    /**
     * Escape HTML to prevent XSS
     * 
     * @param {string} text - Text to escape
     * @returns {string} Escaped text
     */
    escapeHtml(text) {
        const div = document.createElement('div')
        div.textContent = text
        return div.innerHTML
    }

    /**
     * Show the preview modal
     */
    showModal() {
        if (this.hasModalTarget) {
            const bsModal = new bootstrap.Modal(this.modalTarget)
            bsModal.show()
        }
    }

    /**
     * Hide the preview modal
     */
    hideModal() {
        if (this.hasModalTarget) {
            const bsModal = bootstrap.Modal.getInstance(this.modalTarget)
            if (bsModal) {
                bsModal.hide()
            }
        }
    }

    /**
     * Show loading overlay during processing
     */
    showLoadingOverlay() {
        if (this.hasLoadingOverlayTarget) {
            this.loadingOverlayTarget.classList.remove('d-none')
        }
    }

    /**
     * Hide loading overlay
     */
    hideLoadingOverlay() {
        if (this.hasLoadingOverlayTarget) {
            this.loadingOverlayTarget.classList.add('d-none')
        }
    }

    /**
     * Cancel import operation
     * Closes modal and resets state
     */
    cancelImport(event) {
        event.preventDefault()
        this.hideModal()
        this.importData = null
        this.resetFileInput()
        this.resetModalContent()
    }

    /**
     * Reset modal content for next use
     */
    resetModalContent() {
        if (this.hasSummaryTarget) this.summaryTarget.innerHTML = ''
        if (this.hasAddListTarget) this.addListTarget.innerHTML = ''
        if (this.hasRemoveListTarget) this.removeListTarget.innerHTML = ''
    }

    /**
     * Confirm and execute the import
     * Sends the import data to the server for processing
     */
    async confirmImport(event) {
        event.preventDefault()

        if (!this.importData) {
            alert('No import data available.')
            return
        }

        if (this.hasConfirmBtnTarget) {
            this.confirmBtnTarget.disabled = true
            this.confirmBtnTarget.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Importing...'
        }

        try {
            const response = await fetch(this.importUrlValue, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                    'X-CSRF-Token': document.querySelector("meta[name='csrf-token']").content,
                },
                body: JSON.stringify({
                    import_data: this.importData,
                }),
            })

            const data = await response.json()

            if (!response.ok || data.error) {
                alert(data.error || 'Import failed.')
                return
            }

            // Show success message
            const results = data.results
            let message = `Import completed successfully!\n\nAdded: ${results.added} policies\nRemoved: ${results.removed} policies`
            if (results.errors && results.errors.length > 0) {
                message += `\n\nWarnings:\n${results.errors.join('\n')}`
            }
            alert(message)

            // Close modal and refresh page
            this.hideModal()
            window.location.reload()

        } catch (error) {
            console.error('Import error:', error)
            alert('An error occurred during import.')
        } finally {
            if (this.hasConfirmBtnTarget) {
                this.confirmBtnTarget.disabled = false
                this.confirmBtnTarget.innerHTML = 'Confirm Import'
            }
            this.importData = null
            this.resetFileInput()
        }
    }

    /**
     * Clean up on disconnect
     */
    disconnect() {
        this.importData = null
        // Remove event listeners from external elements
        if (this.externalFileInput && this.boundHandleFileSelect) {
            this.externalFileInput.removeEventListener('change', this.boundHandleFileSelect)
        }

        if (this.externalImportButton && this.boundTriggerFileSelect) {
            this.externalImportButton.removeEventListener('click', this.boundTriggerFileSelect)
        }

        this.boundHandleFileSelect = null
        this.boundTriggerFileSelect = null
        this.externalFileInput = null
        this.externalImportButton = null
    }
}

if (!window.Controllers) {
    window.Controllers = {}
}
window.Controllers["permission-import"] = PermissionImport