/**
 * Wild Dragon MAM - Premiere Pro ExtendScript
 *
 * This file runs in the Premiere Pro host context (not the browser panel).
 * It is registered via <ScriptPath> in manifest.xml and called from the
 * panel via csInterface.evalScript().
 *
 * ExtendScript is ES3-level JavaScript — no arrow functions, no const/let,
 * no template literals, no destructuring.
 */

// ============================================================================
// Core Import Functions
// ============================================================================

/**
 * Imports a media file into the active Premiere Pro project.
 * @param {string} filePath - Full path to the file to import
 * @returns {string} JSON string with success status and message
 */
function importFileToProject(filePath) {
    var result = {
        success: false,
        message: "",
        itemID: null
    };

    try {
        if (!app.project) {
            result.message = "No active Premiere Pro project";
            return JSON.stringify(result);
        }

        var file = new File(filePath);
        if (!file.exists) {
            result.message = "File does not exist: " + filePath;
            return JSON.stringify(result);
        }

        app.project.importFiles([filePath], true);

        result.success = true;
        result.message = "File imported successfully";
        return JSON.stringify(result);
    } catch (error) {
        result.message = "Error importing file: " + error.message;
        return JSON.stringify(result);
    }
}

/**
 * Gets the active sequence in the project.
 * @returns {string} JSON string with sequence name and ID, or nulls
 */
function getActiveSequence() {
    var result = {
        sequenceName: null,
        sequenceID: null
    };

    try {
        if (!app.project) return JSON.stringify(result);

        var activeSequence = app.project.activeSequence;
        if (activeSequence) {
            result.sequenceName = activeSequence.name;
            result.sequenceID   = activeSequence.sequenceID;
        }
        return JSON.stringify(result);
    } catch (error) {
        return JSON.stringify(result);
    }
}

/**
 * Inserts a clip into the active sequence at the playhead position.
 * @param {string} filePath   - Full path to the media file
 * @param {number} trackIndex - Video track index (1-based)
 * @returns {string} JSON string with success status
 */
function insertClipToSequence(filePath, trackIndex) {
    var result = {
        success: false,
        message: ""
    };

    try {
        if (!app.project) {
            result.message = "No active project";
            return JSON.stringify(result);
        }

        var sequence = app.project.activeSequence;
        if (!sequence) {
            result.message = "No active sequence";
            return JSON.stringify(result);
        }

        app.project.importFiles([filePath], true);

        var file = new File(filePath);
        var fileName = file.displayName;
        var rootBin = app.project.rootItem;
        var projectItem = findProjectItemByName(rootBin, fileName);

        if (!projectItem) {
            result.message = "Could not find imported file in project";
            return JSON.stringify(result);
        }

        if (trackIndex < 1 || trackIndex > sequence.videoTracks.numTracks) {
            result.message = "Invalid track index: " + trackIndex;
            return JSON.stringify(result);
        }

        var track = sequence.videoTracks[trackIndex - 1];
        var playheadTime = sequence.getPlayerPosition();
        var trackItem = track.insertClip(projectItem, playheadTime);

        if (trackItem) {
            result.success = true;
            result.message = "Clip inserted at track " + trackIndex;
        } else {
            result.message = "Failed to insert clip into track";
        }

        return JSON.stringify(result);
    } catch (error) {
        result.message = "Error inserting clip: " + error.message;
        return JSON.stringify(result);
    }
}

/**
 * Gets the current project file path.
 * @returns {string} JSON string with project path and name
 */
function getProjectPath() {
    var result = {
        projectPath: null,
        projectName: null
    };

    try {
        if (!app.project) return JSON.stringify(result);

        result.projectPath = app.project.path;
        result.projectName = app.project.name;
        return JSON.stringify(result);
    } catch (error) {
        return JSON.stringify(result);
    }
}

/**
 * Gets information about all video tracks in the active sequence.
 * @returns {string} JSON string with an array of track objects
 */
