/**
 * KooKoa Folders — Modals (CRUD + Shortcuts)
 *
 * Extracted from admin.js (blocco 13).
 * Adds modal handlers, keyboard shortcuts, and CRUD operations to FO.
 *
 * Dependencies: admin.js (FO core), context-menu.js,
 *               sidebar-ui.js, breadcrumb.js, bulk-organize.js,
 *               custom-select.js, media-modal.js
 */
(function($) {
    'use strict';

    const FO = window.KookoaWPF;
    if (!FO) return;

    // ========================
    // Modals
    // ========================

    FO.initModals = function() {
        const self = this;

        $(document).on('click', '.kookoawpf-modal-close, .kookoawpf-modal-cancel, .kookoawpf-modal-overlay', function() {
            $(this).closest('.kookoawpf-modal').hide();
            $('#kookoawpf-empty-state').removeClass('kookoawpf-empty-state--active');
            self.applyFolderIcons();
        });
        $(document).on('keydown', function(e) {
            if (e.keyCode === 27) {
                const $visible = $('.kookoawpf-modal:visible');
                if ($visible.length) {
                    $visible.hide();
                    $('#kookoawpf-empty-state').removeClass('kookoawpf-empty-state--active');
                    self.applyFolderIcons();
                }
            }
        });

        /* ── Keyboard Shortcuts ── */
        // Capture phase: intercepts BEFORE jsTree's own keydown (e.g. F2 inline rename)
        document.addEventListener('keydown', function(e) {
            // Skip all shortcuts if disabled in settings
            // But still block F2 to prevent jsTree inline rename
            if (!self.data.shortcutsEnabled) {
                if (e.keyCode === 113) {
                    e.preventDefault();
                    e.stopImmediatePropagation();
                }
                return;
            }

            // When a modal is open, block tree navigation keys entirely
            if ($('.kookoawpf-modal:visible').length) {
                const blocked = [37, 38, 39, 40, 36, 35]; // ← ↑ → ↓ Home End
                if (blocked.indexOf(e.keyCode) !== -1) {
                    e.stopImmediatePropagation();
                }
                return;
            }
            // Skip if focus is on input/textarea/select/contenteditable
            const tag = (e.target.tagName || '').toLowerCase();
            if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
            if (e.target.isContentEditable) return;
            // Skip in folder bulk mode
            if (self._fbActive) return;

            // Alt+K — Keyboard Shortcuts panel
            if (e.altKey && e.keyCode === 75) {
                e.preventDefault();
                e.stopImmediatePropagation();
                self._ensureDialogInModal($('#kookoawpf-modal-shortcuts'), 'kookoawpf-modal-shortcuts');
                $('#kookoawpf-modal-shortcuts').show();
                return;
            }

            // Alt+N — New Folder (no folder selection required)
            if (e.altKey && e.keyCode === 78) {
                e.preventDefault();
                e.stopImmediatePropagation();
                self.openFolderModal('create');
                return;
            }

            // Alt+B — Toggle Bulk Organize. Routes through whichever button
            // exists on the current screen (grid view = upload.php, list view
            // = upload.php list mode / posts / pages / CPT). The button's own
            // click handler is a toggle, so Alt+B opens the bulk toolbar when
            // closed and exits bulk mode when open — symmetric with ESC.
            if (e.altKey && e.keyCode === 66) {
                let $bulkBtn = $('#kookoawpf-btn-bulk-organize:visible');
                if (!$bulkBtn.length) {
                    $bulkBtn = $('#kookoawpf-btn-bulk-organize-list:visible:not([disabled])');
                }
                if ($bulkBtn.length) {
                    e.preventDefault();
                    e.stopImmediatePropagation();
                    $bulkBtn.first().trigger('click');
                }
                return;
            }

            // Resolve target node from jsTree selection
            const inst = self.tree ? self.tree.jstree(true) : null;
            if (!inst) return;

            // →← — Expand / Collapse subfolders (only if folder has children)
            if (e.keyCode === 39 || e.keyCode === 37) {
                e.preventDefault();
                e.stopImmediatePropagation();
                const sel = inst.get_selected();
                if (!sel.length) return;
                const nd = inst.get_node(sel[0]);
                if (!nd || !inst.is_parent(nd)) return;
                if (e.keyCode === 39) inst.open_node(nd);
                else inst.close_node(nd);
                return;
            }

            // ↑↓ — Navigate folders with selection (green line follows)
            // Also handles transitions between nav items (All/Uncategorized) and tree
            if (!e.shiftKey && (e.keyCode === 38 || e.keyCode === 40)) {
                const sel = inst.get_selected();

                // No tree selection — check if we're on a nav item (All/Uncategorized)
                if (!sel.length) {
                    e.preventDefault();
                    e.stopImmediatePropagation();
                    if (e.keyCode === 40) { // ↓
                        if (self.currentFolder === 'all') {
                            self.goToFolder('uncategorized');
                        } else if (self.currentFolder === 'uncategorized') {
                            const rootChildren = inst.get_node('#').children;
                            if (rootChildren.length) {
                                inst.select_node(rootChildren[0]);
                            }
                        }
                    } else { // ↑
                        if (self.currentFolder === 'uncategorized') {
                            self.goToFolder('all');
                        }
                    }
                    return;
                }

                const $cur = inst.get_node(sel[0], true);
                if (!$cur || !$cur.length) return;

                if (e.keyCode === 38) { // ↑
                    const $target = inst.get_prev_dom($cur);
                    if ($target && $target.length) {
                        e.preventDefault();
                        e.stopImmediatePropagation();
                        inst.deselect_all();
                        inst.select_node($target);
                    } else {
                        // First root node — go to Uncategorized
                        e.preventDefault();
                        e.stopImmediatePropagation();
                        inst.deselect_all(true);
                        self.goToFolder('uncategorized');
                    }
                } else { // ↓
                    const $target = inst.get_next_dom($cur);
                    if ($target && $target.length) {
                        e.preventDefault();
                        e.stopImmediatePropagation();
                        inst.deselect_all();
                        inst.select_node($target);
                    }
                }
                return;
            }

            // Resolve current node for other shortcuts
            let node = null;
            const sel = inst.get_selected();
            if (sel.length) node = inst.get_node(sel[0]);
            if (!node) return;
            const fid = self.getFolderId(node);
            if (!fid) return;

            // Shift+↑↓ — Move folder up/down among siblings
            // Allowed on locked folders: reorder among siblings doesn't change parent
            if (e.shiftKey && (e.keyCode === 38 || e.keyCode === 40)) {
                e.preventDefault();
                e.stopImmediatePropagation();
                self.moveFolderPosition(fid, e.keyCode === 38 ? 'move-up' : 'move-down');
            }

            // F2 — Rename folder (blocked if locked)
            if (e.keyCode === 113) {
                e.preventDefault();
                e.stopImmediatePropagation();
                if (node.data && node.data.locked) {
                    self.showNotice('This folder is locked', 'error');
                    return;
                }
                const name = $('<div>').html(node.text).find('.kookoawpf-count').remove().end().text().trim();
                self.openFolderModal('rename', name, fid);
            }

            // Del — Delete folder (opens confirmation modal, blocked if locked)
            if (e.keyCode === 46) {
                e.preventDefault();
                e.stopImmediatePropagation();
                if (node.data && node.data.locked) {
                    self.showNotice('This folder is locked', 'error');
                    return;
                }
                self.openDeleteModal(fid);
            }

            // Ctrl+C — Copy folder to clipboard (blocked on smart folders —
            // copy would imply duplicate, which is not allowed on saved queries)
            if (e.ctrlKey && e.keyCode === 67) {
                e.preventDefault();
                e.stopImmediatePropagation();
                if (node.data && node.data.is_dynamic) {
                    self.showNotice(self.data.strings.actionNotAllowedSmart, 'error');
                    return;
                }
                const copyName = $('<div>').html(node.text).find('.kookoawpf-count').remove().end().text().trim();
                self._clipboard = { id: fid, name: copyName, cut: false };
                self.showNotice('"' + copyName + '" copied', 'success');
            }

            // Ctrl+X — Cut folder to clipboard (blocked if locked)
            if (e.ctrlKey && e.keyCode === 88) {
                e.preventDefault();
                e.stopImmediatePropagation();
                if (node.data && node.data.locked) {
                    self.showNotice('Cannot cut a locked folder', 'error');
                    return;
                }
                const cutName = $('<div>').html(node.text).find('.kookoawpf-count').remove().end().text().trim();
                self._clipboard = { id: fid, name: cutName, cut: true };
                self.showNotice('"' + cutName + '" cut', 'success');
            }

            // Ctrl+V — Paste folder from clipboard into selected folder
            if (e.ctrlKey && e.keyCode === 86) {
                e.preventDefault();
                e.stopImmediatePropagation();
                if (!self._clipboard) return;
                // Block paste into dynamic folder
                if (node.data && node.data.is_dynamic) {
                    self.showNotice('Cannot paste into a smart folder', 'error');
                    return;
                }
                // Block paste into locked folder
                if (node.data && node.data.locked) {
                    self.showNotice('Cannot paste into a locked folder', 'error');
                    return;
                }
                // Block paste into self or own descendants
                const clipNodeId = 'folder_' + self._clipboard.id;
                if (node.id === clipNodeId) {
                    self.showNotice('Cannot paste folder into itself', 'error');
                    return;
                }
                const ancestors = inst.get_path(node, false, true);
                if (ancestors && ancestors.indexOf(clipNodeId) !== -1) {
                    self.showNotice('Cannot paste folder into its own subfolder', 'error');
                    return;
                }
                // Cut + paste into current parent is a no-op — block silently.
                if (self._clipboard.cut) {
                    const _cutSrcKb = inst.get_node(clipNodeId);
                    if (_cutSrcKb && _cutSrcKb.parent === node.id) return;
                }

                if (self._clipboard.cut) {
                    // Cut → optimistic move via jsTree (triggers handleMoveNode with animation
                    // and shows the "folder moved" notice — no separate notice here).
                    // Pass 'last' so the cut folder lands at the end of the destination's
                    // children — consistent with create / copy-paste / drag & drop. Default
                    // jstree position is 0 (first child).
                    self._clipboard = null;
                    inst.move_node(clipNodeId, node.id, 'last');
                    inst.open_node(node.id, false, 0);
                } else {
                    // Copy → duplicate on server, then add node in-place
                    const copyName = self._clipboard.name;
                    const pasteTargetId = node.id;
                    const kbSrcNode = inst.get_node('folder_' + self._clipboard.id);
                    const kbSrcData = kbSrcNode && kbSrcNode.data ? kbSrcNode.data : {};
                    // If source has children, fall back to refreshTree (same as duplicateFolder).
                    const kbHasChildren = !!(kbSrcNode && kbSrcNode.children && kbSrcNode.children.length);
                    self.ajax('kookoawpf_duplicate_folder', {
                        folder_id: self._clipboard.id,
                        post_type: self.data.postType,
                        target_parent: fid
                    }, function(r) {
                        if (r.success) {
                            self.showNotice('"' + copyName + '" pasted', 'success');
                            self._clipboard = null;
                            const newId = r.data && r.data.term_id ? r.data.term_id : null;
                            const dupName = r.data && r.data.name ? r.data.name : copyName + ' (Copy)';
                            if (inst && newId && inst.get_node(pasteTargetId) && !kbHasChildren) {
                                const newNode = {
                                    id: 'folder_' + newId,
                                    text: '<span class="kookoawpf-folder-text">' + $('<span>').text(dupName).html() + '</span>',
                                    data: { folder_id: newId, color: kbSrcData.color || ((FO.data && FO.data.defaultColor) ? FO.data.defaultColor : '#39FF14'), locked: false, pinned: false, count: 0, icon: kbSrcData.icon || '', is_dynamic: false, original_text: dupName },
                                    icon: 'kookoawpf-folder-icon',
                                    state: { opened: false }
                                };
                                inst.create_node(pasteTargetId, newNode, 'last', function(n) {
                                    inst.open_node(pasteTargetId, false, 0);
                                    self.applyFolderIcons();
                                    self._makeTreeNodesDroppable();
                                    self._folderListCache = null;
                                    if (self._repopulateGridFilter) self._repopulateGridFilter();
                                    self._toggleEmptyState(1);
                                    self._updateFreeLimits();
                                    self._updateNoFolderState();
                                    self._updateSubfolderCards(self.currentFolder);
                                    // In auto sort, snap the new node to its sorted spot.
                                    self._resortChildren(pasteTargetId, inst);
                                    const $n = $('#' + n.id);
                                    $n.addClass('kookoawpf-just-moved');
                                    setTimeout(function() { $n.removeClass('kookoawpf-just-moved'); }, 500);
                                });
                            } else {
                                self.refreshTree();
                            }
                        } else {
                            self.showNotice((r.data && r.data.message) || 'Error', 'error');
                        }
                    });
                }
            }

            // Ctrl+D — Duplicate folder (blocked on smart folders)
            if (e.ctrlKey && e.keyCode === 68) {
                e.preventDefault();
                e.stopImmediatePropagation();
                if (node.data && node.data.is_dynamic) {
                    self.showNotice(self.data.strings.actionNotAllowedSmart, 'error');
                    return;
                }
                self._executeContextAction('duplicate', fid, node, self._ctxContext(), true);
            }

            // Alt+M — Move to Folder (blocked if locked)
            if (e.altKey && e.keyCode === 77) {
                e.preventDefault();
                e.stopImmediatePropagation();
                if (node.data && node.data.locked) return;
                self.openMoveToFolderModal(fid);
            }

            // Alt+L — Lock / Unlock
            if (e.altKey && e.keyCode === 76) {
                e.preventDefault();
                e.stopImmediatePropagation();
                self._executeContextAction('lock', fid, node, undefined, true);
            }

            // Alt+P — Pin / Unpin
            if (e.altKey && e.keyCode === 80) {
                e.preventDefault();
                e.stopImmediatePropagation();
                self._executeContextAction('pin', fid, node, undefined, true);
            }

            // Alt+I — Change Icon (blocked if dynamic or locked)
            if (e.altKey && e.keyCode === 73) {
                e.preventDefault();
                e.stopImmediatePropagation();
                if (node.data && node.data.is_dynamic) return;
                if (node.data && node.data.locked) {
                    self.showNotice('This folder is locked', 'error');
                    return;
                }
                self.openIconModal(fid);
            }

            // Alt+C — Change Color (blocked if has custom icon, dynamic, or locked)
            if (e.altKey && e.keyCode === 67) {
                e.preventDefault();
                e.stopImmediatePropagation();
                if (node.data && node.data.is_dynamic) return;
                if (node.data && node.data.icon) return;
                if (node.data && node.data.locked) {
                    self.showNotice('This folder is locked', 'error');
                    return;
                }
                self.openColorModal(fid);
            }

            // Alt+S — New Subfolder (blocked if folder is locked)
            if (e.altKey && e.keyCode === 83) {
                e.preventDefault();
                e.stopImmediatePropagation();
                if (node.data && node.data.locked) return;
                self.openFolderModal('create', '', fid);
            }
        }, true); // ← capture phase

        $(document).on('click', '#kookoawpf-more-shortcuts', function(e) {
            e.preventDefault();
            $('#kookoawpf-more-dropdown').hide();
            FO._ensureDialogInModal($('#kookoawpf-modal-shortcuts'), 'kookoawpf-modal-shortcuts');
            $('#kookoawpf-modal-shortcuts').show();
        });
        $(document).on('click', '#kookoawpf-btn-new-folder, #kookoawpf-empty-new-folder', function() {
            if ($(this).hasClass('kookoawpf-disabled')) { self.showNotice(self.data.strings.folderLimitMsg, 'warning'); return; }
            // Keep the hover (adamo2) image shown while the "first folder" modal is open.
            if (this.id === 'kookoawpf-empty-new-folder') {
                $('#kookoawpf-empty-state').addClass('kookoawpf-empty-state--active');
            }
            self.openFolderModal('create');
        });
        $(document).on('click', '#kookoawpf-modal-folder-save', function() { self.saveFolder(); });
        $(document).on('keypress', '#kookoawpf-folder-name', function(e) {
            if (e.which === 13) { e.preventDefault(); self.saveFolder(); }
        });
        $(document).on('click', '#kookoawpf-confirm-delete', function() {
            self.deleteFolder($('#kookoawpf-delete-folder-id').val());
        });
        $(document).on('keydown', function(e) {
            if (e.keyCode === 13 && $('#kookoawpf-modal-delete:visible').length) {
                e.preventDefault();
                $('#kookoawpf-confirm-delete').trigger('click');
            }
        });
        // Color swatch click — update hex input + apply
        $(document).on('click', '.kookoawpf-color-swatch', function() {
            const color = $(this).data('color');
            $('.kookoawpf-color-swatch').removeClass('kookoawpf-color-active');
            $(this).addClass('kookoawpf-color-active');
            self.updateFolderColor($('#kookoawpf-color-folder-id').val(), color);
            $('#kookoawpf-modal-color').hide();
        });

        // Hex input: live preview as user types
        $(document).on('input', '#kookoawpf-color-hex', function() {
            const val = $(this).val().trim();
            if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
                $('#kookoawpf-color-preview').css('background-color', val);
            }
        });

        // Hex input: apply on Enter
        $(document).on('keypress', '#kookoawpf-color-hex', function(e) {
            if (e.which === 13) {
                e.preventDefault();
                $('#kookoawpf-color-apply').trigger('click');
            }
        });

        // Apply custom hex color
        $(document).on('click', '#kookoawpf-color-apply', function() {
            let hex = $('#kookoawpf-color-hex').val().trim();
            if (hex && hex.charAt(0) !== '#') hex = '#' + hex;
            if (!/^#[0-9A-Fa-f]{6}$/.test(hex)) {
                self.showNotice(self.data.strings.invalidHexColor, 'error');
                return;
            }
            self.updateFolderColor($('#kookoawpf-color-folder-id').val(), hex);
            $('#kookoawpf-modal-color').hide();
        });

        // Reset folder color to default
        $(document).on('click', '#kookoawpf-color-reset', function(e) {
            e.preventDefault();
            const defaultColor = self._getDefaultFolderColor();
            const fid = $('#kookoawpf-color-folder-id').val();
            self.updateFolderColor(fid, defaultColor, true);
            self.showNotice(self.data.strings.colorReset || 'Color reset.');
            $('#kookoawpf-modal-color').hide();
        });

        // ── Icon Picker ──
        $(document).on('click', '.kookoawpf-icon-option', function() {
            const icon = $(this).data('icon');
            const fid = $('#kookoawpf-icon-folder-id').val();
            self.updateFolderIcon(fid, icon);
        });

        // Reset icon to default
        $(document).on('click', '#kookoawpf-icon-reset', function() {
            const fid = $('#kookoawpf-icon-folder-id').val();
            self.updateFolderIcon(fid, '');
        });

        // ── Move to Folder modal ──
        $(document).on('keydown', function(e) {
            if (e.keyCode === 13 && $('#kookoawpf-modal-move-to-folder:visible').length) {
                e.preventDefault();
                $('#kookoawpf-confirm-move-to-folder').trigger('click');
            }
        });
        $(document).on('click', '#kookoawpf-confirm-move-to-folder', function() {
            const folderId = parseInt($('#kookoawpf-move-to-folder-id').val(), 10);
            const targetParent = $('#kookoawpf-move-to-folder-select').val();
            if (!folderId || !targetParent) return;

            const inst = self.tree ? self.tree.jstree(true) : null;
            const nodeId = 'folder_' + folderId;
            const parentNodeId = (targetParent === 'root') ? '#' : 'folder_' + targetParent;

            // No-op check: if the selected destination is already the current parent
            // of this folder, skip the AJAX call entirely and tell the user nothing
            // changed. Avoids the misleading "Folder moved" success notice when no
            // actual move took place.
            if (inst && inst.get_node(nodeId)) {
                const _currentParent = inst.get_node(nodeId).parent;
                if (_currentParent === parentNodeId) {
                    self.showNotice(self.data.strings.folderAlreadyHere || 'Folder is already in this location.', 'warning');
                    $('#kookoawpf-modal-move-to-folder').hide();
                    return;
                }
            }

            // Pre-check: reject immediately if target parent is locked (avoids
            // optimistic expand/move flash followed by server rollback).
            if (inst && targetParent !== 'root') {
                const _targetNode = inst.get_node(parentNodeId);
                if (_targetNode && _targetNode.data && _targetNode.data.locked) {
                    self.showNotice(self.data.strings.folderLockedTarget || self.data.strings.folderLockedSelf || 'This folder is locked.', 'error');
                    return;
                }
            }

            $('#kookoawpf-modal-move-to-folder').hide();

            if (inst && inst.get_node(nodeId)) {
                // Use jsTree move_node — triggers handleMoveNode (animation + AJAX save).
                // Pass 'last' so the moved folder lands at the end of the destination's
                // children — consistent with create / copy-paste / drag & drop. Default
                // jstree position is 0 (first child).
                inst.move_node(nodeId, parentNodeId, 'last');
            } else {
                // Fallback: direct AJAX (e.g. no tree on page)
                const newParentId = (targetParent === 'root') ? 0 : parseInt(targetParent, 10);
                const _mNode = inst ? inst.get_node(nodeId) : null;
                const _mDynamic = _mNode && _mNode.data && _mNode.data.is_dynamic;
                self._folderListCache = null;
                self.ajax('kookoawpf_move_folder', {
                    folder_id: folderId,
                    new_parent_id: newParentId,
                    post_type: self.data.postType,
                    sibling_order: []
                }, function(r) {
                    if (r.success) {
                        self.showNotice(_mDynamic ? self.data.strings.smartFolderMoved : self.data.strings.folderMoved, 'success');
                        self.refreshTree();
                    } else {
                        self.showNotice((r.data && r.data.message) || self.data.strings.errorGeneric, 'error');
                    }
                });
            }
        });

        // Flag: jQuery handlers registered — capture-phase handlers in media-modal.js can skip
        FO._modalsInitialized = true;
    };

    FO.openFolderModal = function(action, name, parentId) {
        const self = this;

        // Block rename on locked folders
        if (action === 'rename' && parentId) {
            const _rlInst = self.tree ? self.tree.jstree(true) : null;
            const _rlNode = _rlInst ? _rlInst.get_node('folder_' + parentId) : null;
            if (_rlNode && _rlNode.data && _rlNode.data.locked) {
                self.showNotice(self.data.strings.folderLockedSelf || 'This folder is locked.', 'error');
                return;
            }
        }

        self._ensureDialogInModal($('#kookoawpf-modal-folder'), 'kookoawpf-modal-folder');

        $('#kookoawpf-modal-folder-title').text(action === 'rename' ? self.data.strings.rename : self.data.strings.newFolder);
        $('#kookoawpf-folder-name').val(name || '');
        $('#kookoawpf-folder-action').val(action);
        $('#kookoawpf-folder-id').val(action === 'rename' ? parentId : 0);

        if (action === 'create') {
            $('#kookoawpf-parent-folder-row').show();
            const $parentSelect = $('#kookoawpf-folder-parent-select');
            $parentSelect.find('option:gt(0)').remove();
            self.populateFolderSelect($parentSelect, parentId || '0');

            $parentSelect.off('change.kwpfParent').on('change.kwpfParent', function() {
                $('#kookoawpf-folder-parent').val($(this).val());
            });
            // Set parent value immediately — no setTimeout race condition
            $('#kookoawpf-folder-parent').val(parentId || '0');
        } else {
            $('#kookoawpf-parent-folder-row').hide();
            $('#kookoawpf-folder-parent').val(0);
        }

        $('#kookoawpf-modal-folder').show();

        // Save button disabled until input has text
        const $saveBtn = $('#kookoawpf-modal-folder-save');
        const $nameInput = $('#kookoawpf-folder-name');
        const toggleSave = function() {
            if ($nameInput.val().trim().length > 0) {
                $saveBtn.prop('disabled', false).css('opacity', '');
            } else {
                $saveBtn.prop('disabled', true).css('opacity', '0.4');
            }
        };
        toggleSave();
        $nameInput.off('input.kwpfSave').on('input.kwpfSave', toggleSave);

        setTimeout(function() { $nameInput.focus().select(); }, 100);
    };

    FO.openDeleteModal = function(id) {
        const inst = this.tree ? this.tree.jstree(true) : null;
        let node = inst ? inst.get_node('folder_' + id) : null;
        if (!node) {
            const $mt = $('.kookoawpf-modal-jstree');
            const mi = $mt.length ? $mt.jstree(true) : null;
            if (mi) node = mi.get_node('folder_' + id);
        }

        // Locked folder: block with notice
        if (node && node.data && node.data.locked) {
            this.showNotice(this.data.strings.folderLockedSelf || 'This folder is locked.', 'warning');
            return;
        }

        // Has locked subfolder: block with notice
        if (node && this._hasLockedDescendant(node)) {
            this.showNotice(this.data.strings.folderLockedDelete, 'warning');
            return;
        }

        this._ensureDialogInModal($('#kookoawpf-modal-delete'), 'kookoawpf-modal-delete');
        $('#kookoawpf-delete-folder-id').val(id);

        // Dynamic title/text: smart folder vs regular folder
        const isDynamic = node && node.data && node.data.is_dynamic;
        const $modal = $('#kookoawpf-modal-delete');
        $modal.find('.kookoawpf-modal-header h3').text(
            isDynamic ? (this.data.strings.deleteSmartTitle || 'Delete Smart Folder')
                      : (this.data.strings.deleteTitle || 'Delete Folder')
        );
        $modal.find('.kookoawpf-modal-body p').text(
            isDynamic ? (this.data.strings.deleteSmartMsg || 'Are you sure you want to delete this smart folder? The items inside will NOT be deleted.')
                      : (this.data.strings.deleteMsg || 'Are you sure you want to delete this folder? All subfolders will also be deleted. The items inside will NOT be deleted.')
        );

        $modal.show();
    };

    FO.openColorModal = function(id) {
        // Block color change on locked folders
        let _clNode = this.tree ? this.tree.jstree('get_node', 'folder_' + id) : null;
        if (!_clNode) { const _clMt = $('.kookoawpf-modal-jstree'); const _clMi = _clMt.length ? _clMt.jstree(true) : null; if (_clMi) _clNode = _clMi.get_node('folder_' + id); }
        if (_clNode && _clNode.data && _clNode.data.locked) {
            this.showNotice(this.data.strings.folderLockedSelf || 'This folder is locked.', 'error');
            return;
        }
        this._ensureDialogInModal($('#kookoawpf-modal-color'), 'kookoawpf-modal-color');
        $('#kookoawpf-color-folder-id').val(id);

        // Show current folder color in hex input and preview
        // Try main tree first, then modal tree as fallback
        let node = this.tree ? this.tree.jstree('get_node', 'folder_' + id) : null;
        if (!node) {
            const $mt = $('.kookoawpf-modal-jstree');
            const mi = $mt.length ? $mt.jstree(true) : null;
            if (mi) node = mi.get_node('folder_' + id);
        }
        const currentColor = (node && node.data && node.data.color) ? node.data.color : '#39FF14';
        const defaultColor = this._getDefaultFolderColor();
        $('#kookoawpf-color-hex').val(currentColor);
        $('#kookoawpf-color-preview').css('background-color', currentColor);

        // Show "Reset color" only if folder has a non-default color
        if (currentColor.toUpperCase() !== defaultColor.toUpperCase()) {
            $('#kookoawpf-color-reset').show();
        } else {
            $('#kookoawpf-color-reset').hide();
        }

        // Highlight active color in palette
        $('.kookoawpf-color-swatch').removeClass('kookoawpf-color-active');
        $('.kookoawpf-color-swatch').each(function() {
            if ($(this).data('color').toUpperCase() === currentColor.toUpperCase()) {
                $(this).addClass('kookoawpf-color-active');
            }
        });

        $('#kookoawpf-modal-color').show();
    };

    /**
     * Get the site's default folder color (from settings or fallback).
     */
    FO._getDefaultFolderColor = function() {
        return (this.data && this.data.defaultColor) ? this.data.defaultColor : '#39FF14';
    };

    FO.openIconModal = function(id) {
        // Block icon change on locked folders
        let _ilNode = this.tree ? this.tree.jstree('get_node', 'folder_' + id) : null;
        if (!_ilNode) { const _ilMt = $('.kookoawpf-modal-jstree'); const _ilMi = _ilMt.length ? _ilMt.jstree(true) : null; if (_ilMi) _ilNode = _ilMi.get_node('folder_' + id); }
        if (_ilNode && _ilNode.data && _ilNode.data.locked) {
            this.showNotice(this.data.strings.folderLockedSelf || 'This folder is locked.', 'error');
            return;
        }
        this._ensureDialogInModal($('#kookoawpf-modal-icon'), 'kookoawpf-modal-icon');
        $('#kookoawpf-icon-folder-id').val(id);

        // Show "Reset to Default" only if folder has a custom icon
        // Try main tree first, then modal tree as fallback
        const inst = this.tree ? this.tree.jstree(true) : null;
        let node = inst ? inst.get_node('folder_' + id) : null;
        if (!node) {
            const $mt = $('.kookoawpf-modal-jstree');
            const mi = $mt.length ? $mt.jstree(true) : null;
            if (mi) node = mi.get_node('folder_' + id);
        }
        const hasCustomIcon = node && node.data && node.data.icon && node.data.icon !== '';
        $('#kookoawpf-icon-reset-row').toggle(!!hasCustomIcon);

        $('#kookoawpf-modal-icon').show();
        $('#kookoawpf-modal-icon .kookoawpf-icon-picker-body').scrollTop(0);
    };

    /**
     * Open the "Move to folder" modal for reparenting a folder.
     * Populates the dropdown with all folders except the folder itself
     * and its descendants (to prevent circular references).
     */
    FO.openMoveToFolderModal = function(folderId, postType) {
        const self = this;
        self._ensureDialogInModal($('#kookoawpf-modal-move-to-folder'), 'kookoawpf-modal-move-to-folder');
        const pt = postType || self.data.postType;
        $('#kookoawpf-move-to-folder-id').val(folderId);
        const $select = $('#kookoawpf-move-to-folder-select');
        $select.find('option:gt(0)').remove();

        // Get all descendants of this folder (to exclude from dropdown)
        // Try main tree first, then modal tree as fallback
        const excludeIds = [folderId];
        let inst = self.tree ? self.tree.jstree(true) : null;
        if (!inst) {
            const $mt = $('.kookoawpf-modal-jstree');
            inst = $mt.length ? $mt.jstree(true) : null;
        }
        if (inst) {
            const nodeId = 'folder_' + folderId;
            const node = inst.get_node(nodeId);
            if (node && node.children_d) {
                for (let i = 0; i < node.children_d.length; i++) {
                    const childFid = self.getFolderId(inst.get_node(node.children_d[i]));
                    if (childFid) excludeIds.push(childFid);
                }
            }
        }

        // Populate with filtered folder list
        // Use fresh AJAX if postType differs from page postType (e.g. modal on post.php)
        const _populate = function(folders) {
            $.each(folders, function(i, f) {
                if (excludeIds.indexOf(parseInt(f.id, 10)) !== -1) return;
                let label = f.text;
                if (typeof f.count !== 'undefined') label += ' (' + f.count + ')';
                if (f.pinned) label += ' ⭐';
                if (f.locked) label += ' 🔒';
                $select.append($('<option></option>').val(f.id).text(label));
            });
            if (typeof KookoawpfCustomSelect !== 'undefined') {
                if (!$select.data('kookoawpf-cs-init')) {
                    KookoawpfCustomSelect.replace($select);
                } else {
                    KookoawpfCustomSelect.refresh($select);
                }
            }
        };

        if (pt === self.data.postType) {
            self.getCachedFolderList(_populate);
        } else {
            // Fresh AJAX for the correct post type (no cache)
            self.ajax('kookoawpf_get_folder_list', { post_type: pt }, function(r) {
                _populate(r.success && r.data.folders ? r.data.folders : []);
            });
        }

        $('#kookoawpf-modal-move-to-folder').show();
    };

    FO.updateFolderIcon = function(id, icon) {
        const self = this;
        // Optimistic: update node data + repaint immediately
        const inst = self.tree ? self.tree.jstree(true) : null;
        const nodeId = 'folder_' + id;
        if (inst && inst.get_node(nodeId)) {
            inst.get_node(nodeId).data.icon = icon;
        }
        // Also update modal tree if present
        const $mt = $('.kookoawpf-modal-jstree');
        const mi = $mt.length ? $mt.jstree(true) : null;
        if (mi && mi.get_node(nodeId)) {
            mi.get_node(nodeId).data.icon = icon;
            if (typeof KookoawpfMediaModal !== 'undefined') KookoawpfMediaModal._applyModalFolderIcons($mt);
        }
        if (inst) {
            self.applyFolderIcons();
            self._updateSubfolderCards(self.currentFolder);
        }
        $('#kookoawpf-modal-icon').hide();
        self.showNotice(icon ? self.data.strings.iconUpdated : self.data.strings.iconReset, 'success');
        if (typeof self._blinkUnderscore === 'function') self._blinkUnderscore();

        // Inside a media modal the folder always belongs to 'attachment' —
        // on post.php the page postType can be 'post'/'product' and would fail
        // the server-side taxonomy check.
        const _iconInModal = $('#kookoawpf-modal-icon').closest('.media-modal').length > 0;
        const _iconPostType = _iconInModal ? 'attachment' : ((self.data && self.data.postType) ? self.data.postType : 'attachment');
        self.ajax('kookoawpf_update_folder_icon', { folder_id: id, post_type: _iconPostType, icon: icon }, function(r) {
            if (!r.success) {
                self.showNotice((r.data && r.data.message) || self.data.strings.errorGeneric, 'error');
                self.refreshTree();
            }
        });
    };

    FO.saveFolder = function() {
        const self = this;
        if (self._isSaving) return;
        const action = $('#kookoawpf-folder-action').val();
        const name = $('#kookoawpf-folder-name').val().trim();
        if (!name) return;
        self._isSaving = true;

        const ajaxAction = action === 'create' ? 'kookoawpf_create_folder' : 'kookoawpf_rename_folder';
        const data = action === 'create'
            ? { name: name, post_type: self.data.postType, parent_id: $('#kookoawpf-folder-parent').val() }
            : { folder_id: $('#kookoawpf-folder-id').val(), name: name, post_type: self.data.postType };

        // Optimistic: close modal and show toast immediately
        $('#kookoawpf-modal-folder').hide();

        // Invalidate folder list cache (dropdown "Move to" etc.)
        self._folderListCache = null;

        if (action === 'rename') {
            // Update node text in tree immediately, preserving pin/lock badges
            const inst = self.tree.jstree(true);
            const nodeId = 'folder_' + data.folder_id;
            let nodeData = {};
            if (inst && inst.get_node(nodeId)) {
                nodeData = inst.get_node(nodeId).data || {};
                let newText = '<span class="kookoawpf-folder-text">' + $('<span>').text(name).html() + '</span>';
                if (nodeData.pinned) {
                    newText += ' <span class="kookoawpf-badge-icon kookoawpf-badge-pin" title="Pinned"><span class="dashicons dashicons-star-filled"></span></span>';
                }
                if (nodeData.locked) {
                    newText += ' <span class="kookoawpf-badge-icon kookoawpf-badge-lock" title="Locked"><span class="dashicons dashicons-lock"></span></span>';
                }
                inst.rename_node(nodeId, newText);
                // Update original_text so _compareFolders uses the new name
                nodeData.original_text = name;
            }
            self.applyFolderIcons();
            self.applyActiveFolderColor();
            self._updateBreadcrumb(self.currentFolder);
            self._updateSubfolderCards(self.currentFolder);
            // Re-sort siblings if auto sort is active (e.g. name A-Z)
            if (inst && inst.get_node(nodeId)) {
                self._resortChildren(inst.get_node(nodeId).parent, inst);
            }
            // Refresh grid filter dropdown AFTER tree is updated
            if (self._repopulateGridFilter) self._repopulateGridFilter();
            self.showNotice(nodeData.is_dynamic ? self.data.strings.smartFolderRenamed : self.data.strings.folderRenamed, 'success');
        } else {
            self.showNotice(self.data.strings.folderCreated, 'success');
        }

        self.ajax(ajaxAction, data, function(r) {
            self._isSaving = false;
            if (r.success) {
                if (action === 'create') {
                    const newTermId = r.data && r.data.term_id ? r.data.term_id : null;
                    const inst = self.tree ? self.tree.jstree(true) : null;
                    const pid = parseInt(data.parent_id, 10);
                    const parentJsId = pid ? 'folder_' + pid : '#';

                    if (newTermId && inst) {
                        // Optimistic: add node in-place (no refreshTree)
                        const newNode = {
                            id: 'folder_' + newTermId,
                            text: '<span class="kookoawpf-folder-text">' + $('<span>').text(name).html() + '</span>',
                            data: { folder_id: newTermId, color: (FO.data && FO.data.defaultColor) ? FO.data.defaultColor : '#39FF14', locked: false, pinned: false, count: 0, icon: '', is_dynamic: false, original_text: name },
                            icon: 'kookoawpf-folder-icon',
                            state: { opened: false }
                        };
                        // Hide empty state and show tree
                        self._toggleEmptyState(1);
                        inst.create_node(parentJsId, newNode, 'last', function(n) {
                            // Open parent AFTER create_node so jstree recognizes it has children.
                            // Calling open_node before create_node is a no-op on leaf nodes.
                            if (parentJsId !== '#' && inst.get_node(parentJsId)) {
                                inst.open_node(parentJsId, false, 0);
                            }
                            self.applyFolderIcons();
                            self.applyActiveFolderColor();
                            self._makeTreeNodesDroppable();
                            self._folderListCache = null;
                            if (self._repopulateGridFilter) self._repopulateGridFilter();
                            self._updateFreeLimits();
                            self._updateNoFolderState();
                            self._updateSubfolderCards(self.currentFolder);
                            // In auto sort, snap the new node to its sorted spot.
                            self._resortChildren(parentJsId, inst);
                            // Drop animation
                            const $n = $('#' + n.id);
                            $n.addClass('kookoawpf-just-moved');
                            setTimeout(function() { $n.removeClass('kookoawpf-just-moved'); }, 500);
                        });
                    } else {
                        self.refreshTree(null, newTermId ? { openToNodeId: newTermId } : {});
                    }
                }
            } else {
                self.showNotice((r.data && r.data.message) || self.data.strings.errorGeneric, 'error');
                self.refreshTree();
            }
        });
    };

    FO.deleteFolder = function(id, postType) {
        const self = this;
        if (self._isDeleting) return;
        self._isDeleting = true;
        const inst = self.tree ? self.tree.jstree(true) : null;
        const nodeId = 'folder_' + id;

        // Hide dynamic tooltip if visible
        $('#kookoawpf-dynamic-tooltip').hide();

        // Check if dynamic before optimistic delete (node won't exist after)
        const _delNode = inst ? inst.get_node(nodeId) : null;
        const _delDynamic = _delNode && _delNode.data && _delNode.data.is_dynamic;

        // Optimistic: remove from main sidebar tree immediately
        if (_delNode) {
            inst.delete_node(nodeId);
        }

        // Optimistic: remove from modal tree (always, not just when no main tree)
        const $mTree = $('.kookoawpf-modal-jstree');
        const mInst = $mTree.length ? $mTree.jstree(true) : null;
        if (mInst && mInst.get_node(nodeId)) {
            mInst.delete_node(nodeId);
        }

        self._folderListCache = null;
        if (self._repopulateGridFilter) self._repopulateGridFilter();
        if (inst) {
            self.applyFolderIcons();
            self._updateNoFolderState();
            self._updateFreeLimits();
        }
        // Delay subfolder cards update to ensure jsTree has finished removing the node
        setTimeout(function() {
            if (typeof self._updateSubfolderCards === 'function') {
                self._updateSubfolderCards(self.currentFolder);
            }
        }, 50);
        // Flag: prevent _boostAndPoll from calling refreshFn (we handle refresh ourselves)
        FO._deleteHandlesRefresh = true;
        $('#kookoawpf-modal-delete').hide();
        if (self.currentFolder == id) self.goToFolder('all');
        // Mirror that behaviour in the media modal: if the deleted folder
        // was the active one in the modal sidebar, reset to All Items
        // (selects the nav row + reloads attachments) instead of leaving
        // the modal pinned to a folder that no longer exists.
        if (FO._modalCurrentFolder == id && typeof KookoawpfMediaModal !== 'undefined') {
            const $visModalDel = $('.media-modal:visible');
            if ($visModalDel.length) {
                const $mSidebarDel = $visModalDel.find('.kookoawpf-modal-sidebar');
                const $mFrameDel = $visModalDel.find('.media-frame');
                if ($mSidebarDel.length) {
                    KookoawpfMediaModal._selectModalFolder('all', $mSidebarDel, $mFrameDel);
                }
            }
        }
        self.showNotice(_delDynamic ? self.data.strings.smartFolderDeleted : self.data.strings.folderDeleted, 'success');

        // If the delete dialog is inside a media modal, always use 'attachment'
        // (modal folders are always media, even on product/post/page edit screens)
        const isInMediaModal = $('#kookoawpf-modal-delete').closest('.media-modal').length > 0;
        const deletePostType = postType || (isInMediaModal ? 'attachment' : ((self.data && self.data.postType) ? self.data.postType : 'attachment'));
        self.ajax('kookoawpf_delete_folder', { folder_id: id, post_type: deletePostType }, function(r) {
            self._isDeleting = false;
            FO._deleteHandlesRefresh = false;
            if (!r.success) {
                self.showNotice((r.data && r.data.message) || self.data.strings.errorGeneric, 'error');
                self.refreshTree();
                // Refresh modal tree on error too (restore the node)
                if (typeof KookoawpfMediaModal !== 'undefined') {
                    const $visModal = $('.media-modal:visible');
                    if ($visModal.length) {
                        const $mSidebar = $visModal.find('.kookoawpf-modal-sidebar');
                        const $mFrame = $visModal.find('.media-frame');
                        if ($mSidebar.length) {
                            KookoawpfMediaModal._refreshModalTree($mSidebar, $mFrame);
                        }
                    }
                }
            } else {
                self._updateCounts();
                // Refresh modal tree after server confirms delete
                if (typeof KookoawpfMediaModal !== 'undefined') {
                    const $visModal = $('.media-modal:visible');
                    if ($visModal.length) {
                        const $mSidebar = $visModal.find('.kookoawpf-modal-sidebar');
                        const $mFrame = $visModal.find('.media-frame');
                        if ($mSidebar.length) {
                            KookoawpfMediaModal._refreshModalTree($mSidebar, $mFrame);
                        }
                    }
                }
            }
        });
    };

    FO.updateFolderColor = function(id, color, silent) {
        const self = this;
        // Optimistic: update node data + repaint icons immediately
        const inst = self.tree ? self.tree.jstree(true) : null;
        const nodeId = 'folder_' + id;
        if (inst && inst.get_node(nodeId)) {
            inst.get_node(nodeId).data.color = color;
        }
        // Also update modal tree if present
        const $mt = $('.kookoawpf-modal-jstree');
        const mi = $mt.length ? $mt.jstree(true) : null;
        if (mi && mi.get_node(nodeId)) {
            mi.get_node(nodeId).data.color = color;
            if (typeof KookoawpfMediaModal !== 'undefined') KookoawpfMediaModal._applyModalFolderIcons($mt);
        }
        if (inst) {
            self.applyFolderIcons();
            self.applyActiveFolderColor();
            self._updateSubfolderCards(self.currentFolder);
        }
        if (!silent) {
            self.showNotice(self.data.strings.colorUpdated, 'success');
        }
        if (typeof self._blinkUnderscore === 'function') self._blinkUnderscore();

        // Inside a media modal the folder always belongs to 'attachment'.
        const _colorInModal = $('#kookoawpf-modal-color').closest('.media-modal').length > 0;
        const _colorPostType = _colorInModal ? 'attachment' : ((self.data && self.data.postType) ? self.data.postType : 'attachment');
        self.ajax('kookoawpf_update_folder_color', { folder_id: id, post_type: _colorPostType, color: color }, function(r) {
            if (!r.success) {
                self.showNotice((r.data && r.data.message) || self.data.strings.errorGeneric, 'error');
                self.refreshTree();
            }
        });
    };

    FO.toggleLock = function(id) {
        const self = this;
        const _tlInst = self.tree ? self.tree.jstree(true) : null;
        const _tlNode = _tlInst ? _tlInst.get_node('folder_' + id) : null;
        const _tlDynamic = _tlNode && _tlNode.data && _tlNode.data.is_dynamic;
        self.ajax('kookoawpf_toggle_lock', { folder_id: id, post_type: self.data.postType }, function(r) {
            if (r.success) {
                let _tlMsg = r.data.message || 'Done';
                if (_tlDynamic) {
                    _tlMsg = r.data.locked ? self.data.strings.smartFolderLocked : self.data.strings.smartFolderUnlocked;
                }
                self.showNotice(_tlMsg, 'success');
                const inst = self.tree ? self.tree.jstree(true) : null;
                const nodeId = 'folder_' + id;
                const node = inst ? inst.get_node(nodeId) : null;

                if (inst && node && node.data) {
                    node.data.locked = !node.data.locked;

                    // Update badges in node text
                    const nameText = node.data.original_text || '';
                    let newHtml = '<span class="kookoawpf-folder-text">' + $('<span>').text(nameText).html() + '</span>';
                    if (node.data.pinned) {
                        newHtml += ' <span class="kookoawpf-badge-icon kookoawpf-badge-pin" title="Pinned"><span class="dashicons dashicons-star-filled"></span></span>';
                    }
                    if (node.data.locked) {
                        newHtml += ' <span class="kookoawpf-badge-icon kookoawpf-badge-lock" title="Locked"><span class="dashicons dashicons-lock"></span></span>';
                    }
                    inst.rename_node(nodeId, newHtml);
                    inst.redraw_node(nodeId, true);

                    self.applyFolderIcons();
                    self.applyActiveFolderColor();
                    self._updateFreeLimits();
                    self._updateSubfolderCards(self.currentFolder);
                    self._folderListCache = null;
                    if (self._repopulateGridMoveDropdown) self._repopulateGridMoveDropdown();
                    if (self._repopulateGridFilter) self._repopulateGridFilter();

                    // Drop animation
                    const $n = $('#' + nodeId);
                    $n.addClass('kookoawpf-just-moved');
                    setTimeout(function() { $n.removeClass('kookoawpf-just-moved'); }, 500);
                } else {
                    self.refreshTree();
                }
            } else {
                self.showNotice((r.data && r.data.message) || self.data.strings.errorGeneric, 'error');
                self.refreshTree();
            }
        });
    };

    FO.togglePin = function(id) {
        const self = this;
        const _tpInst = self.tree ? self.tree.jstree(true) : null;
        const _tpNode = _tpInst ? _tpInst.get_node('folder_' + id) : null;
        const _tpDynamic = _tpNode && _tpNode.data && _tpNode.data.is_dynamic;
        self.ajax('kookoawpf_toggle_pin', { folder_id: id, post_type: self.data.postType }, function(r) {
            if (r.success) {
                let _tpMsg = r.data.message || 'Done';
                if (_tpDynamic) {
                    _tpMsg = r.data.pinned ? self.data.strings.smartPinnedToTop : self.data.strings.smartUnpinned;
                }
                self.showNotice(_tpMsg, 'success');
                const inst = self.tree ? self.tree.jstree(true) : null;
                const nodeId = 'folder_' + id;
                const node = inst ? inst.get_node(nodeId) : null;

                if (inst && node && node.data) {
                    // Toggle pinned state
                    node.data.pinned = !node.data.pinned;

                    // Update badge in node text
                    const nameText = node.data.original_text || '';
                    let newHtml = '<span class="kookoawpf-folder-text">' + $('<span>').text(nameText).html() + '</span>';
                    if (node.data.pinned) {
                        newHtml += ' <span class="kookoawpf-badge-icon kookoawpf-badge-pin" title="Pinned"><span class="dashicons dashicons-star-filled"></span></span>';
                    }
                    if (node.data.locked) {
                        newHtml += ' <span class="kookoawpf-badge-icon kookoawpf-badge-lock" title="Locked"><span class="dashicons dashicons-lock"></span></span>';
                    }
                    inst.rename_node(nodeId, newHtml);

                    // Reorder siblings: pinned first, then non-pinned
                    const parentNode = inst.get_node(node.parent);
                    if (parentNode) {
                        const sibs = parentNode.children.slice();
                        const pinned = [], nonPinned = [];
                        for (let i = 0; i < sibs.length; i++) {
                            const sn = inst.get_node(sibs[i]);
                            if (sn && sn.data && sn.data.pinned) { pinned.push(sibs[i]); } else { nonPinned.push(sibs[i]); }
                        }
                        parentNode.children = pinned.concat(nonPinned);
                        self._skipMoveHandler = true;
                        inst.redraw_node(parentNode, true);
                        self._skipMoveHandler = false;
                    }

                    self.applyFolderIcons();
                    self.applyActiveFolderColor();
                    self._updateFreeLimits();
                    self._updateSubfolderCards(self.currentFolder);
                    self._makeTreeNodesDroppable();
                    self._folderListCache = null;
                    if (self._repopulateGridMoveDropdown) self._repopulateGridMoveDropdown();
                    if (self._repopulateGridFilter) self._repopulateGridFilter();

                    // Drop animation
                    const $n = $('#' + nodeId);
                    $n.addClass('kookoawpf-just-moved');
                    setTimeout(function() { $n.removeClass('kookoawpf-just-moved'); }, 500);
                } else {
                    self.refreshTree();
                }
            } else {
                self.showNotice((r.data && r.data.message) || self.data.strings.errorGeneric, 'error');
                self.refreshTree();
            }
        });
    };

    FO.duplicateFolder = function(id) {
        const self = this;
        const inst = self.tree ? self.tree.jstree(true) : null;
        const sourceNode = inst ? inst.get_node('folder_' + id) : null;
        const parentId = sourceNode ? sourceNode.parent : '#';

        // If the source folder has subfolders, the optimistic create_node would
        // only show the top-level duplicate (the server duplicates the whole
        // subtree but we don't know the new child IDs). Fall back to refreshTree
        // in that case so the full duplicated subtree shows up.
        const hasChildren = !!(sourceNode && sourceNode.children && sourceNode.children.length);

        self.ajax('kookoawpf_duplicate_folder', { folder_id: id, post_type: self.data.postType }, function(r) {
            if (r.success) {
                self.showNotice(self.data.strings.folderDuplicated, 'success');
                const newId = r.data && r.data.term_id ? r.data.term_id : null;
                const dupName = r.data && r.data.name ? r.data.name : '';
                if (inst && newId && dupName && !hasChildren) {
                    const srcData = sourceNode && sourceNode.data ? sourceNode.data : {};
                    const newNode = {
                        id: 'folder_' + newId,
                        text: '<span class="kookoawpf-folder-text">' + $('<span>').text(dupName).html() + '</span>',
                        data: { folder_id: newId, color: srcData.color || ((FO.data && FO.data.defaultColor) ? FO.data.defaultColor : '#39FF14'), locked: false, pinned: false, count: 0, icon: srcData.icon || '', is_dynamic: !!srcData.is_dynamic, original_text: dupName },
                        icon: 'kookoawpf-folder-icon',
                        state: { opened: false }
                    };
                    inst.create_node(parentId, newNode, 'last', function(n) {
                        self.applyFolderIcons();
                        self._makeTreeNodesDroppable();
                        self._folderListCache = null;
                        if (self._repopulateGridFilter) self._repopulateGridFilter();
                        self._updateFreeLimits();
                        // In auto sort, snap the new node to its sorted spot.
                        self._resortChildren(parentId, inst);
                        const $n = $('#' + n.id);
                        $n.addClass('kookoawpf-just-moved');
                        setTimeout(function() { $n.removeClass('kookoawpf-just-moved'); }, 500);
                    });
                } else {
                    // Source had subfolders: refreshTree to show the whole
                    // duplicated subtree. No animation here — combining
                    // refreshTree with kookoawpf-just-moved causes a black flash
                    // because the animation starts from opacity:0 while the DOM
                    // is being rebuilt.
                    self.refreshTree();
                }
            } else {
                self.showNotice((r.data && r.data.message) || self.data.strings.errorGeneric, 'error');
            }
        });
    };

    // ========================
    // Move Folder Position (Context Menu)
    // ========================

    FO.moveFolderPosition = function(folderId, direction) {
        const self = this;
        const inst = self.tree.jstree(true);
        const nodeId = 'folder_' + folderId;
        const node = inst.get_node(nodeId);
        if (!node) return;

        const parentNode = inst.get_node(node.parent);
        const siblings = parentNode ? parentNode.children.slice() : [];
        const idx = siblings.indexOf(node.id);
        if (idx === -1) return;

        const isPinned = node.data && node.data.pinned;

        // Compute pin boundary
        let pinBoundary = 0;
        for (let p = 0; p < siblings.length; p++) {
            const sn = inst.get_node(siblings[p]);
            if (sn && sn.data && sn.data.pinned) { pinBoundary = p + 1; } else { break; }
        }

        // Determine movement bounds
        const groupStart = isPinned ? 0 : pinBoundary;
        const groupEnd = isPinned ? pinBoundary : siblings.length;

        const newSiblings = siblings.slice();

        switch (direction) {
            case 'move-top':
                if (idx === groupStart) return;
                newSiblings.splice(idx, 1);
                newSiblings.splice(groupStart, 0, node.id);
                break;
            case 'move-up':
                if (idx <= groupStart) return;
                newSiblings[idx] = newSiblings[idx - 1];
                newSiblings[idx - 1] = node.id;
                break;
            case 'move-down':
                if (idx >= groupEnd - 1) return;
                newSiblings[idx] = newSiblings[idx + 1];
                newSiblings[idx + 1] = node.id;
                break;
            case 'move-bottom':
                if (idx === groupEnd - 1) return;
                newSiblings.splice(idx, 1);
                newSiblings.splice(groupEnd - 1, 0, node.id);
                break;
            default: return;
        }

        // Convert node IDs to folder IDs
        const siblingOrder = [];
        for (let j = 0; j < newSiblings.length; j++) {
            const sfid = self.getFolderId(inst.get_node(newSiblings[j]));
            if (sfid) siblingOrder.push(sfid);
        }

        // Optimistic: reorder in jsTree immediately (no waiting for AJAX)
        self._skipMoveHandler = true;
        parentNode.children = newSiblings;
        inst.redraw_node(parentNode, true);
        self._skipMoveHandler = false;
        // Drop animation (same as drag & drop)
        const $moved = $('#' + nodeId);
        $moved.addClass('kookoawpf-just-moved');
        setTimeout(function() { $moved.removeClass('kookoawpf-just-moved'); }, 500);
        self.applyFolderIcons();
        self.applyActiveFolderColor();
        self._updateSubfolderCards(self.currentFolder);

        self._folderListCache = null;
        if (self._repopulateGridFilter) self._repopulateGridFilter();
        const isDynamic = node.data && node.data.is_dynamic;
        self.ajax('kookoawpf_move_folder', {
            folder_id: folderId,
            new_parent_id: self.getParentFolderId(inst, node),
            post_type: self.data.postType,
            sibling_order: siblingOrder
        }, function(r) {
            if (r.success) {
                self.showNotice(isDynamic ? self.data.strings.smartFolderMoved : self.data.strings.folderMoved, 'success');
            } else {
                self.showNotice((r.data && r.data.message) || self.data.strings.errorGeneric, 'error');
                self.refreshTree(); // Revert on failure
            }
        });
    };

})(jQuery);
