/**
 * Wire protocol between `WorkerCryptoProvider` (main thread) and the
 * worker entry (`worker.ts`).
 *
 * Shape:
 *   main → worker:  WorkerRequest
 *   worker → main:  WorkerResponse  (matched by `id`)
 *
 * Binary inputs are passed as `ArrayBuffer` (transferable) — never
 * `Uint8Array` — so we can hand them off without copying. The worker
 * wraps them in `Uint8Array` for use, and the response transfers result
 * buffers the same way.
 *
 * Versioning: `__protocolVersion` is bumped on any breaking change.
 * `worker-init` echoes the version so a mismatched bundle aborts
 * deterministically instead of silently producing garbage.
 */

export const WORKER_PROTOCOL_VERSION = 1;

export type WorkerRequest =
  // ─── lifecycle ────────────────────────────────────────────
  | { id: number; method: 'init'; protocolVersion: number }
  | { id: number; method: 'ping' }
  // ─── crypto.* — generic CryptoProvider ────────────────────
  | { id: number; method: 'crypto.generateX25519KeyPair' }
  | { id: number; method: 'crypto.x25519'; privateKey: ArrayBuffer; publicKey: ArrayBuffer }
  | { id: number; method: 'crypto.generateEd25519KeyPair' }
  | { id: number; method: 'crypto.sign'; privateKey: ArrayBuffer; message: ArrayBuffer }
  | {
      id: number;
      method: 'crypto.verify';
      publicKey: ArrayBuffer;
      message: ArrayBuffer;
      signature: ArrayBuffer;
    }
  | {
      id: number;
      method: 'crypto.aesGcmEncrypt';
      key: ArrayBuffer;
      plaintext: ArrayBuffer;
      aad: ArrayBuffer | null;
    }
  | {
      id: number;
      method: 'crypto.aesGcmDecrypt';
      key: ArrayBuffer;
      ciphertext: ArrayBuffer;
      nonce: ArrayBuffer;
      aad: ArrayBuffer | null;
    }
  | {
      id: number;
      method: 'crypto.hkdf';
      ikm: ArrayBuffer;
      salt: ArrayBuffer;
      info: ArrayBuffer;
      length: number;
    }
  | { id: number; method: 'crypto.hmacSha256'; key: ArrayBuffer; data: ArrayBuffer }
  // ─── stream.* — host StreamSender / StreamReceiver ────────
  | {
      id: number;
      method: 'stream.createSender';
      senderId: number;
      streamId: ArrayBuffer;
      streamSecret: ArrayBuffer;
      laneId: number;
      startSeq: number;
    }
  | {
      id: number;
      method: 'stream.encryptChunk';
      senderId: number;
      plaintext: ArrayBuffer;
      isLast: boolean;
    }
  | { id: number; method: 'stream.getSenderLaneSha256'; senderId: number }
  | { id: number; method: 'stream.destroySender'; senderId: number }
  | {
      id: number;
      method: 'stream.createReceiver';
      receiverId: number;
      streamId: ArrayBuffer;
      streamSecret: ArrayBuffer;
      laneId: number;
      startSeq: number;
    }
  | {
      id: number;
      method: 'stream.decryptChunk';
      receiverId: number;
      wireBytes: ArrayBuffer;
    }
  | { id: number; method: 'stream.getReceiverLaneSha256'; receiverId: number }
  | { id: number; method: 'stream.destroyReceiver'; receiverId: number };

export type WorkerResponse =
  | { id: number; ok: true; result: WorkerResult }
  | {
      id: number;
      ok: false;
      error: { name: string; message: string; code?: string };
    };

export type WorkerResult =
  | { kind: 'ack' } // void/init/destroy
  | { kind: 'pong' }
  | { kind: 'keypair'; publicKey: ArrayBuffer; privateKey: ArrayBuffer }
  | { kind: 'bytes'; bytes: ArrayBuffer }
  | { kind: 'aead-encrypt'; ciphertext: ArrayBuffer; nonce: ArrayBuffer }
  | { kind: 'verify'; valid: boolean }
  | { kind: 'chunk-encrypt'; bytes: ArrayBuffer; seq: number }
  | { kind: 'chunk-decrypt'; plaintext: ArrayBuffer; seq: number; isLast: boolean };

/**
 * Pull every transferable `ArrayBuffer` out of a result so the runtime
 * can hand ownership to the receiving thread. Order doesn't matter; the
 * structured-clone algorithm matches buffers by reference.
 */
export function transferablesOf(result: WorkerResult): ArrayBuffer[] {
  switch (result.kind) {
    case 'keypair':
      return [result.publicKey, result.privateKey];
    case 'bytes':
      return [result.bytes];
    case 'aead-encrypt':
      return [result.ciphertext, result.nonce];
    case 'chunk-encrypt':
      return [result.bytes];
    case 'chunk-decrypt':
      return [result.plaintext];
    default:
      return [];
  }
}

/**
 * Wrap a `Uint8Array` as an `ArrayBuffer` suitable for transfer. If the
 * view doesn't span its underlying buffer, copy into a fresh one so we
 * never transfer slack the caller still owns.
 */
export function toTransferable(u: Uint8Array): ArrayBuffer {
  if (u.byteOffset === 0 && u.byteLength === u.buffer.byteLength) {
    return u.buffer as ArrayBuffer;
  }
  const copy = new Uint8Array(u.byteLength);
  copy.set(u);
  return copy.buffer;
}

/**
 * Like `toTransferable`, but always copies. Use when the original buffer
 * must remain valid on the calling thread (e.g. when the caller owns a
 * key the worker should not mutate).
 */
export function toTransferableCopy(u: Uint8Array): ArrayBuffer {
  const copy = new Uint8Array(u.byteLength);
  copy.set(u);
  return copy.buffer;
}

export function fromTransferable(buf: ArrayBuffer): Uint8Array {
  return new Uint8Array(buf);
}
