// ═══════════════════════════════════════════════════════════════════════════
// Mysterion OFFSCREEN document — video keyframe DCT + pHash extraction.
// MV3 service workers do NOT expose VideoDecoder (WebCodecs). This offscreen
// document has a full window context where VideoDecoder + MP4Box work. The
// service worker (background.js) proxies decode requests here via messages.
//
// Loaded scripts (see offscreen.html): mp4box.all.js, blake3.js, then this file.
// Exposes a message handler that runs the relocated decode functions and returns
// their results. Function bodies are moved verbatim from background.js so behavior
// is identical to the pre-MV3 (Firefox) path.
// ═══════════════════════════════════════════════════════════════════════════

const _HEX32_RE = /^[0-9a-f]{32}$/i;
const _UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

function _normalizeStegId(s) {
  if (!s || typeof s !== 'string') return null;
  if (_HEX32_RE.test(s)) return s.toLowerCase();
  if (_UUID_RE.test(s))  return s.replace(/-/g, '').toLowerCase();
  return null;
}

function _dct8(x) {
  const N = 8, out = new Float64Array(N);
  for (let k = 0; k < N; k++) {
    let s = 0;
    const kOff = k * N;
    for (let n = 0; n < N; n++)
      s += x[n] * _COS8[kOff + n];
    out[k] = s * _SCALE8[k];
  }
  return out;
}

function _dct2d8(block) {
  // Row-wise DCT
  const tmp = Array.from({length:8}, (_,r) => _dct8(block[r]));
  // Column-wise DCT
  const out = Array.from({length:8}, () => new Float64Array(8));
  for (let c = 0; c < 8; c++) {
    const col = new Float64Array(8);
    for (let r = 0; r < 8; r++) col[r] = tmp[r][c];
    const dc = _dct8(col);
    for (let r = 0; r < 8; r++) out[r][c] = dc[r];
  }
  return out;
}

function _computeImagePHash(pixels, width, height) {
  try {
    const N = 32;
    // Bilinear resize to 32×32 grayscale (closer to PIL LANCZOS than nearest)
    const gray = new Float64Array(N * N);
    for (let r = 0; r < N; r++) {
      for (let c = 0; c < N; c++) {
        const srcX = (c + 0.5) * width  / N - 0.5;
        const srcY = (r + 0.5) * height / N - 0.5;
        const x0 = Math.max(0, Math.floor(srcX));
        const y0 = Math.max(0, Math.floor(srcY));
        const x1 = Math.min(width  - 1, x0 + 1);
        const y1 = Math.min(height - 1, y0 + 1);
        const fx = srcX - x0, fy = srcY - y0;
        const i00 = (y0 * width + x0) * 4;
        const i10 = (y0 * width + x1) * 4;
        const i01 = (y1 * width + x0) * 4;
        const i11 = (y1 * width + x1) * 4;
        const g00 = 0.299*pixels[i00] + 0.587*pixels[i00+1] + 0.114*pixels[i00+2];
        const g10 = 0.299*pixels[i10] + 0.587*pixels[i10+1] + 0.114*pixels[i10+2];
        const g01 = 0.299*pixels[i01] + 0.587*pixels[i01+1] + 0.114*pixels[i01+2];
        const g11 = 0.299*pixels[i11] + 0.587*pixels[i11+1] + 0.114*pixels[i11+2];
        gray[r*N+c] = (1-fx)*(1-fy)*g00 + fx*(1-fy)*g10 +
                       (1-fx)*fy*g01     + fx*fy*g11;
      }
    }
    // UNNORMALIZED 2D DCT — scipy.fftpack.dct type-II:
    //   X[k] = 2 * sum_n x[n] * cos(pi*k*(2n+1)/(2N))
    // v1.6.0: Uses precomputed _COS32 table (same values, no Math.cos())
    // Row DCT
    const tmp = new Float64Array(N * N);
    for (let r = 0; r < N; r++) {
      const rOff = r * N;
      for (let k = 0; k < N; k++) {
        let s = 0;
        const kOff = k * N;
        for (let n = 0; n < N; n++)
          s += gray[rOff + n] * _COS32[kOff + n];
        tmp[rOff + k] = 2.0 * s;
      }
    }
    // Col DCT
    const dct2 = new Float64Array(N * N);
    for (let c = 0; c < N; c++) {
      for (let k = 0; k < N; k++) {
        let s = 0;
        const kOff = k * N;
        for (let n = 0; n < N; n++)
          s += tmp[n*N+c] * _COS32[kOff + n];
        dct2[k*N+c] = 2.0 * s;
      }
    }
    // Top-left 8×8 — all 64 coefficients INCLUDING DC at [0][0]
    const top = new Float64Array(64);
    for (let r = 0; r < 8; r++)
      for (let c = 0; c < 8; c++)
        top[r*8+c] = dct2[r*N+c];
    // Median of all 64 values (matches numpy.median on 8×8)
    const sorted = Float64Array.from(top).sort();
    const median = (sorted[31] + sorted[32]) / 2;
    // 64-bit hash: every bit = coefficient > median
    const hashBits = new Uint8Array(64);
    for (let i = 0; i < 64; i++)
      hashBits[i] = top[i] > median ? 1 : 0;
    // Pack all 64 bits → 16-char hex
    let hex = '';
    for (let i = 0; i < 64; i += 4) {
      let nibble = 0;
      for (let j = 0; j < 4; j++) nibble |= (hashBits[i + j] << (3 - j));
      hex += nibble.toString(16);
    }
    return hex;
  } catch (_) { return null; }
}

