/**
 * COMP-O-RAMA ExtendScript Host
 * After Effects bridge for composition creation and management
 *
 * Architecture: Single, clean bridge - no scattered eval calls
 * All AE operations flow through deterministic, undo-safe functions
 */

// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================

/**
 * Safe JSON response wrapper
 */
function jsonResponse(success, message, data) {
    return JSON.stringify({
        success: success,
        message: message,
        data: data || null
    });
}

/**
 * Safe undo group wrapper
 */
function withUndo(name, fn) {
    app.beginUndoGroup(name);
    try {
        var result = fn();
        app.endUndoGroup();
        return result;
    } catch (e) {
        app.endUndoGroup();
        throw e;
    }
}

/**
 * Parse hex color to AE color array [r, g, b]
 */
function hexToRGB(hex) {
    hex = hex.replace('#', '');
    return [
        parseInt(hex.substr(0, 2), 16) / 255,
        parseInt(hex.substr(2, 2), 16) / 255,
        parseInt(hex.substr(4, 2), 16) / 255
    ];
}

/**
 * Get color from name or hex
 */
function getColorArray(colorInput) {
    var colors = {
        'black': [0, 0, 0],
        'white': [1, 1, 1],
        'red': [1, 0, 0],
        'green': [0, 1, 0],
        'blue': [0, 0, 1],
        'gray': [0.5, 0.5, 0.5]
    };

    if (colorInput.indexOf('#') === 0) {
        return hexToRGB(colorInput);
    }

    return colors[colorInput] || [0, 0, 0];
}

// ============================================================================
// BUILTIN PRESETS
// ============================================================================