function getSequenceTracks() {
    var result = { tracks: [] };

    try {
        if (!app.project) return JSON.stringify(result);

        var sequence = app.project.activeSequence;
        if (!sequence) return JSON.stringify(result);

        for (var i = 0; i < sequence.videoTracks.numTracks; i++) {
            var track = sequence.videoTracks[i];
            result.tracks.push({
                index: i + 1,
                name:  track.name || ("V" + (i + 1)),
                type:  "video"
            });
        }
        return JSON.stringify(result);
    } catch (error) {
        return JSON.stringify(result);
    }
}

/**
 * Gets the current playhead position in the active sequence.
 * @returns {string} JSON string with timeInSeconds and SMPTE timeCode
 */
function getPlayheadPosition() {
    var result = {
        timeInSeconds: 0,
        timeCode: ""
    };

    try {
        if (!app.project) return JSON.stringify(result);

        var sequence = app.project.activeSequence;
        if (!sequence) return JSON.stringify(result);

        var time   = sequence.getPlayerPosition();
        var ticks  = parseFloat(time.ticks);
        // Premiere Pro ticks: 254016000000 ticks per second
        var TICKS_PER_SECOND = 254016000000;
        var totalSeconds = ticks / TICKS_PER_SECOND;
        result.timeInSeconds = totalSeconds;

        // Build SMPTE timecode — use sequence frame rate
        var frameRate   = sequence.timebase ? (TICKS_PER_SECOND / parseFloat(sequence.timebase)) : 25;
        var totalFrames = Math.floor(totalSeconds * frameRate);
        var hours   = Math.floor(totalFrames / (frameRate * 3600));
        var minutes = Math.floor((totalFrames % (frameRate * 3600)) / (frameRate * 60));
        var seconds = Math.floor((totalFrames % (frameRate * 60))  / frameRate);
        var frames  = totalFrames % Math.round(frameRate);

        result.timeCode = pad(hours, 2) + ":" + pad(minutes, 2) + ":" +
                          pad(seconds, 2) + ":" + pad(frames, 2);
        return JSON.stringify(result);
    } catch (error) {
        return JSON.stringify(result);
    }
}

/**
 * Gets basic project information.
 * @returns {string} JSON string with project name, path, and sequence count
 */
function getProjectInfo() {
    var result = {
        projectName: "",
        projectPath: "",
        sequenceCount: 0
    };

    try {
        if (!app.project) return JSON.stringify(result);

        result.projectName = app.project.name;
        result.projectPath = app.project.path;

        var rootBin = app.project.rootItem;
        if (rootBin && rootBin.children) {
            for (var i = 0; i < rootBin.children.numItems; i++) {
                if (rootBin.children[i].type === ProjectItemType.SEQUENCE) {
                    result.sequenceCount++;
                }
            }
        }
        return JSON.stringify(result);
    } catch (error) {
        return JSON.stringify(result);
    }
}

/**
 * Exports the current sequence using Adobe Media Encoder.
 *
 * AME must be installed. This function launches AME (if not already running)
 * and queues the export job — it returns immediately; encoding happens in the
 * background and the output file appears when AME finishes.
 *
 * @param {string} outputPath  - Absolute path for the output file
 * @param {string} presetPath  - Absolute path to an AME preset file (.epr);
 *                               pass an empty string to use the sequence's
 *                               current export settings.
 * @returns {string} JSON string with success flag, message, and jobId
 */
function exportSequence(outputPath, presetPath) {
    var result = {
        success: false,
        message: "",
        jobId: null
    };

    try {
        if (!app.project) {
            result.message = "No active project";
            return JSON.stringify(result);
        }

        var sequence = app.project.activeSequence;
        if (!sequence) {
            result.message = "No active sequence";
            return JSON.stringify(result);
        }

        // Ensure Adobe Media Encoder is running before queuing
        app.encoder.launchEncoder();

        // encodeSequence(sequence, outputFilePath, presetPath, workAreaType, removeOnCompletion)
        // workAreaType: ENCODE_ENTIRE = 0, ENCODE_IN_TO_OUT = 1
        var jobId = app.encoder.encodeSequence(
            sequence,
            outputPath,
            presetPath || "",
            app.encoder.ENCODE_ENTIRE,
            false  // keep job in AME queue after completion
        );

        if (jobId) {
            result.success = true;
            result.message = "Export queued in Adobe Media Encoder";
            result.jobId   = jobId;
        } else {
            result.message = "AME returned no job ID — verify that the preset path is valid";
        }

        return JSON.stringify(result);
    } catch (error) {
        result.message = "Export error: " + error.message;
        return JSON.stringify(result);
    }
}

