/**
 * Folder Tree Module
 * Handles folder tree rendering and multi-select
 */
(function() {
    'use strict';

    // ── Classify & Copy helpers ────────────────────────────────────────────
    function classifyOn() {
        var c = window.app.modules.classify;
        return c && c.isEnabled();
    }
    // All file objects in a folder's (already-scanned) subtree — group-drag.
    function subtreeFiles(folder, out) {
        out = out || [];
        (folder.files || []).forEach(function (f) { out.push(f); });
        (folder.children || []).forEach(function (c) { subtreeFiles(c, out); });
        return out;
    }
    function keysFor(files) {
        var c = window.app.modules.classify;
        return files.map(function (f) { return c.srcKeyForFile(f); });
    }
    // A small status dot reflecting a file's classification state.
    var STATE_TITLE = {
        none: 'unassigned', tracking: 'has tracking number, needs a transmittal',
        transmittal: 'in a transmittal, needs a tracking number',
        partial: 'placed, but the name is incomplete', done: 'fully classified',
        excluded: 'excluded — will not be copied',
    };
    function stateDot(state) {
        var dot = document.createElement('span');
        dot.className = 'cl-dot cl-dot--' + state;
        dot.title = STATE_TITLE[state] || '';
        return dot;
    }

    // ── Classify-mode source-tree filters ──────────────────────────────────
    // The goal in either target tab is to assign-or-exclude every file. Each
    // file falls in one bucket FOR THE ACTIVE AXIS — unassigned / assigned /
    // excluded — and three "Show …" toggles control which buckets are visible
    // (so unchecking Assigned+Excluded leaves only what's left to do). A folder
    // whose whole scanned subtree is filtered away is itself hidden.
    var showFilters = { unassigned: true, assigned: true, excluded: true };
    var showEmpty = true;   // show folders that contain no files
    function setShowFilters(f) {
        showFilters = {
            unassigned: f.unassigned !== false,
            assigned: f.assigned !== false,
            excluded: f.excluded !== false,
        };
        showEmpty = f.empty !== false;
        render();
    }
    function allFiltersOn() { return showFilters.unassigned && showFilters.assigned && showFilters.excluded; }
    function activeAxis() {
        var tt = window.app.modules.targetTree;
        return (tt && tt.activeAxis) ? tt.activeAxis() : 'tracking';
    }
    // Bucket a file relative to the active axis: 'excluded' | 'assigned' | 'unassigned'.
    function fileCategory(file) {
        var c = window.app.modules.classify;
        var a = c.getAssignment(c.srcKeyForFile(file));
        if (a && a.excluded) return 'excluded';
        var assigned = a && (activeAxis() === 'transmittal' ? a.transmittalNodeId : a.trackingNodeId);
        return assigned ? 'assigned' : 'unassigned';
    }
    function classifyAllows(file) { return !classifyOn() || !!showFilters[fileCategory(file)]; }

    // ── name filter (the autofilter box above the tree) ────────────────────
    // Live substring search over each file's full path+name (and folder names),
    // ANDing space-separated terms. Matches reveal their whole folder hierarchy.
    var nameFilter = '', filterTerms = [];
    function setNameFilter(q) {
        nameFilter = (q || '').trim();
        filterTerms = nameFilter.toLowerCase().split(/\s+/).filter(Boolean);
        render();
    }
    function filterActive() { return filterTerms.length > 0; }
    function nameHit(text) {
        if (!filterTerms.length) return true;
        var t = String(text || '').toLowerCase();
        for (var i = 0; i < filterTerms.length; i++) { if (t.indexOf(filterTerms[i]) === -1) return false; }
        return true;
    }
    // Anything narrowing the tree (a name search, a show-filter off, or hiding empties).
    function anyFilter() { return filterActive() || (classifyOn() && (!allFiltersOn() || !showEmpty)); }
    // One pass → the set of folder paths + file keys to render. A file shows when
    // it passes the show-filters AND (no name search, OR an ancestor folder
    // matched, OR its own path/name matches). A folder shows when it (or an
    // ancestor) matches, or anything inside it shows — so the path to a hit is
    // always revealed.
    var visible = null;   // { folders, files } while filtering, else null
    function computeVisible() {
        var c = window.app.modules.classify;
        var folders = Object.create(null), files = Object.create(null), open = Object.create(null);
        var nf = filterActive();
        function walk(folder, ancMatched) {
            var selfMatch = nf && nameHit(folder.path || folder.name);
            var matched = ancMatched || selfMatch;
            var show = false, hasFile = false, descMatch = false;
            (folder.children || []).forEach(function (ch) {
                var r = walk(ch, matched);
                if (r.show) show = true;
                if (r.hasFile) hasFile = true;
                if (r.subtreeMatch) descMatch = true;   // a child leads to a match
            });
            (folder.files || []).forEach(function (f) {
                hasFile = true;
                if (!classifyAllows(f)) return;
                var fileMatch = nf && nameHit(c.srcKeyForFile(f));
                if (!nf || matched || fileMatch) { files[c.srcKeyForFile(f)] = true; show = true; }
                if (fileMatch) descMatch = true;         // a match sits directly in this folder
            });
            if (matched) show = true;
            // "Show Empty" off → hide folders whose whole subtree holds no files.
            if (!hasFile && !showEmpty && !matched) show = false;
            if (show) folders[folder.path] = true;
            // Auto-open ONLY the connector folders on the path down to a match —
            // never the matched node itself. Terminal matches and everything
            // off-path keep their real collapse state; the root's expand-all
            // covers the rest. (Search reveals where hits are; it doesn't reshape
            // the tree.)
            if (nf && descMatch) open[folder.path] = true;
            return { show: show, hasFile: hasFile, subtreeMatch: descMatch || selfMatch };
        }
        (window.app.folderTree || []).forEach(function (root) { walk(root, false); });
        return { folders: folders, files: files, open: open };
    }
    // True only for folders the search needs opened to expose a hit beneath them.
    function autoOpen(folder) { return !!(visible && visible.open && visible.open[folder.path]); }
    function folderShown(folder) { return !visible || !!visible.folders[folder.path]; }
    function fileShown(file) {
        if (!classifyAllows(file)) return false;
        return !visible || !!visible.files[window.app.modules.classify.srcKeyForFile(file)];
    }
    // All scanned files (for the per-bucket counts on the filter checkboxes).
    function allClassifyFiles() {
        var out = [];
        (window.app.folderTree || []).forEach(function (f) { subtreeFiles(f, out); });
        return out;
    }
    function updateFilterCounts() {
        if (!classifyOn()) return;
        var n = { unassigned: 0, assigned: 0, excluded: 0 };
        allClassifyFiles().forEach(function (f) { n[fileCategory(f)]++; });
        ['unassigned', 'assigned', 'excluded'].forEach(function (k) {
            var el = document.getElementById('show' + k.charAt(0).toUpperCase() + k.slice(1) + 'Count');
            if (el) el.textContent = '(' + n[k] + ')';
        });
    }

    /**
     * Render the folder tree
     */
    function render() {
        const container = window.app.dom.folderTree;
        // Preserve scroll across re-render — toggling a Show filter shouldn't
        // jump the view back to the top.
        const prevScroll = container.scrollTop;
        wireClassifyInteractions();
        container.innerHTML = '';
        updateFilterCounts();
        visible = anyFilter() ? computeVisible() : null;

        if (window.app.folderTree.length === 0) {
            container.innerHTML = '<div class="tree-empty">No folders found</div>';
            return;
        }

        window.app.folderTree.forEach(folder => {
            if (!folderShown(folder)) return;
            const element = createFolderElement(folder);
            container.appendChild(element);
        });
        if (!container.children.length) {
            container.innerHTML = '<div class="tree-empty">'
                + (filterActive() ? 'No files match “' + nameFilter + '”.' : 'Nothing matches the current filters.') + '</div>';
        }

        updateSelectedCount();
        container.scrollTop = prevScroll;
    }

    /**
     * Populate a folder row's count element with "direct+total" counts, e.g.
     * "(2+10 folders, 15+300 files)" — direct (immediate children) shows as
     * soon as the folder's own directory is read; the total (whole subtree)
     * grows and flashes grey until the subtree is fully scanned, then goes
     * solid. The "+total" part is omitted once scanning is done and there's
     * nothing deeper (direct == total).
     */
    function populateCount(el, folder) {
        el.textContent = '';
        el.classList.remove('done');
        const st = folder.scanState;
        if (st === 'pending') return;
        if (st === 'zip-pending') { el.textContent = '(zip — open to scan)'; return; }
        if (st === 'scanning') { el.textContent = 'scanning…'; return; }

        const done = st === 'done';
        // When fully scanned both numbers are blue; .done turns the labels blue too.
        if (done) el.classList.add('done');
        const dDir = folder.subdirCount || 0, tDir = folder.runDirs || 0;
        const dFile = folder.fileCount || 0, tFile = folder.runFiles || 0;

        const frag = document.createDocumentFragment();
        frag.appendChild(document.createTextNode('('));
        if (dDir > 0 || tDir > 0) {
            appendPair(frag, dDir, tDir, done);
            appendLabel(frag, tDir === 1 ? ' folder, ' : ' folders, ');
        }
        appendPair(frag, dFile, tFile, done);
        appendLabel(frag, tFile === 1 ? ' file)' : ' files)');
        el.appendChild(frag);
    }

    // The "folders"/"files" word labels — blue only once the row is .done.
    function appendLabel(frag, text) {
        const s = document.createElement('span');
        s.className = 'ct-label';
        s.textContent = text;
        frag.appendChild(s);
    }

    // Append "<direct>" (always a completed/blue number) and, when there's a
    // subtree (or scanning is ongoing), "+<total>" with the total in a span
    // that greys + pulses until final, then turns blue.
    function appendPair(frag, direct, total, done) {
        const d = document.createElement('span');
        d.className = 'ct-direct';
        d.textContent = String(direct);
        frag.appendChild(d);
        if (!done || total > direct) {
            frag.appendChild(document.createTextNode('+'));
            const t = document.createElement('span');
            t.className = 'ct-total' + (done ? '' : ' pending');
            t.textContent = String(total);
            frag.appendChild(t);
        }
    }

    /**
     * Create a folder element
     */
    function createFolderElement(folder, level = 0) {
        const div = document.createElement('div');
        
        const item = document.createElement('div');
        item.className = 'folder-item';
        // Grey the row until its subtree is fully scanned (scanState 'done');
        // 'scanning' rows also get a subtle pulse via CSS.
        if (folder.scanState && folder.scanState !== 'done') {
            item.classList.add('scanning');
        }
        item.dataset.path = folder.path;
        item.style.paddingLeft = `${level * 1.5}rem`;

        // Check if selected
        if (window.app.selectedFolders.has(folder.path)) {
            item.classList.add('selected');
        }

        // Classify mode: the folder row is a drag source for a group-drag of
        // every file in its subtree.
        if (classifyOn()) {
            item.draggable = true;
            item.addEventListener('dragstart', function (e) {
                e.stopPropagation();
                var files = subtreeFiles(folder);
                if (!files.length) { e.preventDefault(); return; }
                window.app.modules.dnd.setDrag(keysFor(files), e);
            });
        }

        // Toggle button: shown when the folder has children OR hasn't been
        // scanned yet (it might have children — expanding triggers its scan).
        const toggle = document.createElement('span');
        toggle.className = 'folder-toggle';
        const mightHaveChildren = (folder.children && folder.children.length > 0)
            || folder.scanState === 'pending'
            || folder.scanState === 'zip-pending'
            // Classify mode: a folder with files (even none of subfolders) is
            // expandable so its files can be revealed and dragged.
            || (classifyOn() && folder.files && folder.files.length > 0);
        if (mightHaveChildren) {
            toggle.textContent = (folder.expanded || autoOpen(folder)) ? '▼' : '▶';
            toggle.addEventListener('click', (e) => {
                e.stopPropagation();
                const recursive = e.ctrlKey || e.metaKey;
                toggleFolder(folder, recursive);
            });
        } else {
            toggle.textContent = ' ';
        }
        item.appendChild(toggle);

        // Folder icon (different for ZIP files)
        const icon = document.createElement('span');
        icon.className = 'folder-icon';
        if (folder.isZipRoot) {
            icon.innerHTML = '&#128230;';  // 📦
        } else if (folder.isVirtualDir) {
            icon.innerHTML = '&#128194;';  // 📂
        } else {
            icon.innerHTML = '&#128193;';  // 📁
        }
        item.appendChild(icon);

        // Classify mode: an aggregate state dot for the folder's subtree, and a
        // struck-through name when the WHOLE subtree is excluded (mirrors files).
        if (classifyOn()) {
            const agg = aggregateState(subtreeFiles(folder));
            if (agg) item.appendChild(stateDot(agg));
            if (agg === 'excluded') item.classList.add('excluded');
        }

        // Folder name
        const name = document.createElement('span');
        name.className = 'folder-name';
        name.textContent = folder.name;
        item.appendChild(name);

        // Subfolder / file counts (immediate). Greyed via the row's .scanning
        // class until the subtree is fully scanned.
        const count = document.createElement('span');
        count.className = 'folder-count';
        populateCount(count, folder);
        item.appendChild(count);
        
        // Extract button for ZIP roots
        if (folder.isZipRoot) {
            const extractBtn = document.createElement('button');
            extractBtn.className = 'btn btn-sm zip-extract-btn';
            extractBtn.textContent = '📤 Extract';
            extractBtn.title = 'Extract ZIP contents to folder';
            extractBtn.addEventListener('click', async (e) => {
                e.stopPropagation();
                await handleExtractZip(folder);
            });
            item.appendChild(extractBtn);
        }
        
        // Extract All button for folders with ZIP descendants (but not ZIP roots themselves)
        if (!folder.isZipRoot && !folder.isVirtualDir) {
            const zipCount = countZipDescendants(folder);
            if (zipCount > 0) {
                const extractAllBtn = document.createElement('button');
                extractAllBtn.className = 'btn btn-sm zip-extract-all-btn';
                extractAllBtn.textContent = `📤 Extract All (${zipCount})`;
                extractAllBtn.title = `Extract all ${zipCount} ZIP file(s) in this folder`;
                extractAllBtn.addEventListener('click', async (e) => {
                    e.stopPropagation();
                    await handleExtractAllZips(folder);
                });
                item.appendChild(extractAllBtn);
            }
        }

        // Click handler for selection
        item.addEventListener('click', (e) => {
            handleFolderClick(folder, e);
        });

        div.appendChild(item);

        // Children — when expanded, or opened on the path to a search hit below.
        // The Show toggles never force-expand; search opens only connector folders.
        if ((folder.expanded || autoOpen(folder)) && folder.children && folder.children.length > 0) {
            const childrenDiv = document.createElement('div');
            childrenDiv.className = 'folder-children';
            folder.children.forEach(child => {
                if (!folderShown(child)) return;
                const childElement = createFolderElement(child, level + 1);
                childrenDiv.appendChild(childElement);
            });
            div.appendChild(childrenDiv);
        }

        // Classify mode: list this folder's own files (draggable leaves) when
        // expanded (or opened to reveal a search hit), so they can be dropped.
        if (classifyOn() && (folder.expanded || autoOpen(folder)) && folder.files && folder.files.length > 0) {
            const filesDiv = document.createElement('div');
            filesDiv.className = 'folder-children folder-files';
            folder.files.forEach(function (file) {
                if (!fileShown(file)) return;
                filesDiv.appendChild(createFileElement(file, level + 1));
            });
            div.appendChild(filesDiv);
        }

        return div;
    }

    /**
     * Create a draggable source-file row (classify mode only).
     */
    function createFileElement(file, level) {
        const c = window.app.modules.classify;
        const item = document.createElement('div');
        item.className = 'file-item';
        item.style.paddingLeft = `${level * 1.5}rem`;
        item.draggable = true;
        item.title = 'Click to preview · drag onto a tracking folder or transmittal to assign';
        const key = c.srcKeyForFile(file);
        item.dataset.key = key;
        const st = c.fileState(file);
        if (st === 'excluded') item.classList.add('excluded');

        item.appendChild(stateDot(st));

        const icon = document.createElement('span');
        icon.className = 'file-icon';
        icon.innerHTML = '&#128196;'; // 📄
        item.appendChild(icon);

        const name = document.createElement('span');
        name.className = 'file-name';
        name.textContent = zddc.joinExtension(file.originalFilename, file.extension);
        item.appendChild(name);

        item.addEventListener('dragstart', function (e) {
            e.stopPropagation();
            window.app.modules.dnd.setDrag([key], e);
        });
        return item;
    }

    /**
     * Handle folder click with multi-select support
     */
    function handleFolderClick(folder, event) {
        if (event.ctrlKey || event.metaKey) {
            // Ctrl+Click: Toggle selection
            if (window.app.selectedFolders.has(folder.path)) {
                window.app.selectedFolders.delete(folder.path);
            } else {
                window.app.selectedFolders.add(folder.path);
            }
        } else if (event.shiftKey) {
            // Shift+Click: Range selection
            const visibleFolders = getVisibleFolders();
            const currentIndex = visibleFolders.findIndex(f => f.path === folder.path);
            
            if (currentIndex >= 0 && window.app.lastSelectedFolderPath) {
                const lastIndex = visibleFolders.findIndex(f => f.path === window.app.lastSelectedFolderPath);
                
                if (lastIndex >= 0) {
                    const start = Math.min(currentIndex, lastIndex);
                    const end = Math.max(currentIndex, lastIndex);
                    
                    // Select range
                    for (let i = start; i <= end; i++) {
                        window.app.selectedFolders.add(visibleFolders[i].path);
                    }
                }
            } else {
                window.app.selectedFolders.add(folder.path);
            }
        } else {
            // Normal click: Single selection
            window.app.selectedFolders.clear();
            window.app.selectedFolders.add(folder.path);
        }

        // Remember last selected for shift-click
        window.app.lastSelectedFolderPath = folder.path;

        // Re-render tree
        render();

        // Load files from selected folders
        loadFilesFromSelectedFolders();
    }

    /**
     * Handle ZIP extraction
     */
    async function handleExtractZip(folder) {
        if (!folder.isZipRoot || !folder.zipPath) return;
        
        try {
            const confirmed = confirm(`Extract "${folder.name}" to a new folder?\n\nThis will create a folder named "${folder.name.replace(/\.zip$/i, '')}" with the ZIP contents.`);
            if (!confirmed) return;
            
            // Show extracting state
            const btn = document.querySelector(`.folder-item[data-path="${folder.path}"] .zip-extract-btn`);
            if (btn) {
                btn.textContent = '⏳ Extracting...';
                btn.disabled = true;
            }
            
            await window.app.modules.scanner.extractZip(folder.zipPath);
            
            // Auto-refresh preserving tree state
            await window.app.modules.scanner.scanDirectory(window.app.rootHandle, true);
        } catch (err) {
            console.error('Error extracting ZIP:', err);
            alert('Error extracting ZIP: ' + err.message);
            
            // Reset button
            const btn = document.querySelector(`.folder-item[data-path="${folder.path}"] .zip-extract-btn`);
            if (btn) {
                btn.textContent = '📤 Extract';
                btn.disabled = false;
            }
        }
    }
    
    /**
     * Count ZIP descendants in a folder
     */
    function countZipDescendants(folder) {
        let count = 0;
        if (folder.children) {
            for (const child of folder.children) {
                if (child.isZipRoot) {
                    count++;
                }
                count += countZipDescendants(child);
            }
        }
        return count;
    }
    
    /**
     * Get all ZIP folders as flat list
     */
    function getZipDescendants(folder, zips = []) {
        if (folder.children) {
            for (const child of folder.children) {
                if (child.isZipRoot) {
                    zips.push(child);
                }
                getZipDescendants(child, zips);
            }
        }
        return zips;
    }
    
    /**
     * Handle extracting all ZIPs in a folder
     */
    async function handleExtractAllZips(folder) {
        const zips = getZipDescendants(folder);
        if (zips.length === 0) return;
        
        const confirmed = confirm(`Extract ${zips.length} ZIP file(s)?\n\nThis will create folders for each ZIP with their contents.`);
        if (!confirmed) return;
        
        try {
            // Show extracting state on button
            const btn = document.querySelector(`.folder-item[data-path="${folder.path}"] .zip-extract-all-btn`);
            if (btn) {
                btn.textContent = '⏳ Extracting...';
                btn.disabled = true;
            }
            
            // Extract all ZIPs
            for (const zip of zips) {
                if (zip.zipPath) {
                    await window.app.modules.scanner.extractZip(zip.zipPath);
                }
            }
            
            // Auto-refresh preserving tree state
            await window.app.modules.scanner.scanDirectory(window.app.rootHandle, true);
        } catch (err) {
            console.error('Error extracting ZIPs:', err);
            alert('Error extracting ZIPs: ' + err.message);
            
            // Reset button
            const btn = document.querySelector(`.folder-item[data-path="${folder.path}"] .zip-extract-all-btn`);
            if (btn) {
                btn.textContent = `📤 Extract All (${zips.length})`;
                btn.disabled = false;
            }
        }
    }

    /**
     * Toggle folder expansion
     */
    function toggleFolder(folder, recursive = false) {
        folder.expanded = !folder.expanded;

        if (recursive && folder.children) {
            // Recursively expand/collapse all children
            const newState = folder.expanded;
            function setAllExpanded(f) {
                f.expanded = newState;
                if (f.children) {
                    f.children.forEach(setAllExpanded);
                }
            }
            folder.children.forEach(setAllExpanded);
        }

        render();

        // Opening a not-yet-complete folder jumps its subtree to the front of
        // the scan so its contents are complete on open (re-renders as it
        // fills in). Background scanning continues for everything else.
        if (folder.expanded && folder.scanState !== 'done'
            && window.app.modules.scanner && window.app.modules.scanner.ensureScanned) {
            window.app.modules.scanner.ensureScanned(folder).then(render).catch(() => {});
        }
    }

    /**
     * Load files from all selected folders
     */
    async function loadFilesFromSelectedFolders() {
        // Use store to manage files
        window.app.modules.store.setSelectedFolders(Array.from(window.app.selectedFolders));
    }

    /**
     * Find folder by path in tree
     */
    function findFolderByPath(path) {
        function search(folders) {
            for (const folder of folders) {
                if (folder.path === path) {
                    return folder;
                }
                if (folder.children) {
                    const found = search(folder.children);
                    if (found) return found;
                }
            }
            return null;
        }
        return search(window.app.folderTree);
    }

    /**
     * Update selected folders count
     */
    function updateSelectedCount() {
        const el = window.app.dom.selectedFoldersCount;
        if (!el) return;   // count no longer shown in the folder-tree header
        const count = window.app.selectedFolders.size;
        el.textContent = `${count} folder${count !== 1 ? 's' : ''} selected`;
    }

    /**
     * Build folder tree from scanned data
     */
    function buildTree(rootHandle, foldersMap) {
        const tree = [];
        
        // Convert flat map to tree structure
        function buildNode(handle, path) {
            // For virtual folders, look up by path string; for real folders, use handle
            let files;
            if (handle.isVirtualDir || handle.isZipRoot) {
                files = foldersMap.get(handle.virtualPath || handle.zipPath) || [];
            } else {
                files = foldersMap.get(handle) || [];
            }
            
            // Add folderPath to each file for folder highlighting (filter out null files)
            files.filter(file => file !== null).forEach(file => {
                if (!file.isDirectory) {
                    file.folderPath = path;
                }
            });
            
            // Filter out null files for the node
            const validFiles = files.filter(f => f !== null);
            
            const node = {
                name: handle.name,
                path: path,
                handle: handle,
                files: validFiles,
                fileCount: validFiles.length,
                children: [],
                expanded: false
            };
            
            // Mark ZIP-related nodes
            if (handle.isZipRoot) {
                node.isZipRoot = true;
                node.zipPath = handle.zipPath;
            }
            if (handle.isVirtualDir) {
                node.isVirtualDir = true;
                node.zipPath = handle.zipPath;
            }
            
            return node;
        }

        // Recursively build tree
        function addChildren(node) {
            // Get subdirectories (filter out null files first)
            // For virtual folders, look up by path string
            let files;
            if (node.handle.isVirtualDir || node.handle.isZipRoot) {
                files = foldersMap.get(node.handle.virtualPath || node.handle.zipPath) || [];
            } else {
                files = foldersMap.get(node.handle) || [];
            }
            const validFiles = files.filter(f => f !== null);
            const subdirs = validFiles.filter(f => f.isDirectory);
            
            subdirs.forEach(subdir => {
                const childPath = node.path + '/' + subdir.handle.name;
                const childNode = buildNode(subdir.handle, childPath);
                addChildren(childNode);
                node.children.push(childNode);
            });
            
            // Update file count to exclude directories and null files
            node.files = validFiles.filter(f => !f.isDirectory);
            node.fileCount = node.files.length;
        }

        // Build root
        const root = buildNode(rootHandle, rootHandle.name);
        addChildren(root);
        
        // Expand root by default
        root.expanded = true;
        
        tree.push(root);
        return tree;
    }

    /**
     * Get all currently visible folders (expanded tree)
     */
    function getVisibleFolders() {
        const visible = [];
        
        function traverse(folders) {
            for (const folder of folders) {
                visible.push(folder);
                if (folder.expanded && folder.children) {
                    traverse(folder.children);
                }
            }
        }
        
        traverse(window.app.folderTree);
        return visible;
    }

    /**
     * Select all visible folders
     */
    function selectAllVisible() {
        const visible = getVisibleFolders();
        window.app.selectedFolders.clear();
        visible.forEach(f => window.app.selectedFolders.add(f.path));
        render();
        loadFilesFromSelectedFolders();
    }

    /**
     * Expand all folders in tree
     */
    function expandAll() {
        function setAllExpanded(folder) {
            folder.expanded = true;
            if (folder.children) {
                folder.children.forEach(setAllExpanded);
            }
        }
        
        window.app.folderTree.forEach(setAllExpanded);
        render();
    }

    /**
     * Select all folders in tree
     */
    function selectAll() {
        function collectAllPaths(folders, paths = []) {
            folders.forEach(folder => {
                paths.push(folder.path);
                if (folder.children) {
                    collectAllPaths(folder.children, paths);
                }
            });
            return paths;
        }
        
        const allPaths = collectAllPaths(window.app.folderTree);
        allPaths.forEach(path => window.app.selectedFolders.add(path));
        
        render();
        loadFilesFromSelectedFolders();
    }

    /**
     * Set up keyboard shortcuts for folder tree
     */
    function setupKeyboardShortcuts() {
        const container = window.app.dom.folderTree;
        
        container.addEventListener('keydown', (e) => {
            // Ctrl+A: Select all visible
            if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
                e.preventDefault();
                selectAllVisible();
            }
        });
        
        // Make container focusable
        container.tabIndex = 0;
    }

    // ── Classify interactions (exclude menu, cross-tree reveal) ─────────────
    var classifyWired = false;
    function wireClassifyInteractions() {
        if (classifyWired) return;
        classifyWired = true;
        var ft = window.app.dom.folderTree;
        if (!ft) { classifyWired = false; return; }
        ft.addEventListener('contextmenu', onContextMenu);
        // Single-click a source file → preview it (the "look at it, then assign"
        // half of the workflow). Drag still assigns; right-click excludes.
        ft.addEventListener('click', function (e) {
            if (!classifyOn()) return;
            var fe = e.target.closest('.file-item');
            if (!fe || !fe.dataset.key) return;
            var file = findFileByKey(fe.dataset.key);
            if (file && window.app.modules.preview && window.app.modules.preview.previewFile) {
                window.app.modules.preview.previewFile(file);
            }
        });
    }

    // Aggregate classification state across a folder's loaded subtree files.
    function aggregateState(files) {
        if (!files.length) return null;
        var c = window.app.modules.classify;
        var ex = 0, done = 0, placed = 0;
        files.forEach(function (f) {
            var s = c.fileState(f);
            if (s === 'excluded') ex++;
            else if (s === 'done') done++;
            else if (s !== 'none') placed++;
        });
        if (ex === files.length) return 'excluded';
        var active = files.length - ex;
        if (active > 0 && done === active) return 'done';
        if (done > 0 || placed > 0) return 'partial';
        return 'none';
    }

    function findFolderByPath(path) {
        var hit = null;
        (function walk(nodes) {
            (nodes || []).forEach(function (n) {
                if (hit) return;
                if (n.path === path) { hit = n; return; }
                walk(n.children);
            });
        })(window.app.folderTree);
        return hit;
    }
    function findFileByKey(key) {
        var c = window.app.modules.classify, hit = null;
        (function walk(nodes) {
            (nodes || []).forEach(function (n) {
                if (hit) return;
                (n.files || []).forEach(function (f) { if (!hit && c.srcKeyForFile(f) === key) hit = f; });
                walk(n.children);
            });
        })(window.app.folderTree);
        return hit;
    }
    function expandToPath(folderPath) {
        (function walk(nodes) {
            (nodes || []).forEach(function (n) {
                if (n.path === folderPath || folderPath.indexOf(n.path + '/') === 0) {
                    n.expanded = true;
                    walk(n.children);
                }
            });
        })(window.app.folderTree);
    }

    // Reveal a source file (target → source). Expands its folder chain, renders,
    // scrolls + flashes the row.
    function revealFile(key) {
        var file = findFileByKey(key);
        if (!file) return;
        expandToPath(file.folderPath);
        render();
        var rows = window.app.dom.folderTree.querySelectorAll('.file-item');
        var row = Array.prototype.filter.call(rows, function (r) { return r.dataset.key === key; })[0];
        if (row) {
            row.scrollIntoView({ block: 'center' });
            row.classList.add('match-highlight');
            setTimeout(function () { row.classList.remove('match-highlight'); }, 1500);
        }
    }

    // ── per-zip mode toggle (single file ⇄ expandable folder) ───────────────
    function persistTreeChange() {
        var ws = window.app.modules.workspace;
        if (ws && ws.onRescanned) ws.onRescanned();
    }
    async function expandZip(file) {
        if (!file.handle && !window.app.rootHandle) {
            if (window.zddc) window.zddc.toast('Connect the source directory first to expand this archive.', 'warning');
            return;
        }
        try {
            var node = await window.app.modules.scanner.expandZipAsFolder(file);
            if (node) { render(); persistTreeChange(); }
        } catch (e) {
            if (window.zddc) window.zddc.toast('Couldn’t expand the archive — ' + (e.message || e), 'error');
        }
    }
    function collapseZip(zipNode) {
        if (!zipNode) return;
        window.app.modules.scanner.collapseZipToFile(zipNode);
        render();
        persistTreeChange();
    }

    // ── context menu (exclude / include / clear / zip mode) ─────────────────
    var menuEl = null;
    function hideMenu() { if (menuEl) { menuEl.remove(); menuEl = null; } }
    function showMenu(x, y, items) {
        hideMenu();
        menuEl = document.createElement('div');
        menuEl.className = 'cl-menu';
        items.forEach(function (it) {
            var b = document.createElement('button');
            b.className = 'cl-menu__item';
            b.textContent = it.label;
            b.addEventListener('click', function () { hideMenu(); it.fn(); });
            menuEl.appendChild(b);
        });
        menuEl.style.left = x + 'px';
        menuEl.style.top = y + 'px';
        document.body.appendChild(menuEl);
        setTimeout(function () {
            document.addEventListener('click', hideMenu, { once: true });
            document.addEventListener('scroll', hideMenu, { once: true, capture: true });
        }, 0);
    }
    function onContextMenu(e) {
        if (!classifyOn()) return;
        var c = window.app.modules.classify;
        var fileEl = e.target.closest('.file-item');
        var folderEl = e.target.closest('.folder-item');
        if (!fileEl && !folderEl) return;
        e.preventDefault();
        var items = [];
        if (fileEl) {
            var key = fileEl.dataset.key;
            var a = c.getAssignment(key);
            var excluded = !!(a && a.excluded);
            items.push({ label: excluded ? 'Include in copy' : 'Exclude from copy', fn: function () { c.setExcluded([key], !excluded); } });
            if (a && (a.trackingNodeId || a.transmittalNodeId)) {
                if (a.trackingNodeId) items.push({ label: 'Clear tracking', fn: function () { c.place([key], null, 'tracking'); } });
                if (a.transmittalNodeId) items.push({ label: 'Clear transmittal', fn: function () { c.place([key], null, 'transmittal'); } });
            }
            var file = findFileByKey(key);
            if (file && file.isVirtual) {
                items.push({ label: 'Collapse archive to single file', fn: function () { collapseZip(findFolderByPath(file.zipPath)); } });
            } else if (file && file.extension === 'zip') {
                items.push({ label: 'Expand as folder', fn: function () { expandZip(file); } });
            }
        } else {
            var folder = findFolderByPath(folderEl.dataset.path);
            if (folder && folder.isZipRoot) {
                items.push({ label: 'Collapse to single file', fn: function () { collapseZip(folder); } });
            }
            var keys = keysFor(subtreeFiles(folder || { files: [], children: [] }));
            if (keys.length) {
                var allExcl = keys.every(function (k) { var a = c.getAssignment(k); return a && a.excluded; });
                items.push({
                    label: (allExcl ? 'Include' : 'Exclude') + ' folder (' + keys.length + ' file' + (keys.length === 1 ? '' : 's') + ')',
                    fn: function () { c.setExcluded(keys, !allExcl); },
                });
            }
            if (!items.length) return;
        }
        showMenu(e.clientX, e.clientY, items);
    }

    // Export module
    window.app.modules.tree = {
        render,
        buildTree,
        loadFilesFromSelectedFolders,
        setupKeyboardShortcuts,
        expandAll,
        selectAll,
        revealFile,
        setShowFilters,
        setNameFilter
    };
})();