function getBuiltinPresets() {
    // Curated preset list (30 presets). See DOCS/EXTRA_PRESETS_BACKUP.md for extended options.
    var presets = [
        // =====================================================================
        // STANDARD - Core resolutions (6)
        // =====================================================================
        { name: 'Full HD', width: 1920, height: 1080, frameRate: 30, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Standard' },
        { name: 'Full HD Film', width: 1920, height: 1080, frameRate: 24, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Standard' },
        { name: 'Vertical HD', width: 1080, height: 1920, frameRate: 30, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Standard' },
        { name: 'HD', width: 1280, height: 720, frameRate: 30, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Standard' },
        { name: '4K', width: 3840, height: 2160, frameRate: 30, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Standard' },
        { name: '4K Cinema', width: 4096, height: 2160, frameRate: 24, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Standard' },

        // =====================================================================
        // SOCIAL MEDIA - Platform-specific formats (13)
        // =====================================================================
        { name: 'Instagram Post', width: 1080, height: 1080, frameRate: 30, duration: 15, bgColor: 'white', labelIndex: 0, category: 'Social Media' },
        { name: 'IG/LinkedIn Portrait', width: 1080, height: 1350, frameRate: 30, duration: 15, bgColor: 'white', labelIndex: 0, category: 'Social Media' },
        { name: 'Instagram Story', width: 1080, height: 1920, frameRate: 30, duration: 15, bgColor: 'black', labelIndex: 0, category: 'Social Media' },
        { name: 'Instagram Reel', width: 1080, height: 1920, frameRate: 30, duration: 90, bgColor: 'black', labelIndex: 0, category: 'Social Media' },
        { name: 'TikTok', width: 1080, height: 1920, frameRate: 30, duration: 60, bgColor: 'black', labelIndex: 0, category: 'Social Media' },
        { name: 'YouTube', width: 1920, height: 1080, frameRate: 30, duration: 60, bgColor: 'black', labelIndex: 0, category: 'Social Media' },
        { name: 'YouTube Short', width: 1080, height: 1920, frameRate: 30, duration: 60, bgColor: 'black', labelIndex: 0, category: 'Social Media' },
        { name: 'YouTube 4K', width: 3840, height: 2160, frameRate: 30, duration: 60, bgColor: 'black', labelIndex: 0, category: 'Social Media' },
        { name: 'X / Twitter', width: 1280, height: 720, frameRate: 30, duration: 140, bgColor: 'black', labelIndex: 0, category: 'Social Media' },
        { name: 'LinkedIn', width: 1920, height: 1080, frameRate: 30, duration: 30, bgColor: 'black', labelIndex: 0, category: 'Social Media' },
        { name: 'LinkedIn Post', width: 1200, height: 628, frameRate: 30, duration: 30, bgColor: 'black', labelIndex: 0, category: 'Social Media' },
        { name: 'Facebook', width: 1280, height: 720, frameRate: 30, duration: 60, bgColor: 'black', labelIndex: 0, category: 'Social Media' },
        { name: 'Pinterest Pin', width: 1000, height: 1500, frameRate: 30, duration: 15, bgColor: 'white', labelIndex: 0, category: 'Social Media' },

        // =====================================================================
        // CINEMATIC - Film formats and aspect ratios (4)
        // =====================================================================
        { name: 'Anamorphic', width: 2048, height: 858, frameRate: 24, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Cinematic' },
        { name: 'Anamorphic 4K', width: 4096, height: 1716, frameRate: 24, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Cinematic' },
        { name: 'Widescreen', width: 1998, height: 1080, frameRate: 24, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Cinematic' },
        { name: 'Widescreen 4K', width: 3996, height: 2160, frameRate: 24, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Cinematic' },

        // =====================================================================
        // BROADCAST - Common broadcast standards (4)
        // =====================================================================
        { name: 'NTSC', width: 1920, height: 1080, frameRate: 29.97, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Broadcast' },
        { name: 'NTSC Film', width: 1920, height: 1080, frameRate: 23.976, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Broadcast' },
        { name: 'PAL', width: 1920, height: 1080, frameRate: 25, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Broadcast' },
        { name: 'NTSC 720p', width: 1280, height: 720, frameRate: 59.94, duration: 10, bgColor: 'black', labelIndex: 0, category: 'Broadcast' },

        // =====================================================================
        // HIGH FRAME RATE - Gaming, sports, slow-motion (3)
        // =====================================================================
        { name: 'Smooth HD', width: 1920, height: 1080, frameRate: 60, duration: 10, bgColor: 'black', labelIndex: 0, category: 'High Frame Rate' },
        { name: 'Smooth 4K', width: 3840, height: 2160, frameRate: 60, duration: 10, bgColor: 'black', labelIndex: 0, category: 'High Frame Rate' },
        { name: 'Slow-Mo', width: 1920, height: 1080, frameRate: 120, duration: 10, bgColor: 'black', labelIndex: 0, category: 'High Frame Rate' },

        // =====================================================================
        // ANIMATION - Low frame rates for animation (3)
        // =====================================================================
        { name: 'Animation HD', width: 1920, height: 1080, frameRate: 12, duration: 5, bgColor: 'black', labelIndex: 0, category: 'Animation' },
        { name: 'Square', width: 1080, height: 1080, frameRate: 12, duration: 5, bgColor: 'black', labelIndex: 0, category: 'Animation' },
        { name: 'Preview', width: 960, height: 540, frameRate: 24, duration: 5, bgColor: 'black', labelIndex: 0, category: 'Animation' }
    ];

    return JSON.stringify(presets);
}

// ============================================================================
// COMPOSITION CREATION
// ============================================================================

/**
 * Create a single composition with specified settings
 */
function createSingleComposition(compData) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        return withUndo('Create Composition', function() {
            var comp = app.project.items.addComp(
                compData.finalName || compData.name,
                compData.width,
                compData.height,
                1.0,
                compData.duration,
                compData.frameRate
            );

            // Set background color
            if (compData.bgColor) {
                comp.bgColor = getColorArray(compData.bgColor);
            }

            // Set label if provided
            if (compData.labelIndex !== undefined && compData.labelIndex !== null) {
                comp.label = compData.labelIndex;
            }

            // Move to destination folder if specified
            if (compData.destinationFolder) {
                var destFolder = getOrCreateFolderPath(compData.destinationFolder);
                comp.parentFolder = destFolder;
            }

            return jsonResponse(true, 'Composition created: ' + comp.name, {
                id: comp.id,
                name: comp.name,
                width: comp.width,
                height: comp.height
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error creating composition: ' + (e.message || 'Unknown error'));
    }
}

/**
 * Create multiple compositions from queue
 */
function createCompositionsFromQueue(compositionsArray) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        if (!compositionsArray || compositionsArray.length === 0) {
            return jsonResponse(false, 'Queue is empty');
        }

        return withUndo('Create Compositions from Queue', function() {
            var created = [];
            var failed = [];

            for (var i = 0; i < compositionsArray.length; i++) {
                try {
                    var compData = compositionsArray[i];
                    var comp = app.project.items.addComp(
                        compData.finalName || compData.name,
                        compData.width,
                        compData.height,
                        1.0,
                        compData.duration,
                        compData.frameRate
                    );

                    // Set background color
                    if (compData.bgColor) {
                        comp.bgColor = getColorArray(compData.bgColor);
                    }

                    // Set label if provided
                    if (compData.labelIndex !== undefined && compData.labelIndex !== null) {
                        comp.label = compData.labelIndex;
                    }

                    // Note: alphaChannel flag is stored in preset for user reference
                    // AE comps inherently support alpha - this flag indicates user intent
                    // for transparent-background workflows (e.g., render settings automation)

                    // Move to destination folder if specified
                    if (compData.destinationFolder) {
                        var destFolder = getOrCreateFolderPath(compData.destinationFolder);
                        comp.parentFolder = destFolder;
                    }

                    created.push({
                        id: comp.id,
                        name: comp.name
                    });
                } catch (e) {
                    failed.push({
                        name: compData.finalName || compData.name,
                        error: e.toString()
                    });
                }
            }

            var message = 'Created ' + created.length + ' composition(s)';
            if (failed.length > 0) {
                message += ', ' + failed.length + ' failed';
            }

            return jsonResponse(true, message, {
                created: created,
                failed: failed
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error creating compositions: ' + (e.message || 'Unknown error'));
    }
}

// ============================================================================
// PROJECT INSPECTION
// ============================================================================

/**
 * Get all compositions in current project
 */
function openProjectDialog() {
    try {
        app.open();
        return jsonResponse(true, 'Open dialog triggered');
    } catch (e) {
        return jsonResponse(false, 'Failed to open project dialog: ' + e.toString());
    }
}

function getAllCompositions() {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        var comps = [];
        var folders = [];
        for (var i = 1; i <= app.project.items.length; i++) {
            var item = app.project.items[i];
            if (item instanceof CompItem) {
                // Convert bgColor array [r,g,b] (0-1) to hex string
                var bg = item.bgColor;
                var bgHex = '#' +
                    ('0' + Math.round(bg[0] * 255).toString(16)).slice(-2) +
                    ('0' + Math.round(bg[1] * 255).toString(16)).slice(-2) +
                    ('0' + Math.round(bg[2] * 255).toString(16)).slice(-2);

                // Convert displayStartTime to timecode string
                var startTime = item.displayStartTime;
                var fps = item.frameRate;
                var totalFrames = Math.round(startTime * fps);
                var frames = totalFrames % Math.round(fps);
                var totalSecs = Math.floor(totalFrames / fps);
                var secs = totalSecs % 60;
                var mins = Math.floor(totalSecs / 60) % 60;
                var hrs = Math.floor(totalSecs / 3600);
                var startTC = hrs + ':' + ('0' + mins).slice(-2) + ':' + ('0' + secs).slice(-2) + ':' + ('0' + frames).slice(-2);

                comps.push({
                    id: item.id,
                    name: item.name,
                    width: item.width,
                    height: item.height,
                    frameRate: item.frameRate,
                    duration: item.duration,
                    numLayers: item.numLayers,
                    bgColor: bgHex,
                    labelIndex: item.label,
                    pixelAspect: item.pixelAspect,
                    startTimecode: startTC,
                    type: 'composition',
                    parentFolderId: (item.parentFolder && item.parentFolder !== app.project.rootFolder) ? item.parentFolder.id : null
                });
            } else if (item instanceof FolderItem) {
                folders.push({
                    id: item.id,
                    name: item.name,
                    type: 'folder',
                    parentFolderId: (item.parentFolder && item.parentFolder !== app.project.rootFolder) ? item.parentFolder.id : null
                });
            }
        }

        // Combine folders and compositions in AE's native item order
        var allItems = folders.concat(comps);

        // Get project name (filename without path, or "Untitled Project" if unsaved)
        var projectName = 'Untitled Project';
        if (app.project.file) {
            projectName = app.project.file.name;
        }

        return jsonResponse(true, 'Found ' + comps.length + ' composition(s) and ' + folders.length + ' folder(s)', {
            compositions: comps,
            folders: folders,
            allItems: allItems,
            projectName: projectName
        });
    } catch (e) {
        return jsonResponse(false, 'Error getting compositions: ' + e.toString());
    }
}

/**
 * Get active/selected composition settings
 */
function getActiveCompSettings() {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        var activeItem = app.project.activeItem;
        if (!activeItem || !(activeItem instanceof CompItem)) {
            return jsonResponse(false, 'No active composition');
        }

        // Convert bgColor array to hex
        var bg = activeItem.bgColor;
        var bgHex = '#' +
            ('0' + Math.round(bg[0] * 255).toString(16)).slice(-2) +
            ('0' + Math.round(bg[1] * 255).toString(16)).slice(-2) +
            ('0' + Math.round(bg[2] * 255).toString(16)).slice(-2);

        // Convert displayStartTime to timecode string
        var startTime = activeItem.displayStartTime;
        var fps = activeItem.frameRate;
        var totalFrames = Math.round(startTime * fps);
        var frames = totalFrames % Math.round(fps);
        var totalSecs = Math.floor(totalFrames / fps);
        var secs = totalSecs % 60;
        var mins = Math.floor(totalSecs / 60) % 60;
        var hrs = Math.floor(totalSecs / 3600);
        var startTC = hrs + ':' + ('0' + mins).slice(-2) + ':' + ('0' + secs).slice(-2) + ':' + ('0' + frames).slice(-2);

        return jsonResponse(true, 'Active composition found', {
            name: activeItem.name,
            width: activeItem.width,
            height: activeItem.height,
            frameRate: activeItem.frameRate,
            duration: activeItem.duration,
            pixelAspect: activeItem.pixelAspect,
            bgColor: bgHex,
            startTimecode: startTC
        });
    } catch (e) {
        return jsonResponse(false, 'Error getting active composition: ' + (e.message || 'Unknown error'));
    }
}

/**
 * Get label colors and names from AE preferences.
 * AE 26+ stores labels in `Prefs-indep-general.txt`, which the legacy
 * `app.preferences.getPrefAsLong/String` API cannot read (no PREFType enum
 * maps to that file). So we read the file directly and parse out the
 * label sections — colour values from `Label Preference Color Section 5`
 * and customised names from `Label Preference Text Section 7` (falling back
 * to `Section 5` if 7 doesn't exist on older AE versions).
 *
 * Colour values use AE's mixed binary format: hex pairs interspersed with
 * quoted-printable runs. Decoder below handles both modes.
 */
function getLabelColors() {
    try {
        // ---- Locate the indep-general prefs file ----
        var verStr = String(app.version).split('x')[0]; // "26.0x67" -> "26.0"
        var prefsPath = $.getenv('HOME') + '/Library/Preferences/Adobe/After Effects/' + verStr + '/Adobe After Effects ' + verStr + ' Prefs-indep-general.txt';
        var prefsFile = new File(prefsPath);
        // Windows users — different default path; bail out gracefully and let JS fall back.
        if (!prefsFile.exists) {
            return jsonResponse(true, 'Prefs file not found at ' + prefsPath, getDefaultLabels());
        }

        // ---- Read the file ----
        prefsFile.encoding = 'UTF-8';
        if (!prefsFile.open('r')) {
            return jsonResponse(true, 'Could not open prefs file', getDefaultLabels());
        }
        var content = prefsFile.read();
        prefsFile.close();

        // ---- Helpers ----
        // Decode AE's mixed hex/quoted-literal byte format: e.g. `FF"VL"E4` -> [255,86,76,228].
        // Inside quotes, characters are literal ASCII bytes; `""` denotes a literal `"`.
        // Outside quotes, every two chars are a hex byte.
        function decodeBytes(s) {
            var bytes = [];
            var i = 0;
            while (i < s.length) {
                if (s.charAt(i) === '"') {
                    i++;
                    while (i < s.length) {
                        if (s.charAt(i) === '"') {
                            if (i + 1 < s.length && s.charAt(i + 1) === '"') {
                                bytes.push(34); i += 2;
                            } else { i++; break; }
                        } else {
                            bytes.push(s.charCodeAt(i)); i++;
                        }
                    }
                } else {
                    bytes.push(parseInt(s.substr(i, 2), 16)); i += 2;
                }
            }
            return bytes;
        }
        function toHex2(n) { var h = (n & 0xFF).toString(16); return h.length === 1 ? '0' + h : h; }

        // Slice out the body of a section (between its header and the next `[...]` header).
        // ExtendScript ES3 regex chokes on `(?:X|$)` lazy alternation here, so we do
        // start/end index slicing manually instead.
        function getSectionBody(text, headerName) {
            var marker = '["' + headerName + '"]';
            var start = text.indexOf(marker);
            if (start === -1) return null;
            var bodyStart = start + marker.length;
            var nextHeader = text.indexOf('["', bodyStart);
            return nextHeader === -1 ? text.substring(bodyStart) : text.substring(bodyStart, nextHeader);
        }

        // Walk a section body line-by-line; for each line matching the pattern
        // `"<keyPrefix> N" = <value>`, call onMatch(N, value).
        function parseSectionLines(body, keyPrefix, onMatch) {
            if (!body) return;
            var lines = body.split('\n');
            for (var li = 0; li < lines.length; li++) {
                var line = lines[li].replace(/^\s+/, '').replace(/\s+$/, '');
                if (!line) continue;
                var eq = line.indexOf('" = ');
                if (eq === -1) continue;
                var keyPart = line.substring(0, eq + 1); // includes closing quote
                var valPart = line.substring(eq + 4);
                // keyPart looks like `"Label Color ID 2 # 1"` — strip outer quotes
                var keyStr = keyPart.replace(/^"/, '').replace(/"$/, '');
                if (keyStr.indexOf(keyPrefix) !== 0) continue;
                var rest = keyStr.substring(keyPrefix.length).replace(/^\s+/, '');
                var idx = parseInt(rest, 10);
                if (!isNaN(idx)) onMatch(idx, valPart);
            }
        }

        // ---- Parse colours from `Label Preference Color Section 5` ----
        var colorBody = getSectionBody(content, 'Label Preference Color Section 5');
        var colors = {};
        parseSectionLines(colorBody, 'Label Color ID 2 #', function (idx, raw) {
            try {
                var bs = decodeBytes(raw);
                if (bs.length >= 4) {
                    colors[idx] = '#' + toHex2(bs[1]) + toHex2(bs[2]) + toHex2(bs[3]);
                }
            } catch (eDec) {}
        });

        // ---- Parse names — prefer customised Section 7, fall back to Section 5 ----
        var nameBody = getSectionBody(content, 'Label Preference Text Section 7')
                    || getSectionBody(content, 'Label Preference Text Section 5');
        var names = {};
        parseSectionLines(nameBody, 'Label Text ID 2 #', function (idx, raw) {
            // Value is a quoted string: `"Rouge"` — strip outer quotes
            var s = raw.replace(/^"/, '').replace(/"$/, '');
            names[idx] = s;
        });

        // ---- Build labels array ----
        var labels = [{ index: 0, name: 'None', color: null }];
        var fallback = getDefaultLabels();
        var defaultByIdx = {};
        for (var d = 0; d < fallback.length; d++) defaultByIdx[fallback[d].index] = fallback[d];

        for (var i = 1; i <= 16; i++) {
            var def = defaultByIdx[i] || { name: 'Label ' + i, color: '#888888' };
            labels.push({
                index: i,
                name: names[i] || def.name,
                color: colors[i] || def.color
            });
        }

        var colorCount = 0; for (var ck in colors) { if (colors.hasOwnProperty(ck)) colorCount++; }
        var nameCount = 0; for (var nk in names) { if (names.hasOwnProperty(nk)) nameCount++; }
        var msg = 'Parsed prefs: ' + colorCount + ' colours, ' + nameCount + ' names';
        return jsonResponse(true, msg, labels);
    } catch (e) {
        return jsonResponse(true, 'Using defaults: ' + (e.message || 'error'), getDefaultLabels());
    }
}

/**
 * Convert number to 2-digit hex
 */
function toHex(n) {
    var h = n.toString(16);
    return h.length === 1 ? '0' + h : h;
}

/**
 * Fallback default AE labels
 */
function getDefaultLabels() {
    return [
        { index: 0, name: 'None', color: null },
        { index: 1, name: 'Red', color: '#C83232' },
        { index: 2, name: 'Yellow', color: '#E4D84C' },
        { index: 3, name: 'Aqua', color: '#4FA89F' },
        { index: 4, name: 'Pink', color: '#D47AB2' },
        { index: 5, name: 'Lavender', color: '#9775CE' },
        { index: 6, name: 'Peach', color: '#E88550' },
        { index: 7, name: 'Sea Foam', color: '#65CBBD' },
        { index: 8, name: 'Blue', color: '#677DE0' },
        { index: 9, name: 'Green', color: '#4AA44C' },
        { index: 10, name: 'Purple', color: '#8E2C9A' },
        { index: 11, name: 'Orange', color: '#E76A29' },
        { index: 12, name: 'Brown', color: '#935039' },
        { index: 13, name: 'Fuchsia', color: '#C44593' },
        { index: 14, name: 'Cyan', color: '#2FA4D7' },
        { index: 15, name: 'Sandstone', color: '#CAA465' },
        { index: 16, name: 'Dark Green', color: '#46735C' }
    ];
}

// ============================================================================
// COMPOSITION OPERATIONS
// ============================================================================

/**
 * Rename multiple compositions using smart naming patterns
 */
function applySmartRenames(renamesArray) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        return withUndo('Rename Compositions', function() {
            var renamed = [];
            var failed = [];

            for (var i = 0; i < renamesArray.length; i++) {
                try {
                    var renameData = renamesArray[i];
                    var item = app.project.itemByID(renameData.id);

                    if (item && item instanceof CompItem) {
                        item.name = renameData.name;
                        renamed.push({
                            id: item.id,
                            name: item.name
                        });
                    } else {
                        failed.push({
                            id: renameData.id,
                            error: 'Composition not found'
                        });
                    }
                } catch (e) {
                    failed.push({
                        id: renameData.id,
                        error: e.toString()
                    });
                }
            }

            var message = 'Renamed ' + renamed.length + ' composition(s)';
            if (failed.length > 0) {
                message += ', ' + failed.length + ' failed';
            }

            return jsonResponse(true, message, {
                renamed: renamed,
                failed: failed
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error renaming compositions: ' + e.toString());
    }
}

/**
 * Update composition settings (width, height, etc.)
 */
function updateCompositionSettings(updatesArray) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        return withUndo('Update Composition Settings', function() {
            var updated = [];
            var failed = [];

            for (var i = 0; i < updatesArray.length; i++) {
                try {
                    var updateData = updatesArray[i];
                    var item = app.project.itemByID(updateData.id);

                    if (item && item instanceof CompItem) {
                        if (updateData.width) item.width = updateData.width;
                        if (updateData.height) item.height = updateData.height;
                        if (updateData.frameRate) item.frameRate = updateData.frameRate;
                        if (updateData.duration) item.duration = updateData.duration;
                        if (updateData.bgColor) item.bgColor = getColorArray(updateData.bgColor);
                        if (updateData.labelIndex !== undefined && updateData.labelIndex !== null) item.label = updateData.labelIndex;
                        if (updateData.pixelAspect) item.pixelAspect = updateData.pixelAspect;
                        if (updateData.startTimecode) {
                            // Parse timecode string H:MM:SS:FF to seconds
                            var parts = updateData.startTimecode.split(':');
                            if (parts.length === 4) {
                                var h = parseInt(parts[0], 10) || 0;
                                var m = parseInt(parts[1], 10) || 0;
                                var s = parseInt(parts[2], 10) || 0;
                                var f = parseInt(parts[3], 10) || 0;
                                item.displayStartTime = h * 3600 + m * 60 + s + (f / item.frameRate);
                            }
                        }

                        updated.push({
                            id: item.id,
                            name: item.name
                        });
                    } else {
                        failed.push({
                            id: updateData.id,
                            error: 'Composition not found'
                        });
                    }
                } catch (e) {
                    failed.push({
                        id: updateData.id,
                        error: e.toString()
                    });
                }
            }

            var message = 'Updated ' + updated.length + ' composition(s)';
            if (failed.length > 0) {
                message += ', ' + failed.length + ' failed';
            }

            return jsonResponse(true, message, {
                updated: updated,
                failed: failed
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error updating compositions: ' + e.toString());
    }
}

/**
 * Duplicate compositions
 */
function duplicateCompositions(compsArray) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        return withUndo('Duplicate Compositions', function() {
            var duplicated = [];
            var failed = [];

            for (var i = 0; i < compsArray.length; i++) {
                try {
                    var compData = compsArray[i];
                    var item = app.project.itemByID(compData.id);

                    if (item && item instanceof CompItem) {
                        var duplicate = item.duplicate();
                        duplicated.push({
                            id: duplicate.id,
                            name: duplicate.name
                        });
                    } else {
                        failed.push({
                            id: compData.id,
                            error: 'Composition not found'
                        });
                    }
                } catch (e) {
                    failed.push({
                        id: compData.id,
                        error: e.toString()
                    });
                }
            }

            var message = 'Duplicated ' + duplicated.length + ' composition(s)';
            if (failed.length > 0) {
                message += ', ' + failed.length + ' failed';
            }

            return jsonResponse(true, message, {
                duplicated: duplicated,
                failed: failed
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error duplicating compositions: ' + e.toString());
    }
}

/**
 * Duplicate compositions and assign caller-provided names in one undo group.
 */
function duplicateCompositionsFromList(rowsArray) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        return withUndo('COR: Duplicate from List', function() {
            var created = [];
            var failed = [];

            for (var i = 0; i < rowsArray.length; i++) {
                var row = rowsArray[i];
                try {
                    var item = app.project.itemByID(row.id);
                    if (!item || !(item instanceof CompItem)) {
                        failed.push({
                            id: row.id,
                            rowLabel: row.rowLabel,
                            error: 'Composition not found'
                        });
                        continue;
                    }

                    var duplicate = item.duplicate();
                    if (row.newName) {
                        duplicate.name = row.newName;
                    }

                    created.push({
                        id: duplicate.id,
                        sourceId: item.id,
                        sourceName: item.name,
                        rowLabel: row.rowLabel,
                        requestedName: row.newName,
                        name: duplicate.name
                    });
                } catch (inner) {
                    failed.push({
                        id: row.id,
                        rowLabel: row.rowLabel,
                        error: inner.toString()
                    });
                }
            }

            var message = 'Created ' + created.length + ' composition(s)';
            if (failed.length > 0) {
                message += ', ' + failed.length + ' failed';
            }

            return jsonResponse(true, message, {
                created: created,
                failed: failed
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error duplicating from list: ' + e.toString());
    }
}

function getFirstCompItem(optionalId) {
    if (optionalId) {
        var requested = app.project.itemByID(optionalId);
        if (requested && requested instanceof CompItem) return requested;
    }
    for (var i = 1; i <= app.project.items.length; i++) {
        var item = app.project.items[i];
        if (item instanceof CompItem) return item;
    }
    return null;
}

/**
 * Read output module templates from AE. AE exposes templates through an
 * OutputModule, so this creates and immediately removes a temporary queue item.
 */
function getRenderQueueOutputModuleTemplates(options) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        var comp = getFirstCompItem(options && options.compId);
        if (!comp) {
            return jsonResponse(false, 'No compositions available');
        }

        var rqItem = null;
        var templates = [];
        try {
            rqItem = app.project.renderQueue.items.add(comp);
            var outputModule = rqItem.outputModule(1);
            var rawTemplates = outputModule.templates || [];
            for (var i = 0; i < rawTemplates.length; i++) {
                templates.push(String(rawTemplates[i]));
            }
        } finally {
            if (rqItem) {
                try { rqItem.remove(); } catch (_) {}
            }
        }

        return jsonResponse(true, 'Output module templates loaded', {
            templates: templates
        });
    } catch (e) {
        return jsonResponse(false, 'Error loading output module templates: ' + e.toString());
    }
}

function folderIsWritable(folder) {
    try {
        if (!folder || !folder.exists) return false;
        var testFile = new File(folder.fsName + '/.cor-write-test-' + new Date().getTime());
        if (!testFile.open('w')) return false;
        testFile.write('ok');
        testFile.close();
        testFile.remove();
        return true;
    } catch (e) {
        return false;
    }
}

// v1.1 §3.4 — enumerate COR-tagged items in the AE Render Queue. Items
// land in the queue with comment = 'COR|<isoTimestamp>|<compName>' from
// addCompsToRenderQueue. Returns lightweight metadata for the manager UI.
function getCorQueuedItems() {
    try {
        if (!app.project) {
            return jsonResponse(true, 'No project', { items: [] });
        }
        var rq = app.project.renderQueue;
        var items = [];
        for (var i = 1; i <= rq.numItems; i++) {
            var item = rq.item(i);
            var comment = (item.comment || '');
            // v1.1 fix-2: marker is now [COR-<6char>]. Substring match keeps
            // backwards-compat for any items still tagged with the legacy
            // 'COR|<iso>|<name>' format from earlier v1.1 dev builds.
            if (comment.indexOf('[COR-') !== 0 && comment.indexOf('COR|') !== 0) continue;
            var compName = '';
            try { compName = item.comp.name; } catch (_) {}
            var outputCount = 0;
            try { outputCount = item.numOutputModules; } catch (_) {}
            var firstOutputPath = '';
            try {
                if (outputCount > 0 && item.outputModule(1) && item.outputModule(1).file) {
                    firstOutputPath = item.outputModule(1).file.fsName;
                }
            } catch (_) {}
            items.push({
                index: i,
                compName: compName,
                outputCount: outputCount,
                firstOutputPath: firstOutputPath,
                status: item.status,
                comment: comment
            });
        }
        return jsonResponse(true, 'COR queued items', { items: items });
    } catch (e) {
        return jsonResponse(false, 'Error enumerating queue: ' + e.toString());
    }
}

// v1.1 §3.4 — remove a specific rqItem by index. We refetch by comment
// rather than trusting a stale index (queue can change between enumerate
// and remove). The caller passes the COR comment marker as a unique key.
function removeRqItemByComment(commentMarker) {
    try {
        if (!app.project) return jsonResponse(false, 'No project');
        var rq = app.project.renderQueue;
        for (var i = rq.numItems; i >= 1; i--) {
            if (rq.item(i).comment === commentMarker) {
                rq.item(i).remove();
                return jsonResponse(true, 'Removed', { index: i });
            }
        }
        return jsonResponse(false, 'Item not found');
    } catch (e) {
        return jsonResponse(false, 'Error removing item: ' + e.toString());
    }
}

function corShellQuote(path) {
    return "'" + String(path).replace(/'/g, "'\\''") + "'";
}

function corOpenFolder(folderPath) {
    if ($.os && $.os.toLowerCase().indexOf('mac') >= 0) {
        system.callSystem('/usr/bin/open ' + corShellQuote(folderPath));
        return true;
    }
    return new Folder(folderPath).execute();
}

function corRevealFile(filePath) {
    if ($.os && $.os.toLowerCase().indexOf('mac') >= 0) {
        system.callSystem('/usr/bin/open -R ' + corShellQuote(filePath));
        return true;
    }
    var file = new File(filePath);
    if (typeof file.reveal === 'function') {
        return file.reveal();
    }
    if (file.parent && file.parent.exists) {
        return file.parent.execute();
    }
    return file.execute();
}

// v1.1 §3.4 — open a folder in Finder/Explorer.
function revealOutputFolder(folderPath) {
    try {
        if (!folderPath) return jsonResponse(false, 'No path');
        var folder = new Folder(folderPath);
        if (!folder.exists) return jsonResponse(false, 'Folder missing');
        corOpenFolder(folder.fsName);
        return jsonResponse(true, 'Folder revealed');
    } catch (e) {
        return jsonResponse(false, 'Error revealing folder: ' + e.toString());
    }
}

// v1.1 audit slice 4 — JSON-wrapped writability probe callable from JS for
// the FOLDER_UNWRITABLE audit class. Returns { exists, writable } so the
// caller can distinguish "missing folder" from "exists but locked."
function probeFolderWritable(folderPath) {
    try {
        if (!folderPath) {
            return jsonResponse(true, 'Folder probe', { writable: false, exists: false });
        }
        var folder = new Folder(folderPath);
        if (!folder.exists) {
            return jsonResponse(true, 'Folder probe', { writable: false, exists: false });
        }
        return jsonResponse(true, 'Folder probe', {
            writable: folderIsWritable(folder),
            exists: true
        });
    } catch (e) {
        return jsonResponse(true, 'Folder probe', { writable: false, exists: false, error: e.toString() });
    }
}

function getCurrentProjectKey() {
    try {
        if (!app.project) {
            return jsonResponse(true, 'No active project', { key: '' });
        }
        var f = app.project.file;
        if (!f) {
            // Unsaved project — fall back to a placeholder so per-project
            // defaults still work within a session but reset on save.
            return jsonResponse(true, 'Unsaved project', { key: '__unsaved__' });
        }
        return jsonResponse(true, 'Project key', { key: f.fsName });
    } catch (e) {
        return jsonResponse(false, 'Error reading project key: ' + e.toString());
    }
}

function selectRenderOutputFolder() {
    try {
        var folder = Folder.selectDialog('Choose render output folder');
        if (!folder) {
            return jsonResponse(false, 'Folder selection cancelled');
        }
        if (!folder.exists) {
            return jsonResponse(false, 'Folder does not exist');
        }
        if (!folderIsWritable(folder)) {
            return jsonResponse(false, 'Folder is not writable');
        }
        return jsonResponse(true, 'Output folder selected', {
            path: folder.fsName
        });
    } catch (e) {
        return jsonResponse(false, 'Error selecting output folder: ' + e.toString());
    }
}

function checkOutputFileCollisions(pathsArray) {
    try {
        var existing = [];
        for (var i = 0; i < pathsArray.length; i++) {
            var file = new File(pathsArray[i]);
            if (file.exists) existing.push(pathsArray[i]);
        }
        return jsonResponse(true, 'Output file collisions checked', {
            existing: existing
        });
    } catch (e) {
        return jsonResponse(false, 'Error checking output files: ' + e.toString());
    }
}

/**
 * Add compositions to AE's Render Queue in one undo group.
 *
 * Row shape (v1.0.2+):
 *   {
 *     id: <comp id>,
 *     compName: <string>,
 *     outputs: [{ outputModule: <template name>, outputPath: <fsName> }, ...]
 *   }
 *
 * Legacy single-output shape ({ outputModule, outputPath }) is still accepted
 * so the function is forward-compatible if any other caller hasn't migrated.
 */
function addCompsToRenderQueue(rowsArray) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        return withUndo('COR: Add to Render Queue', function() {
            var queued = [];
            var failed = [];

            for (var i = 0; i < rowsArray.length; i++) {
                var row = rowsArray[i];
                try {
                    var comp = app.project.itemByID(row.id);
                    if (!comp || !(comp instanceof CompItem)) {
                        failed.push({
                            id: row.id,
                            compName: row.compName,
                            error: 'Composition not found'
                        });
                        continue;
                    }

                    var outputs = row.outputs;
                    if (!outputs || !outputs.length) {
                        outputs = [{ outputModule: row.outputModule, outputPath: row.outputPath }];
                    }

                    var rqItem = app.project.renderQueue.items.add(comp);
                    // v1.1 §3.4 + fix-2: tag rqItem with a compact unique
                    // marker so the queue manager can identify COR items
                    // without flooding AE's user-visible comment column.
                    // Format: [COR-<6char alpha-num>]
                    try {
                        var _corChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
                        var _corMarker = '[COR-';
                        for (var _ci = 0; _ci < 6; _ci++) {
                            _corMarker += _corChars.charAt(Math.floor(Math.random() * _corChars.length));
                        }
                        _corMarker += ']';
                        rqItem.comment = _corMarker;
                    } catch (_) {}
                    var queuedOutputs = [];

                    for (var k = 0; k < outputs.length; k++) {
                        var out = outputs[k];
                        // AE creates outputModule(1) by default; reuse it for the
                        // first entry, add() new ones for subsequent entries.
                        var om = (k === 0) ? rqItem.outputModule(1) : rqItem.outputModules.add();
                        if (out.outputModule) {
                            om.applyTemplate(out.outputModule);
                        }
                        om.file = new File(out.outputPath);
                        queuedOutputs.push({
                            outputModule: out.outputModule,
                            outputPath: out.outputPath
                        });
                    }

                    queued.push({
                        id: comp.id,
                        compName: comp.name,
                        outputs: queuedOutputs
                    });
                } catch (inner) {
                    failed.push({
                        id: row.id,
                        compName: row.compName,
                        error: inner.toString()
                    });
                }
            }

            var totalOutputs = 0;
            for (var qi = 0; qi < queued.length; qi++) {
                totalOutputs += (queued[qi].outputs || []).length;
            }
            var message = 'Added ' + queued.length + ' composition(s), ' + totalOutputs + ' output(s) to Render Queue';
            if (failed.length > 0) {
                message += ', ' + failed.length + ' failed';
            }

            return jsonResponse(true, message, {
                queued: queued,
                failed: failed
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error adding to Render Queue: ' + e.toString());
    }
}

/**
 * Delete compositions
 */
function deleteCompositions(compsArray) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        return withUndo('Delete Compositions', function() {
            var deleted = [];
            var failed = [];

            for (var i = 0; i < compsArray.length; i++) {
                try {
                    var compData = compsArray[i];
                    var item = app.project.itemByID(compData.id);

                    if (item && item instanceof CompItem) {
                        var name = item.name;
                        item.remove();
                        deleted.push({
                            id: compData.id,
                            name: name
                        });
                    } else {
                        failed.push({
                            id: compData.id,
                            error: 'Composition not found'
                        });
                    }
                } catch (e) {
                    failed.push({
                        id: compData.id,
                        error: e.toString()
                    });
                }
            }

            var message = 'Deleted ' + deleted.length + ' composition(s)';
            if (failed.length > 0) {
                message += ', ' + failed.length + ' failed';
            }

            return jsonResponse(true, message, {
                deleted: deleted,
                failed: failed
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error deleting compositions: ' + e.toString());
    }
}

// ============================================================================
// FOLDER MANAGEMENT
// ============================================================================

/**
 * Create a folder in the AE project
 */
function createFolder(folderData) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        return withUndo('Create Folder', function() {
            var folder = app.project.items.addFolder(folderData.name || 'New Folder');

            return jsonResponse(true, 'Folder created: ' + folder.name, {
                id: folder.id,
                name: folder.name
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error creating folder: ' + e.toString());
    }
}

function createSubfolder(folderData) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        return withUndo('Create Subfolder', function() {
            // Find parent folder by ID
            var parentFolder = null;
            for (var i = 1; i <= app.project.items.length; i++) {
                var item = app.project.items[i];
                if (item instanceof FolderItem && item.id === folderData.parentFolderId) {
                    parentFolder = item;
                    break;
                }
            }

            if (!parentFolder) {
                return jsonResponse(false, 'Parent folder not found');
            }

            var newFolder = app.project.items.addFolder(folderData.name || 'New Folder');
            newFolder.parentFolder = parentFolder;

            return jsonResponse(true, 'Subfolder created: ' + newFolder.name, {
                id: newFolder.id,
                name: newFolder.name,
                parentFolderId: parentFolder.id
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error creating subfolder: ' + e.toString());
    }
}

function renameProjectFolder(folderData) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        return withUndo('Rename Folder', function() {
            var folder = app.project.itemByID(folderData.folderId);
            if (!folder || !(folder instanceof FolderItem)) {
                return jsonResponse(false, 'Folder not found');
            }

            folder.name = folderData.name || folder.name;
            return jsonResponse(true, 'Folder renamed to "' + folder.name + '"', {
                id: folder.id,
                name: folder.name
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error renaming folder: ' + e.toString());
    }
}

function deleteProjectFolders(folderData) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        var folderIds = folderData.folderIds || [];
        if (!folderIds.length) {
            return jsonResponse(false, 'No folders selected');
        }

        return withUndo('Delete Folders', function() {
            var deleted = [];
            var failed = [];

            for (var i = 0; i < folderIds.length; i++) {
                try {
                    var folder = app.project.itemByID(folderIds[i]);
                    if (!folder || !(folder instanceof FolderItem)) {
                        failed.push({ id: folderIds[i], error: 'Folder not found' });
                        continue;
                    }
                    var name = folder.name;
                    folder.remove();
                    deleted.push({ id: folderIds[i], name: name });
                } catch (err) {
                    failed.push({ id: folderIds[i], error: err.toString() });
                }
            }

            if (deleted.length === 0) {
                return jsonResponse(false, 'No folders were deleted');
            }

            var message = 'Deleted ' + deleted.length + ' folder(s)';
            if (failed.length > 0) {
                message += ', ' + failed.length + ' failed';
            }

            return jsonResponse(true, message, {
                deleted: deleted,
                failed: failed
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error deleting folders: ' + e.toString());
    }
}

/**
 * Move project items (compositions) into a folder by ID
 * @param {Object} moveData - {itemIds: [number], folderId: number}
 */
function moveItemsToFolder(moveData) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        var itemIds = moveData.itemIds;
        var folderId = moveData.folderId;

        if (!itemIds || itemIds.length === 0) {
            return jsonResponse(false, 'No items to move');
        }

        var folder = app.project.itemByID(folderId);
        if (!folder || !(folder instanceof FolderItem)) {
            return jsonResponse(false, 'Target folder not found');
        }

        return withUndo('Move Items to Folder', function() {
            var moved = [];
            var failed = [];

            for (var i = 0; i < itemIds.length; i++) {
                try {
                    var item = app.project.itemByID(itemIds[i]);
                    if (item) {
                        item.parentFolder = folder;
                        moved.push({ id: item.id, name: item.name });
                    } else {
                        failed.push({ id: itemIds[i], error: 'Item not found' });
                    }
                } catch (e) {
                    failed.push({ id: itemIds[i], error: e.toString() });
                }
            }

            var message = 'Moved ' + moved.length + ' item(s) to "' + folder.name + '"';
            if (failed.length > 0) {
                message += ', ' + failed.length + ' failed';
            }

            return jsonResponse(true, message, {
                moved: moved,
                failed: failed,
                folderName: folder.name
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error moving items: ' + e.toString());
    }
}

function moveItemsToRoot(moveData) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        var itemIds = moveData.itemIds || [];
        if (!itemIds.length) {
            return jsonResponse(false, 'No items selected');
        }

        return withUndo('Move Items To Root', function() {
            var moved = [];
            var failed = [];

            for (var i = 0; i < itemIds.length; i++) {
                try {
                    var item = app.project.itemByID(itemIds[i]);
                    if (!item) {
                        failed.push({ id: itemIds[i], error: 'Item not found' });
                        continue;
                    }

                    item.parentFolder = app.project.rootFolder;
                    moved.push({ id: item.id, name: item.name });
                } catch (e) {
                    failed.push({ id: itemIds[i], error: e.toString() });
                }
            }

            if (moved.length === 0) {
                return jsonResponse(false, 'No items were moved');
            }

            return jsonResponse(true, 'Moved ' + moved.length + ' item(s) to root', {
                moved: moved,
                failed: failed
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error moving items to root: ' + e.toString());
    }
}

function moveProjectFolders(moveData) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        var folderIds = moveData.folderIds || [];
        if (!folderIds.length) {
            return jsonResponse(false, 'No folders to move');
        }

        var targetFolder = null;
        if (moveData.targetFolderId !== null && moveData.targetFolderId !== undefined) {
            targetFolder = app.project.itemByID(moveData.targetFolderId);
            if (!targetFolder || !(targetFolder instanceof FolderItem)) {
                return jsonResponse(false, 'Target folder not found');
            }
        }

        return withUndo('Move Project Folders', function() {
            var moved = [];
            var failed = [];
            var destination = targetFolder || app.project.rootFolder;

            for (var i = 0; i < folderIds.length; i++) {
                try {
                    var folder = app.project.itemByID(folderIds[i]);
                    if (!folder || !(folder instanceof FolderItem)) {
                        failed.push({ id: folderIds[i], error: 'Folder not found' });
                        continue;
                    }
                    if (targetFolder && folder.id === targetFolder.id) {
                        failed.push({ id: folderIds[i], error: 'Cannot move folder into itself' });
                        continue;
                    }

                    folder.parentFolder = destination;
                    moved.push({ id: folder.id, name: folder.name });
                } catch (e) {
                    failed.push({ id: folderIds[i], error: e.toString() });
                }
            }

            if (moved.length === 0) {
                return jsonResponse(false, 'No folders were moved');
            }

            var message = 'Moved ' + moved.length + ' folder(s)';
            if (targetFolder) {
                message += ' to "' + targetFolder.name + '"';
            } else {
                message += ' to root';
            }
            if (failed.length > 0) {
                message += ', ' + failed.length + ' failed';
            }

            return jsonResponse(true, message, {
                moved: moved,
                failed: failed,
                targetFolderId: targetFolder ? targetFolder.id : null
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error moving folders: ' + e.toString());
    }
}

/**
 * Create a folder and optionally move items into it
 * @param {Object} folderData - {name: string, itemIds: [number] (optional)}
 */
function createFolderWithItems(folderData) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        return withUndo('Create Folder', function() {
            var folder = app.project.items.addFolder(folderData.name || 'New Folder');
            var moved = [];
            var failed = [];

            // Move items into the new folder if provided
            var itemIds = folderData.itemIds || [];
            for (var i = 0; i < itemIds.length; i++) {
                try {
                    var item = app.project.itemByID(itemIds[i]);
                    if (item) {
                        item.parentFolder = folder;
                        moved.push({ id: item.id, name: item.name });
                    } else {
                        failed.push({ id: itemIds[i], error: 'Item not found' });
                    }
                } catch (e) {
                    failed.push({ id: itemIds[i], error: e.toString() });
                }
            }

            var message = 'Folder created: ' + folder.name;
            if (moved.length > 0) {
                message += ' with ' + moved.length + ' item(s)';
            }

            return jsonResponse(true, message, {
                id: folder.id,
                name: folder.name,
                moved: moved,
                failed: failed
            });
        });
    } catch (e) {
        return jsonResponse(false, 'Error creating folder: ' + e.toString());
    }
}

/**
 * Ensure multiple folder paths exist in the project (creates if missing)
 * @param {string[]} paths - Array of folder paths like "Parent/Child"
 */
function ensureFolderPaths(paths) {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }
        return withUndo('Ensure Folder Paths', function() {
            for (var i = 0; i < paths.length; i++) {
                if (paths[i]) {
                    getOrCreateFolderPath(paths[i]);
                }
            }
            return jsonResponse(true, 'Folders ensured: ' + paths.length);
        });
    } catch (e) {
        return jsonResponse(false, 'Error creating folders: ' + (e.message || 'Unknown error'));
    }
}

/**
 * Get or create a folder path (supports nested paths like "Precomps/Characters")
 * @param {string} pathString - Folder path, separated by "/"
 * @returns {FolderItem} The folder at the specified path
 */
function getOrCreateFolderPath(pathString) {
    if (!pathString) return app.project.rootFolder;

    var parts = pathString.split(/[\/\\]/);
    var currentFolder = app.project.rootFolder;

    for (var i = 0; i < parts.length; i++) {
        var folderName = parts[i].trim();
        if (!folderName) continue;

        // Search for existing folder at this level
        var found = null;
        for (var j = 1; j <= currentFolder.numItems; j++) {
            var item = currentFolder.item(j);
            if (item instanceof FolderItem && item.name === folderName) {
                found = item;
                break;
            }
        }

        // Create if not found
        if (!found) {
            found = app.project.items.addFolder(folderName);
            found.parentFolder = currentFolder;
        }
        currentFolder = found;
    }

    return currentFolder;
}

/**
 * Get all folders in the project (with full paths)
 */
function getAllFoldersWithPaths() {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        var folders = [];

        // Recursive function to get folder path
        function getFolderPath(folder) {
            if (folder === app.project.rootFolder || !folder.parentFolder) {
                return '';
            }
            var parentPath = getFolderPath(folder.parentFolder);
            return parentPath ? parentPath + '/' + folder.name : folder.name;
        }

        // Iterate all items to find folders
        for (var i = 1; i <= app.project.items.length; i++) {
            var item = app.project.items[i];
            if (item instanceof FolderItem) {
                folders.push({
                    id: item.id,
                    name: item.name,
                    path: getFolderPath(item)
                });
            }
        }

        return jsonResponse(true, 'Found ' + folders.length + ' folder(s)', {
            folders: folders
        });
    } catch (e) {
        return jsonResponse(false, 'Error getting folders: ' + e.toString());
    }
}

/**
 * Get all folders in the project
 */
function getAllFolders() {
    try {
        if (!app.project) {
            return jsonResponse(false, 'No active project');
        }

        var folders = [];
        for (var i = 1; i <= app.project.items.length; i++) {
            var item = app.project.items[i];
            if (item instanceof FolderItem) {
                folders.push({
                    id: item.id,
                    name: item.name
                });
            }
        }

        return jsonResponse(true, 'Found ' + folders.length + ' folder(s)', {
            folders: folders
        });
    } catch (e) {
        return jsonResponse(false, 'Error getting folders: ' + e.toString());
    }
}

// ============================================================================
// FILE SYSTEM OPERATIONS
// ============================================================================

/**
 * Save text content to file using File object
 */
function saveTextToFile(filePath, content) {
    try {
        var file = new File(filePath);

        // Ensure parent folder exists
        var folder = file.parent;
        if (!folder.exists) {
            var created = folder.create();
            if (!created) {
                return jsonResponse(false, 'Could not create folder: ' + folder.fsName);
            }
        }

        // Set encoding and open file
        file.encoding = 'UTF-8';
        file.lineFeed = 'unix';
        var opened = file.open('w');
        if (!opened) {
            return jsonResponse(false, 'Could not open file for writing: ' + filePath);
        }

        file.write(content);
        file.close();
        return jsonResponse(true, 'File saved successfully', { path: filePath });
    } catch (e) {
        return jsonResponse(false, 'Error saving file: ' + e.toString());
    }
}

/**
 * Reveal a file in Finder / File Explorer.
 * Falls back to opening the parent folder if reveal() isn't available.
 */
function revealFileInExplorer(filePath) {
    try {
        var file = new File(filePath);
        if (!file.exists) {
            return jsonResponse(false, 'File does not exist: ' + filePath);
        }

        corRevealFile(file.fsName);

        return jsonResponse(true, 'Revealed file', { path: filePath });
    } catch (e) {
        return jsonResponse(false, 'Error revealing file: ' + e.toString());
    }
}

/**
 * Open a folder in Finder / File Explorer.
 */
function revealFolderInExplorer(folderPath) {
    try {
        var folder = new Folder(folderPath);
        if (!folder.exists) {
            return jsonResponse(false, 'Folder does not exist: ' + folderPath);
        }
        corOpenFolder(folder.fsName);
        return jsonResponse(true, 'Opened folder', { path: folderPath });
    } catch (e) {
        return jsonResponse(false, 'Error opening folder: ' + e.toString());
    }
}

/**
 * Export project outline with save dialog
 * @param {Object} options - Export options (format: 'txt'|'csv'|'json', content: string)
 * @returns {string} JSON response with saved path
 */
function exportOutlineWithDialog(options) {
    try {
        if (!options || !options.content) {
            return jsonResponse(false, 'No content to export');
        }

        var format = options.format || 'txt';
        var isBinary = options.isBinary || false;
        var defaultName = 'comporama_report_' + getDateStamp();
        var extension = '.' + format;
        var filterDesc, filterExt;

        if (format === 'csv') {
            filterDesc = 'CSV Files';
            filterExt = '*.csv';
        } else if (format === 'json') {
            filterDesc = 'JSON Files';
            filterExt = '*.json';
        } else if (format === 'pdf') {
            filterDesc = 'PDF Files';
            filterExt = '*.pdf';
        } else {
            filterDesc = 'Text Files';
            filterExt = '*.txt';
        }

        // Show save dialog
        var saveFile = File.saveDialog('Export Project Report', filterDesc + ':' + filterExt);

        if (!saveFile) {
            return jsonResponse(false, 'Export cancelled by user', { cancelled: true });
        }

        // Ensure correct extension
        var filePath = saveFile.fsName;
        if (filePath.indexOf(extension) !== filePath.length - extension.length) {
            filePath = filePath + extension;
            saveFile = new File(filePath);
        }

        if (isBinary && format === 'pdf') {
            // Decode base64 and write as binary
            var base64 = options.content;
            var decoded = base64Decode(base64);
            saveFile.encoding = 'BINARY';
            var opened = saveFile.open('w');
            if (!opened) {
                return jsonResponse(false, 'Could not open file for writing');
            }
            saveFile.write(decoded);
            saveFile.close();
        } else {
            // Write text file
            saveFile.encoding = 'UTF-8';
            saveFile.lineFeed = 'unix';
            var opened = saveFile.open('w');
            if (!opened) {
                return jsonResponse(false, 'Could not open file for writing');
            }
            saveFile.write(options.content);
            saveFile.close();
        }

        // Reveal in Finder
        if (typeof saveFile.reveal === 'function') {
            saveFile.reveal();
        } else if (saveFile.parent && saveFile.parent.exists) {
            saveFile.parent.execute();
        }

        return jsonResponse(true, 'Report exported successfully', {
            path: saveFile.fsName,
            format: format
        });
    } catch (e) {
        return jsonResponse(false, 'Error exporting report: ' + e.toString());
    }
}

/**
 * Decode base64 string to binary string (ES3 compatible)
 */
function base64Decode(input) {
    var keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
    var output = '';
    var chr1, chr2, chr3;
    var enc1, enc2, enc3, enc4;
    var i = 0;

    // Remove non-base64 characters
    input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');

    while (i < input.length) {
        enc1 = keyStr.indexOf(input.charAt(i++));
        enc2 = keyStr.indexOf(input.charAt(i++));
        enc3 = keyStr.indexOf(input.charAt(i++));
        enc4 = keyStr.indexOf(input.charAt(i++));

        chr1 = (enc1 << 2) | (enc2 >> 4);
        chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
        chr3 = ((enc3 & 3) << 6) | enc4;

        output += String.fromCharCode(chr1);
        if (enc3 !== 64) output += String.fromCharCode(chr2);
        if (enc4 !== 64) output += String.fromCharCode(chr3);
    }

    return output;
}

/**
 * Get date stamp in YYYY-MM-DD format
 */
function getDateStamp() {
    var d = new Date();
    var year = d.getFullYear();
    var month = ('0' + (d.getMonth() + 1)).slice(-2);
    var day = ('0' + d.getDate()).slice(-2);
    return year + '-' + month + '-' + day;
}

/**
 * Open/view a composition in After Effects (makes it the active item)
 * @param {Object} compData - Object with id property
 * @returns {string} JSON response
 */
function openComposition(compData) {
    try {
        if (!compData || !compData.id) {
            return jsonResponse(false, 'No composition ID provided');
        }

        var comp = app.project.itemByID(compData.id);
        if (!comp || !(comp instanceof CompItem)) {
            return jsonResponse(false, 'Composition not found');
        }

        // Open the composition (makes it visible in the timeline)
        comp.openInViewer();

        return jsonResponse(true, 'Composition opened', { name: comp.name });
    } catch (e) {
        return jsonResponse(false, 'Error opening composition: ' + e.toString());
    }
}

// ============================================================================
// EXPORTS (for CEP interface)
// ============================================================================

// All functions are automatically available to CSInterface.evalScript()
// No explicit exports needed in ExtendScript

// ============================================================================
// HTTP REQUEST BRIDGE (for licensing — CEP browser context cannot make network requests)
// Writes JSON body to file, creates shell script, runs in background.
// Zero shell escaping — all data passes through ExtendScript File I/O.
// ============================================================================
// Sanitise strings before embedding in shell commands — strip any chars
// that could break out of double-quoted bash context.
function _shellSafe(str) {
    return str.replace(/[^a-zA-Z0-9\/:._\-=@?&%+,]/g, '');
}

function httpBridgeFromFile(url, prefix) {
    try {
        url = _shellSafe(url);
        prefix = _shellSafe(prefix);
        // Body file already written by JS side — just create script and run
        var scriptCmd = '/usr/bin/printf \'#!/bin/bash\\n'
            + '/usr/bin/curl -sL -m 15 -X POST "' + url + '" '
            + '-H "Content-Type: application/json" '
            + '-H "User-Agent: COMP-O-RAMA/1.0" '
            + '-H "Accept: application/json" '
            + '-d @"' + prefix + '.body" '
            + '> "' + prefix + '.result" 2>&1\\n'
            + 'echo 1 > "' + prefix + '.done"\\n\''
            + ' > "' + prefix + '.sh"';
        system.callSystem(scriptCmd);
        system.callSystem('chmod +x "' + prefix + '.sh"');
        system.callSystem('"' + prefix + '.sh" &');
        return 'ok';
    } catch (e) {
        return 'error: ' + e.toString();
    }
}

function httpBridge(url, prefix, bodyJson) {
    try {
        url = _shellSafe(url);
        prefix = _shellSafe(prefix);
        // Write JSON body via ExtendScript File (content doesn't need newlines)
        var bodyFile = new File(prefix + '.body');
        bodyFile.open('w');
        bodyFile.write(bodyJson);
        bodyFile.close();

        // Write shell script via printf (ExtendScript File.write uses \r on Mac, bash needs \n)
        var scriptCmd = '/usr/bin/printf \'#!/bin/bash\\n'
            + '/usr/bin/curl -sL -m 15 -X POST "' + url + '" '
            + '-H "Content-Type: application/json" '
            + '-H "User-Agent: COMP-O-RAMA/1.0" '
            + '-H "Accept: application/json" '
            + '-d @"' + prefix + '.body" '
            + '> "' + prefix + '.result" 2>&1\\n'
            + 'echo 1 > "' + prefix + '.done"\\n\''
            + ' > "' + prefix + '.sh"';
        system.callSystem(scriptCmd);
        system.callSystem('chmod +x "' + prefix + '.sh"');
        system.callSystem('"' + prefix + '.sh" &');

        return 'ok';
    } catch (e) {
        return 'error: ' + e.toString();
    }
}