// ============================================================================
// Timeline Export — read sequence clips for MAM round-trip
// ============================================================================

/**
 * Reads all clips from all video tracks in the active sequence.
 * Returns JSON with sequence settings and a flat clip array.
 *
 * Each clip entry contains:
 *   filePath          – absolute OS path to source media (for MAM asset ID lookup)
 *   fileName          – project panel display name
 *   trackIndex        – 0-based video track index (0 = V1, 1 = V2, …)
 *   sourceInFrames    – in-point within source media, in frames
 *   sourceOutFrames   – out-point within source media, in frames
 *   timelineInFrames  – clip start position on timeline, in frames
 *   timelineOutFrames – clip end position on timeline, in frames
 *
 * sequence.timebase is ticks-per-frame.  Premiere uses 254,016,000,000
 * ticks/second, so fps = 254016000000 / timebase.
 */
function exportTimelineData() {
    var result = {
        success:      false,
        message:      "",
        sequenceName: "",
        frameRate:    59.94,
        width:        1920,
        height:       1080,
        clips:        []
    };

    try {
        if (!app.project) {
            result.message = "No active project";
            return JSON.stringify(result);
        }

        var sequence = app.project.activeSequence;
        if (!sequence) {
            result.message = "No active sequence";
            return JSON.stringify(result);
        }

        result.sequenceName = sequence.name;

        var TICKS_PER_SECOND = 254016000000;
        var timebaseTicks    = parseFloat(sequence.timebase) || 4240384;
        result.frameRate     = parseFloat((TICKS_PER_SECOND / timebaseTicks).toFixed(4));

        try { result.width  = sequence.frameSizeHorizontal || 1920; } catch (e) {}
        try { result.height = sequence.frameSizeVertical   || 1080; } catch (e) {}

        var videoTracks = sequence.videoTracks;
        for (var t = 0; t < videoTracks.numTracks; t++) {
            var track = videoTracks[t];
            if (!track || !track.clips) continue;
            var numClips = track.clips.numItems;
            for (var c = 0; c < numClips; c++) {
                try {
                    var clip = track.clips[c];
                    if (!clip || !clip.projectItem) continue;

                    var filePath = "";
                    try { filePath = clip.projectItem.getMediaPath(); } catch (e) {}

                    var srcIn  = Math.round(parseFloat(clip.inPoint.ticks)  / timebaseTicks);
                    var srcOut = Math.round(parseFloat(clip.outPoint.ticks) / timebaseTicks);
                    var recIn  = Math.round(parseFloat(clip.start.ticks)    / timebaseTicks);
                    var recOut = Math.round(parseFloat(clip.end.ticks)      / timebaseTicks);

                    // Skip degenerate clips (zero or negative duration)
                    if (srcOut <= srcIn || recOut <= recIn) continue;

                    result.clips.push({
                        trackIndex:        t,
                        filePath:          filePath,
                        fileName:          clip.projectItem.name || "",
                        sourceInFrames:    srcIn,
                        sourceOutFrames:   srcOut,
                        timelineInFrames:  recIn,
                        timelineOutFrames: recOut
                    });
                } catch (e) { /* skip malformed clip */ }
            }
        }

        result.success = true;
        result.message = result.clips.length + " clip(s) across " + videoTracks.numTracks + " track(s)";
        return JSON.stringify(result);
    } catch (error) {
        result.message = "Error reading timeline: " + error.message;
        return JSON.stringify(result);
    }
}

/**
 * Returns all media ProjectItems in the current project with name + file path.
 * Useful for rebuilding the asset-path lookup map after a Premiere restart
 * when temp-file paths may have changed.
 */
function getProjectItems() {
    var result = { items: [] };
    try {
        if (!app.project) return JSON.stringify(result);
        _collectProjectItems(app.project.rootItem, result.items);
    } catch (e) {}
    return JSON.stringify(result);
}

