/* ============================================================================
 * opfs-worker.js — off-main-thread video download → OPFS
 *
 * One dedicated Worker per download (spawned by OPFS.downloadVideo in
 * opfs.js, terminated when finished). The worker does the whole job itself —
 * fetch from /api/download plus streaming writes via createSyncAccessHandle —
 * so a multi-hundred-MB save never allocates buffers or runs stream pumps on
 * the main thread. createSyncAccessHandle is worker-only but has wider
 * support than createWritable (Safari 15.2+ vs 18.2+), which also removes
 * the whole-file ArrayBuffer fallback the main-thread path needs on WebKit.
 *
 * In message:  { videoId, url }
 * Out messages:
 *   { type: 'unsupported' }               → caller falls back to main thread
 *   { type: 'progress', received }        → bytes written so far
 *   { type: 'done', ext }                 → file stored as <videoId>.<ext>
 *   { type: 'error', error }              → failed; .part cleaned up
 * ========================================================================== */

'use strict';

async function getVideosDir() {
  const root = await navigator.storage.getDirectory();
  return root.getDirectoryHandle('videos', { create: true });
}

function extFromContentType(ct) {
  ct = ct || 'video/mp4';
  return ct.includes('webm') ? 'webm' : ct.includes('ogg') ? 'ogg' : 'mp4';
}

self.onmessage = async (e) => {
  const { videoId, url } = e.data || {};

  if (
    typeof navigator === 'undefined' ||
    !navigator.storage ||
    typeof navigator.storage.getDirectory !== 'function' ||
    typeof FileSystemFileHandle === 'undefined' ||
    typeof FileSystemFileHandle.prototype.createSyncAccessHandle !== 'function'
  ) {
    self.postMessage({ type: 'unsupported' });
    return;
  }

  let dir = null;
  let partName = null;
  try {
    const res = await fetch(url);
    if (!res.ok) {
      let msg = 'HTTP ' + res.status;
      try { msg = (await res.json()).error || msg; } catch { /* non-JSON */ }
      throw new Error(msg);
    }

    const ext = extFromContentType(res.headers.get('content-type'));
    const filename = videoId + '.' + ext;
    partName = filename + '.part';

    dir = await getVideosDir();
    const partHandle = await dir.getFileHandle(partName, { create: true });
    const access = await partHandle.createSyncAccessHandle();
    let offset = 0;
    try {
      const reader = res.body.getReader();
      for (;;) {
        const { done, value } = await reader.read();
        if (done) break;
        access.write(value, { at: offset });
        offset += value.byteLength;
        self.postMessage({ type: 'progress', received: offset });
      }
      access.truncate(offset);
      access.flush();
    } finally {
      access.close();
    }

    // Finalize: .part → permanent name. Prefer the native rename, but treat
    // ANY move() failure as "unavailable" and fall back to a chunked copy —
    // WebKit's move() has a different signature/behavior than Chrome's and
    // throws TypeError ("Not enough arguments") rather than being absent.
    try { await dir.removeEntry(filename); } catch { /* no previous copy */ }
    let renamed = false;
    if (typeof partHandle.move === 'function') {
      try { await partHandle.move(filename); renamed = true; } catch { /* copy below */ }
    }
    if (!renamed) {
      const finalHandle = await dir.getFileHandle(filename, { create: true });
      const out = await finalHandle.createSyncAccessHandle();
      try {
        const file = await partHandle.getFile();
        const CHUNK = 8 * 1024 * 1024;
        let pos = 0;
        while (pos < file.size) {
          const buf = await file.slice(pos, pos + CHUNK).arrayBuffer();
          out.write(new Uint8Array(buf), { at: pos });
          pos += buf.byteLength;
        }
        out.truncate(file.size);
        out.flush();
      } finally {
        out.close();
      }
      await dir.removeEntry(partName);
    }

    self.postMessage({ type: 'done', ext });
  } catch (err) {
    // Never leave a corrupt partial behind
    try { if (dir && partName) await dir.removeEntry(partName); } catch { /* gone */ }
    self.postMessage({ type: 'error', error: err && err.message ? err.message : String(err) });
  }
};
