// timeline.js — v2.1.6
// premierepro API: docs say sync, runtime returns Promises. Await everything.

(function () {
  const Timeline = {};

  function ppro() {
    if (Timeline._ppro) return Timeline._ppro;
    try { Timeline._ppro = require('premierepro'); }
    catch (e) { throw new Error('UXP premierepro unavailable: ' + e.message); }
    return Timeline._ppro;
  }

  // Safe helper — returns the numeric seconds value from a Time object,
  // or 0 if the object is null/undefined/malformed.
  function _secs(t) {
    if (!t) return 0;
    if (typeof t.seconds === 'number') return t.seconds;
    return 0;
  }

  // ── Read active sequence ─────────────────────────────────────────
  Timeline.readActiveSequence = async function () {
    const P = ppro();
    const project = await P.Project.getActiveProject();
    if (!project) throw new Error('No active Premiere project');
    const seq = await project.getActiveSequence();
    if (!seq) throw new Error('No active sequence — open a sequence in the timeline first');

    const name     = seq.name || (seq.getName ? await seq.getName() : '') || 'Sequence 1';
    const settings = await seq.getSettings();
    let frameRate = 29.97;
    try {
      const fr = settings && settings.videoFrameRate;
      if (fr && typeof fr.seconds === 'number' && fr.seconds > 0) frameRate = 1 / fr.seconds;
    } catch (_) {}
    const width  = (settings && settings.videoFrameWidth)  || 1920;
    const height = (settings && settings.videoFrameHeight) || 1080;

    const clips      = [];
    const trackCount = await seq.getVideoTrackCount();

    for (let ti = 0; ti < trackCount; ti++) {
      const track = await seq.getVideoTrack(ti);
      if (!track) continue;
      let items = [];
      try { items = await track.getTrackItems(1, false); } catch (_) { continue; }
      if (!items || !items.length) continue;

      for (const clip of items) {
        try {
          const projItem = await clip.getProjectItem();
          if (!projItem) continue;

          let filePath = '';
          try {
            const clipItem = await P.ClipProjectItem.cast(projItem);
            filePath = await clipItem.getMediaFilePath() || '';
          } catch (_) {}

          const clipName = await clip.getName().catch(() => '');
          const fileName = clipName || (filePath ? path.basename(filePath) : 'clip');

          // Null-safe time access — non-clip items can return null Time objects
          const startT = await clip.getStartTime().catch(() => null);
          const endT   = await clip.getEndTime().catch(() => null);
          const inT    = await clip.getInPoint().catch(() => null);
          const outT   = await clip.getOutPoint().catch(() => null);

          const tlIn   = _secs(startT);
          const tlOut  = _secs(endT);
          const srcIn  = _secs(inT);
          const srcOut = _secs(outT);

          // Skip filler/gap items where all times are zero
          if (tlIn === 0 && tlOut === 0 && srcIn === 0 && srcOut === 0 && !filePath) continue;

          clips.push({
            fileName, filePath, trackIndex: ti,
            timelineInSec: tlIn, timelineOutSec: tlOut,
            sourceInSec: srcIn, sourceOutSec: srcOut,
            timelineInFrames:  Math.round(tlIn  * frameRate),
            timelineOutFrames: Math.round(tlOut * frameRate),
            sourceInFrames:    Math.round(srcIn  * frameRate),
            sourceOutFrames:   Math.round(srcOut * frameRate),
          });
        } catch (clipErr) {
          console.warn('[df] readActiveSequence: skipped clip on track', ti, '—', clipErr && clipErr.message);
        }
      }
    }
    return { sequenceName: name, frameRate, width, height, clips };
  };

  // ── Sequence info bar ────────────────────────────────────────────
  Timeline.refreshSeqBar = async function () {
    try {
      const P = ppro();
      const project = await P.Project.getActiveProject();
      if (!project) { UI.setHidden('#seq-info-bar', true); return; }
      const seq = await project.getActiveSequence();
      if (!seq)  { UI.setHidden('#seq-info-bar', true); return; }
      const name = seq.name || '';
      if (name) {
        document.getElementById('seq-info-name').textContent = name;
        UI.setHidden('#seq-info-bar', false);
      } else {
        UI.setHidden('#seq-info-bar', true);
      }
    } catch (_) { UI.setHidden('#seq-info-bar', true); }
  };

  // ── FCP XML (xmeml / FCP 7) ──────────────────────────────────────
  // The worker's parseFcpXml expects the legacy xmeml schema
  // (root <xmeml><sequence>), NOT the modern fcpxml schema
  // (root <fcpxml><resources><library>...). We previously emitted
  // fcpxml and every conform job failed with "Invalid FCP XML: no
  // sequence element". Now we emit xmeml v5 with:
  //   xmeml/sequence/{name,duration,rate,media/video/{format,track*}}
  //   track[@currentExplodedTrackIndex] / clipitem / file{name,pathurl}
  //   clipitem in/out are SOURCE frames; start/end are TIMELINE frames.
  Timeline.generateFcpXml = function (td) {
    const seqName = UI.escapeXml(td.sequenceName || 'Sequence 1');
    const fps     = Math.round(td.frameRate || 29.97);
    const w       = td.width  || 1920;
    const h       = td.height || 1080;
    const clips   = td.clips  || [];

    let totalF = 0;
    clips.forEach(c => { if ((c.timelineOutFrames || 0) > totalF) totalF = c.timelineOutFrames; });
    if (totalF < 1) totalF = 100;

    // Group clips by their video track so each <track> contains its
    // own clipitems in timeline order. The worker iterates tracks and
    // collects clipitems off each.
    const byTrack = {};
    clips.forEach(c => {
      const ti = Number(c.trackIndex || 0);
      (byTrack[ti] = byTrack[ti] || []).push(c);
    });

    const out = [];
    out.push('<?xml version="1.0" encoding="UTF-8"?>');
    out.push('<!DOCTYPE xmeml>');
    out.push('<xmeml version="5">');
    out.push('  <sequence>');
    out.push('    <name>' + seqName + '</name>');
    out.push('    <duration>' + totalF + '</duration>');
    out.push('    <rate><timebase>' + fps + '</timebase><ntsc>FALSE</ntsc></rate>');
    out.push('    <media>');
    out.push('      <video>');
    out.push('        <format>');
    out.push('          <samplecharacteristics>');
    out.push('            <width>' + w + '</width>');
    out.push('            <height>' + h + '</height>');
    out.push('          </samplecharacteristics>');
    out.push('        </format>');

    let fileId = 1;
    Object.keys(byTrack)
      .sort((a, b) => Number(a) - Number(b))
      .forEach((ti) => {
        out.push('        <track currentExplodedTrackIndex="' + ti + '">');
        byTrack[ti].forEach((c, idx) => {
          const name = UI.escapeXml(c.fileName || 'Clip');
          // file:// URI normalises path separators and gives the worker
          // a parseable pathurl in case it ever resolves locally.
          const rawPath = c.filePath || '';
          const pathUri = rawPath
            ? 'file://localhost/' + UI.escapeXml(String(rawPath).replace(/\\/g, '/').replace(/^\/+/, ''))
            : '';
          const dur = Math.max(1, (c.sourceOutFrames || 0) - (c.sourceInFrames || 0));
          out.push('          <clipitem id="clipitem-' + ti + '-' + idx + '">');
          out.push('            <name>' + name + '</name>');
          out.push('            <duration>' + dur + '</duration>');
          out.push('            <rate><timebase>' + fps + '</timebase></rate>');
          out.push('            <in>'    + (c.sourceInFrames    || 0) + '</in>');
          out.push('            <out>'   + (c.sourceOutFrames   || 0) + '</out>');
          out.push('            <start>' + (c.timelineInFrames  || 0) + '</start>');
          out.push('            <end>'   + (c.timelineOutFrames || 0) + '</end>');
          out.push('            <file id="file-' + fileId + '">');
          out.push('              <name>' + name + '</name>');
          if (pathUri) out.push('              <pathurl>' + pathUri + '</pathurl>');
          out.push('            </file>');
          out.push('          </clipitem>');
          fileId++;
        });
        out.push('        </track>');
      });

    out.push('      </video>');
    out.push('    </media>');
    out.push('  </sequence>');
    out.push('</xmeml>');
    return out.join('\n');
  };

  // ── Push Timeline to MAM ─────────────────────────────────────────
  Timeline.pushToMAM = async function (seqName, projectId, td) {
    const resolved = Library.resolveClipsToAssets(td.clips || []);
    const matched  = resolved.filter(c => c.asset_id);
    if (!matched.length) throw new Error('No clips matched MAM assets — import proxies first so the plugin can map file paths to asset IDs');
    const seqs = await API.listSequences(projectId);
    let seqId;
    const existing = seqs.find(s => s.name === seqName);
    if (existing) {
      await API.updateSequence(existing.id, { frame_rate: td.frameRate, width: td.width, height: td.height });
      seqId = existing.id;
    } else {
      const created = await API.createSequence(projectId, seqName, td.frameRate, td.width, td.height);
      seqId = created.id;
    }
    await API.pushClips(seqId, matched.map(c => ({
      asset_id: c.asset_id, track: c.trackIndex,
      timeline_in_frames: c.timelineInFrames, timeline_out_frames: c.timelineOutFrames,
      source_in_frames: c.sourceInFrames, source_out_frames: c.sourceOutFrames,
    })));
    return { seqId, matched: matched.length, skipped: resolved.length - matched.length };
  };

  // ── Conform ──────────────────────────────────────────────────────
  Timeline.startConform = async function (projectId, seqName, td, opts) {
    const { seqId } = await Timeline.pushToMAM(seqName, projectId, td);
    const resMap = { '1080p':[1920,1080], '720p':[1280,720], 'uhd':[3840,2160], 'source':[0,0] };
    const [w, h] = resMap[opts.resolution] || [1920,1080];
    const job = await API.startConform(seqId, { codec:opts.codec, quality:opts.quality, width:w||undefined, height:h||undefined, audio:opts.audio, fcp_xml:Timeline.generateFcpXml(td) });
    return job.jobId || job.id;
  };

  Timeline.pollConform = function (jobId, onProgress, onDone) {
    const t = setInterval(async () => {
      try {
        const job = await API.getJob(jobId);
        onProgress(job.progress||0, job.status);
        if (job.status==='completed'||job.status==='failed') { clearInterval(t); onDone(job); }
      } catch (_) {}
    }, 2000);
    return t;
  };

  // ── Batch Hi-Res Relink ──────────────────────────────────────────
  Timeline.batchRelink = async function (selectedClips) {
    const P = ppro();
    const project = await P.Project.getActiveProject();
    if (!project) throw new Error('No active Premiere project');
    const results = { succeeded: 0, failed: 0, errors: [] };
    for (const clip of selectedClips) {
      if (!clip.asset_id) continue;
      try {
        UI.showProgress('Fetching hi-res for ' + clip.fileName + '…', 20);
        const info     = await API.getHiresInfo(clip.asset_id);
        const safeName = UI.sanitizeFilename(info.filename || clip.fileName + '.' + (info.ext||'mxf'));
        const dest     = await Import._tempPath(safeName);
        UI.showProgress('Downloading ' + safeName + '…', 30);
        const r = await API.requestExternal(info.url);
        if (!r.ok) throw new Error('Download HTTP ' + r.status);
        const buf = await r.arrayBuffer();
        await Import._writeBuffer(dest, buf);
        UI.showProgress('Relinking ' + clip.fileName + '…', 85);
        await Timeline._relinkInProject(project, clip.filePath, dest);
        results.succeeded++;
      } catch (e) {
        results.failed++;
        results.errors.push(clip.fileName + ': ' + e.message);
      }
    }
    return results;
  };

  // Relink via ClipProjectItem.changeMediaFilePath — await all calls
  Timeline._relinkInProject = async function (project, oldPath, newPath) {
    const P    = ppro();
    const root = await project.getRootItem();
    let count  = 0;

    async function walk(item) {
      let children = [];
      try { children = await item.getItems(); } catch (_) {}
      for (const child of children) {
        try {
          const ci = await P.ClipProjectItem.cast(child);
          const mp = await ci.getMediaFilePath();
          if (mp && mp === oldPath) { await ci.changeMediaFilePath(newPath, false); count++; }
        } catch (_) {
          try { await walk(child); } catch (__) {}
        }
      }
    }

    await walk(root);

    // Fallback: findItemsMatchingMediaPath
    if (count === 0) {
      try {
        const rootCi = await P.ClipProjectItem.cast(root);
        const matches = await rootCi.findItemsMatchingMediaPath(oldPath, true);
        for (const item of (matches||[])) {
          try { const ci = await P.ClipProjectItem.cast(item); await ci.changeMediaFilePath(newPath, false); count++; } catch (_) {}
        }
      } catch (_) {}
    }

    if (count === 0) throw new Error('No matching clips found for: ' + (oldPath||'unknown'));
    return count;
  };

  // ── Local Export ─────────────────────────────────────────────────
  // Server trims each timeline clip's hi-res via FFMPEG, then we download
  // the trimmed segments and relink the project items to them.
  // CAVEAT: relink keys on the source media path, so a source used by
  // multiple timeline clips with different in/out points will relink to a
  // single segment (last one wins). Common single-use case is exact.
  Timeline.localExport = async function (resolvedClips, onProgress) {
    const P = ppro();
    const project = await P.Project.getActiveProject();
    if (!project) throw new Error('No active Premiere project');
    const matched = (resolvedClips || []).filter(c => c.asset_id);
    if (!matched.length) throw new Error('No clips matched MAM assets to export');

    const payload = matched.map(c => ({
      assetId: c.asset_id,
      filename: c.fileName || (c.filePath ? path.basename(c.filePath) : 'clip'),
      sourceInFrames: c.sourceInFrames, sourceOutFrames: c.sourceOutFrames,
      timelineInFrames: c.timelineInFrames, timelineOutFrames: c.timelineOutFrames,
      trackIndex: c.trackIndex,
    }));

    onProgress && onProgress('Requesting trim of ' + matched.length + ' clip(s)…', 10);
    const job = await API.batchTrim(payload);
    const jobId = job.jobId;
    const clipByInstance = {};
    (job.clips || []).forEach((cr, i) => { if (cr.clipInstanceId) clipByInstance[cr.clipInstanceId] = matched[i]; });

    // Poll until every segment is ready (s3Key set) or the job fails.
    const ready = {};
    await new Promise((resolve, reject) => {
      const t = setInterval(async () => {
        try {
          const st = await API.getTrimStatus(jobId);
          const clips = st.clips || [];
          const completed = clips.filter(c => c.status === 'completed' && c.s3Key);
          onProgress && onProgress('Trimming on server… (' + completed.length + '/' + clips.length + ')',
            15 + (completed.length / Math.max(1, clips.length)) * 45);
          if (st.status === 'failed') { clearInterval(t); reject(new Error('Server trim job failed')); return; }
          if (clips.length && completed.length === clips.length) {
            clearInterval(t); completed.forEach(c => { ready[c.clipInstanceId] = c; }); resolve();
          }
        } catch (_) { /* transient — keep polling */ }
      }, 2000);
    });

    // Download each segment and relink the source media path to it.
    const results = { succeeded: 0, failed: 0, errors: [] };
    const ids = Object.keys(ready);
    for (let i = 0; i < ids.length; i++) {
      const cid  = ids[i];
      const clip = clipByInstance[cid];
      if (!clip) continue;
      try {
        onProgress && onProgress('Downloading segment ' + (i + 1) + '/' + ids.length + '…', 60 + (i / ids.length) * 35);
        const seg  = await API.getTempSegmentUrl(cid);
        const ext  = (seg.s3Key && seg.s3Key.split('.').pop()) || 'mov';
        const base = UI.sanitizeFilename((clip.fileName || 'clip') + '-trim-' + cid.slice(0, 8) + '.' + ext);
        const dest = await Import._tempPath(base);
        const r    = await API.requestExternal(seg.url);
        if (!r.ok) throw new Error('Segment download HTTP ' + r.status);
        await Import._writeBuffer(dest, await r.arrayBuffer());
        if (clip.filePath) await Timeline._relinkInProject(project, clip.filePath, dest);
        results.succeeded++;
      } catch (e) {
        results.failed++;
        results.errors.push((clip && clip.fileName || 'clip') + ': ' + e.message);
      }
    }
    return results;
  };

  window.Timeline = Timeline;
})();