function _collectProjectItems(bin, out) {
    if (!bin || !bin.children) return;
    for (var i = 0; i < bin.children.numItems; i++) {
        var item = bin.children[i];
        if (!item) continue;
        if (item.type === ProjectItemType.BIN) {
            _collectProjectItems(item, out);
        } else {
            var path = "";
            try { path = item.getMediaPath(); } catch (e) {}
            out.push({ name: item.name, path: path });
        }
    }
}

// ============================================================================
// Advanced Features — FCP XML Export & Hi-Res Relink
// ============================================================================

/**
 * Enhanced timeline export that includes a unique clipInstanceId per clip.
 * Each instance ID is derived from the project item's nodeId + track index +
 * in-point frame so it is stable across repeated reads of the same timeline.
 * @returns {string} JSON string with clip array including clipInstanceId fields
 */
function exportTimelineDataWithIds() {
    var result = {
        success:      false,
        message:      "",
        sequenceName: "",
        frameRate:    59.94,
        width:        1920,
        height:       1080,
        clips:        []
    };

    try {
        if (!app.project) {
            result.message = "No active project";
            return JSON.stringify(result);
        }

        var sequence = app.project.activeSequence;
        if (!sequence) {
            result.message = "No active sequence";
            return JSON.stringify(result);
        }

        result.sequenceName = sequence.name;

        var TICKS_PER_SECOND = 254016000000;
        var timebaseTicks    = parseFloat(sequence.timebase) || 4240384;
        result.frameRate     = parseFloat((TICKS_PER_SECOND / timebaseTicks).toFixed(4));

        try { result.width  = sequence.frameSizeHorizontal || 1920; } catch (e) {}
        try { result.height = sequence.frameSizeVertical   || 1080; } catch (e) {}

        var videoTracks = sequence.videoTracks;
        for (var t = 0; t < videoTracks.numTracks; t++) {
            var track = videoTracks[t];
            if (!track || !track.clips) continue;
            var numClips = track.clips.numItems;
            for (var c = 0; c < numClips; c++) {
                try {
                    var clip = track.clips[c];
                    if (!clip || !clip.projectItem) continue;

                    var filePath = "";
                    try { filePath = clip.projectItem.getMediaPath(); } catch (e) {}

                    var srcIn  = Math.round(parseFloat(clip.inPoint.ticks)  / timebaseTicks);
                    var srcOut = Math.round(parseFloat(clip.outPoint.ticks) / timebaseTicks);
                    var recIn  = Math.round(parseFloat(clip.start.ticks)    / timebaseTicks);
                    var recOut = Math.round(parseFloat(clip.end.ticks)      / timebaseTicks);

                    if (srcOut <= srcIn || recOut <= recIn) continue;

                    // Build a stable unique ID from the project item nodeId,
                    // track index, and timeline in-point.
                    var nodeId = "";
                    try { nodeId = clip.projectItem.nodeId; } catch (e) {}
                    if (!nodeId) nodeId = clip.projectItem.name + "_" + t;
                    var clipInstanceId = nodeId + "_" + t + "_" + recIn;

                    result.clips.push({
                        clipInstanceId:   clipInstanceId,
                        assetId:          null,
                        trackIndex:       t,
                        filePath:         filePath,
                        fileName:         clip.projectItem.name || "",
                        sourceInFrames:   srcIn,
                        sourceOutFrames:  srcOut,
                        timelineInFrames: recIn,
                        timelineOutFrames:recOut
                    });
                } catch (e) { /* skip malformed clip */ }
            }
        }

        result.success = true;
        result.message = result.clips.length + " clip(s) with instance IDs";
        return JSON.stringify(result);
    } catch (error) {
        result.message = "Error reading timeline with IDs: " + error.message;
        return JSON.stringify(result);
    }
}

