// ============================================================
// AI Video Studio - Premiere Pro ExtendScript (JSX)
// ============================================================
// File data transfer: Node.js fs from CEP panel
// ExtendScript: save dialog + PR timeline operations
// ============================================================

function safeJSON(obj) {
  try { return JSON.stringify(obj); } catch(e) { return '{"error":"JSON stringify failed"}'; }
}

// ============================================================
// 1. Save dialog - returns selected file path
// ============================================================
function getSavePath(defaultName) {
  try {
    var docsFolder = new Folder('~/Documents/AI Video Studio');
    if (!docsFolder.exists) docsFolder.create();
    var saveFile = File.saveDialog('保存文件', defaultName || 'ai-media.mp4', docsFolder);
    if (!saveFile) return safeJSON({ success: false, error: 'User cancelled' });
    return safeJSON({ success: true, filePath: saveFile.fsName });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.getSavePath = getSavePath;

// ============================================================
// 1.5 Choose a persistent media folder for generated assets
// ============================================================
function selectMediaFolder() {
  try {
    var folder = Folder.selectDialog('选择 AI 生成作品保存文件夹（建议：Adobe Premiere Pro Auto-Save）');
    if (!folder) return safeJSON({ success: false, error: 'User cancelled' });
    if (!folder.exists) folder.create();
    return safeJSON({ success: true, folderPath: folder.fsName });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.selectMediaFolder = selectMediaFolder;

// ============================================================
// 2. Find project item - try multiple strategies
// ============================================================
function findProjectItem(filePath) {
  try {
    var file = new File(filePath);
    var fileName = file.name;
    // 统一路径分隔符：Windows 下正反斜杠均可匹配
    var isWindows = ($.os || '').indexOf('Windows') >= 0;
    var norm = function(p) {
      var s = String(p || '');
      try { s = decodeURI(s); } catch(e) {}
      if (isWindows) s = s.replace(/\//g, '\\');
      return s.toLowerCase();
    };
    var targetNorm = norm(filePath);
    var targetNameNorm = String(fileName || '').toLowerCase();
    
    // Strategy 1: Search recursively by media path match
    function searchByPath(parent, targetPath) {
      for (var i = 0; i < parent.children.numItems; i++) {
        var child = parent.children[i];
        if (child.type === 1) { // Bin
          var found = searchByPath(child, targetPath);
          if (found) return found;
        } else {
          try {
            var mp = child.getMediaPath();
            if (mp) {
              if (norm(mp) === targetPath) return child;
            }
          } catch(e) {}
        }
      }
      return null;
    }
    
    var byPath = searchByPath(app.project.rootItem, targetNorm);
    if (byPath) return byPath;
    
    // Strategy 2: Search by file name (last resort)
    function searchByName(parent, name) {
      var lastMatch = null;
      for (var i = 0; i < parent.children.numItems; i++) {
        var child = parent.children[i];
        if (child.type === 1) {
          var found = searchByName(child, name);
          if (found) lastMatch = found;
        } else if (String(child.name || '').toLowerCase() === name) {
          lastMatch = child;
        }
      }
      return lastMatch;
    }
    
    return searchByName(app.project.rootItem, targetNameNorm);
  } catch(e) {
    return null;
  }
}

// ============================================================
// 3. Import file - fixed for PPro 25+ and proper path handling
// ============================================================
function importFile(filePath) {
  try {
    if (!filePath) return safeJSON({ success: false, error: 'No file path' });
    var file = new File(filePath);
    if (!file.exists) {
      // Try alternative path format（仅 Windows：正斜杠转反斜杠；macOS 保持 POSIX 路径）
      var isWindows = ($.os || '').indexOf('Windows') >= 0;
      var altPath = isWindows ? filePath.replace(/\//g, '\\') : filePath;
      file = new File(altPath);
      if (!file.exists) return safeJSON({ success: false, error: 'File not found: ' + filePath });
    }
    
    // Check if already in project
    var existing = findProjectItem(file.fsName);
    if (existing) return safeJSON({ success: true, projectItem: existing.name, alreadyExists: true });
    
    // Import using project's importFiles with proper settings
    try {
      app.project.importFiles([file.fsName], false, app.project.getInsertionBin(), false);
    } catch(importErr) {
      // Fallback: try without specifying bin
      try {
        app.project.importFiles([file.fsName], false, app.project.rootItem, false);
      } catch(e2) {
        // Last fallback: try with File object directly
        try {
          app.project.importFiles([file], false, app.project.getInsertionBin(), false);
        } catch(e3) {
          return safeJSON({ success: false, error: 'Import failed: ' + importErr.toString() });
        }
      }
    }
    
    // Wait for import to complete
    $.sleep(1000);
    var imported = null;
    for (var retry = 0; retry < 10; retry++) {
      imported = findProjectItem(file.fsName);
      if (imported) break;
      $.sleep(1500);
    }
    
    if (imported) {
      return safeJSON({ success: true, projectItem: imported.name });
    }
    
    // Even if auto-find failed, the file should be in the project
    return safeJSON({ success: true, projectItem: file.name, note: 'Imported to project bin (auto-find timed out)' });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.importFile = importFile;

// ============================================================
// 4. Insert/replace media in timeline
// ============================================================
function replaceOrInsertMedia(filePath) {
  try {
    var importResult = importFile(filePath);
    
    var seq = app.project.activeSequence;
    if (!seq) {
      return safeJSON({ success: true, action: 'imported_only', message: 'Imported to project panel. Please open a sequence first.' });
    }
    try { var parsed = JSON.parse(importResult); } catch(e) {
      return safeJSON({ success: false, error: 'Import parse error' });
    }
    
    var projectItem = null;
    for (var retry = 0; retry < 5; retry++) {
      projectItem = findProjectItem(filePath);
      if (projectItem) break;
      $.sleep(1000);
    }
    
    if (!projectItem) {
      // Try with platform-native path（仅 Windows 需要反斜杠变体，macOS 保持 POSIX）
      var isWindows = ($.os || '').indexOf('Windows') >= 0;
      var nativePath = isWindows ? filePath.replace(/\//g, '\\') : filePath;
      for (var retry2 = 0; retry2 < 5; retry2++) {
        projectItem = findProjectItem(nativePath);
        if (projectItem) break;
        $.sleep(1000);
      }
    }
    
    if (!projectItem) {
      return safeJSON({ success: true, action: 'imported_only', message: 'Imported to project bin. Please drag to timeline manually.' });
    }
    
    var playhead = seq.getPlayerPosition();
    var playheadSec = playhead.seconds;
    
    var selectedClip = null;
    var selectedTrack = -1;
    
    try {
      var selection = seq.getSelection();
      if (selection && selection.length > 0) selectedClip = selection[0];
    } catch(e) {}
    
    if (!selectedClip) {
      for (var t = seq.videoTracks.numTracks - 1; t >= 0; t--) {
        try {
          var track = seq.videoTracks[t];
          for (var c = 0; c < track.clips.numItems; c++) {
            var clip = track.clips[c];
            if (playheadSec >= clip.start.seconds && playheadSec < clip.end.seconds) {
              selectedClip = clip; selectedTrack = t; break;
            }
          }
        } catch(e) {}
        if (selectedClip) break;
      }
    } else {
      for (var t2 = 0; t2 < seq.videoTracks.numTracks; t2++) {
        try {
          for (var c2 = 0; c2 < seq.videoTracks[t2].clips.numItems; c2++) {
            if (seq.videoTracks[t2].clips[c2] === selectedClip) { selectedTrack = t2; break; }
          }
        } catch(e) {}
        if (selectedTrack >= 0) break;
      }
    }
    
    // Never delete the existing clip before Premiere confirms the new insert.
    var isAudio = /\.(mp3|wav|m4a|aac|flac|ogg)$/i.test(filePath);
    var targetTime = selectedClip ? selectedClip.start : playhead;
    var tracks = isAudio ? seq.audioTracks : seq.videoTracks;
    var targetTrack = null;
    if (tracks && tracks.numTracks > 0) {
      var targetIndex = (!isAudio && selectedTrack >= 0) ? selectedTrack : tracks.numTracks - 1;
      targetTrack = tracks[targetIndex];
    }
    if (targetTrack) {
      try {
        if (selectedClip && typeof targetTrack.overwriteClip === 'function') {
          // PR API 期望 ticks；先 ticks 后 seconds 兜底（兼容旧版本）
          try { targetTrack.overwriteClip(projectItem, secToTicks(targetTime.seconds)); }
          catch(e) { targetTrack.overwriteClip(projectItem, targetTime.seconds); }
          return safeJSON({ success: true, action: 'overwritten', track: selectedTrack, time: targetTime.seconds || 0 });
        }
        if (typeof targetTrack.insertClip === 'function') {
          try { targetTrack.insertClip(projectItem, secToTicks(targetTime.seconds)); }
          catch(e) { targetTrack.insertClip(projectItem, targetTime.seconds); }
          return safeJSON({ success: true, action: 'inserted', time: targetTime.seconds || 0 });
        }
      } catch(insertError) {
        return safeJSON({ success: true, action: 'imported_only', message: 'Imported safely; timeline insert failed: ' + insertError.toString() });
      }
    }
    
    return safeJSON({ success: true, action: 'imported_only', message: 'Imported to bin. Select a clip on timeline first to replace it.' });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.replaceOrInsertMedia = replaceOrInsertMedia;

// ============================================================
// 5. Read PR media - ONLY from Source Monitor with InPoint/OutPoint
// ============================================================
function tcToSeconds(tc) {
  if (tc === null || tc === undefined) return -1;
  if (typeof tc === 'number') return (tc >= 0 && tc < 86400) ? tc : -1;
  if (typeof tc === 'object') {
    try { if (typeof tc.seconds !== 'undefined') return Number(tc.seconds); } catch(e) {}
    try { if (typeof tc.ticks !== 'undefined') return Number(tc.ticks) / 254016000000; } catch(e) {}
    try { tc = tc.toString(); } catch(e) { return -1; }
  }
  var str = String(tc).trim();
  if (!str) return -1;
  str = str.replace(/;/g, ':');
  var parts = str.split(':');
  if (parts.length < 3) {
    var asNum = parseFloat(str);
    return (isFinite(asNum) && asNum >= 0) ? asNum : -1;
  }
  var h = parseInt(parts[0], 10) || 0;
  var m = parseInt(parts[1], 10) || 0;
  var s = parseInt(parts[2], 10) || 0;
  var f = 0;
  if (parts.length > 3) { f = parseInt(parts[3], 10) || 0; }
  else if (parts[2].indexOf('.') >= 0) {
    var ssParts = parts[2].split('.');
    s = parseInt(ssParts[0], 10) || 0;
    f = parseInt(ssParts[1], 10) || 0;
  }
  var fps = 30;
  try { fps = qe.source.getFrameRate() || 30; } catch(e) {}
  if (fps <= 0) fps = 30;
  return h * 3600 + m * 60 + s + f / fps;
}

function getSelectedPRMedia() {
  try {
    var debugInfo = [];
    try { debugInfo.push('PR' + app.version); } catch(e) {}

    // ONLY Source Monitor - no project selection, no bin traversal
    try {
      if (typeof app.enableQE !== 'undefined') app.enableQE();
    } catch(e) {}

    if (typeof qe === 'undefined' || typeof qe.source === 'undefined' || qe.source === null) {
      return safeJSON({ success: false, error: '源监视器未加载媒体素材，请先在源监视器中打开素材' });
    }

    var clip = null;
    try { clip = qe.source.clip; } catch(e) { debugInfo.push('clipErr'); }
    if (!clip) {
      return safeJSON({ success: false, error: '源监视器未加载媒体素材，请先在源监视器中打开素材' });
    }

    // --- Read clip name and path ---
    var clipName = '';
    try { clipName = clip.name || ''; } catch(e) {}
    var clipPath = '';
    try { clipPath = clip.mediaPath || clip.filePath || clip.path || ''; } catch(e) {}
    if (!clipPath) { try { var pi0 = clip.projectItem; if (pi0) clipPath = pi0.getMediaPath(); } catch(e) {} }

    if (!clipPath || !clipName) {
      return safeJSON({ success: false, error: '无法读取源监视器素材路径，请确认素材已保存在本地' });
    }

    // --- Read In/Out points: ProjectItem API FIRST (official PPro API, PR 26+ compatible) ---
    var inRaw = null, outRaw = null;
    var inPointSet = false, outPointSet = false;

    // Method A (PRIORITY): ProjectItem.getInPoint()/getOutPoint() — official PPro ExtendScript API
    // Source Monitor in/out points are stored on the ProjectItem
    var pi = null;
    // Path 1: QE DOM getProjectItem (may not exist in PR 26+)
    try { pi = qe.source.getProjectItem(); } catch(eA) { debugInfo.push('qe.PI:err'); }
    // Path 2: clip.projectItem property
    if (!pi) { try { pi = clip.projectItem; } catch(eB) { debugInfo.push('clip.PI:err'); } }
    // Path 3: Search standard project tree by media path (most reliable, does NOT require QE DOM)
    if (!pi && clipPath) {
      try { pi = findProjectItem(clipPath); if (pi) debugInfo.push('PI:byPath'); } catch(eC) { debugInfo.push('PI:pathErr'); }
    }
    // Path 4: Search project tree by clip name (fallback)
    if (!pi && clipName) {
      try {
        function findPIByName(item, name) {
          if (item.name === name) return item;
          if (item.children && item.children.numItems > 0) {
            for (var ci = 0; ci < item.children.numItems; ci++) {
              var found = findPIByName(item.children[ci], name);
              if (found) return found;
            }
          }
          return null;
        }
        pi = findPIByName(app.project.rootItem, clipName);
        if (pi) debugInfo.push('PI:byName');
      } catch(eD) { debugInfo.push('PI:nameErr'); }
    }
    if (pi) {
      debugInfo.push('PI:' + pi.name);
      try {
        var ipt = pi.getInPoint();
        if (ipt !== null && ipt !== undefined) { inRaw = ipt; inPointSet = true; debugInfo.push('A.inOK'); }
      } catch(eE) { debugInfo.push('A.inErr'); }
      try {
        var opt = pi.getOutPoint();
        if (opt !== null && opt !== undefined) { outRaw = opt; outPointSet = true; debugInfo.push('A.outOK'); }
      } catch(eF) { debugInfo.push('A.outErr'); }
    } else {
      debugInfo.push('no-PI');
    }

    // Method B: clip.getInPoint()/getOutPoint() — QE, some versions
    if (!inPointSet) { try { inRaw = clip.getInPoint(); if (inRaw !== null && inRaw !== undefined) { inPointSet = true; debugInfo.push('B.inOK'); } } catch(e) { debugInfo.push('B.inErr'); } }
    if (!outPointSet) { try { outRaw = clip.getOutPoint(); if (outRaw !== null && outRaw !== undefined) { outPointSet = true; debugInfo.push('B.outOK'); } } catch(e) { debugInfo.push('B.outErr'); } }

    // Method C: clip.inPoint / clip.outPoint as properties
    if (!inPointSet) { try { inRaw = clip.inPoint; if (inRaw !== null && inRaw !== undefined) { inPointSet = true; debugInfo.push('C.inOK'); } } catch(e) {} }
    if (!outPointSet) { try { outRaw = clip.outPoint; if (outRaw !== null && outRaw !== undefined) { outPointSet = true; debugInfo.push('C.outOK'); } } catch(e) {} }

    // Method D: qe.source.getInPoint()/getOutPoint() — older QE
    if (!inPointSet) { try { inRaw = qe.source.getInPoint(); if (inRaw !== null && inRaw !== undefined) { inPointSet = true; debugInfo.push('D.inOK'); } } catch(e) { debugInfo.push('D.inErr'); } }
    if (!outPointSet) { try { outRaw = qe.source.getOutPoint(); if (outRaw !== null && outRaw !== undefined) { outPointSet = true; debugInfo.push('D.outOK'); } } catch(e) { debugInfo.push('D.outErr'); } }

    // Method E: qe.source.inPoint / outPoint as properties
    if (!inPointSet) { try { inRaw = qe.source.inPoint; if (inRaw !== null && inRaw !== undefined) { inPointSet = true; debugInfo.push('E.inOK'); } } catch(e) {} }
    if (!outPointSet) { try { outRaw = qe.source.outPoint; if (outRaw !== null && outRaw !== undefined) { outPointSet = true; debugInfo.push('E.outOK'); } } catch(e) {} }

    // Method F: Enumerate clip properties to find in/out related fields
    if (!inPointSet || !outPointSet) {
      var allProps = [];
      try { for (var ck in clip) { allProps.push(ck); } } catch(e) {}
      var ioProps = allProps.filter(function(name) { return /in|out|point|mark/i.test(name); });
      if (ioProps.length > 0) debugInfo.push('F.props=' + ioProps.slice(0, 6).join(','));
      for (var ip = 0; ip < ioProps.length; ip++) {
        var shortName = ioProps[ip];
        try {
          var val = clip[shortName];
          if (val !== null && val !== undefined && val !== '') {
            if (!inPointSet && /^in/i.test(shortName)) { inRaw = val; inPointSet = true; debugInfo.push('F.in=' + val); }
            if (!outPointSet && /^out/i.test(shortName)) { outRaw = val; outPointSet = true; debugInfo.push('F.out=' + val); }
          }
        } catch(e) {}
      }
    }

    // Convert to seconds — 0 is valid (video start)
    var inSec = -1, outSec = -1;
    if (inPointSet) inSec = tcToSeconds(inRaw);
    if (outPointSet) outSec = tcToSeconds(outRaw);
    if (inPointSet && inSec < 0) inPointSet = false;
    if (outPointSet && outSec < 0) outPointSet = false;

    debugInfo.push('inSec=' + inSec + ' outSec=' + outSec);

    // --- Require In/Out points to be set ---
    // Keep debug SHORT to avoid message truncation — only show last 8 entries
    if (!inPointSet && !outPointSet) {
      var dbg = debugInfo.slice(-8).join(' | ');
      return safeJSON({ success: false, error: '请先在源监视器标记素材入点、出点（调试: ' + dbg + '）' });
    }
    if (!inPointSet) {
      return safeJSON({ success: false, error: '请先在源监视器标记素材入点（调试: ' + debugInfo.slice(-6).join(' | ') + '）' });
    }
    if (!outPointSet) {
      return safeJSON({ success: false, error: '请先在源监视器标记素材出点（调试: ' + debugInfo.slice(-6).join(' | ') + '）' });
    }
    if (inSec >= outSec) {
      return safeJSON({ success: false, error: '入点必须早于出点（入点=' + inSec + 's 出点=' + outSec + 's）' });
    }

    // --- Build result ---
    var fullPath = clipPath.replace(/\/+/g, '/');
    if (fullPath.indexOf(clipName) === -1) {
      fullPath = fullPath.replace(/\/+$/, '') + '/' + clipName;
    }
    var typ = 'video';
    if (clipName.match(/\.(jpg|jpeg|png|gif|bmp|webp|tif|tiff)$/i)) typ = 'image';
    else if (clipName.match(/\.(mp3|wav|aac|flac|ogg|wma|m4a)$/i)) typ = 'audio';

    return safeJSON({
      success: true,
      items: [{ name: clipName, path: fullPath, type: typ }],
      inPoint: inSec,
      outPoint: outSec,
      inPointTC: String(inRaw),
      outPointTC: String(outRaw)
    });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.getSelectedPRMedia = getSelectedPRMedia;

// ============================================================
// 6. List ALL project media (recursive bin traversal)
// ============================================================
function listAllProjectMedia() {
  try {
    var items = [];
    function walk(parent) {
      if (!parent || !parent.children) return;
      var n = 0;
      try { n = parent.children.numItems; } catch(e) { return; }
      for (var i = 0; i < n; i++) {
        var child = null;
        try { child = parent.children[i]; } catch(e) { continue; }
        if (!child) continue;
        var isBin = false;
        try { isBin = child.type === 1 || String(child.typeName || '').toLowerCase() === 'bin' || (child.children && child.children.numItems !== undefined && !child.getMediaPath); } catch(e) {}
        if (isBin && child.children) { try { walk(child); } catch(e) {} continue; }
        var path = '';
        try { path = child.getMediaPath() || ''; } catch(e) {}
        var name = '';
        try { name = child.name || ''; } catch(e) {}
        if (!path) continue;
        try {
        var hasVideo = false, hasAudio = false, duration = 0;
        try { hasVideo = child.hasVideo; } catch(e) {}
        try { hasAudio = child.hasAudio; } catch(e) {}
        try {
          var d = child.getDuration ? child.getDuration() : null;
          if (d && typeof d.seconds !== 'undefined') duration = Number(d.seconds) || 0;
          else if (typeof d === 'number') duration = d;
        } catch(e) {}
        var ext = (name.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || '';
        var type = 'other';
        if (/^(mp4|mov|avi|webm|mkv|mts|m2ts|mpg|mpeg|wmv|flv)$/i.test(ext)) type = 'video';
        else if (/^(jpg|jpeg|png|gif|bmp|webp|tif|tiff)$/i.test(ext)) type = 'image';
        else if (/^(mp3|wav|aac|flac|ogg|wma|m4a)$/i.test(ext)) type = 'audio';
        if (type === 'other' && (hasVideo || hasAudio)) type = hasVideo ? 'video' : 'audio';
        if (type === 'other') continue;
        items.push({ name: name, path: path, type: type, duration: duration });
        } catch(e) { continue; }
      }
    }
    if (!app.project || !app.project.rootItem) return safeJSON({ success: false, error: '请先打开一个PR项目' });
    walk(app.project.rootItem);
    return safeJSON({ success: true, items: items, count: items.length });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.listAllProjectMedia = listAllProjectMedia;

// ============================================================
// 2b. Playhead position in seconds (for single-clip targeting)
// ============================================================
function getPlayheadSeconds() {
  try {
    var seq = app.project.activeSequence;
    if (!seq) return safeJSON({ success: false, error: '请先打开一个序列' });
    var t = 0;
    try { t = seq.getPlayerPosition().seconds; } catch(e) {
      try { t = seq.playerPosition.seconds; } catch(e2) {}
    }
    return safeJSON({ success: true, time: t });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.getPlayheadSeconds = getPlayheadSeconds;

// ============================================================
// 7. List timeline clips (track, index, name, time range, source)
// ============================================================
function listTimelineClips() {
  try {
    var seq = app.project.activeSequence;
    if (!seq) return safeJSON({ success: false, error: '请先打开一个序列' });
    var clips = [];
    for (var t = 0; t < seq.videoTracks.numTracks; t++) {
      var track = seq.videoTracks[t];
      for (var c = 0; c < track.clips.numItems; c++) {
        var clip = track.clips[c];
        var start = 0, end = 0, name = '', sourcePath = '';
        try { start = clip.start.seconds; } catch(e) {}
        try { end = clip.end.seconds; } catch(e) {}
        try { name = clip.name || ''; } catch(e) {}
        try {
          var pi = clip.projectItem;
          if (pi) sourcePath = pi.getMediaPath() || '';
        } catch(e) {}
        if (!sourcePath) { try { sourcePath = clip.projectItem && clip.projectItem.name || ''; } catch(e2) {} }
        clips.push({ track: t, clip: c, name: name, start: start, end: end, sourcePath: sourcePath });
      }
    }
    return safeJSON({ success: true, clips: clips, count: clips.length });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.listTimelineClips = listTimelineClips;

// ============================================================
// 2c. 按源文件路径匹配时间线片段（源监视器入出点 → 调色目标片段定位）
// 返回片段在时间线中的 track/clip/name/start/end；用入出点范围进一步限定命中
// ============================================================
function findTimelineClipByPath(sourcePathArg, inSecArg, outSecArg) {
  try {
    var sourcePath = String(sourcePathArg || '');
    if (!sourcePath) return safeJSON({ success: false, error: '缺少素材路径' });
    var seq = app.project.activeSequence;
    if (!seq) return safeJSON({ success: false, error: '请先打开一个序列' });
    var isWin = ($.os || '').indexOf('Windows') >= 0;
    function normPath(p) {
      var s = String(p || '');
      try { s = decodeURI(s); } catch(_) {}
      if (isWin) s = s.replace(/\//g, '\\');
      return s.toLowerCase();
    }
    var srcNorm = normPath(sourcePath);
    var inSec = Number(inSecArg); if (!isFinite(inSec)) inSec = -1;
    var outSec = Number(outSecArg); if (!isFinite(outSec)) outSec = -1;
    for (var t = 0; t < seq.videoTracks.numTracks; t++) {
      var track = seq.videoTracks[t];
      for (var c = 0; c < track.clips.numItems; c++) {
        var clip = track.clips[c];
        var mp = '';
        try { var pi = clip.projectItem; if (pi) mp = pi.getMediaPath() || ''; } catch(_) {}
        if (!mp) { try { mp = clip.name || ''; } catch(_) {} }
        if (!mp) continue;
        if (srcNorm === normPath(mp) || srcNorm.indexOf(normPath(mp)) >= 0 || normPath(mp).indexOf(srcNorm) >= 0) {
          var start = 0, end = 0;
          try { start = clip.start.seconds; } catch(_) {}
          try { end = clip.end.seconds; } catch(_) {}
          return safeJSON({ success: true, match: { track: t, clip: c, name: clip.name || '', start: start, end: end },
            inPoint: inSec >= 0 ? inSec : undefined, outPoint: outSec >= 0 ? outSec : undefined });
        }
      }
    }
    return safeJSON({ success: false, error: '时间线中没有与源素材匹配的片段（请先在时间线使用该素材后再读取）' });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.findTimelineClipByPath = findTimelineClipByPath;

// ============================================================
// 8. Build timeline from an AI edit plan
// plan: [{sourcePath, inPoint, outPoint, track, type, name}]
// ============================================================
function secToTicks(sec) {
  return Math.round(Number(sec) * 254016000000);
}

function buildAutoEditTimeline(planJson) {
  try {
    var plan = JSON.parse(planJson || '[]');
    if (!plan || !plan.length) return safeJSON({ success: false, error: '剪辑计划为空' });
    var seq = app.project.activeSequence;
    var created = false;
    if (!seq) {
      try {
        seq = app.project.newSequence('AI自动剪辑_' + new Date().getTime(), '');
        created = true;
      } catch(e) {}
    }
    if (!seq) return safeJSON({ success: false, error: '请先打开一个序列（自动新建序列失败）' });

    var cursor = seq.getPlayerPosition().seconds;
    var results = [];
    for (var i = 0; i < plan.length; i++) {
      var item = plan[i] || {};
      var sourcePath = item.sourcePath || item.path || item.url || '';
      var inSec = Math.max(0, Number(item.inPoint) || 0);
      var outSec = Math.max(0, Number(item.outPoint) || 0);
      var trackIndex = Math.max(0, Math.round(Number(item.track) || 0));
      if (!sourcePath) { results.push({ index: i, success: false, error: '缺少素材路径' }); continue; }

      var isAudio = item.type === 'audio' || /\.(mp3|wav|m4a|aac|flac|ogg)$/i.test(sourcePath);
      var tracks = isAudio ? seq.audioTracks : seq.videoTracks;
      while (tracks.numTracks <= trackIndex) {
        try { tracks.addTracks(1); } catch(e) { break; }
      }
      if (tracks.numTracks <= trackIndex) { results.push({ index: i, success: false, error: '无法创建轨道' }); continue; }
      var track = tracks[trackIndex];

      var importResult = importFile(sourcePath);
      var projectItem = findProjectItem(sourcePath);
      if (!projectItem) { results.push({ index: i, success: false, error: '素材未导入' }); continue; }

      var inserted = false;
      // PR API：insertClip/overwriteClip 的时间参数为 ticks 或 Time 对象；先传正确的 ticks，旧版本兼容秒数兜底
      try { track.insertClip(projectItem, secToTicks(cursor)); inserted = true; } catch(e1) {
        try { track.insertClip(projectItem, cursor); inserted = true; } catch(e1b) {}
      }
      if (!inserted) {
        try { track.overwriteClip(projectItem, secToTicks(cursor)); inserted = true; } catch(e2) {
          try { track.overwriteClip(projectItem, cursor); inserted = true; } catch(e2b) {}
        }
      }
      if (!inserted) { results.push({ index: i, success: false, error: '时间线插入失败' }); continue; }

      // Trim the last inserted clip to in/out points
      if (outSec > inSec) {
        var lastClip = track.clips[track.clips.numItems - 1];
        try { lastClip.setInPoint(secToTicks(inSec)); } catch(e3) {
          try { lastClip.setInPoint(inSec); } catch(e4) {}
        }
        try { lastClip.setOutPoint(secToTicks(outSec)); } catch(e5) {
          try { lastClip.setOutPoint(outSec); } catch(e6) {}
        }
        cursor += (outSec - inSec);
      } else {
        var addedDuration = 0;
        try {
          var lc = track.clips[track.clips.numItems - 1];
          if (lc && lc.end && lc.start) addedDuration = lc.end.seconds - lc.start.seconds;
        } catch(e7) {}
        cursor += (addedDuration > 0 ? addedDuration : 2);
      }
      results.push({ index: i, success: true, name: item.name || ('片段' + (i + 1)) });
    }
    return safeJSON({ success: true, results: results, created: created });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.buildAutoEditTimeline = buildAutoEditTimeline;

// ============================================================
// 9. Lumetri panel color grading helpers
// ============================================================
function findLumetriComponent(trackItem) {
  try {
    for (var i = 0; i < trackItem.components.numItems; i++) {
      var comp = trackItem.components[i];
      var mn = '', dn = '';
      try { mn = comp.matchName || ''; } catch(e) {}
      try { dn = comp.displayName || ''; } catch(e) {}
      if (/Lumetri/i.test(mn) || /Lumetri/i.test(dn)) return comp;
    }
  } catch(e) {}
  return null;
}

// Lumetri 属性在不同 PR 版本/语言下可能嵌套在分组内（Basic Correction / Creative 等），
// 且 name/displayName 存在中英文差异。这里做深度优先递归查找（限深 5），
// 按别名集合（英文 + 中文）匹配，兼容中文版 Premiere。
var LUMETRI_PARAM_ALIASES = {
  'Temperature': ['Temperature', '色温', 'Temp'],
  'Tint': ['Tint', '色调'],
  'Exposure': ['Exposure', '曝光'],
  'Contrast': ['Contrast', '对比度'],
  'Highlights': ['Highlights', '高光'],
  'Shadows': ['Shadows', '阴影'],
  'Whites': ['Whites', '白色'],
  'Blacks': ['Blacks', '黑色'],
  'Saturation': ['Saturation', '饱和度'],
  'Vibrance': ['Vibrance', '自然饱和度'],
  'Sharpen': ['Sharpen', '清晰度', '锐化'],
  'Faded Film': ['Faded Film', '褪色胶片'],
  // 色轮（Color Wheels）：阴影/中间调/高光三个控制点
  'Shadow Color Wheel': ['Shadow Color Wheel', '阴影色轮', 'Shadow Wheel', 'Color Wheels Shadow'],
  'Midtone Color Wheel': ['Midtone Color Wheel', '中间调色轮', 'Midtone Wheel', 'Color Wheels Midtone'],
  'Highlight Color Wheel': ['Highlight Color Wheel', '高光色轮', 'Highlight Wheel', 'Color Wheels Highlight'],
  // 晕影（Vignette）
  'Vignette Amount': ['Vignette Amount', '晕影数量', 'Vignette', 'Vignette Amount Value'],
  'Vignette Midpoint': ['Vignette Midpoint', '晕影中点', 'Vignette Midpoint Value']
};

function findLumetriParamInCollection(col, names, depth) {
  if (depth > 8 || !col || typeof col.numItems !== 'number') return null;
  for (var i = 0; i < col.numItems; i++) {
    var prop = null;
    try { prop = col[i]; } catch(e) {}
    if (!prop) continue;
    var pn = '', dn = '', mn = '', pk = '';
    try { pn = String(prop.name || ''); } catch(e) {}
    try { dn = String(prop.displayName || ''); } catch(e) {}
    try { mn = String(prop.matchName || ''); } catch(e) {}
    try { pk = String(prop.key || ''); } catch(e) {}
    // 多字段合并匹配：name / displayName / matchName / key，兼容中英文与不同 PR 版本
    var haystack = (pn + '|' + dn + '|' + mn + '|' + pk).toLowerCase();
    for (var n = 0; n < names.length; n++) {
      var want = String(names[n] || '').toLowerCase();
      if (!want) continue;
      // 精确匹配优先；其次子串匹配（部分版本参数名带前缀/后缀，如 ADBE Lumetri-0001）
      if (haystack === want || haystack.indexOf(want) >= 0) return prop;
    }
    // 分组属性：递归进入子属性集合（Basic Correction / Creative 等）
    var subs = null;
    try { subs = prop.properties; } catch(e) {}
    var hit = findLumetriParamInCollection(subs, names, depth + 1);
    if (hit) return hit;
  }
  return null;
}

// Lumetri 参数取值范围（PR 会拒绝/钳制超范围值，先本地钳制避免应用失败）
var LUMETRI_PARAM_RANGES = {
  'Temperature': [-100, 100], 'Tint': [-100, 100], 'Exposure': [-5, 5],
  'Contrast': [-100, 100], 'Highlights': [-100, 100], 'Shadows': [-100, 100],
  'Whites': [-100, 100], 'Blacks': [-100, 100], 'Saturation': [0, 200],
  'Vibrance': [-100, 100], 'Sharpen': [0, 100], 'Faded Film': [0, 100],
  'Shadow Color Wheel': null, 'Midtone Color Wheel': null, 'Highlight Color Wheel': null,
  'Vignette Amount': [-100, 100], 'Vignette Midpoint': [0, 100]
};

// 颜色参数（色轮）：把 '#rrggbb' 转成 PR 色轮可接受的写入形式。
// PR 色轮控制点通常接受 3 分量（R/G/B 0~1 或 0~255）数组/字符串；
// 这里尝试多种形式，任一种被接受即返回 true。
function setLumetriColorParam(component, paramName, value) {
  try {
    if (typeof value !== 'string') return false;
    var m = String(value).match(/^#([0-9a-fA-F]{6})$/);
    if (!m) return false;
    var r = parseInt(m[1].substring(0, 2), 16);
    var g = parseInt(m[1].substring(2, 4), 16);
    var b = parseInt(m[1].substring(4, 6), 16);
    var names = LUMETRI_PARAM_ALIASES[paramName] || [paramName];
    var hit = findLumetriParamInCollection(component.properties, names, 0);
    if (!hit) return false;
    // 多策略：数组(0~255) / 数组(0~1) / 逗号字符串 / 冒号字符串 / 对象字段
    var attempts = [
      [r, g, b],
      [r / 255, g / 255, b / 255],
      r + ',' + g + ',' + b,
      r / 255 + ',' + g / 255 + ',' + b / 255,
      r + ':' + g + ':' + b
    ];
    for (var a = 0; a < attempts.length; a++) {
      try { hit.setValue(attempts[a]); return true; } catch(e1) {}
    }
    try { hit.value = [r, g, b]; return true; } catch(e2) {}
    try { hit.value = r + ',' + g + ',' + b; return true; } catch(e3) {}
    return false;
  } catch(e) { return false; }
}

// Lumetri 参数写入（带读回验证）：返回 true 表示写入且读回一致；
// false 表示写入失败或读回不一致（调用方据此报错，避免“显示成功但画面无变化”）。
function setLumetriParam(component, paramName, value) {
  try {
    var names = LUMETRI_PARAM_ALIASES[paramName] || [paramName];
    var hit = findLumetriParamInCollection(component.properties, names, 0);
    if (hit) {
      var num = Number(value);
      if (!isFinite(num)) return false;
      // 值域钳制，避免 PR 因超范围拒绝写入
      var range = LUMETRI_PARAM_RANGES[paramName];
      if (range && range.length === 2) num = Math.max(range[0], Math.min(range[1], num));
      var written = false;
      // 多策略写入：setValue(number) → setValue(string) → 直接赋值 value
      try { hit.setValue(num); written = true; } catch(e1) {
        try { hit.setValue(String(num)); written = true; } catch(e2) {
          try { hit.value = num; written = true; } catch(e3) {
            try { hit.value = String(num); written = true; } catch(e4) {}
          }
        }
      }
      if (!written) return false;
      // 读回验证：部分 PR 版本 setValue 不抛错但值没写进去（参数锁定/渲染未刷新），
      // 这里读回对比，误差容忍 0.5（PR 内部可能做微小量化）。
      try {
        var readback = null;
        try { readback = Number(hit.getValue()); } catch(r1) {}
        if (readback == null || !isFinite(readback)) { try { readback = Number(hit.value); } catch(r2) {} }
        if (isFinite(readback) && Math.abs(readback - num) > 0.5) return false;
      } catch(r3) {}
      return true;
    }
  } catch(e) {}
  return false;
}

// Lumetri 效果的添加名称（matchName 为主，兼容不同 PR 版本/语言）
var LUMETRI_ADD_NAMES = ['ADBE Lumetri', 'Lumetri Color', 'Lumetri', 'Lumetri\u989c\u8272', 'Lumetri \u989c\u8272'];

// 片段定位：优先按源路径匹配，其次按时间（秒）匹配所在片段，再次按 轨道/序号（含 1-based 容错）。
// 返回 { clip, track, index } 或 null。
function findTargetClip(seq, item) {
  var t, c;
  // 0) 源文件路径定位（源监视器素材场景）：按 projectItem 媒体路径匹配时间线片段
  var srcPath = String(item && (item.sourcePath || item.path || item.url) || '').trim();
  if (srcPath) {
    var srcNorm = '';
    try {
      var isWin = ($.os || '').indexOf('Windows') >= 0;
      srcNorm = srcPath.toLowerCase().replace(/\\/g, '/');
      try { srcNorm = decodeURI(srcNorm); } catch(de) {}
      if (isWin && !/^[a-z]:\//.test(srcNorm) && !/^\//.test(srcNorm)) srcNorm = '/' + srcNorm;
    } catch(e0) {}
    for (t = 0; t < seq.videoTracks.numTracks; t++) {
      var trP = seq.videoTracks[t];
      for (c = 0; c < trP.clips.numItems; c++) {
        try {
          var pi = trP.clips[c].projectItem;
          var mp = '';
          try { if (pi) mp = pi.getMediaPath() || ''; } catch(mpe) {}
          if (!mp) { try { mp = trP.clips[c].name || ''; } catch(mpe2) {} }
          if (mp) {
            var mpNorm = mp.toLowerCase().replace(/\\/g, '/');
            try { mpNorm = decodeURI(mpNorm); } catch(de2) {}
            if (mpNorm === srcNorm || mpNorm.indexOf(srcNorm) >= 0 || srcNorm.indexOf(mpNorm) >= 0) {
              return { clip: trP.clips[c], track: t, index: c };
            }
          }
        } catch(errP) {}
      }
    }
    // 0b) 源路径拿不到（嵌套序列/代理/改名素材）时，退而用项目名匹配
    try {
      var srcBase = srcNorm.split('/').pop();
      if (srcBase.length > 3) {
        for (t = 0; t < seq.videoTracks.numTracks; t++) {
          var trN = seq.videoTracks[t];
          for (c = 0; c < trN.clips.numItems; c++) {
            try {
              var nm = '';
              try { nm = trN.clips[c].name || ''; } catch(nme) {}
              if (nm && String(nm).toLowerCase() === srcBase) return { clip: trN.clips[c], track: t, index: c };
            } catch(errN) {}
          }
        }
      }
    } catch(e0b) {}
  }
  // 1) 时间定位（计划里带 time 时最可靠，不受模型输出的序号基准影响）
  var hasTime = item && item.time !== undefined && item.time !== null && String(item.time).trim() !== '';
  if (hasTime) {
    var byTime = Number(item.time);
    if (isFinite(byTime) && byTime >= 0) {
      // 容差 0.5s：片段边界四舍五入/浮点误差场景也能命中
      for (t = 0; t < seq.videoTracks.numTracks; t++) {
        var trT = seq.videoTracks[t];
        for (c = 0; c < trT.clips.numItems; c++) {
          try {
            var s = trT.clips[c].start.seconds, e = trT.clips[c].end.seconds;
            if (byTime >= s - 0.5 && byTime < e + 0.5) return { clip: trT.clips[c], track: t, index: c };
          } catch(err) {}
        }
      }
    }
  }
  // 2) 轨道/序号定位：AI 模型有时输出 1-based 序号，做 ±1 容错组合
  var trackIndex = Math.max(0, Math.round(Number(item && item.track) || 0));
  var clipIndex = Math.max(0, Math.round(Number(item && item.clip) || 0));
  var candidates = [
    [trackIndex, clipIndex],
    [trackIndex - 1, clipIndex - 1],
    [trackIndex, clipIndex - 1],
    [trackIndex - 1, clipIndex]
  ];
  for (var k = 0; k < candidates.length; k++) {
    var ti = candidates[k][0], ci = candidates[k][1];
    if (ti < 0 || ci < 0) continue;
    try {
      var trk = seq.videoTracks[ti];
      if (trk && trk.clips.numItems > ci && trk.clips[ci]) return { clip: trk.clips[ci], track: ti, index: ci };
    } catch(err2) {}
  }
  return null;
}

// 统计时间线视频片段总数（诊断信息 + 单片段兜底）
function countTimelineVideoClips(seq) {
  var total = 0;
  try {
    for (var t = 0; t < seq.videoTracks.numTracks; t++) total += seq.videoTracks[t].clips.numItems;
  } catch(e) {}
  return total;
}

// 向片段添加 Lumetri 效果：components.add 多名称尝试；部分 PR 版本没有该方法时，
// 再尝试 QE DOM（未公开接口，全部守卫），失败返回 null 由调用方给出明确指引。
function tryComponentsAddLumetri(target, diag) {
  if (!target || !target.components) return null;
  var addFn = null;
  var addNames = ['add', 'addComponent', 'addVideoEffect', 'insertComponent', 'applyComponent', 'addEffect'];
  var i;
  for (i = 0; i < addNames.length && !addFn; i++) {
    try { if (typeof target.components[addNames[i]] === 'function') addFn = target.components[addNames[i]]; } catch(e) {}
  }
  // 部分版本把添加方法直接放在 clip 对象上
  for (i = 0; i < addNames.length && !addFn; i++) {
    try { if (typeof target[addNames[i]] === 'function') addFn = target[addNames[i]]; } catch(e2) {}
  }
  if (!addFn) {
    if (diag) {
      var props = [];
      try { for (var k in target.components) { if (typeof target.components[k] === 'function') props.push(k); } } catch(e3) {}
      diag.push('components.add\u4e0d\u53ef\u7528' + (props.length ? '(' + props.slice(0, 6).join(',') + ')' : ''));
    }
    return null;
  }
  for (var a = 0; a < LUMETRI_ADD_NAMES.length; a++) {
    try {
      var comp = addFn.call(target.components, LUMETRI_ADD_NAMES[a]);
      if (!comp) { try { comp = addFn.call(target, LUMETRI_ADD_NAMES[a]); } catch(e4) {} }
      if (comp) { if (diag) diag.push('add:' + LUMETRI_ADD_NAMES[a]); return comp; }
    } catch(e5) { if (diag) diag.push('add[' + LUMETRI_ADD_NAMES[a] + ']\u5931\u8d25'); }
  }
  return null;
}

// QE DOM 辅助：枚举对象属性（ExtendScript 对宿主对象 for-in 受限，全部 try 守卫）
function qeCollectKeys(obj, out) {
  if (!obj || typeof obj !== 'object') return;
  try { for (var k in obj) { if (out.indexOf(k) < 0) out.push(k); } } catch(e) {}
}

function qeFindSequence(seq, diag) {
  if (typeof qe === 'undefined' || !qe) return null;
  var candidates = [];
  try { if (qe.project && typeof qe.project.getActiveSequence === 'function') candidates.push(qe.project.getActiveSequence()); } catch(e) {}
  try { if (qe.project && typeof qe.project.activeSequence !== 'undefined') candidates.push(qe.project.activeSequence); } catch(e2) {}
  try { if (typeof qe.sequence !== 'undefined') candidates.push(qe.sequence); } catch(e3) {}
  try { if (seq) candidates.push(seq); } catch(e4) {}
  var qeKeys = [];
  qeCollectKeys(qe, qeKeys);
  for (var i = 0; i < qeKeys.length; i++) {
    try {
      var v = qe[qeKeys[i]];
      if (v && typeof v === 'object' && (typeof v.getVideoTrackAt === 'function' || typeof v.numVideoTracks === 'number' || v.videoTracks)) candidates.push(v);
    } catch(e5) {}
  }
  for (var c = 0; c < candidates.length; c++) {
    try { if (candidates[c] && typeof candidates[c].getVideoTrackAt === 'function') return candidates[c]; } catch(e6) {}
  }
  if (diag) diag.push('qe:\u65e0\u5e8f\u5217\u5bf9\u8c61');
  return null;
}

function qeFindTrack(qeSeq, trackIndex, diag) {
  var track = null;
  try { track = qeSeq.getVideoTrackAt(trackIndex); } catch(e) {}
  if (!track) { try { track = qeSeq.videoTracks && qeSeq.videoTracks[trackIndex]; } catch(e2) {} }
  if (!track) { try { track = qeSeq[trackIndex]; } catch(e3) {} }
  if (!track) { if (diag) diag.push('qe:\u65e0\u8f68\u9053'); return null; }
  return track;
}

function qeFindItem(qeTrack, clipIndex, target, diag) {
  var item = null;
  var getters = ['getItemAt', 'getClipAt', 'itemAt', 'getTrackItemAt'];
  for (var g = 0; g < getters.length && !item; g++) {
    try { if (typeof qeTrack[getters[g]] === 'function') item = qeTrack[getters[g]](clipIndex); } catch(e) {}
  }
  if (!item) { try { item = qeTrack[clipIndex]; } catch(e2) {} }
  if (!item) { try { item = qeTrack.items && qeTrack.items[clipIndex]; } catch(e3) {} }
  if (!item && target) {
    // 枚举轨道上的集合属性，逐个尝试取片段（数量很少，安全）
    var keys = [];
    qeCollectKeys(qeTrack, keys);
    for (var k = 0; k < keys.length && !item; k++) {
      var v = null;
      try { v = qeTrack[keys[k]]; } catch(e4) { continue; }
      if (!v || typeof v !== 'object') continue;
      var isArrayLike = false;
      try { isArrayLike = (typeof v.numItems === 'number') || (typeof v.length === 'number') || (typeof v.getItemAt === 'function'); } catch(e5) {}
      if (!isArrayLike) continue;
      var n = 0;
      try { n = v.numItems !== undefined ? v.numItems : v.length; } catch(e6) {}
      for (var j = 0; j < n && !item; j++) {
        try { item = v[j]; if (!item && typeof v.getItemAt === 'function') item = v.getItemAt(j); } catch(e7) {}
      }
    }
  }
  if (!item) { if (diag) diag.push('qe:\u65e0\u6cd5\u5b9a\u4f4d\u7247\u6bb5'); return null; }
  return item;
}

// 官方 QE DOM 用法（Adobe Film Impact Dashboard / PR 2026 同款）：
// qe.project.getVideoEffectByName(matchName, true) 拿到效果对象，
// 再用 qeItem.addVideoEffect(effectObject) 添加到片段。
// 兼容两种查找模式：第二参 true = 按 matchName（如 ADBE Lumetri），
// false = 按显示名（如 Lumetri Color / Lumetri 颜色）。
function qeAddComponent(qeItem, qeProject, diag) {
  var a, m;
  if (qeProject && typeof qeProject.getVideoEffectByName === 'function' && qeItem && typeof qeItem.addVideoEffect === 'function') {
    for (a = 0; a < LUMETRI_ADD_NAMES.length; a++) {
      for (m = 0; m < 2; m++) {
        try {
          var effectObj = qeProject.getVideoEffectByName(LUMETRI_ADD_NAMES[a], m === 0);
          if (effectObj && effectObj.name && String(effectObj.name).length > 0) {
            var added = qeItem.addVideoEffect(effectObj);
            if (added) { if (diag) diag.push('qe:addVideoEffect:' + LUMETRI_ADD_NAMES[a] + (m === 0 ? ':matchName' : ':name')); return true; }
          }
        } catch(e) {}
      }
    }
  }
  // 兜底：字符串形式的各种添加方法（老版本 PR）
  var names = ['addVideoEffect', 'addComponent', 'add', 'addEffect', 'insertComponent', 'applyComponent'];
  var firstMethod = null;
  for (var n = 0; n < names.length; n++) {
    try {
      if (qeItem && typeof qeItem[names[n]] === 'function') {
        if (!firstMethod) firstMethod = names[n];
        for (a = 0; a < LUMETRI_ADD_NAMES.length; a++) {
          try {
            var added2 = qeItem[names[n]](LUMETRI_ADD_NAMES[a]);
            if (added2) { if (diag) diag.push('qe:' + names[n] + ':' + LUMETRI_ADD_NAMES[a]); return true; }
          } catch(e2) {}
        }
      }
    } catch(e3) {}
  }
  if (diag) diag.push('qe:\u65e0\u6dfb\u52a0\u65b9\u6cd5' + (firstMethod ? '(' + firstMethod + ')' : ''));
  return false;
}

function tryQEAddLumetri(trackIndex, clipIndex, target, diag) {
  try {
    if (typeof app.enableQE !== 'function') return null;
    app.enableQE();
  } catch(e) { return null; }
  try {
    if (typeof qe === 'undefined' || !qe || !qe.project) { if (diag) diag.push('qe:\u65e0\u9879\u76ee\u5bf9\u8c61'); return null; }
    var qeProject = qe.project;
    var qeSeq = null;
    try { qeSeq = qeProject.getActiveSequence(); } catch(e0) {}
    if (!qeSeq) {
      var seq = app.project.activeSequence;
      qeSeq = qeFindSequence(seq, diag);
    }
    if (!qeSeq) return null;
    var qeTrack = qeFindTrack(qeSeq, trackIndex, diag);
    if (!qeTrack) return null;
    var qeItem = qeFindItem(qeTrack, clipIndex, target, diag);
    if (!qeItem) { if (diag) diag.push('qe:\u65e0\u6cd5\u5b9a\u4f4d\u7247\u6bb5'); return null; }
    qeAddComponent(qeItem, qeProject, diag);
  } catch(eOuter) { if (diag) diag.push('qe:\u5f02\u5e38'); }
  return null;
}

// 拿到片段的 Lumetri 组件：已存在直接返回；否则多策略添加后重新查找
function ensureLumetriComponent(target, trackIndex, clipIndex, diag) {
  var comp = findLumetriComponent(target);
  if (comp) return comp;
  comp = tryComponentsAddLumetri(target, diag);
  if (comp) return comp;
  comp = findLumetriComponent(target);
  if (comp) return comp;
  tryQEAddLumetri(trackIndex, clipIndex, target, diag);
  return findLumetriComponent(target);
}

// plan: [{track, clip, time, scene, temperature, tint, exposure, contrast,
//         highlights, shadows, saturation, vibrance, note}]
function applyLumetriGrade(planJson) {
  try {
    var plan = JSON.parse(planJson || '[]');
    var seq = app.project.activeSequence;
    if (!seq) return safeJSON({ success: false, error: '请先打开一个序列' });
    if (!plan || !plan.length) return safeJSON({ success: false, error: '调色计划为空' });
    var totalClips = countTimelineVideoClips(seq);
    // 顺序兜底：按时间线轨道/片段顺序收集全部视频片段。
    // 当计划条数与时间线片段数一致、且源路径/时间/序号都定位不到时，
    // 按计划顺序逐一映射到时间线片段（对应「读取当前时间线」生成的批量计划）。
    var orderedClips = [];
    for (var ot = 0; ot < seq.videoTracks.numTracks; ot++) {
      var otr = seq.videoTracks[ot];
      for (var oc = 0; oc < otr.clips.numItems; oc++) {
        try { orderedClips.push({ clip: otr.clips[oc], track: ot, index: oc }); } catch (e) {}
      }
    }
    var results = [];
    for (var i = 0; i < plan.length; i++) {
      var item = plan[i] || {};
      var diag = [];
      var found = null;
      try { found = findTargetClip(seq, item); } catch(e) {}
      // 兜底：计划只有一条且时间线只有一个视频片段时，直接应用，避免模型序号偏差
      if (!found && plan.length === 1 && totalClips === 1) {
        try {
          for (var ft = 0; ft < seq.videoTracks.numTracks && !found; ft++) {
            var ftr = seq.videoTracks[ft];
            if (ftr.clips.numItems > 0 && ftr.clips[0]) found = { clip: ftr.clips[0], track: ft, index: 0 };
          }
        } catch(e2) {}
      }
      // 顺序兜底：插件生成的批量计划（条数 == 时间线片段数）定位失败时按计划索引映射。
      // 仅当计划项显式声明 _batchFallback（批量调色面板生成）才启用，避免误伤其他调用。
      if (!found && item && item._batchFallback === true && plan.length > 1 && plan.length === orderedClips.length && orderedClips[i]) {
        found = orderedClips[i];
      }
      // 单条兜底：单独调色模式（计划 1 条）定位失败时，用播放头时间定位当前片段；
      // 播放头也不在任意片段上时，回退到时间线第一个视频片段（用户已显式读取时间线并选择该模式）。
      if (!found && item && item._singleFallback === true && plan.length === 1) {
        try {
          var ph = 0;
          try { ph = Number(seq.getPlayerPosition().seconds) || 0; } catch(ph1) {
            try { ph = Number(seq.playerPosition.seconds) || 0; } catch(ph2) {}
          }
          for (var ft2 = 0; ft2 < seq.videoTracks.numTracks && !found; ft2++) {
            var ftr2 = seq.videoTracks[ft2];
            for (var fc2 = 0; fc2 < ftr2.clips.numItems; fc2++) {
              try {
                var fs2 = ftr2.clips[fc2].start.seconds, fe2 = ftr2.clips[fc2].end.seconds;
                if (ph >= fs2 - 0.5 && ph < fe2 + 0.5) { found = { clip: ftr2.clips[fc2], track: ft2, index: fc2 }; break; }
              } catch(e) {}
            }
          }
          if (!found) {
            for (var ft3 = 0; ft3 < seq.videoTracks.numTracks && !found; ft3++) {
              var ftr3 = seq.videoTracks[ft3];
              if (ftr3.clips.numItems > 0 && ftr3.clips[0]) found = { clip: ftr3.clips[0], track: ft3, index: 0 };
            }
          }
        } catch(e3) {}
      }
      if (!found) {
        results.push({ index: i, track: item.track, clip: item.clip, success: false, error: '未找到目标片段（时间线共' + totalClips + '个视频片段；计划指向轨道' + (Number(item.track) + 1) + ' #' + item.clip + (item.time !== undefined ? '、' + Number(item.time).toFixed(1) + 's' : '') + '）。请先点「读取当前时间线」再生成计划' });
        continue;
      }
      var target = found.clip;
      var targetTrack = found.track, targetClip = found.index;

      var comp = ensureLumetriComponent(target, targetTrack, targetClip, diag);
      if (!comp) {
        results.push({ index: i, track: targetTrack, clip: targetClip, success: false, error: '无法自动添加Lumetri效果（' + (diag.join('；') || '接口不可用') + '）。请在PR效果面板手动给该片段添加一次「Lumetri Color」效果后重试' });
        continue;
      }

      var params = {
        'Temperature': item.temperature, 'Tint': item.tint, 'Exposure': item.exposure,
        'Contrast': item.contrast, 'Highlights': item.highlights, 'Shadows': item.shadows,
        'Whites': item.whites, 'Blacks': item.blacks, 'Saturation': item.saturation,
        'Vibrance': item.vibrance, 'Sharpen': item.sharpen, 'Faded Film': item.fadedFilm,
        'Shadow Color Wheel': item.shadowColorWheel,
        'Midtone Color Wheel': item.midtoneColorWheel,
        'Highlight Color Wheel': item.highlightColorWheel,
        'Vignette Amount': item.vignetteAmount,
        'Vignette Midpoint': item.vignetteMidpoint
      };
      var applied = 0;
      var failedParams = [];
      // 颜色类参数（色轮）走专用写入；数值类走 setLumetriParam
      var colorParams = ['Shadow Color Wheel', 'Midtone Color Wheel', 'Highlight Color Wheel'];
      for (var p in params) {
        if (params[p] === null || params[p] === undefined || params[p] === '') continue;
        if (colorParams.indexOf(p) >= 0) {
          if (setLumetriColorParam(comp, p, params[p])) applied++;
          else failedParams.push(p);
        } else if (setLumetriParam(comp, p, params[p])) applied++;
        else failedParams.push(p);
      }
      var foundParamNames = [];
      try {
        var __col = comp.properties;
        var __walk = function(c, d) {
          if (!c || typeof c.numItems !== 'number' || d > 3) return;
          for (var i = 0; i < c.numItems; i++) {
            try {
              var p = c[i];
              if (!p) continue;
              var n = '';
              try { n = String(p.displayName || p.name || ''); } catch(e) {}
              if (n && foundParamNames.indexOf(n) < 0) foundParamNames.push(n);
              var subs = null;
              try { subs = p.properties; } catch(e2) {}
              if (subs) __walk(subs, d + 1);
            } catch(e3) {}
          }
        };
        __walk(__col, 0);
      } catch(e4) {}
      if (applied === 0) {
        results.push({ index: i, track: targetTrack, clip: targetClip, success: false, scene: item.scene || '', error: 'Lumetri效果已就位，但调色参数写入失败或未生效（组件内可见参数：' + (foundParamNames.slice(0, 20).join('/') || '无') + '）。请确认片段未被锁定、Lumetri参数未被关键帧锁定，或更新Premiere版本后重试' });
        continue;
      }
      if (failedParams.length) {
        // 部分参数写入失败：仍然算成功（其余参数已生效），但提示用户哪些没写入
        results.push({ index: i, track: targetTrack, clip: targetClip, success: true, scene: item.scene || '', applied: applied, failedParams: failedParams });
        continue;
      }
      results.push({ index: i, track: targetTrack, clip: targetClip, success: true, scene: item.scene || '', applied: applied });
    }
    return safeJSON({ success: true, results: results });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.applyLumetriGrade = applyLumetriGrade;

// ============================================================
// 10. Reset Lumetri grade to defaults for target clips
// plan: [{track, clip, time}] — same targeting as applyLumetriGrade
// ============================================================
function resetLumetriGrade(planJson) {
  try {
    var plan = JSON.parse(planJson || '[]');
    var seq = app.project.activeSequence;
    if (!seq) return safeJSON({ success: false, error: '请先打开一个序列' });
    if (!plan || !plan.length) return safeJSON({ success: false, error: '重置计划为空' });
    var orderedClips = [];
    for (var ot = 0; ot < seq.videoTracks.numTracks; ot++) {
      var otr = seq.videoTracks[ot];
      for (var oc = 0; oc < otr.clips.numItems; oc++) {
        try { orderedClips.push({ clip: otr.clips[oc], track: ot, index: oc }); } catch (e) {}
      }
    }
    var defaults = {
      'Temperature': 0, 'Tint': 0, 'Exposure': 0, 'Contrast': 0,
      'Highlights': 0, 'Shadows': 0, 'Whites': 0, 'Blacks': 0,
      'Saturation': 100, 'Vibrance': 0, 'Sharpen': 0, 'Faded Film': 0
    };
    var results = [];
    for (var i = 0; i < plan.length; i++) {
      var item = plan[i] || {};
      var found = null;
      try { found = findTargetClip(seq, item); } catch(e) {}
      if (!found && item && item._batchFallback === true && plan.length > 1 && plan.length === orderedClips.length && orderedClips[i]) {
        found = orderedClips[i];
      }
      if (!found) { results.push({ index: i, track: item.track, clip: item.clip, success: false, error: '未找到目标片段' }); continue; }
      var target = found.clip;
      var comp = findLumetriComponent(target);
      if (!comp) { results.push({ index: i, success: false, error: '片段没有Lumetri效果（无需重置）' }); continue; }
      var applied = 0;
      for (var p in defaults) {
        if (setLumetriParam(comp, p, defaults[p])) applied++;
      }
      results.push({ index: i, track: found.track, clip: found.index, success: true, applied: applied });
    }
    return safeJSON({ success: true, results: results });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.resetLumetriGrade = resetLumetriGrade;

// ============================================================
// 12. Export current sequence frame to PNG at given time
// opts: {time (seconds), path (output png)}
// Uses Sequence.exportFramePNG (Premiere 2021+), waits for file.
// ============================================================
function exportSequenceFrame(jsonArg) {
  try {
    // 兼容两种传参：对象字面量（evalScript 直接传对象）或 JSON 字符串
    var opts = (typeof jsonArg === 'string') ? JSON.parse(jsonArg || '{}') : (jsonArg || {});
    var seq = app.project.activeSequence;
    if (!seq) return safeJSON({ success: false, error: '请先打开一个序列' });
    if (typeof seq.exportFramePNG !== 'function') return safeJSON({ success: false, error: '当前PR版本不支持帧导出（需要 Premiere 2021+）' });
    var seconds = 0;
    var hasTime = opts.time !== undefined && opts.time !== null && String(opts.time).trim() !== '';
    if (hasTime) {
      seconds = Math.max(0, Number(opts.time) || 0);
    } else {
      try { seconds = seq.getPlayerPosition().seconds; } catch(e) { seconds = 0; }
    }
    var outPath = String(opts.path || '').trim();
    if (!outPath) return safeJSON({ success: false, error: '缺少输出路径' });
    var outFile = new File(outPath);
    var ticks = secToTicks(seconds);
    var exported = false;
    try { seq.exportFramePNG(ticks, outFile); exported = true; } catch(e1) {
      try { seq.exportFramePNG(ticks, outPath); exported = true; } catch(e2) {
        try { seq.exportFramePNG(ticks, outFile.fsName); exported = true; } catch(e3) {}
      }
    }
    if (!exported) return safeJSON({ success: false, error: '帧导出失败（exportFramePNG 调用异常）' });
    var waited = 0;
    while (!outFile.exists && waited < 30000) {
      $.sleep(250);
      waited += 250;
    }
    if (!outFile.exists) return safeJSON({ success: false, error: '帧导出超时（渲染较慢）' });
    return safeJSON({ success: true, path: outPath, size: outFile.length, time: seconds });
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.exportSequenceFrame = exportSequenceFrame;

// ============================================================
// 12.5 Export a frame from a LOCAL media file using PR's decoder
// 用途：CEP 内嵌浏览器无法解码 HEVC/部分编码时，用 PR 解码导出一帧真实画面，
// 供视频预览与视觉大模型分析使用，避免「画面全黑」。
// ============================================================
function exportLocalFileFrame(jsonArg) {
  try {
    // 兼容两种传参：对象字面量（evalScript 直接传对象）或 JSON 字符串
    var opts = (typeof jsonArg === 'string') ? JSON.parse(jsonArg || '{}') : (jsonArg || {});
    var filePath = String(opts.path || '').trim();
    var outPath = String(opts.output || '').trim();
    if (!filePath || !outPath) return safeJSON({ success: false, error: '缺少输入/输出路径' });
    var file = new File(filePath);
    if (!file.exists) return safeJSON({ success: false, error: '文件不存在: ' + filePath });
    var item = findProjectItem(filePath);
    if (!item) {
      var imported = importFile(filePath);
      if (imported && imported.success === false) return safeJSON({ success: false, error: imported.error || '素材导入失败' });
      item = findProjectItem(filePath);
    }
    if (!item) return safeJSON({ success: false, error: '素材未导入PR' });
    var durationSec = 0;
    try {
      var d = item.getDuration();
      if (d && typeof d.seconds !== 'undefined') durationSec = Number(d.seconds) || 0;
    } catch(e) {}
    if (durationSec <= 0) durationSec = 60;
    var seconds = Math.max(0, Math.min(durationSec - 0.05, Number(opts.time) || 0));
    var tempSeq = null;
    var tempName = '__AVS_预览帧_' + Date.now();
    try { tempSeq = app.project.newSequence(tempName, ''); } catch(e1) {}
    if (!tempSeq) { try { tempSeq = app.project.newSequence(tempName); } catch(e1b) {} }
    if (!tempSeq) { try { tempSeq = app.project.newSequence(tempName, null); } catch(e1c) {} }
    if (!tempSeq) return safeJSON({ success: false, error: '无法创建临时序列（预览帧导出）' });
    try {
      if (tempSeq.videoTracks.numTracks < 1) { try { tempSeq.videoTracks.addTracks(1); } catch(e2) {} }
      var track = tempSeq.videoTracks[0];
      if (!track) return safeJSON({ success: false, error: '无法获取视频轨道' });
      var inserted = false;
      try { track.insertClip(item, secToTicks(0)); inserted = true; } catch(e3) {
        try { track.insertClip(item, 0); inserted = true; } catch(e4) {}
      }
      if (!inserted) return safeJSON({ success: false, error: '插入临时序列失败' });
      var outFile = new File(outPath);
      var exported = false;
      try { tempSeq.exportFramePNG(secToTicks(seconds), outFile); exported = true; } catch(e5) {
        try { tempSeq.exportFramePNG(secToTicks(seconds), outPath); exported = true; } catch(e6) {
          try { tempSeq.exportFramePNG(secToTicks(seconds), outFile.fsName); exported = true; } catch(e7) {}
        }
      }
      if (!exported) return safeJSON({ success: false, error: '帧导出失败（exportFramePNG 调用异常）' });
      var waited = 0;
      while (!outFile.exists && waited < 30000) {
        $.sleep(250);
        waited += 250;
      }
      if (!outFile.exists) return safeJSON({ success: false, error: '帧导出超时（PR 渲染较慢）' });
      return safeJSON({ success: true, path: outPath, size: outFile.length, time: seconds });
    } finally {
      try {
        if (app.project.deleteSequence) app.project.deleteSequence(tempSeq);
        else if (tempSeq.delete) tempSeq.delete();
      } catch(e8) {}
    }
  } catch(e) {
    return safeJSON({ success: false, error: e.toString() });
  }
}
this.exportLocalFileFrame = exportLocalFileFrame;

$.writeln('[AI Video Studio] ExtendScript initialized');
