/**
 * Binary wire format used inside the Shade WebRTC `DataChannel`.
 *
 * The DataChannel is a single bidirectional pipe shared by every in-flight
 * stream between two peers. Each frame begins with a 1-byte type tag and
 * a 16-byte requestId so the responder can correlate replies back to the
 * caller.
 *
 *   Client → server frames
 *   ──────────────────────
 *   0x01  chunk          : requestId(16) streamId(16) laneId(u32 BE) seq(u64 BE) envelope(...)
 *   0x02  resume-query   : requestId(16) streamId(16)
 *   0x03  ping           : requestId(16) nonce(u64 BE)
 *
 *   Server → client frames
 *   ──────────────────────
 *   0x81  chunk-ack      : requestId(16) lastSeq(u32 BE) bytesReceived(u32 BE)
 *   0x82  resume-state   : requestId(16) jsonBody(utf-8)
 *   0x83  pong           : requestId(16) nonce(u64 BE)
 *   0xFE  error          : requestId(16) jsonBody(utf-8)
 *
 * The wire matches `ShadeTransferWsTransport` (see
 * `@shade/transfer/transport/ws-transport.ts`) on purpose so the same
 * mental model applies to both transports.
 */

export const WIRE_CHUNK = 0x01;
export const WIRE_RESUME_QUERY = 0x02;
export const WIRE_PING = 0x03;
export const WIRE_CHUNK_ACK = 0x81;
export const WIRE_RESUME_STATE = 0x82;
export const WIRE_PONG = 0x83;
export const WIRE_ERROR = 0xfe;

export const REQUEST_ID_LEN = 16;
export const STREAM_ID_LEN = 16;
/** Header length before the chunk envelope begins (TYPE + reqId + streamId + laneId + seq). */
export const CHUNK_HEADER_LEN = 1 + REQUEST_ID_LEN + STREAM_ID_LEN + 4 + 8;
/** Header length for resume-query frames (TYPE + reqId + streamId). */
export const RESUME_QUERY_HEADER_LEN = 1 + REQUEST_ID_LEN + STREAM_ID_LEN;
/** Header length for chunk-ack frames (TYPE + reqId + lastSeq + bytesReceived). */
export const CHUNK_ACK_LEN = 1 + REQUEST_ID_LEN + 4 + 4;
/** Header length for ping/pong frames (TYPE + reqId + nonce). */
export const PING_FRAME_LEN = 1 + REQUEST_ID_LEN + 8;

export interface ChunkFrame {
  type: typeof WIRE_CHUNK;
  requestId: Uint8Array;
  streamId: Uint8Array;
  laneId: number;
  seq: bigint;
  envelope: Uint8Array;
}

export interface ResumeQueryFrame {
  type: typeof WIRE_RESUME_QUERY;
  requestId: Uint8Array;
  streamId: Uint8Array;
}

export interface PingFrame {
  type: typeof WIRE_PING;
  requestId: Uint8Array;
  nonce: bigint;
}

export interface ChunkAckFrame {
  type: typeof WIRE_CHUNK_ACK;
  requestId: Uint8Array;
  lastSeq: number;
  bytesReceived: number;
}

export interface ResumeStateFrame {
  type: typeof WIRE_RESUME_STATE;
  requestId: Uint8Array;
  json: string;
}

export interface PongFrame {
  type: typeof WIRE_PONG;
  requestId: Uint8Array;
  nonce: bigint;
}

export interface ErrorFrame {
  type: typeof WIRE_ERROR;
  requestId: Uint8Array;
  json: string;
}

export type ServerFrame = ChunkAckFrame | ResumeStateFrame | PongFrame | ErrorFrame;
export type ClientFrame = ChunkFrame | ResumeQueryFrame | PingFrame;
export type AnyFrame = ServerFrame | ClientFrame;

// ─── Encoders ──────────────────────────────────────────────

export function encodeChunkFrame(f: ChunkFrame): Uint8Array {
  if (f.requestId.length !== REQUEST_ID_LEN) {
    throw new Error(`requestId must be ${REQUEST_ID_LEN} bytes`);
  }
  if (f.streamId.length !== STREAM_ID_LEN) {
    throw new Error(`streamId must be ${STREAM_ID_LEN} bytes`);
  }
  const out = new Uint8Array(CHUNK_HEADER_LEN + f.envelope.length);
  const view = new DataView(out.buffer);
  out[0] = WIRE_CHUNK;
  out.set(f.requestId, 1);
  out.set(f.streamId, 1 + REQUEST_ID_LEN);
  view.setUint32(1 + REQUEST_ID_LEN + STREAM_ID_LEN, f.laneId, false);
  view.setBigUint64(1 + REQUEST_ID_LEN + STREAM_ID_LEN + 4, f.seq, false);
  out.set(f.envelope, CHUNK_HEADER_LEN);
  return out;
}

export function encodeResumeQueryFrame(f: ResumeQueryFrame): Uint8Array {
  const out = new Uint8Array(RESUME_QUERY_HEADER_LEN);
  out[0] = WIRE_RESUME_QUERY;
  out.set(f.requestId, 1);
  out.set(f.streamId, 1 + REQUEST_ID_LEN);
  return out;
}

export function encodePingFrame(f: PingFrame): Uint8Array {
  const out = new Uint8Array(PING_FRAME_LEN);
  const view = new DataView(out.buffer);
  out[0] = WIRE_PING;
  out.set(f.requestId, 1);
  view.setBigUint64(1 + REQUEST_ID_LEN, f.nonce, false);
  return out;
}