/**
 * Relinks a specific clip in the Premiere project by finding it via
 * clipInstanceId (from timeline export) and swapping its media path.
 *
 * ExtendScript cannot receive complex objects, so clipInstanceId and newPath
 * are passed as separate string arguments.
 *
 * @param {string} clipInstanceId - The clip instance ID from exportTimelineDataWithIds()
 * @param {string} newMediaPath   - Absolute path to the replacement media file
 * @returns {string} JSON with success flag, message, and relinked count
 */
function relinkClipToNewMedia(clipInstanceId, newMediaPath) {
    var result = {
        success:   false,
        message:   "",
        relinked:  0
    };

    try {
        if (!app.project) {
            result.message = "No active project";
            return JSON.stringify(result);
        }

        // Parse clipInstanceId: nodeId_track_frame
        var parts = clipInstanceId.split("_");
        if (parts.length < 3) {
            result.message = "Invalid clipInstanceId format";
            return JSON.stringify(result);
        }
        var targetTrack  = parseInt(parts[parts.length - 2], 10);
        var targetInFrame = parseInt(parts[parts.length - 1], 10);

        var sequence = app.project.activeSequence;
        if (!sequence) {
            result.message = "No active sequence";
            return JSON.stringify(result);
        }

        var TICKS_PER_SECOND = 254016000000;
        var timebaseTicks    = parseFloat(sequence.timebase) || 4240384;

        // Walk the target track and find matching clip
        var videoTracks = sequence.videoTracks;
        if (targetTrack < 0 || targetTrack >= videoTracks.numTracks) {
            result.message = "Track " + targetTrack + " out of range";
            return JSON.stringify(result);
        }

        var track = videoTracks[targetTrack];
        if (!track || !track.clips) {
            result.message = "No clips on track " + targetTrack;
            return JSON.stringify(result);
        }

        var matched = false;
        for (var c = 0; c < track.clips.numItems; c++) {
            try {
                var clip = track.clips[c];
                if (!clip) continue;

                var recIn = Math.round(parseFloat(clip.start.ticks) / timebaseTicks);
                if (recIn === targetInFrame) {
                    var newFile = new File(newMediaPath);
                    if (!newFile.exists) {
                        result.message = "New media not found: " + newMediaPath;
                        return JSON.stringify(result);
                    }

                    // Import the new media file into the project
                    app.project.importFiles([newMediaPath], true);

                    // Replace the clip source with the new project item
                    var fileName = newFile.displayName;
                    var rootBin = app.project.rootItem;
                    var newItem = findProjectItemByName(rootBin, fileName);

                    if (newItem) {
                        clip.projectItem = newItem;
                        matched = true;
                        result.relinked = 1;
                        break;
                    }
                }
            } catch (e) { /* continue searching */ }
        }

        if (matched) {
            result.success = true;
            result.message = "Clip relinked on track " + (targetTrack + 1);
        } else {
            result.message = "No matching clip found at specified position";
        }

        return JSON.stringify(result);
    } catch (error) {
        result.message = "Relink error: " + error.message;
        return JSON.stringify(result);
    }
}

/**
 * FCP XML Export — exports the active sequence as Final Cut Pro XML
 * using Premiere Pro's native export function.
 *
 * Note: Premiere Pro ExtendScript does not expose a direct
 * exportFCPXML() call in all versions. If the native call is
 * unavailable, this function falls back to constructing the XML
 * from the timeline data, which the panel JS can then use directly.
 *
 * @param {string} exportPath - Absolute path to write the .xml file
 * @returns {string} JSON with success flag and message
 */
