/// Binary-safe transport for chrome.runtime.sendMessage.
///
/// chrome.runtime.sendMessage serializes messages with a JSON-like algorithm
/// that does NOT preserve ArrayBuffer / TypedArray payloads — they arrive at
/// the other end as empty objects (`{}`), so `new Uint8Array(buf)` yields a
/// zero-length array. This bit vault creation: a valid carrier JPEG reached
/// the service worker as 0 bytes ("no SOF marker found in JPEG").
///
/// `encodeBinary` walks a message/response tree and replaces every
/// ArrayBuffer/TypedArray with a `{ [MARKER]: <base64> }` envelope (a plain
/// JSON value that survives the channel). `decodeBinary` reverses it, turning
/// each envelope back into an ArrayBuffer. Plain `number[]` arrays (e.g. TOTP
/// secrets) are left untouched — only real binary buffers are transcoded.
///
/// Apply `encodeBinary` to every outbound request/response and `decodeBinary`
/// to every inbound one. Both are deep but cheap: the structural part of a
/// message is tiny, and each binary buffer is transcoded exactly once.

const MARKER = '__relicario_b64__';

/// Chunked base64 encode — avoids the call-stack blow-up of
/// `btoa(String.fromCharCode(...bytes))` on multi-MB buffers.
function bytesToBase64(bytes: Uint8Array): string {
  let binary = '';
  const CHUNK = 0x8000; // 32 KiB per fromCharCode call
  for (let i = 0; i < bytes.length; i += CHUNK) {
    binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
  }
  return btoa(binary);
}

function base64ToBytes(b64: string): Uint8Array {
  const binary = atob(b64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
  return bytes;
}

function isBinary(v: unknown): v is ArrayBuffer | ArrayBufferView {
  return v instanceof ArrayBuffer || ArrayBuffer.isView(v);
}

function toBytes(v: ArrayBuffer | ArrayBufferView): Uint8Array {
  if (v instanceof ArrayBuffer) return new Uint8Array(v);
  return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
}

/// Replace every binary buffer in `value` with a base64 envelope. Returns a
/// new structure; the input is not mutated.
export function encodeBinary<T>(value: T): T {
  if (isBinary(value)) {
    return { [MARKER]: bytesToBase64(toBytes(value)) } as unknown as T;
  }
  if (Array.isArray(value)) {
    return value.map((v) => encodeBinary(v)) as unknown as T;
  }
  if (value !== null && typeof value === 'object') {
    const out: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
      out[k] = encodeBinary(v);
    }
    return out as T;
  }
  return value;
}

/// Reverse `encodeBinary`: each base64 envelope becomes an ArrayBuffer.
export function decodeBinary<T>(value: T): T {
  if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
    const keys = Object.keys(value as Record<string, unknown>);
    if (keys.length === 1 && keys[0] === MARKER) {
      const b64 = (value as Record<string, string>)[MARKER];
      return base64ToBytes(b64).buffer as unknown as T;
    }
    const out: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
      out[k] = decodeBinary(v);
    }
    return out as T;
  }
  if (Array.isArray(value)) {
    return value.map((v) => decodeBinary(v)) as unknown as T;
  }
  return value;
}