function _parseMP4VideoKeyframes(buffer) {
  try {
    const view = new DataView(buffer);
    const len = buffer.byteLength;
    function u32(off) { return view.getUint32(off, false); }
    function u16(off) { return view.getUint16(off, false); }
    function str(off, n) {
      return String.fromCharCode(...new Uint8Array(buffer, off, Math.min(n, len - off)));
    }
    function findAtom(name, start, end) {
      let off = start;
      while (off + 8 <= end) {
        const size = u32(off);
        if (size < 8) break;
        if (str(off + 4, 4) === name) return { off, size };
        off += size;
      }
      return null;
    }
    function findAllAtoms(name, start, end) {
      const results = [];
      let off = start;
      while (off + 8 <= end) {
        const size = u32(off);
        if (size < 8) break;
        if (str(off + 4, 4) === name) results.push({ off, size });
        off += size;
      }
      return results;
    }

    const moov = findAtom('moov', 0, len);
    if (!moov) return null;
    const traks = findAllAtoms('trak', moov.off + 8, moov.off + moov.size);
    let videoTrak = null;
    for (const trak of traks) {
      const mdia = findAtom('mdia', trak.off + 8, trak.off + trak.size);
      if (!mdia) continue;
      const hdlr = findAtom('hdlr', mdia.off + 8, mdia.off + mdia.size);
      if (!hdlr) continue;
      if (str(hdlr.off + 20, 4) === 'vide') { videoTrak = trak; break; }
    }
    if (!videoTrak) return null;

    const mdia = findAtom('mdia', videoTrak.off + 8, videoTrak.off + videoTrak.size);
    if (!mdia) return null;

    const mdhd = findAtom('mdhd', mdia.off + 8, mdia.off + mdia.size);
    let timescale = 30000;
    if (mdhd) {
      const ver = view.getUint8(mdhd.off + 8);
      timescale = ver === 1 ? u32(mdhd.off + 28) : u32(mdhd.off + 20);
    }

    const minf = findAtom('minf', mdia.off + 8, mdia.off + mdia.size);
    if (!minf) return null;
    const stbl = findAtom('stbl', minf.off + 8, minf.off + minf.size);
    if (!stbl) return null;
    const stblEnd = stbl.off + stbl.size;

    // Codec string + description from stsd > avc1 > avcC
    const stsd = findAtom('stsd', stbl.off + 8, stblEnd);
    if (!stsd) return null;
    const entryOff = stsd.off + 16;
    const entrySize = u32(entryOff);
    const entryCodec = str(entryOff + 4, 4);
    let codec = 'avc1.640028';
    let description = null;
    const vidWidth = u16(entryOff + 32);
    const vidHeight = u16(entryOff + 34);

    if (entryCodec === 'avc1' || entryCodec === 'avc3') {
      const avcC = findAtom('avcC', entryOff + 86, entryOff + entrySize);
      if (avcC && avcC.size > 13) {
        const profile = view.getUint8(avcC.off + 9);
        const compat = view.getUint8(avcC.off + 10);
        const level = view.getUint8(avcC.off + 11);
        codec = `avc1.${profile.toString(16).padStart(2,'0')}${compat.toString(16).padStart(2,'0')}${level.toString(16).padStart(2,'0')}`;
        description = new Uint8Array(buffer, avcC.off + 8, avcC.size - 8);
      }
    } else if (entryCodec === 'hvc1' || entryCodec === 'hev1') {
      codec = 'hvc1.1.6.L120.B0';
      const hvcC = findAtom('hvcC', entryOff + 86, entryOff + entrySize);
      if (hvcC) description = new Uint8Array(buffer, hvcC.off + 8, hvcC.size - 8);
    }

    // Sample sizes
    const stsz = findAtom('stsz', stbl.off + 8, stblEnd);
    if (!stsz) return null;
    const defaultSz = u32(stsz.off + 16);
    const sampleCount = u32(stsz.off + 20);
    const sizes = [];
    for (let i = 0; i < sampleCount && i < 10000; i++) {
      sizes.push(defaultSz > 0 ? defaultSz : u32(stsz.off + 24 + i * 4));
    }

    // Chunk offsets
    let chunkOffs = [];
    const stco = findAtom('stco', stbl.off + 8, stblEnd);
    const co64 = findAtom('co64', stbl.off + 8, stblEnd);
    if (stco) {
      const cnt = u32(stco.off + 12);
      for (let i = 0; i < cnt; i++) chunkOffs.push(u32(stco.off + 16 + i * 4));
    } else if (co64) {
      const cnt = u32(co64.off + 12);
      for (let i = 0; i < cnt; i++) {
        chunkOffs.push(u32(co64.off + 16 + i * 8) * 0x100000000 + u32(co64.off + 20 + i * 8));
      }
    } else return null;

    const stsc = findAtom('stsc', stbl.off + 8, stblEnd);
    if (!stsc) return null;
    const stscN = u32(stsc.off + 12);
    const stscE = [];
    for (let i = 0; i < stscN; i++) {
      stscE.push({ fc: u32(stsc.off + 16 + i * 12), spc: u32(stsc.off + 20 + i * 12) });
    }

    // Build full sample offset list
    const sampleOffsets = [];
    let sIdx = 0;
    for (let ci = 0; ci < chunkOffs.length && sIdx < sizes.length; ci++) {
      let spc = stscE[0]?.spc || 1;
      for (const e of stscE) if (ci + 1 >= e.fc) spc = e.spc;
      let co = chunkOffs[ci];
      for (let s = 0; s < spc && sIdx < sizes.length; s++) {
        sampleOffsets.push({ offset: co, size: sizes[sIdx] });
        co += sizes[sIdx]; sIdx++;
      }
    }

    // Sync sample table (stss) — identifies keyframes (1-based indices)
    const keyframeIndices = new Set();
    const stss = findAtom('stss', stbl.off + 8, stblEnd);
    if (stss) {
      const cnt = u32(stss.off + 12);
      for (let i = 0; i < cnt; i++) {
        keyframeIndices.add(u32(stss.off + 16 + i * 4) - 1);
      }
    } else {
      for (let i = 0; i < sampleOffsets.length; i++) keyframeIndices.add(i);
    }

    // Decode timing (stts)
    const stts = findAtom('stts', stbl.off + 8, stblEnd);
    const durations = [];
    if (stts) {
      const cnt = u32(stts.off + 12);
      for (let i = 0; i < cnt; i++) {
        const sc = u32(stts.off + 16 + i * 8);
        const dur = u32(stts.off + 20 + i * 8);
        for (let j = 0; j < sc && durations.length < sampleCount; j++) durations.push(dur);
      }
    }

    const keyframes = [];
    let ts = 0;
    for (let i = 0; i < sampleOffsets.length; i++) {
      if (keyframeIndices.has(i)) {
        keyframes.push({
          offset: sampleOffsets[i].offset,
          size: sampleOffsets[i].size,
          timestamp: Math.round(ts * 1000000 / timescale),
        });
      }
      ts += (durations[i] || 1001);
    }

    return { codec, keyframes, description, width: vidWidth, height: vidHeight };
  } catch (e) {
    console.warn('[Mysterion] Video keyframe parse error:', e.message);
    return null;
  }
}