function exportSequenceAsFCPXML(exportPath) {
    var result = {
        success: false,
        message: "",
        xmlContent: ""
    };

    try {
        if (!app.project) {
            result.message = "No active project";
            return JSON.stringify(result);
        }

        var sequence = app.project.activeSequence;
        if (!sequence) {
            result.message = "No active sequence";
            return JSON.stringify(result);
        }

        // Try Premiere's built-in FCP XML export first
        // Some versions expose exportFCPXML on the project object.
        var exported = false;

        // Method 1: app.project.exportFCPXML (available in some versions)
        if (typeof app.project.exportFCPXML === "function") {
            try {
                app.project.exportFCPXML(sequence, exportPath);
                exported = true;
            } catch (e) { /* fall through */ }
        }

        // Method 2: app.encoder with FCP XML preset
        if (!exported && typeof app.encoder !== "undefined" && typeof app.encoder.launchEncoder === "function") {
            try {
                app.encoder.launchEncoder();
                // There is no standard FCP XML AME preset — fall through to manual
            } catch (e) { /* fall through */ }
        }

        if (!exported) {
            // Fall back to constructing XML from timeline data
            var timelineData = JSON.parse(exportTimelineData());

            if (!timelineData.success) {
                result.message = "Failed to read timeline data";
                return JSON.stringify(result);
            }

            var fps = parseFloat(timelineData.frameRate) || 59.94;
            var width = timelineData.width || 1920;
            var height = timelineData.height || 1080;

            var xml = [];
            xml.push('<?xml version="1.0" encoding="UTF-8"?>');
            xml.push('<!DOCTYPE xmeml>');
            xml.push('<xmeml version="5">');
            xml.push('  <sequence>');
            xml.push('    <name>' + escapeXmlStr(timelineData.sequenceName || "Untitled") + '</name>');
            xml.push('    <duration>' + getSequenceDuration(timelineData.clips, fps) + '</duration>');
            xml.push('    <rate>');
            xml.push('      <timebase>' + Math.round(fps) + '</timebase>');
            xml.push('      <ntsc>' + (Math.abs(fps - 29.97) < 0.02 || Math.abs(fps - 59.94) < 0.02 ? "TRUE" : "FALSE") + '</ntsc>');
            xml.push('    </rate>');
            xml.push('    <media>');
            xml.push('      <video>');
            xml.push('        <format>');
            xml.push('          <samplecharacteristics>');
            xml.push('            <width>' + width + '</width>');
            xml.push('            <height>' + height + '</height>');
            xml.push('          </samplecharacteristics>');
            xml.push('        </format>');
            xml.push('        <track>');

            // Group clips by track
            var tracks = {};
            for (var i = 0; i < timelineData.clips.length; i++) {
                var cl = timelineData.clips[i];
                if (!tracks[cl.trackIndex]) tracks[cl.trackIndex] = [];
                tracks[cl.trackIndex].push(cl);
            }

            var trackKeys = Object.keys(tracks).sort();
            for (var tk = 0; tk < trackKeys.length; tk++) {
                var tIdx = trackKeys[tk];
                var trackClips = tracks[tIdx];
                xml.push('          <clipitem id="clip-item-' + tIdx + '">');
                xml.push('            <name>Track ' + (parseInt(tIdx) + 1) + '</name>');

                for (var ci = 0; ci < trackClips.length; ci++) {
                    var clipData = trackClips[ci];
                    xml.push('            <clipitem id="clip-' + tIdx + '-' + ci + '">');
                    xml.push('              <name>' + escapeXmlStr(clipData.fileName || "Clip " + (ci + 1)) + '</name>');
                    xml.push('              <duration>' + (clipData.sourceOutFrames - clipData.sourceInFrames) + '</duration>');
                    xml.push('              <rate>');
                    xml.push('                <timebase>' + Math.round(fps) + '</timebase>');
                    xml.push('              </rate>');
                    xml.push('              <in>' + clipData.sourceInFrames + '</in>');
                    xml.push('              <out>' + clipData.sourceOutFrames + '</out>');
                    xml.push('              <start>' + clipData.timelineInFrames + '</start>');
                    xml.push('              <end>' + clipData.timelineOutFrames + '</end>');
                    xml.push('              <file>');
                    xml.push('                <name>' + escapeXmlStr(clipData.filePath || clipData.fileName || "Unknown") + '</name>');
                    xml.push('                <pathurl>' + escapeXmlStr(clipData.filePath || "") + '</pathurl>');
                    xml.push('              </file>');
                    xml.push('            </clipitem>');
                }

                xml.push('          </clipitem>');
                xml.push('        </track>');
            }

            xml.push('      </video>');
            xml.push('    </media>');
            xml.push('  </sequence>');
            xml.push('</xmeml>');

            result.xmlContent = xml.join("\n");
            result.success = true;
            result.message = "FCP XML constructed from timeline data";
        } else {
            // Read the exported file
            try {
                var file = new File(exportPath);
                if (file.exists) {
                    file.open("r");
                    result.xmlContent = file.read();
                    file.close();
                    result.success = true;
                    result.message = "FCP XML exported via Premiere native function";
                } else {
                    result.message = "Export file not found at " + exportPath;
                }
            } catch (e) {
                result.message = "Failed to read exported file: " + e.message;
            }
        }

        return JSON.stringify(result);
    } catch (error) {
        result.message = "FCP XML export error: " + error.message;
        return JSON.stringify(result);
    }
}

