// Main-thread wrapper around the decrypt Web Worker.
//
// Spawns a single worker lazily on first use. Uses request-id correlation so
// concurrent batches (e.g. a backfill arriving while a realtime insert
// dispatches) can't mix up their results. Falls back to inline decrypt if
// the Worker constructor isn't available (non-browser environments, some
// strict CSPs).

import sodium from 'libsodium-wrappers-sumo';

export interface DecryptItem {
  id: string;
  ciphertext: Uint8Array;
  nonce: Uint8Array;
  key: Uint8Array;
}

interface Pending {
  resolve: (results: DecryptResult[]) => void;
  reject: (err: unknown) => void;
}

interface DecryptResult {
  id: string;
  plaintext: string | null;
}

let worker: Worker | null = null;
let workerBroken = false;
const pending = new Map<string, Pending>();

function spawn(): Worker | null {
  if (workerBroken) return null;
  if (worker) return worker;
  try {
    worker = new Worker(new URL('../workers/decrypt.worker.ts', import.meta.url), {
      type: 'module',
    });
    worker.addEventListener('message', (ev: MessageEvent) => {
      const data = ev.data as { id?: string; results?: DecryptResult[] };
      if (!data.id) return;
      const entry = pending.get(data.id);
      if (!entry) return;
      pending.delete(data.id);
      entry.resolve(data.results ?? []);
    });
    worker.addEventListener('error', (ev) => {
      console.warn('decrypt worker error, falling back to inline', ev.message);
      workerBroken = true;
      worker?.terminate();
      worker = null;
      for (const entry of pending.values()) {
        entry.reject(new Error('decrypt worker crashed'));
      }
      pending.clear();
    });
    return worker;
  } catch (err: unknown) {
    console.warn('decrypt worker spawn failed, falling back to inline', err);
    workerBroken = true;
    return null;
  }
}

export async function decryptBatch(items: DecryptItem[]): Promise<DecryptResult[]> {
  if (items.length === 0) return [];
  const w = spawn();
  if (!w) return inlineDecrypt(items);

  return new Promise<DecryptResult[]>((resolve, reject) => {
    const id = crypto.randomUUID();
    pending.set(id, { resolve, reject });
    try {
      w.postMessage({ id, items });
    } catch (err: unknown) {
      pending.delete(id);
      // postMessage can fail if the Uint8Array view is detached — retry inline.
      console.warn('decrypt worker postMessage failed, using inline', err);
      inlineDecrypt(items).then(resolve, reject);
    }
  });
}

// Fallback path — same semantics as the worker but on the main thread. Used
// when the worker failed to spawn or crashed mid-session.
async function inlineDecrypt(items: DecryptItem[]): Promise<DecryptResult[]> {
  await sodium.ready;
  return items.map((item) => {
    try {
      const plain = sodium.crypto_secretbox_open_easy(item.ciphertext, item.nonce, item.key);
      return {
        id: item.id,
        plaintext: new TextDecoder('utf-8', { fatal: false }).decode(plain),
      };
    } catch {
      return { id: item.id, plaintext: null };
    }
  });
}