export function encodeChunkAckFrame(f: ChunkAckFrame): Uint8Array {
  const out = new Uint8Array(CHUNK_ACK_LEN);
  const view = new DataView(out.buffer);
  out[0] = WIRE_CHUNK_ACK;
  out.set(f.requestId, 1);
  view.setUint32(1 + REQUEST_ID_LEN, f.lastSeq, false);
  view.setUint32(1 + REQUEST_ID_LEN + 4, f.bytesReceived, false);
  return out;
}

export function encodeResumeStateFrame(f: ResumeStateFrame): Uint8Array {
  const enc = new TextEncoder();
  const body = enc.encode(f.json);
  const out = new Uint8Array(1 + REQUEST_ID_LEN + body.length);
  out[0] = WIRE_RESUME_STATE;
  out.set(f.requestId, 1);
  out.set(body, 1 + REQUEST_ID_LEN);
  return out;
}

export function encodePongFrame(f: PongFrame): Uint8Array {
  const out = new Uint8Array(PING_FRAME_LEN);
  const view = new DataView(out.buffer);
  out[0] = WIRE_PONG;
  out.set(f.requestId, 1);
  view.setBigUint64(1 + REQUEST_ID_LEN, f.nonce, false);
  return out;
}

export function encodeErrorFrame(f: ErrorFrame): Uint8Array {
  const enc = new TextEncoder();
  const body = enc.encode(f.json);
  const out = new Uint8Array(1 + REQUEST_ID_LEN + body.length);
  out[0] = WIRE_ERROR;
  out.set(f.requestId, 1);
  out.set(body, 1 + REQUEST_ID_LEN);
  return out;
}

// ─── Decoder ───────────────────────────────────────────────

export function decodeFrame(buf: ArrayBuffer | Uint8Array): AnyFrame {
  const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
  if (bytes.length < 1 + REQUEST_ID_LEN) {
    throw new Error('frame truncated (under header length)');
  }
  const type = bytes[0]!;
  const requestId = bytes.slice(1, 1 + REQUEST_ID_LEN);
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);

  switch (type) {
    case WIRE_CHUNK: {
      if (bytes.length < CHUNK_HEADER_LEN) throw new Error('chunk frame truncated');
      const streamId = bytes.slice(1 + REQUEST_ID_LEN, 1 + REQUEST_ID_LEN + STREAM_ID_LEN);
      const laneId = view.getUint32(1 + REQUEST_ID_LEN + STREAM_ID_LEN, false);
      const seq = view.getBigUint64(1 + REQUEST_ID_LEN + STREAM_ID_LEN + 4, false);
      const envelope = bytes.slice(CHUNK_HEADER_LEN);
      return { type: WIRE_CHUNK, requestId, streamId, laneId, seq, envelope };
    }
    case WIRE_RESUME_QUERY: {
      if (bytes.length < RESUME_QUERY_HEADER_LEN) {
        throw new Error('resume-query frame truncated');
      }
      const streamId = bytes.slice(1 + REQUEST_ID_LEN, RESUME_QUERY_HEADER_LEN);
      return { type: WIRE_RESUME_QUERY, requestId, streamId };
    }
    case WIRE_PING: {
      if (bytes.length < PING_FRAME_LEN) throw new Error('ping frame truncated');
      const nonce = view.getBigUint64(1 + REQUEST_ID_LEN, false);
      return { type: WIRE_PING, requestId, nonce };
    }
    case WIRE_CHUNK_ACK: {
      if (bytes.length < CHUNK_ACK_LEN) throw new Error('chunk-ack frame truncated');
      const lastSeq = view.getUint32(1 + REQUEST_ID_LEN, false);
      const bytesReceived = view.getUint32(1 + REQUEST_ID_LEN + 4, false);
      return { type: WIRE_CHUNK_ACK, requestId, lastSeq, bytesReceived };
    }
    case WIRE_RESUME_STATE: {
      const body = bytes.slice(1 + REQUEST_ID_LEN);
      return { type: WIRE_RESUME_STATE, requestId, json: new TextDecoder().decode(body) };
    }
    case WIRE_PONG: {
      if (bytes.length < PING_FRAME_LEN) throw new Error('pong frame truncated');
      const nonce = view.getBigUint64(1 + REQUEST_ID_LEN, false);
      return { type: WIRE_PONG, requestId, nonce };
    }
    case WIRE_ERROR: {
      const body = bytes.slice(1 + REQUEST_ID_LEN);
      return { type: WIRE_ERROR, requestId, json: new TextDecoder().decode(body) };
    }
    default:
      throw new Error(`unknown frame type 0x${type.toString(16)}`);
  }
}

export function randomRequestId(): Uint8Array {
  const out = new Uint8Array(REQUEST_ID_LEN);
  globalThis.crypto.getRandomValues(out);
  return out;
}

export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

// ─── streamId base64url codec (matches @shade/streams) ──────

export function streamIdStringToBytes(s: string): Uint8Array {
  const padded = s.replace(/-/g, '+').replace(/_/g, '/');
  const pad = padded.length % 4 === 0 ? '' : '='.repeat(4 - (padded.length % 4));
  const bin = atob(padded + pad);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

export function streamIdBytesToString(bytes: Uint8Array): string {
  let bin = '';
  for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!);
  return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
