(function($) {
    'use strict';

    const FO = window.KookoaWPF;
    const SVG_FOLDER = FO.SVG_FOLDER;
    const SVG_FOLDER_OPEN = FO.SVG_FOLDER_OPEN;

    // ========================
    // Media Modal Integration (MutationObserver approach)
    // ========================

    const KookoawpfMediaModal = window.KookoawpfMediaModal = {

        _observer: null,
        _active: false,

        /**
         * Start watching for media modals appearing in the DOM.
         */
        _prefilterRegistered: false,

        _modalHandlersBound: false,

        init: function() {
            if (this._active) return;
            this._active = true;

            const self = this;

            // Apply body-level settings that FO.init() normally handles
            if (FO.data && !FO.tree) {
                if (!FO.data.showCount) $('body').addClass('kookoawpf-hide-counts');
            }

            // Register ajaxPrefilter for folder filtering (needed on pages without main sidebar)
            self._registerAjaxPrefilter();

            // Bind modal handlers (close, swatch click, icon click, delete confirm)
            // if FO.init() hasn't run (i.e. no main sidebar on this page).
            self._bindModalHandlers();

            // On frontend builder pages, Divi strips display:none elements.
            // Create modals dynamically from JSON data passed by PHP.
            // On frontend builder pages (Divi), modals created at page load
            // get stripped by Divi's DOM cleanup. Flag that we need on-demand creation.
            if (window.kookoawpfBuilderModals) {
                FO._builderModalData = window.kookoawpfBuilderModals;
            }

            // Register keyboard shortcuts early (before WP media modal registers its own)
            self._initModalShortcuts();

            // Watch for .media-modal being added to the DOM
            self._observer = new MutationObserver(function(mutations) {
                for (let i = 0; i < mutations.length; i++) {
                    const added = mutations[i].addedNodes;
                    for (let j = 0; j < added.length; j++) {
                        if (added[j].nodeType !== 1) continue;
                        const $node = $(added[j]);

                        // Check if this node IS a media-modal or CONTAINS one
                        const $modal = $node.hasClass('media-modal') ? $node : $node.find('.media-modal');
                        if ($modal.length) {
                            self._onModalAppear($modal.first());
                        }
                    }
                }
            });

            self._observer.observe(document.body, { childList: true, subtree: true });

            // Some builders (Elementor) create the .media-modal at page load and toggle visibility.
            // The childList observer misses these. Poll for visible unhandled modals.
            self._visibilityPoll = setInterval(function() {
                $('.media-modal:visible').each(function() {
                    const $m = $(this);
                    const $frame = $m.find('.media-frame');
                    if ($frame.length && !$frame.data('kookoawpf-mounted')) {
                        self._onModalAppear($m);
                    }
                });
            }, 500);
        },

        /**
         * Called when a .media-modal element appears in the DOM.
         */
        _onModalAppear: function($modal) {
            const self = this;

            // Don't inject on pages with the main sidebar (upload.php, edit.php)
            // to avoid duplicate #kookoawpf-jstree IDs
            if ($('#kookoawpf-sidebar-wrapper').length) return;

            // Wait for the .media-frame inside the modal to be ready.
            // We don't require .attachments-browser here because some hosts
            // (e.g. WooCommerce product images) open the modal on the
            // "Upload Files" tab, where .attachments-browser doesn't exist
            // until the user switches to "Media Library". Waiting on
            // .media-frame-content guarantees the frame's content area is
            // mounted and is present in BOTH tabs.
            let attempts = 0;
            const waitForFrame = setInterval(function() {
                attempts++;
                const $frame = $modal.find('.media-frame');
                if ($frame.length && $frame.find('.media-frame-content').length) {
                    clearInterval(waitForFrame);

                    // Avoid double-mounting
                    if ($frame.data('kookoawpf-mounted')) return;
                    $frame.data('kookoawpf-mounted', true);

                    self._injectSidebar($frame);

                    // Force the modal to open on the Media Library tab even when
                    // the host (e.g. WooCommerce product image) defaults to the
                    // "Upload Files" tab. Showing the folder sidebar with counts
                    // next to an empty upload pane feels disconnected — the user
                    // expects to see the actual files of the selected folder.
                    self._forceLibraryTab($frame);

                    // Move PHP-rendered plugin dialogs inside the media modal
                    // so they appear above it in the stacking context.
                    $('#kookoawpf-modal-color, #kookoawpf-modal-icon, #kookoawpf-modal-delete, #kookoawpf-modal-move-to-folder, #kookoawpf-modal-shortcuts').each(function() {
                        if (!$modal.find('#' + this.id).length) {
                            $(this).appendTo($modal);
                        }
                    });

                    self._watchModalClose($modal, $frame);
                }
                if (attempts > 50) clearInterval(waitForFrame); // Safety: stop after 5s
            }, 100);
        },

        /**
         * Watch for modal close to reset state.
         */
        _watchModalClose: function($modal, $frame) {
            const self = this;
            const closeObs = new MutationObserver(function(mutations) {
                // Check if modal was hidden or removed
                if (!$modal.is(':visible') || !$.contains(document, $modal[0])) {
                    FO._modalCurrentFolder = null;
                    $frame.removeData('kookoawpf-mounted');
                    $(document).off('click.kookoawpf-modal-ctx');
                    $(document).off('ajaxComplete.kookoawpf-modal-delete');
                    if (self._modalUploadObserver) { self._modalUploadObserver.disconnect(); self._modalUploadObserver = null; }
                    if (self._modalContentObserver) { self._modalContentObserver.disconnect(); self._modalContentObserver = null; }
                    closeObs.disconnect();

                    // Move plugin dialogs back to body so they're not lost
                    $modal.find('#kookoawpf-modal-color, #kookoawpf-modal-icon, #kookoawpf-modal-delete, #kookoawpf-modal-move-to-folder, #kookoawpf-modal-shortcuts').appendTo('body');
                    // Also move context menu back
                    const $ctx = $modal.find('#kookoawpf-context-menu');
                    if ($ctx.length) $ctx.appendTo('body');
                }
            });
            closeObs.observe($modal[0].parentNode || document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
        },

        /**
         * Register $.ajaxPrefilter for folder filtering.
         * This is needed on pages without the main sidebar (e.g. product edit pages)
         * where FO.init() / _initMediaGridFilter() never runs.
         */
        _registerAjaxPrefilter: function() {
            if (this._prefilterRegistered) return;
            this._prefilterRegistered = true;

            $.ajaxPrefilter(function(options) {
                if (!options.data) return;

                // Backbone (wp.media.model.Query) passes options.data as a plain
                // object. jQuery only stringifies it AFTER the prefilter chain,
                // so handle both shapes — otherwise the modal request bypasses
                // injection and the PHP filter never sees the folder/nonce.
                const dataIsObject = (typeof options.data === 'object');
                const dataIsString = (typeof options.data === 'string');
                if (!dataIsObject && !dataIsString) return;

                const isQueryAttachments = dataIsString
                    ? options.data.indexOf('action=query-attachments') !== -1
                    : (options.data.action === 'query-attachments');
                if (!isQueryAttachments) return;

                // Only inject folder filter if our sidebar is actually in the DOM
                if (!$('#kookoawpf-sidebar-wrapper').length && !$('.kookoawpf-modal-sidebar').length) return;

                let folder = null;
                if (FO._modalCurrentFolder && FO._modalCurrentFolder !== 'all') {
                    folder = FO._modalCurrentFolder;
                } else if (!$('.kookoawpf-modal-sidebar').length && FO.currentFolder && FO.currentFolder !== 'all') {
                    // Main sidebar context (only when no modal sidebar present)
                    folder = FO.currentFolder;
                }
                if (!folder) return;

                const kkNonce = (FO.data && FO.data.nonce) ? FO.data.nonce : '';
                const folderStr = String(folder);

                if (dataIsObject) {
                    options.data.kookoawpf_folder = folderStr;
                    options.data.kookoawpf_nonce = kkNonce;
                    if (!options.data.query || typeof options.data.query !== 'object') {
                        options.data.query = {};
                    }
                    options.data.query.kookoawpf_folder = folderStr;
                    return;
                }

                // String path (legacy: jQuery-style data already stringified)
                options.data = options.data.replace(/&?query%5Bkookoawpf_folder%5D=[^&]*/g, '');
                options.data = options.data.replace(/&?kookoawpf_folder=[^&]*/g, '');
                options.data = options.data.replace(/&?kookoawpf_nonce=[^&]*/g, '');
                options.data += '&kookoawpf_folder=' + encodeURIComponent(folderStr);
                options.data += '&kookoawpf_nonce=' + encodeURIComponent(kkNonce);
                options.data += '&query%5Bkookoawpf_folder%5D=' + encodeURIComponent(folderStr);
            });
        },

        /**
         * Inject the folder sidebar into a media modal frame.
         */
        _injectSidebar: function($frame) {
            const self = this;
            const sidebarW = FO.data.sidebarWidth || 300;

            // Remove stale sidebar from a previous modal session (reopen without page reload)
            $frame.find('.kookoawpf-modal-sidebar').each(function() {
                const $old = $(this).find('#kookoawpf-jstree');
                if ($old.jstree(true)) $old.jstree('destroy');
                $(this).remove();
            });
            $frame.find('.kookoawpf-modal-breadcrumb').remove();

            // Use the SAME classes as the main sidebar so all existing CSS applies.
            // Width and font-size come from settings; apply via DOM API rather
            // than concatenating into a style attribute so that any odd value
            // (corrupted option, future extension) can't end the style declaration.
            const fontSize = FO.data.treeFontSize || '13px';
            const $sidebar = $('<div class="kookoawpf-modal-sidebar kookoawpf-sidebar-wrapper"></div>');
            $sidebar.css('width', ( parseInt(sidebarW, 10) || 300 ) + 'px');
            if (/^\d+px$/.test(fontSize) && $sidebar[0]) {
                $sidebar[0].style.setProperty('--kookoawpf-tree-font-size', fontSize);
            }

            // Static skeleton — every localized string is set via .text() /
            // .attr() below so .po-controlled values never reach an HTML
            // literal.
            const html = ''
                + '<div class="kookoawpf-sidebar-header">'
                + '  <div class="kookoawpf-sidebar-title">'
                + '    <span class="kookoawpf-brand">KK&gt;_</span> Folders'
                + '  </div>'
                + '  <div class="kookoawpf-sidebar-actions">'
                + '    <button type="button" class="button kookoawpf-btn-new kookoawpf-modal-btn-new">'
                + '      <span class="dashicons dashicons-plus-alt2"></span>'
                + '    </button>'
                + '  </div>'
                + '</div>'
                + '<div class="kookoawpf-sidebar-search">'
                + '  <input type="text" id="kookoawpf-modal-search-folders" class="kookoawpf-modal-search-input" />'
                + '</div>'
                + '<div class="kookoawpf-sidebar-nav">'
                + '  <a href="#" class="kookoawpf-nav-item kookoawpf-nav-all kookoawpf-modal-nav-item active" data-folder="all">'
                + '    <span class="dashicons dashicons-category"></span>'
                + '    <span class="kookoawpf-nav-label kookoawpf-nav-all-label"></span>'
                + '    <span class="kookoawpf-nav-count" data-counter="all"></span>'
                + '  </a>'
                + '  <a href="#" class="kookoawpf-nav-item kookoawpf-nav-uncategorized kookoawpf-modal-nav-item" data-folder="uncategorized">'
                + '    <span class="dashicons dashicons-portfolio"></span>'
                + '    <span class="kookoawpf-nav-label kookoawpf-nav-unc-label"></span>'
                + '    <span class="kookoawpf-nav-count" data-counter="uncategorized"></span>'
                + '  </a>'
                + '</div>'
                + '<div class="kookoawpf-tree-container">'
                // Use same ID as main tree so all existing CSS applies. No
                // conflict: on pages with main sidebar (upload.php/edit.php),
                // modal injection is skipped.
                + '  <div id="kookoawpf-jstree" class="kookoawpf-modal-jstree"></div>'
                + '</div>'
                + '<div class="kookoawpf-modal-empty-state" style="display:none;">'
                + '  <p class="kookoawpf-empty-state-text"></p>'
                + '  <button type="button" class="kookoawpf-empty-state-btn kookoawpf-modal-empty-new-folder">'
                + '    <span class="dashicons dashicons-plus-alt2"></span> <span class="kookoawpf-empty-state-btn-label"></span>'
                + '  </button>'
                + '</div>'
                + '<div class="kookoawpf-modal-resize-handle"></div>';

            $sidebar.html(html);
            $sidebar.find('.kookoawpf-modal-btn-new').attr('title', FO.data.strings.newFolder || 'New Folder');
            $sidebar.find('#kookoawpf-modal-search-folders').attr('placeholder', FO.data.strings.searchFolders || 'Search folders...');
            $sidebar.find('.kookoawpf-nav-all-label').text(FO.data.strings.allItems || 'All Items');
            $sidebar.find('.kookoawpf-nav-unc-label').text(FO.data.strings.uncategorized || 'Uncategorized');
            $sidebar.find('.kookoawpf-empty-state-text').text(FO.data.strings.createFirstFolder || 'Create your first folder!');
            $sidebar.find('.kookoawpf-empty-state-btn-label').text(FO.data.strings.newFolder || 'New Folder');
            $frame.addClass('kookoawpf-modal-has-sidebar');
            $frame.prepend($sidebar);

            // Inject breadcrumb bar into the content area (if enabled in settings)
            if (FO.data.showBreadcrumb) {
                const $breadcrumb = $('<div class="kookoawpf-breadcrumb-bar kookoawpf-modal-breadcrumb"></div>');
                if (FO.data.breadcrumbStyle === 'retro') {
                    $breadcrumb.addClass('kookoawpf-breadcrumb-retro');
                }
                $breadcrumb.append(
                    $('<nav class="kookoawpf-breadcrumb-nav"></nav>')
                        .attr('aria-label', FO.data.strings.breadcrumbLabel || 'Folder navigation')
                );
                const _insertBreadcrumb = function() {
                    const $content = $frame.find('.media-frame-content');
                    if ($content.length && !$content.find('.kookoawpf-modal-breadcrumb').length) {
                        $content.prepend($breadcrumb);
                        self._updateModalBreadcrumb($sidebar, $frame, FO._modalCurrentFolder || 'all');
                    }
                };
                _insertBreadcrumb();
                setTimeout(_insertBreadcrumb, 300);

                // Re-inject breadcrumb when WP replaces .media-frame-content
                // (e.g. switching between "Upload Files" and "Media Library" tabs)
                const contentEl = $frame.find('.media-frame-content')[0];
                if (contentEl) {
                    const _bcObs = new MutationObserver(function() {
                        _insertBreadcrumb();
                    });
                    _bcObs.observe(contentEl, { childList: true });
                }
            }

            // Set dynamic left offset based on sidebar width
            self._updateModalLayout($frame, sidebarW);

            // Block drag events on the sidebar so WP doesn't trigger "Drop files to upload"
            $sidebar[0].addEventListener('dragstart', function(e) { e.preventDefault(); e.stopPropagation(); }, true);
            $sidebar[0].addEventListener('dragenter', function(e) { e.preventDefault(); e.stopPropagation(); }, true);
            $sidebar[0].addEventListener('dragover',  function(e) { e.preventDefault(); e.stopPropagation(); }, true);
            $sidebar[0].addEventListener('drop',      function(e) { e.preventDefault(); e.stopPropagation(); }, true);

            // Block drag-and-drop uploads on the entire modal when a dynamic folder is selected
            const modalEl = $frame.closest('.media-modal')[0];
            if (modalEl) {
                function blockIfDynamicModal(e) {
                    if (!FO._modalCurrentFolderIsDynamic) return;
                    const dt = e.dataTransfer;
                    if (!dt) return;
                    const types = dt.types;
                    for (let i = 0; i < types.length; i++) {
                        if (types[i] === 'Files') {
                            e.preventDefault();
                            e.stopImmediatePropagation();
                            dt.dropEffect = 'none';
                            return;
                        }
                    }
                }
                modalEl.addEventListener('dragenter', blockIfDynamicModal, true);
                modalEl.addEventListener('dragover',  blockIfDynamicModal, true);
                modalEl.addEventListener('drop',      blockIfDynamicModal, true);
            }

            // Init resize drag
            self._initModalResize($sidebar, $frame);

            self._initModalTree($sidebar, $frame);
            self._bindModalEvents($sidebar, $frame);
        },

        /**
         * Load folder tree data and initialize jsTree inside the modal sidebar.
         */
        _initModalTree: function($sidebar, $frame) {
            const self = this;
            const $tree = $sidebar.find('#kookoawpf-jstree');

            // Destroy previous jsTree instance if re-initializing
            if ($tree.jstree(true)) {
                $tree.jstree('destroy');
            }

            // Smart Startup Folder — honor user preference on every modal open.
            // Modals always operate on the 'attachment' post type, so pull the
            // attachment-specific id from the startupFolderIds map (the current
            // screen's post type may be something else, e.g. 'post' on edit.php).
            const startupIds = (FO.data && FO.data.startupFolderIds) || {};
            const startupFid = parseInt(startupIds.attachment, 10);
            if (startupFid && startupFid > 0) {
                FO._modalCurrentFolder = String(startupFid);
            } else {
                FO._modalCurrentFolder = 'all';
            }

            // Force manual order in the modal: the modal has no sort UI, so we
            // ignore the user's library sort preference and always show / behave
            // as if sort = custom. This keeps modal interactions predictable
            // ("the new folder always lands at the bottom") regardless of what
            // sort the user has set in the main library sidebar.
            FO.ajax('kookoawpf_get_tree', { post_type: 'attachment', force_sort_by: 'custom' }, function(response) {
                if (!response.success) return;

                const treeData = response.data.tree || [];

                $sidebar.find('[data-counter="all"]').text(response.data.total_items || '');
                $sidebar.find('[data-counter="uncategorized"]').text(response.data.uncategorized || '');

                // Toggle empty state in modal
                if (treeData.length === 0) {
                    $sidebar.find('.kookoawpf-tree-container').hide();
                    $sidebar.find('.kookoawpf-modal-empty-state').show();
                } else {
                    $sidebar.find('.kookoawpf-tree-container').show();
                    $sidebar.find('.kookoawpf-modal-empty-state').hide();
                }

                $tree.jstree({
                    core: {
                        data: treeData,
                        animation: 0,
                        check_callback: function(op) { return op !== 'rename_node'; },
                        themes: { name: 'default', dots: true, icons: true },
                        multiple: false,
                        dblclick_toggle: false
                    },
                    plugins: ['search'],
                    search: {
                        show_only_matches: true,
                        show_only_matches_children: true,
                        search_callback: function(str, node) {
                            let text = node.data && node.data.original_text ? node.data.original_text : '';
                            if (!text) text = (node.text || '').replace(/<[^>]*>/g, '').trim();
                            return text.toLowerCase().indexOf(str.toLowerCase()) !== -1;
                        }
                    }
                });

                $tree.on('ready.jstree', function() {
                    // Disable jsTree inline edit (click-to-rename)
                    const inst = $tree.jstree(true);
                    if (inst) inst.edit = function() {};

                    self._applyModalFolderIcons($tree);
                    self._initModalContextMenu($tree, $sidebar, $frame);
                    self._initModalDragDrop($tree, $sidebar, $frame);
                    self._checkModalScrollbar($sidebar);

                    // Smart Startup Folder — select and reveal the saved folder.
                    if (FO._modalCurrentFolder && FO._modalCurrentFolder !== 'all' && FO._modalCurrentFolder !== 'uncategorized') {
                        const startupNodeId = 'folder_' + FO._modalCurrentFolder;
                        if (inst && inst.get_node(startupNodeId)) {
                            inst._open_to(startupNodeId);
                            self._selectModalFolder(FO._modalCurrentFolder, $sidebar, $frame);
                        } else {
                            // Saved folder no longer exists — fall back to All Files.
                            FO._modalCurrentFolder = 'all';
                            self._updateModalBreadcrumb($sidebar, $frame, 'all');
                        }
                    } else {
                        self._updateModalBreadcrumb($sidebar, $frame, FO._modalCurrentFolder || 'all');
                    }
                });

                // Re-apply icons when nodes are opened/closed/redrawn/searched
                $tree.on('open_node.jstree close_node.jstree redraw.jstree search.jstree clear_search.jstree', function() {
                    self._applyModalFolderIcons($tree);
                    setTimeout(function() { self._checkModalScrollbar($sidebar); }, 50);
                });

                $tree.on('select_node.jstree', function(e, obj) {
                    const folderId = obj.node.id.replace('folder_', '');
                    self._selectModalFolder(folderId, $sidebar, $frame);
                });
            });
        },

        /**
         * Refresh the modal tree in-place without destroy/recreate (no flash).
         * Mirrors the main sidebar refreshTree approach.
         */
        _refreshModalTree: function($sidebar, $frame, options) {
            const opts = options || {};
            const self = this;
            const $tree = $sidebar.find('#kookoawpf-jstree');
            const inst = $tree.length ? $tree.jstree(true) : null;

            // If no jsTree instance, fall back to full init
            if (!inst) {
                self._initModalTree($sidebar, $frame);
                return;
            }

            // Save opened node IDs and selected node
            const openedIds = [];
            $tree.find('.jstree-open').each(function() { openedIds.push(this.id); });
            FO.ajax('kookoawpf_get_tree', { post_type: 'attachment', force_sort_by: 'custom' }, function(response) {
                if (!response.success) return;

                const treeData = response.data.tree || [];

                // Update counters
                $sidebar.find('[data-counter="all"]').text(response.data.total_items || '');
                $sidebar.find('[data-counter="uncategorized"]').text(response.data.uncategorized || '');

                // Toggle empty state in modal
                if (treeData.length === 0) {
                    $sidebar.find('.kookoawpf-tree-container').hide();
                    $sidebar.find('.kookoawpf-modal-empty-state').show();
                } else {
                    $sidebar.find('.kookoawpf-modal-empty-state').hide();
                    $sidebar.find('.kookoawpf-tree-container').show();
                }

                // Reload tree data in-place
                inst.settings.core.data = treeData;
                inst._model.data = {};
                inst._model.data['#'] = {
                    id: '#', parent: null, parents: [],
                    children: [], children_d: [],
                    state: { loaded: false }
                };
                inst.load_node('#', function() {
                    // Restore opened state
                    for (let i = 0; i < openedIds.length; i++) {
                        if (inst.get_node(openedIds[i])) {
                            inst.open_node(openedIds[i], false, 0);
                        }
                    }

                    self._applyModalFolderIcons($tree);
                    self._checkModalScrollbar($sidebar);

                    // Re-init droppable on new tree nodes (load_node recreates DOM)
                    if (self._modalMakeDroppable) {
                        $tree.find('.jstree-node').each(function() {
                            self._modalMakeDroppable($(this));
                        });
                    }

                    // Reveal newly created subfolder by opening its parent chain
                    if (opts.openToNodeId) {
                        const revealId = 'folder_' + opts.openToNodeId;
                        if (inst.get_node(revealId)) {
                            inst._open_to(revealId);
                        }
                    }

                    // Re-select based on current navigated folder (not the folder that was acted upon)
                    const reselect = (FO._modalCurrentFolder && FO._modalCurrentFolder !== 'all' && FO._modalCurrentFolder !== 'uncategorized') ? 'folder_' + FO._modalCurrentFolder : null;
                    if (reselect && inst.get_node(reselect)) {
                        inst._open_to(reselect);
                        self._suppressModalSelect = true;
                        inst.deselect_all(true);
                        inst.select_node(reselect);
                        self._suppressModalSelect = false;

                        // Apply active folder green bar (same as _selectModalFolder)
                        // _selectModalFolder is suppressed during refresh, so apply styles directly
                        const $nodeEl = $tree.find('#' + reselect);
                        $nodeEl.addClass('kookoawpf-node-active');
                        $nodeEl.children('.jstree-anchor').css({
                            'color': '#e6edf3',
                            'border-left': '5px solid #39FF14',
                            'padding-left': '6px',
                            'border-radius': '0'
                        });
                    }

                    // Update breadcrumb
                    self._updateModalBreadcrumb($sidebar, $frame, FO._modalCurrentFolder || 'all');
                });
            });
        },

        /**
         * Bind nav clicks, search, and new folder button.
         */
        _bindModalEvents: function($sidebar, $frame) {
            const self = this;

            $sidebar.on('click', '.kookoawpf-nav-item', function(e) {
                e.preventDefault();
                self._selectModalFolder($(this).data('folder'), $sidebar, $frame);
            });

            let searchTimer = null;
            $sidebar.on('input', '.kookoawpf-modal-search-input', function() {
                const val = $(this).val();
                const $tree = $sidebar.find('#kookoawpf-jstree');
                if (searchTimer) clearTimeout(searchTimer);
                searchTimer = setTimeout(function() {
                    $tree.jstree('search', val);
                }, 250);
            });

            $sidebar.on('click', '.kookoawpf-modal-btn-new, .kookoawpf-modal-empty-new-folder', function(e) {
                e.preventDefault();
                self._createModalFolder($sidebar, $frame);
            });

            // Listen for attachment deletions (delete-post AJAX or Backbone destroy)
            let _deleteTimer = null;
            const _scheduleDeleteRefresh = function() {
                if (_deleteTimer) clearTimeout(_deleteTimer);
                _deleteTimer = setTimeout(function() {
                    self._suppressUploadObserver = true;
                    self._refreshModalTree($sidebar, $frame);
                    self._reloadModalAttachments(FO._modalCurrentFolder);
                    if (FO.tree && FO.tree.length) FO.refreshTree();
                    setTimeout(function() { self._suppressUploadObserver = false; }, 2000);
                }, 400);
            };
            $(document).on('ajaxComplete.kookoawpf-modal-delete', function(event, xhr, settings) {
                if (!settings || !settings.data || typeof settings.data !== 'string') return;
                if (settings.data.indexOf('action=delete-post') !== -1 ||
                    settings.data.indexOf('action=trash-post') !== -1) {
                    _scheduleDeleteRefresh();
                }
            });
            // Also patch Backbone Attachment.destroy if not already patched
            if (typeof wp !== 'undefined' && wp.media && wp.media.model && wp.media.model.Attachment) {
                const proto = wp.media.model.Attachment.prototype;
                if (!proto._kookoawpfModalPatched) {
                    const origDestroy = proto.destroy;
                    proto.destroy = function() {
                        const result = origDestroy.apply(this, arguments);
                        _scheduleDeleteRefresh();
                        return result;
                    };
                    proto._kookoawpfModalPatched = true;
                }
            }

            // Listen for new uploads: observe .attachments container for new .attachment nodes
            // Uses a suppress flag to avoid triggering on grid reloads (which also add .attachment nodes)
            let _uploadTimer = null;
            self._suppressUploadObserver = false;
            const _scheduleUploadRefresh = function() {
                if (_uploadTimer) clearTimeout(_uploadTimer);
                _uploadTimer = setTimeout(function() {
                    self._refreshModalTree($sidebar, $frame);
                    if (FO.tree && FO.tree.length) FO.refreshTree();

                    // Rebuild the grid cleanly so attachments get fresh DOM elements.
                    // After native browser drag-upload, the original attachment elements
                    // may have residual state that prevents jQuery UI draggable from working.
                    // _reloadModalAttachments does library.reset() + mirror(freshQuery)
                    // which creates clean Backbone views with draggable-ready elements.
                    self._suppressUploadObserver = true;
                    self._reloadModalAttachments(FO._modalCurrentFolder);

                    // After the server fetch completes and Backbone renders the grid,
                    // force-init draggable on all attachment elements.
                    // Backbone may recreate DOM elements at unpredictable times when
                    // merging server data with models already in Attachments.all,
                    // so poll repeatedly to catch any late-arriving elements.
                    if (self._ensureModalAttachmentsDraggable) {
                        const _dragPoll = setInterval(function() {
                            self._ensureModalAttachmentsDraggable();
                        }, 500);
                        setTimeout(function() {
                            clearInterval(_dragPoll);
                            self._suppressUploadObserver = false;
                        }, 4000);
                    } else {
                        setTimeout(function() { self._suppressUploadObserver = false; }, 2500);
                    }
                }, 800);
            };
            // Attach upload observer to the grid container.
            // Must be re-attached when WP rebuilds .media-frame-content (tab switch).
            self._modalUploadObserver = null;
            const _attachUploadObserver = function() {
                const $gridContainer = $frame.find('.attachments-browser .attachments');
                if ($gridContainer.length && typeof MutationObserver !== 'undefined') {
                    if (self._modalUploadObserver) self._modalUploadObserver.disconnect();
                    self._modalUploadObserver = new MutationObserver(function(mutations) {
                        if (self._suppressUploadObserver) return;
                        for (let i = 0; i < mutations.length; i++) {
                            const added = mutations[i].addedNodes;
                            if (!added || !added.length) continue;
                            for (let j = 0; j < added.length; j++) {
                                if (added[j].nodeType === 1 && $(added[j]).hasClass('attachment')) {
                                    _scheduleUploadRefresh();
                                    return;
                                }
                            }
                        }
                    });
                    self._modalUploadObserver.observe($gridContainer[0], { childList: true });
                }
            };
            // Delay initial observer to skip first grid population
            setTimeout(_attachUploadObserver, 3000);

            // Suppress upload observer when "Load more" is clicked — WP adds .attachment nodes
            // for pagination the same way uploads do, which would falsely trigger _reloadModalAttachments.
            $frame.on('click.kookoawpf-loadmore', '.load-more', function() {
                self._suppressUploadObserver = true;
                setTimeout(function() { self._suppressUploadObserver = false; }, 5000);
            });

            // Re-attach upload observer when WP rebuilds content (Upload Files ↔ Media Library)
            const _contentEl = $frame.find('.media-frame-content')[0];
            if (_contentEl) {
                self._modalContentObserver = new MutationObserver(function() {
                    // Small delay: wait for WP to finish rendering the new grid
                    setTimeout(_attachUploadObserver, 1500);
                });
                self._modalContentObserver.observe(_contentEl, { childList: true });
            }

            // ── XHR interceptor for uploads (needed on pages without main sidebar, e.g. post.php) ──
            // CRITICAL: load listener registered in open() so it fires BEFORE plupload's
            // handler. This ensures FO._uploadedToFolder is populated before WP creates
            // the Attachment model and _observe propagates it to the library.
            if (!FO._xhrUploadPatched) {
                FO._xhrUploadPatched = true;
                FO._uploadedToFolder = FO._uploadedToFolder || {};

                // Debounced tree refresh triggered by ANY upload completion
                // (covers uploads from "Upload files" tab where MutationObserver can't see)
                let _xhrUploadRefreshTimer = null;
                const _xhrUploadRefresh = function() {
                    clearTimeout(_xhrUploadRefreshTimer);
                    _xhrUploadRefreshTimer = setTimeout(function() {
                        self._refreshModalTree($sidebar, $frame);
                        if (FO.tree && FO.tree.length) FO.refreshTree();
                    }, 1200);
                };

                const _origXHROpen = XMLHttpRequest.prototype.open;
                XMLHttpRequest.prototype.open = function(method, url) {
                    this._kwpfUrl = url;
                    const xhr = this;

                    // Register load listener early for upload URLs (fires before plupload)
                    const isUploadUrl = url.indexOf('upload-attachment') !== -1 || url.indexOf('async-upload.php') !== -1;
                    if (isUploadUrl) {
                        xhr._kwpfIsUpload = true;
                        xhr.addEventListener('load', function() {
                            if (xhr.status >= 200 && xhr.status < 300 && xhr._kwpfUploadFolder !== undefined) {
                                try {
                                    const resp = JSON.parse(xhr.responseText);
                                    const attId = resp && resp.data && resp.data.id;
                                    if (attId) {
                                        FO._uploadedToFolder[attId] = xhr._kwpfUploadFolder;
                                    }
                                } catch (e) {}
                                _xhrUploadRefresh();
                            }
                        });
                    }

                    return _origXHROpen.apply(this, arguments);
                };

                const _origXHRSend = XMLHttpRequest.prototype.send;
                XMLHttpRequest.prototype.send = function(body) {
                    const xhr = this;
                    let isUpload = xhr._kwpfIsUpload || false;

                    // Additional detection via FormData (for URLs not caught in open)
                    if (!isUpload && body instanceof FormData) {
                        try { if (body.get && body.get('action') === 'upload-attachment') isUpload = true; } catch (e) {}
                    }

                    if (isUpload && body instanceof FormData) {
                        const folder = (FO._modalCurrentFolder && FO._modalCurrentFolder !== 'all')
                            ? FO._modalCurrentFolder : null;
                        if (folder && folder !== 'uncategorized' && !isNaN(folder) && !FO._modalCurrentFolderIsDynamic) {
                            body.append('kookoawpf_upload_folder', String(folder));
                        }
                        xhr._kwpfUploadFolder = folder || 'none';

                        // Fallback: if load listener wasn't registered in open(), add it now
                        if (!xhr._kwpfIsUpload) {
                            xhr.addEventListener('load', function() {
                                if (xhr.status >= 200 && xhr.status < 300 && xhr._kwpfUploadFolder !== undefined) {
                                    try {
                                        const resp = JSON.parse(xhr.responseText);
                                        const attId = resp && resp.data && resp.data.id;
                                        if (attId) {
                                            FO._uploadedToFolder[attId] = xhr._kwpfUploadFolder;
                                        }
                                    } catch (e) {}
                                    _xhrUploadRefresh();
                                }
                            });
                        }
                    }
                    return _origXHRSend.apply(this, arguments);
                };
            }

        },

        /**
         * Select a folder and reload attachments.
         */
        _selecting: false,

        _selectModalFolder: function(folder, $sidebar, $frame) {
            const self = this;

            // Guard against infinite recursion and suppress during tree refresh
            if (self._selecting || self._suppressModalSelect) return;
            self._selecting = true;

            FO._modalCurrentFolder = folder;

            // Smart Startup Folder — remember the last opened folder so the next
            // modal open (or main sidebar visit) can restore it.
            if (FO._rememberLastFolder && FO.data.startupMode === 'last') {
                // Force 'attachment' post type — the modal always manages media.
                const _prevPt = FO.data.postType;
                FO.data.postType = 'attachment';
                FO._rememberLastFolder(folder);
                FO.data.postType = _prevPt;
            }

            // Track whether current modal folder is dynamic (smart)
            FO._modalCurrentFolderIsDynamic = false;
            if (folder !== 'all' && folder !== 'uncategorized') {
                const $tree = $sidebar.find('#kookoawpf-jstree');
                const chkInst = $tree.jstree(true);
                if (chkInst) {
                    const chkNode = chkInst.get_node('folder_' + folder);
                    if (chkNode && chkNode.data && chkNode.data.is_dynamic) {
                        FO._modalCurrentFolderIsDynamic = true;
                    }
                }
            }

            // Hide upload UI when dynamic folder is selected
            const $modal = $frame.closest('.media-modal');
            const $uploadTab = $modal.find('#menu-item-upload');
            const $uploaderInline = $frame.find('.uploader-inline');
            if (FO._modalCurrentFolderIsDynamic) {
                $uploadTab.hide();
                $uploaderInline.hide();
            } else {
                $uploadTab.show();
                $uploaderInline.show();
            }

            // Update nav
            $sidebar.find('.kookoawpf-nav-item').removeClass('active');
            if (folder === 'all') {
                $sidebar.find('.kookoawpf-nav-all').addClass('active');
            } else if (folder === 'uncategorized') {
                $sidebar.find('.kookoawpf-nav-uncategorized').addClass('active');
            }

            // Update tree selection + active folder highlight
            const $tree = $sidebar.find('#kookoawpf-jstree');
            const inst = $tree.jstree(true);
            if (inst) {
                inst.deselect_all(true);
                if (folder !== 'all' && folder !== 'uncategorized') {
                    inst.select_node('folder_' + folder, true);
                }
            }

            // Apply active folder green bar — optimized: only clear previous node (not ALL)
            const _newSelId = (folder !== 'all' && folder !== 'uncategorized') ? 'folder_' + folder : null;
            $tree.find('.kookoawpf-node-active').each(function() {
                if (_newSelId && this.id === _newSelId) return; // skip new target
                $(this).removeClass('kookoawpf-node-active');
                $(this).children('.jstree-anchor').css({
                    'background': '', 'color': '', 'border-left': '', 'padding-left': '', 'border-radius': ''
                });
            });

            if (_newSelId) {
                const $nodeEl = $tree.find('#' + _newSelId);
                if (!$nodeEl.hasClass('kookoawpf-node-active')) {
                    $nodeEl.addClass('kookoawpf-node-active');
                    $nodeEl.children('.jstree-anchor').css({
                        'color': '#e6edf3',
                        'border-left': '5px solid #39FF14',
                        'padding-left': '6px',
                        'border-radius': '0'
                    });
                }
            }

            // Suppress upload observer during grid reload
            self._suppressUploadObserver = true;

            // Reload attachments
            self._reloadModalAttachments(folder);

            // Update breadcrumb and subfolder cards
            self._updateModalBreadcrumb($sidebar, $frame, folder);

            self._selecting = false;

            // Re-enable upload observer after grid settles
            setTimeout(function() { self._suppressUploadObserver = false; }, 2000);
        },

        /**
         * Reload modal attachments filtered by folder.
         * Uses the global Backbone collection to trigger a re-query.
         */
        _reloadModalAttachments: function(folder) {
            // Find the active media frame's library collection.
            // Page builders (Divi, Elementor) may register their frame under
            // wp.media.frames.* instead of wp.media.frame, so check both.
            if (typeof wp === 'undefined' || !wp.media) return;

            let library = null;
            let frame = wp.media.frame;

            // Try wp.media.frame first
            try {
                if (frame) {
                    const state = frame.state();
                    if (state) library = state.get('library');
                }
            } catch (e) {}

            // If no library found, scan wp.media.frames for the visible one
            if ((!library || !library.props) && wp.media.frames) {
                const keys = Object.keys(wp.media.frames);
                for (let i = 0; i < keys.length; i++) {
                    try {
                        const f = wp.media.frames[keys[i]];
                        if (f && f.el && $(f.el).is(':visible')) {
                            const s = f.state();
                            if (s) {
                                const lib = s.get('library');
                                if (lib && lib.props) {
                                    library = lib;
                                    frame = f;
                                    break;
                                }
                            }
                        }
                    } catch (e) {}
                }
            }

            // Fallback: scan ALL frames without visibility check (some builders hide the el)
            if ((!library || !library.props) && wp.media.frames) {
                const keys2 = Object.keys(wp.media.frames);
                for (let j = 0; j < keys2.length; j++) {
                    try {
                        const f2 = wp.media.frames[keys2[j]];
                        if (f2 && typeof f2.state === 'function') {
                            const s2 = f2.state();
                            if (s2) {
                                const lib2 = s2.get('library');
                                if (lib2 && lib2.props) {
                                    library = lib2;
                                    frame = f2;
                                    break;
                                }
                            }
                        }
                    } catch (e) {}
                }
            }

            if (!library || !library.props) return;

            // Store folder for ajaxPrefilter to pick up
            FO._modalCurrentFolder = folder;

            const QueryClass = wp.media.model.Query;

            // Clear query cache
            QueryClass._queries = {};

            // Build props and args for the new query
            const props = library.props.toJSON();
            const args = {
                post_mime_type: props.type || 'image',
                orderby: props.orderby || 'date',
                order: 'DESC',
                posts_per_page: 80
            };

            // Add folder to props + args for the ajaxPrefilter to pick up
            if (folder && folder !== 'all') {
                props.kookoawpf_folder = String(folder);
                args.kookoawpf_folder = String(folder);
            } else {
                delete props.kookoawpf_folder;
            }

            // Restore original library.add if previously overridden
            if (library._kwpfOrigAdd) {
                library.add = library._kwpfOrigAdd;
                delete library._kwpfOrigAdd;
            }

            // Detach current mirror and reset collection
            library.unmirror();
            library.reset();

            // Create a brand new Query instance (NOT via Query.get which caches)
            const freshQuery = new QueryClass([], {
                props: new Backbone.Model(props),
                args: args
            });

            // Override validator: reject uploads that belong to a different folder.
            // WP's Query.observe adds ALL new Attachments from the global collection
            // because it can't filter by kookoawpf_folder client-side.
            // Skip for 'uncategorized' — parseInt('uncategorized') = NaN, NaN !== NaN = always true → all files rejected.
            if (folder && folder !== 'all' && folder !== 'uncategorized') {
                const _origValidator = freshQuery.validator;
                const numFolder = parseInt(folder, 10);
                freshQuery.validator = function(attachment) {
                    if (!_origValidator.call(this, attachment)) return false;
                    const attId = attachment.get('id');
                    const map = FO._uploadedToFolder || {};
                    if (attId && map[attId] !== undefined) {
                        return parseInt(map[attId], 10) === numFolder;
                    }
                    return true;
                };
            }

            // Mirror the fresh query — triggers a server fetch
            library.mirror(freshQuery);

            // Override library.add to synchronously block models from wrong folders.
            // This catches ALL code paths (mirror, _observe, direct add, WP internals)
            // preventing any visual flash — models never enter the collection.
            if (folder && folder !== 'all' && folder !== 'uncategorized') {
                library._kwpfOrigAdd = library.add;
                const numFolderAdd = parseInt(folder, 10);
                library.add = function(models, options) {
                    if (!models) return library._kwpfOrigAdd.call(this, models, options);

                    const map = FO._uploadedToFolder || {};
                    const filterModel = function(m) {
                        const mid = m.get ? m.get('id') : (m.id || null);
                        if (mid && map[mid] !== undefined && parseInt(map[mid], 10) !== numFolderAdd) {
                            return false;
                        }
                        return true;
                    };

                    // Handle array, collection, or single model
                    if (Array.isArray(models)) {
                        models = models.filter(filterModel);
                        if (!models.length) return this;
                    } else if (models.models) {
                        const filtered = models.models.filter(filterModel);
                        if (!filtered.length) return this;
                        models = filtered;
                    } else {
                        if (!filterModel(models)) return this;
                    }

                    return library._kwpfOrigAdd.call(this, models, options);
                };
            }
        },

        /**
         * Create a new folder from the modal.
         */
        _createModalFolder: function($sidebar, $frame) {
            this._openModalFolderDialog('create', '', 0, $sidebar, $frame);
        },

        /**
         * Common post-move cleanup for the modal: invalidate caches,
         * clear selection, refresh tree and grid.
         */
        _afterModalMove: function($sidebar, $frame) {
            const self = this;
            // Invalidate folder list cache
            FO._folderListCache = null;
            // Clear WP media selection
            try {
                const state = wp.media.frame && wp.media.frame.state ? wp.media.frame.state() : null;
                if (state && state.get('selection')) {
                    state.get('selection').reset([]);
                }
            } catch (e) {}
            // Suppress upload observer during grid reload to avoid loop
            self._suppressUploadObserver = true;
            // Refresh tree (no flash) + reload grid
            self._refreshModalTree($sidebar, $frame);
            self._reloadModalAttachments(FO._modalCurrentFolder);
            if (FO.tree && FO.tree.length) FO.refreshTree();
            // Re-enable upload observer after grid settles
            setTimeout(function() { self._suppressUploadObserver = false; }, 2000);
        },

        _checkModalScrollbar: function($sidebar) {
            const el = $sidebar.find('.kookoawpf-tree-container')[0];
            if (!el) return;
            // Force layout recalculation
            void el.offsetHeight;
            const hasScroll = el.scrollHeight > el.clientHeight;
            $sidebar.toggleClass('kookoawpf-modal-has-scrollbar', hasScroll);
        },

        /**
         * Force the media frame to switch to the Media Library tab if it
         * opened on the Upload Files tab. Tries the Backbone API first
         * (frame.content.mode('browse')) and falls back to a click on the
         * router link.
         */
        _forceLibraryTab: function($frame) {
            // Try Backbone API first — works for most WP frames.
            // Divi 5 registers its frame under wp.media.frames.* instead of
            // wp.media.frame (same divergence _reloadModalAttachments handles),
            // so resolve the frame from both before giving up on the API.
            try {
                let frame = wp.media.frame;
                const _canMode = function(f) {
                    return f && f.content && typeof f.content.mode === 'function';
                };
                if (!_canMode(frame) && wp.media.frames) {
                    const keys = Object.keys(wp.media.frames);
                    for (let i = 0; i < keys.length; i++) {
                        const f = wp.media.frames[keys[i]];
                        if (_canMode(f) && f.el && $(f.el).is(':visible')) {
                            frame = f;
                            break;
                        }
                    }
                    // Second pass without the visibility check — some builders
                    // (Divi) keep the frame el hidden while the modal is up.
                    if (!_canMode(frame)) {
                        for (let j = 0; j < keys.length; j++) {
                            if (_canMode(wp.media.frames[keys[j]])) {
                                frame = wp.media.frames[keys[j]];
                                break;
                            }
                        }
                    }
                }
                if (_canMode(frame)) {
                    if (frame.content.mode() !== 'browse') {
                        frame.content.mode('browse');
                        return;
                    }
                }
            } catch (e) {}

            // Fallback: click the Media Library router link.
            const $libLink = $frame.find('.media-router .media-menu-item').filter(function() {
                return $(this).attr('href') === '#' && $(this).text() && /library/i.test($(this).text());
            });
            if ($libLink.length && !$libLink.hasClass('active')) {
                $libLink.trigger('click');
            }
        },

        /**
         * Update modal layout offsets based on sidebar width.
         */
        _updateModalLayout: function($frame, w) {
            // Title is a direct child of .media-frame, not inside tab-panel
            $frame.find('.media-frame-title').css('left', w + 'px');
            // Tab-panel offset
            $frame.find('.media-frame-tab-panel').css('left', w + 'px');
        },

        /**
         * Initialize resize handle for the modal sidebar.
         */
        _initModalResize: function($sidebar, $frame) {
            const self = this;
            const $handle = $sidebar.find('.kookoawpf-modal-resize-handle');
            let resizing = false;

            $handle.on('mousedown', function(e) {
                resizing = true;
                $('body').css({ cursor: 'col-resize', 'user-select': 'none' });
                e.preventDefault();
            });

            $(document).on('mousemove.kookoawpf-modal-resize', function(e) {
                if (!resizing) return;
                // Calculate width relative to the modal left edge
                const modalLeft = $frame.closest('.media-modal').offset().left;
                const w = e.clientX - modalLeft;
                if (w >= 350 && w <= 600) {
                    $sidebar.css('width', w + 'px');
                    self._updateModalLayout($frame, w);
                    self._checkModalScrollbar($sidebar);
                }
            });

            $(document).on('mouseup.kookoawpf-modal-resize', function() {
                if (resizing) {
                    resizing = false;
                    $('body').css({ cursor: '', 'user-select': '' });
                    // Persist width
                    const newWidth = parseInt($sidebar.css('width'), 10);
                    if (newWidth >= 200 && newWidth <= 500) {
                        FO.ajax('kookoawpf_save_sidebar_width', { width: newWidth });
                        FO.data.sidebarWidth = newWidth;
                    }
                }
            });
        },

        /**
         * Apply folder icons, colors, counts, and badges in the modal tree.
         * Mirrors FO.applyFolderIcons logic.
         */
        _applyModalFolderIcons: function($tree) {
            const inst = $tree.jstree(true);
            if (!inst) return;

            $tree.find('.jstree-node').each(function() {
                const $node = $(this);
                const node = inst.get_node($node.attr('id'));
                if (!node || !node.data) return;

                const customIcon = node.data.icon || '';
                const color = node.data.color || '#F9A825';
                const isOpen = node.state && node.state.opened;

                const $icon = $node.children('.jstree-anchor').children('.jstree-icon');
                $icon.css({ 'background-image': 'none', 'background-color': 'transparent' });

                if (customIcon) {
                    $icon.html('').append($('<span class="kookoawpf-emoji-icon">').text(customIcon));
                } else {
                    $icon.html(isOpen ? SVG_FOLDER_OPEN : SVG_FOLDER);
                    const $svg = $icon.find('.kookoawpf-svg-folder');
                    $svg.css('color', color);
                    // Black folder needs green stroke to be visible on dark bg
                    if (color && color.toLowerCase() === '#000000') {
                        $svg.find('path').attr('stroke', '#39FF14').attr('stroke-width', '2');
                    } else {
                        $svg.find('path').removeAttr('stroke').removeAttr('stroke-width');
                    }
                }

                // Count badge
                const count = node.data.count !== undefined ? node.data.count : '';
                const $existingCount = $node.children('.kookoawpf-count');
                if ($existingCount.length) {
                    $existingCount.text(count);
                } else {
                    $node.append($('<span class="kookoawpf-count">').text(count));
                }

                // Pin/Lock badges are already in node.text from PHP (inside .kookoawpf-badge-icon)
                // so no need to add them here — they render in the correct position after folder text
            });
        }
    };

    // Start watching for media modals on any admin page
    $(document).ready(function() {
        if (window.kookoawpfData) {
            KookoawpfMediaModal.init();
        }
    });

})(jQuery);
