/**
 * §4.1 Mux frame codec — the 15-byte fixed header + opaque payload.
 *
 * Layout (big-endian):
 *   offset size field
 *     0     1   version    0x01
 *     1     1   type       0x01 OPEN … 0x07 GOAWAY
 *     2     1   flags      bit0 FIN · bit1 RST · others reserved=0
 *     3     4   streamId   uint32 (0 = connection-level)
 *     7     8   payloadLen uint64 (safe-int guarded)
 *    15   var   payload    opaque bytes
 */
import { ContractDecodeError, ContractEncodeError } from '../errors.js'
import { assertUint32, readSafeUint64BE, writeUint32BE, writeUint64BE } from '../bytes.js'
import {
  FLAG_FIN,
  FLAG_RST,
  FLAG_RESERVED_MASK,
  MUX_CODE_TO_TYPE,
  MUX_HEADER_BYTES,
  MUX_TYPE_TO_CODE,
  MUX_VERSION,
  type MuxFrameHeader,
} from './types.js'

function encodeFlags(fin: boolean, rst: boolean): number {
  return (fin ? FLAG_FIN : 0) | (rst ? FLAG_RST : 0)
}

/** Encode a full mux frame. `header.payloadLen` MUST equal `payload.length`. */
export function encodeMuxFrame(header: MuxFrameHeader, payload: Uint8Array): Uint8Array {
  if (header.version !== MUX_VERSION) {
    throw new ContractEncodeError(`unsupported mux version ${header.version} (expected ${MUX_VERSION})`)
  }
  const typeCode = MUX_TYPE_TO_CODE[header.type]
  if (typeCode === undefined) {
    throw new ContractEncodeError(`unknown mux frame type '${header.type}'`)
  }
  assertUint32(header.streamId, 'streamId')
  if (header.payloadLen !== payload.length) {
    throw new ContractEncodeError(
      `header.payloadLen (${header.payloadLen}) must equal payload.length (${payload.length})`,
    )
  }
  const out = new Uint8Array(MUX_HEADER_BYTES + payload.length)
  out[0] = MUX_VERSION
  out[1] = typeCode
  out[2] = encodeFlags(header.fin, header.rst)
  let offset = writeUint32BE(out, 3, header.streamId)
  offset = writeUint64BE(out, offset, BigInt(payload.length))
  out.set(payload, MUX_HEADER_BYTES)
  return out
}

/** Decode ONLY the 15-byte header (no payload slice). */
export function decodeHeader(buf: Uint8Array): MuxFrameHeader {
  if (buf.length < MUX_HEADER_BYTES) {
    throw new ContractDecodeError(
      `mux frame shorter than header: ${buf.length} < ${MUX_HEADER_BYTES}`,
    )
  }
  const version = buf[0]!
  if (version !== MUX_VERSION) {
    throw new ContractDecodeError(`unsupported mux version ${version} (expected ${MUX_VERSION})`)
  }
  const type = MUX_CODE_TO_TYPE[buf[1]!]
  if (type === undefined) {
    throw new ContractDecodeError(`unknown mux frame type code 0x${buf[1]!.toString(16)}`)
  }
  const flags = buf[2]!
  if ((flags & FLAG_RESERVED_MASK) !== 0) {
    throw new ContractDecodeError(`reserved flag bits set: 0x${flags.toString(16)}`)
  }
  const streamId = ((buf[3]! << 24) >>> 0) + (buf[4]! << 16) + (buf[5]! << 8) + buf[6]!
  const payloadLen = readSafeUint64BE(buf, 7, 'payloadLen')
  return {
    version: MUX_VERSION,
    type,
    fin: (flags & FLAG_FIN) !== 0,
    rst: (flags & FLAG_RST) !== 0,
    streamId,
    payloadLen,
  }
}

/** Decode a full mux frame into its header + payload slice. Rejects a truncated payload. */
export function decodeMuxFrame(buf: Uint8Array): { header: MuxFrameHeader; payload: Uint8Array } {
  const header = decodeHeader(buf)
  const end = MUX_HEADER_BYTES + header.payloadLen
  if (buf.length < end) {
    throw new ContractDecodeError(
      `truncated payload: have ${buf.length - MUX_HEADER_BYTES} bytes, header says ${header.payloadLen}`,
    )
  }
  const payload = buf.slice(MUX_HEADER_BYTES, end)
  return { header, payload }
}
