/**
 * Minimal deterministic CBOR (RFC 8949) subset — just enough for the §4.1 MuxOpen
 * payload: text strings (major 3), unsigned integers (major 0), and one flat map
 * (major 5). Hand-rolled (KISS / dependency-free) so the browser and agent bundles
 * agree byte-for-byte without pulling a full CBOR library.
 *
 * Determinism: maps are encoded in a caller-fixed key order (MuxOpen has a frozen
 * field order), giving a stable byte vector for KAT tests. Decoding is order-independent
 * (reads into an object, then Zod validates).
 */
import { ContractDecodeError, ContractEncodeError } from '../errors.js'
import { concatBytes } from '../bytes.js'

export type CborScalar = string | number
export type CborMap = Readonly<Record<string, CborScalar>>

const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder('utf-8', { fatal: true })

/** Encode a CBOR length/value head for the given major type (0..7). */
function encodeHead(major: number, n: number): Uint8Array {
  if (!Number.isInteger(n) || n < 0) {
    throw new ContractEncodeError(`CBOR head requires a non-negative integer; got ${n}`)
  }
  const prefix = major << 5
  if (n < 24) return Uint8Array.of(prefix | n)
  if (n < 0x100) return Uint8Array.of(prefix | 24, n)
  if (n < 0x1_0000) return Uint8Array.of(prefix | 25, (n >> 8) & 0xff, n & 0xff)
  if (n < 0x1_0000_0000) {
    return Uint8Array.of(
      prefix | 26,
      (n >>> 24) & 0xff,
      (n >>> 16) & 0xff,
      (n >>> 8) & 0xff,
      n & 0xff,
    )
  }
  throw new ContractEncodeError(`CBOR head value too large for this subset: ${n}`)
}

function encodeUint(n: number): Uint8Array {
  return encodeHead(0, n)
}

function encodeText(s: string): Uint8Array {
  const utf8 = textEncoder.encode(s)
  return concatBytes([encodeHead(3, utf8.length), utf8])
}

function encodeScalar(value: CborScalar): Uint8Array {
  if (typeof value === 'string') return encodeText(value)
  if (typeof value === 'number') {
    if (!Number.isInteger(value) || value < 0) {
      throw new ContractEncodeError(`CBOR scalar number must be a non-negative integer; got ${value}`)
    }
    return encodeUint(value)
  }
  throw new ContractEncodeError(`unsupported CBOR scalar type: ${typeof value}`)
}

/**
 * Encode a flat map in the caller-provided key order (deterministic).
 * `keyOrder` must list exactly the keys present in `map`.
 */
export function encodeCborMap(map: CborMap, keyOrder: readonly string[]): Uint8Array {
  const keys = Object.keys(map)
  if (keys.length !== keyOrder.length) {
    throw new ContractEncodeError(
      `CBOR map has ${keys.length} keys but keyOrder lists ${keyOrder.length}`,
    )
  }
  const chunks: Uint8Array[] = [encodeHead(5, keyOrder.length)]
  for (const key of keyOrder) {
    if (!Object.prototype.hasOwnProperty.call(map, key)) {
      throw new ContractEncodeError(`CBOR map missing key '${key}' named in keyOrder`)
    }
    chunks.push(encodeText(key))
    chunks.push(encodeScalar(map[key]!))
  }
  return concatBytes(chunks)
}

interface Cursor {
  readonly buf: Uint8Array
  offset: number
}

function readHead(c: Cursor): { major: number; value: number } {
  if (c.offset >= c.buf.length) {
    throw new ContractDecodeError('CBOR truncated: expected a head byte')
  }
  const first = c.buf[c.offset]!
  c.offset += 1
  const major = first >> 5
  const info = first & 0x1f
  if (info < 24) return { major, value: info }
  if (info === 24) {
    if (c.offset + 1 > c.buf.length) throw new ContractDecodeError('CBOR truncated uint8 arg')
    const v = c.buf[c.offset]!
    c.offset += 1
    return { major, value: v }
  }
  if (info === 25) {
    if (c.offset + 2 > c.buf.length) throw new ContractDecodeError('CBOR truncated uint16 arg')
    const v = (c.buf[c.offset]! << 8) | c.buf[c.offset + 1]!
    c.offset += 2
    return { major, value: v }
  }
  if (info === 26) {
    if (c.offset + 4 > c.buf.length) throw new ContractDecodeError('CBOR truncated uint32 arg')
    const v =
      ((c.buf[c.offset]! << 24) >>> 0) +
      (c.buf[c.offset + 1]! << 16) +
      (c.buf[c.offset + 2]! << 8) +
      c.buf[c.offset + 3]!
    c.offset += 4
    return { major, value: v }
  }
  throw new ContractDecodeError(`unsupported CBOR additional-info ${info} in this subset`)
}

function readScalar(c: Cursor): CborScalar {
  const { major, value } = readHead(c)
  if (major === 0) return value // unsigned int
  if (major === 3) {
    // text string
    const end = c.offset + value
    if (end > c.buf.length) throw new ContractDecodeError('CBOR truncated text string')
    const slice = c.buf.subarray(c.offset, end)
    c.offset = end
    try {
      return textDecoder.decode(slice)
    } catch {
      throw new ContractDecodeError('CBOR text string is not valid UTF-8')
    }
  }
  throw new ContractDecodeError(`unsupported CBOR scalar major type ${major} in this subset`)
}

/** Decode a flat CBOR map into a plain object of scalars. Rejects trailing bytes. */
export function decodeCborMap(buf: Uint8Array): Record<string, CborScalar> {
  const c: Cursor = { buf, offset: 0 }
  const { major, value: count } = readHead(c)
  if (major !== 5) {
    throw new ContractDecodeError(`expected CBOR map (major 5); got major ${major}`)
  }
  const out: Record<string, CborScalar> = {}
  for (let i = 0; i < count; i++) {
    const key = readScalar(c)
    if (typeof key !== 'string') {
      throw new ContractDecodeError('CBOR map key must be a text string')
    }
    if (Object.prototype.hasOwnProperty.call(out, key)) {
      throw new ContractDecodeError(`duplicate CBOR map key '${key}'`)
    }
    out[key] = readScalar(c)
  }
  if (c.offset !== buf.length) {
    throw new ContractDecodeError(`trailing bytes after CBOR map: ${buf.length - c.offset} extra`)
  }
  return out
}