async function _extractKeyframeDCT(buffer, maxKeyframes) {
  if (typeof VideoDecoder === 'undefined') {
    console.log('[Mysterion] VideoDecoder not available');
    return null;
  }

  const videoInfo = _parseMP4VideoKeyframes(buffer);
  if (!videoInfo || videoInfo.keyframes.length === 0) {
    console.log('[Mysterion] No video keyframes found');
    return null;
  }

  const kfCount = Math.min(videoInfo.keyframes.length, maxKeyframes);
  console.log(`[Mysterion] Decoding ${kfCount}/${videoInfo.keyframes.length} keyframes for DCT scan`);

  const decodedFrames = [];
  let decodeError = null;
  let resolveAll;
  const allDone = new Promise(r => { resolveAll = r; });
  let pendingDecodes = kfCount;

  try {
    const decoder = new VideoDecoder({
      output(frame) {
        try {
          const w = frame.displayWidth;
          const h = frame.displayHeight;
          const rgba = new Uint8Array(w * h * 4);
          frame.copyTo(rgba, { rect: { x: 0, y: 0, width: w, height: h }, format: 'RGBA' });
          decodedFrames.push({ pixels: rgba, width: w, height: h });
        } catch (e) {
          console.warn('[Mysterion] Frame copy error:', e.message);
        } finally {
          frame.close();
          pendingDecodes--;
          if (pendingDecodes <= 0) resolveAll();
        }
      },
      error(e) {
        decodeError = e;
        resolveAll();
      },
    });

    const config = {
      codec: videoInfo.codec,
      codedWidth: videoInfo.width,
      codedHeight: videoInfo.height,
      hardwareAcceleration: 'prefer-hardware',
    };
    if (videoInfo.description) config.description = videoInfo.description;
    decoder.configure(config);

    for (let i = 0; i < kfCount; i++) {
      const kf = videoInfo.keyframes[i];
      if (kf.offset + kf.size > buffer.byteLength) { pendingDecodes--; continue; }
      decoder.decode(new EncodedVideoChunk({
        type: 'key',
        timestamp: kf.timestamp,
        data: new Uint8Array(buffer, kf.offset, kf.size),
      }));
    }

    await decoder.flush();
    decoder.close();
    resolveAll();
    await allDone;
  } catch (e) {
    console.warn('[Mysterion] VideoDecoder failed:', e.message);
    return null;
  }

  if (decodeError || decodedFrames.length === 0) {
    console.log('[Mysterion] No keyframes decoded:', decodeError?.message);
    return null;
  }

  // Run DCT extraction on each decoded keyframe (WASM first, JS fallback)
  for (const frame of decodedFrames) {
    let dctPayload = null;
    if (_wasmEngine) {
      dctPayload = _wasmEngine.extractDCT(frame.pixels, frame.width, frame.height);
    }
    // Legacy JS DCT fallback removed v5.5.81 — WASM DCT-SS only
    if (dctPayload) {
      const parts = new TextDecoder().decode(dctPayload).split('|');
      const _sid = _normalizeStegId(parts[0]);
      if (_sid) {
        console.log('[Mysterion] Keyframe DCT extraction succeeded:', _sid);
        return {
          stegId: _sid,
          preSignHash: (parts[2] && _HEX64_RE.test(parts[2])) ? parts[2] : null,
        };
      }
    }
  }
  return null;
}