/**
 * Walks every project item and swaps clip media paths matching `oldPath`
 * onto `newPath`. Returns the number of clips relinked.
 * This is used by the hi-res auto-relink workflow after downloading
 * trimmed segments.
 *
 * @param {string} oldPath - Current media path to replace
 * @param {string} newPath - New media path to use
 * @returns {string} JSON with success, relinked count, and message
 */
function replaceMediaPath(oldPath, newPath) {
    var result = {
        success:  false,
        relinked: 0,
        message:  ""
    };

    try {
        if (!app.project) {
            result.message = "No active project";
            return JSON.stringify(result);
        }

        var oldFile = new File(newPath);
        if (!oldFile.exists) {
            result.message = "New media file not found: " + newPath;
            return JSON.stringify(result);
        }

        app.project.importFiles([newPath], true);

        var newName = oldFile.displayName;
        var rootBin = app.project.rootItem;
        var newItem = findProjectItemByName(rootBin, newName);

        if (!newItem) {
            result.message = "Could not find imported media in project";
            return JSON.stringify(result);
        }

        var count = 0;
        function walkAndReplace(item) {
            if (!item || !item.children) return;
            for (var i = 0; i < item.children.numItems; i++) {
                var child = item.children[i];
                if (!child) continue;
                if (child.type === ProjectItemType.BIN) {
                    walkAndReplace(child);
                } else {
                    try {
                        var mediaPath = child.getMediaPath();
                        if (mediaPath === oldPath) {
                            child.changeMediaPath(newPath);
                            count++;
                        }
                    } catch (e) { /* skip inaccessible items */ }
                }
            }
        }

        walkAndReplace(rootBin);

        result.success  = count > 0;
        result.relinked = count;
        result.message  = count + " clip(s) relinked";

        return JSON.stringify(result);
    } catch (error) {
        result.message = "replaceMediaPath error: " + error.message;
        return JSON.stringify(result);
    }
}

// ============================================================================
// Internal Helpers (Advanced Features)
// ============================================================================

/**
 * Calculates total sequence duration in frames from clip array.
 */
function getSequenceDuration(clips, fps) {
    var maxEnd = 0;
    for (var i = 0; i < clips.length; i++) {
        if (clips[i].timelineOutFrames > maxEnd) {
            maxEnd = clips[i].timelineOutFrames;
        }
    }
    return maxEnd || Math.round(fps * 30); // default to 30 sec
}

/**
 * Escapes XML special characters in a string.
 */
function escapeXmlStr(str) {
    if (typeof str !== "string") return "";
    return str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&apos;");
}

// ============================================================================
// Helper Functions
// ============================================================================

/**
 * Recursively searches for a project item by display name.
 * @param {ProjectItem} bin  - The bin/root to search
 * @param {string}      name - Name to match
 * @returns {ProjectItem|null}
 */
function findProjectItemByName(bin, name) {
    if (!bin || !bin.children) return null;

    for (var i = 0; i < bin.children.numItems; i++) {
        var item = bin.children[i];
        if (item.name === name) return item;
        if (item.type === ProjectItemType.BIN) {
            var found = findProjectItemByName(item, name);
            if (found) return found;
        }
    }
    return null;
}

/**
 * Pads a number with leading zeros.
 * @param {number} num    - Number to pad
 * @param {number} digits - Minimum digit count
 * @returns {string}
 */
function pad(num, digits) {
    var str = "" + Math.floor(num);
    while (str.length < digits) str = "0" + str;
    return str;
}