async function _layer6PhashLookup(buffer, videoId) {
  if (typeof VideoDecoder === 'undefined') {
    console.log('[Mysterion] Layer 6: VideoDecoder not available');
    return null;
  }
  if (typeof MP4Box === 'undefined') {
    console.warn('[Mysterion] Layer 6: MP4Box not loaded in background — cannot demux');
    return null;
  }

  // HOTFIX 10.1 (DEBUG-10): use MP4Box demuxer instead of the hand-rolled
  // _parseMP4VideoKeyframes — the latter returns "No video keyframes found" on
  // files whose moov layout was rewritten by `ffmpeg -ss N -c copy` (different
  // stss/stco/stsc layout). MP4Box handles all real-world MP4 variants.
  const MAX_KF = 3;
  let demuxed;
  try {
    demuxed = await new Promise((resolve, reject) => {
      const mp4 = MP4Box.createFile();
      const samples = [];
      let videoTrack = null;
      mp4.onError = (e) => reject(new Error(`MP4Box error: ${e}`));
      mp4.onReady = (info) => {
        videoTrack = info.videoTracks && info.videoTracks[0];
        if (!videoTrack) { reject(new Error('No video track')); return; }
        mp4.setExtractionOptions(videoTrack.id, null, { nbSamples: 128 });
        mp4.start();
      };
      mp4.onSamples = (trackId, ref, sArr) => {
        for (const s of sArr) {
          if (s.is_sync && samples.length < MAX_KF) {
            samples.push({
              data: s.data,
              timestamp: Math.round((s.cts / s.timescale) * 1_000_000),
            });
          }
        }
        if (samples.length >= MAX_KF) {
          const trak = mp4.getTrackById(videoTrack.id);
          const entry = trak.mdia.minf.stbl.stsd.entries[0];
          const cfg = {
            codec: videoTrack.codec,
            codedWidth: videoTrack.video.width,
            codedHeight: videoTrack.video.height,
            // hardwareAcceleration omitted - _pickSupportedConfig picks best option
          };
          for (const bn of ['avcC', 'hvcC', 'vpcC', 'av1C']) {
            if (entry[bn]) {
              const ds = new DataStream(undefined, 0, DataStream.BIG_ENDIAN);
              entry[bn].write(ds);
              cfg.description = new Uint8Array(ds.buffer, 8);
              break;
            }
          }
          resolve({ config: cfg, samples });
        }
      };
      buffer.fileStart = 0;
      mp4.appendBuffer(buffer);
      mp4.flush();
      // Fallback: if flush finished and we have any samples, finalize now
      setTimeout(() => {
        if (samples.length > 0 && videoTrack) {
          const trak = mp4.getTrackById(videoTrack.id);
          const entry = trak.mdia.minf.stbl.stsd.entries[0];
          const cfg = {
            codec: videoTrack.codec,
            codedWidth: videoTrack.video.width,
            codedHeight: videoTrack.video.height,
            // hardwareAcceleration omitted - _pickSupportedConfig picks best option
          };
          for (const bn of ['avcC', 'hvcC', 'vpcC', 'av1C']) {
            if (entry[bn]) {
              const ds = new DataStream(undefined, 0, DataStream.BIG_ENDIAN);
              entry[bn].write(ds);
              cfg.description = new Uint8Array(ds.buffer, 8);
              break;
            }
          }
          resolve({ config: cfg, samples });
        } else if (!videoTrack) {
          reject(new Error('MP4Box did not find a video track'));
        } else {
          reject(new Error('MP4Box: no keyframes extracted'));
        }
      }, 500);
    });
  } catch (e) {
    console.warn('[Mysterion] Layer 6 MP4Box demux failed:', e.message);
    return null;
  }

  if (!demuxed || !demuxed.samples || demuxed.samples.length === 0) {
    console.log('[Mysterion] Layer 6: no keyframes to pHash');
    return null;
  }

  const kfCount = demuxed.samples.length;
  console.log(`[Mysterion] Layer 6: decoding ${kfCount} keyframes (MP4Box) for pHash lookup`);
  console.log('[Mysterion] Layer 6 config:', {
    codec: demuxed.config.codec,
    codedWidth: demuxed.config.codedWidth,
    codedHeight: demuxed.config.codedHeight,
    hasDescription: !!demuxed.config.description,
    descriptionLen: demuxed.config.description ? demuxed.config.description.byteLength : 0,
  });

  // HOTFIX 10.1b: Firefox background VideoDecoder rejects `prefer-hardware` and
  // sometimes needs explicit profile/level in the codec string. Try a sequence
  // of configs and keep the first that `isConfigSupported` accepts.
  async function _pickSupportedConfig(baseCfg) {
    const candidates = [];
    // 1. As-is (with prefer-hardware if set)
    candidates.push({ ...baseCfg });
    // 2. No hardwareAcceleration hint (FF-safe default)
    const { hardwareAcceleration, ...noHw } = baseCfg;
    candidates.push(noHw);
    // 3. Explicit prefer-software
    candidates.push({ ...noHw, hardwareAcceleration: 'prefer-software' });
    // 4. If codec is bare 'avc1' without profile, try deriving from avcC description
    if (/^avc1$/i.test(baseCfg.codec) && baseCfg.description && baseCfg.description.byteLength >= 4) {
      const d = baseCfg.description;
      // avcC: [configVersion, AVCProfile, profile_compat, AVCLevel, ...]
      const hex = [d[1], d[2], d[3]].map(b => b.toString(16).padStart(2, '0')).join('');
      candidates.push({ ...noHw, codec: `avc1.${hex}` });
    }
    for (const cfg of candidates) {
      try {
        const s = await VideoDecoder.isConfigSupported(cfg);
        if (s && s.supported) {
          console.log('[Mysterion] Layer 6 picked config:', cfg.codec, 'hw=', cfg.hardwareAcceleration || '(default)');
          return s.config || cfg;
        }
      } catch (e) {
        // try next
      }
    }
    return null;
  }

  const chosenCfg = await _pickSupportedConfig(demuxed.config);
  if (!chosenCfg) {
    console.warn('[Mysterion] Layer 6: no supported VideoDecoder config for codec', demuxed.config.codec);
    return null;
  }

  const decodedFrames = [];
  let decodeError = null;
  let resolveAll;
  const allDone = new Promise(r => { resolveAll = r; });
  let pendingDecodes = kfCount;

  try {
    const decoder = new VideoDecoder({
      output(frame) {
        try {
          const w = frame.displayWidth;
          const h = frame.displayHeight;
          const rgba = new Uint8Array(w * h * 4);
          frame.copyTo(rgba, { rect: { x: 0, y: 0, width: w, height: h }, format: 'RGBA' });
          decodedFrames.push({ pixels: rgba, width: w, height: h });
        } catch (e) {
          console.warn('[Mysterion] Layer 6 frame copy error:', e.message);
        } finally {
          frame.close();
          pendingDecodes--;
          if (pendingDecodes <= 0) resolveAll();
        }
      },
      error(e) {
        decodeError = e;
        console.warn('[Mysterion] Layer 6 VideoDecoder error callback:', e.message);
        resolveAll();
      },
    });

    decoder.configure(chosenCfg);

    for (const s of demuxed.samples) {
      decoder.decode(new EncodedVideoChunk({
        type: 'key',
        timestamp: s.timestamp,
        data: s.data,
      }));
    }

    await decoder.flush();
    decoder.close();
    resolveAll();
    await allDone;
  } catch (e) {
    console.warn('[Mysterion] Layer 6 VideoDecoder failed:', e.message, '| codec=', chosenCfg.codec);
    return null;
  }

  if (decodeError || decodedFrames.length === 0) {
    console.log('[Mysterion] Layer 6: no keyframes decoded:', decodeError?.message);
    return null;
  }

  // pHash each frame and query server. Return on first match.
  for (const frame of decodedFrames) {
    let phash = null;
    try {
      phash = _computeImagePHash(frame.pixels, frame.width, frame.height);
    } catch (e) {
      console.warn('[Mysterion] Layer 6 pHash compute failed:', e.message);
      continue;
    }
    if (!phash) continue;
    console.log('[Mysterion] Layer 6 pHash:', phash);
    try {
      const resp = await fetch(`${API_URL}/crypto/verify/phash`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          phash,
          content_type: 'video',
          hamming_threshold: 3,
          filename_hint: videoId || null,
          content_hash: null,
        }),
      });
      if (!resp.ok) {
        console.warn('[Mysterion] Layer 6 /verify/phash HTTP', resp.status);
        continue;
      }
      const data = await resp.json();
      const matched = data.matched === true && Array.isArray(data.matches) && data.matches.length > 0;
      if (matched) {
        const best = data.canonical_original || data.matches[0];
        console.log('[Mysterion] Layer 6 HIT:', best.steg_id, 'creator=', best.creator_name, 'qp=', data.querier_provenance);
        return {
          phash,
          match: best,
          querier_provenance: data.querier_provenance || 'unsigned_derivative',
        };
      }
    } catch (e) {
      console.warn('[Mysterion] Layer 6 /verify/phash error:', e.message);
    }
  }
  console.log('[Mysterion] Layer 6: no pHash match for any keyframe');
  return null;
}


// ── Message bridge: background.js -> offscreen ─────────────────────────────
// Requests: { target:'offscreen', kind:'dct'|'phash', buffer:ArrayBuffer, ... }
// Responses: { ok:true, result } | { ok:false, error }
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (!msg || msg.target !== 'offscreen-decode') return false;
  (async () => {
    try {
      if (msg.kind === 'dct') {
        const result = await _extractKeyframeDCT(msg.buffer, msg.maxKeyframes === null ? Infinity : msg.maxKeyframes);
        sendResponse({ ok: true, result });
      } else if (msg.kind === 'phash') {
        const result = await _layer6PhashLookup(msg.buffer, msg.videoId);
        sendResponse({ ok: true, result });
      } else {
        sendResponse({ ok: false, error: 'unknown kind: ' + msg.kind });
      }
    } catch (e) {
      console.error('[Mysterion offscreen] decode error:', e);
      sendResponse({ ok: false, error: String(e && e.message || e) });
    }
  })();
  return true; // async response
});

console.log('[Mysterion offscreen] ready — VideoDecoder available:', typeof VideoDecoder !== 'undefined');

// patch26: single-engine policy applied
