/**
 * `WebSocketLike` adapters. Two of them:
 *   - `makeSocketPair()` — an in-process duplex pair used to drive the REAL relay-node/mux in tests
 *     (no sockets, deterministic). Delivery is synchronous but non-reentrant (a per-endpoint
 *     trampoline) so a send emitted while draining the peer's inbox can't recurse unboundedly.
 *   - `wsToWebSocketLike()` — wraps a real `ws` WebSocket to the same interface for `main.ts`.
 * Bytes are COPIED on send (the mux slices/retains buffers) so no caller can alias another's memory.
 */
import type { WebSocket as WsWebSocket } from 'ws'
import type { WebSocketLike } from 'term-relay/data-plane/ws-like.js'

interface Endpoint extends WebSocketLike {
  /** Internal: deliver bytes that the peer sent to THIS endpoint. */
  _deliver(data: Uint8Array): void
  _closed(): void
}

function makeEndpoint(): Endpoint {
  let onMessage: ((data: Uint8Array) => void) | null = null
  let onClose: (() => void) | null = null
  let peer: Endpoint | null = null
  const inbox: Uint8Array[] = []
  let draining = false
  let closed = false

  const ep: Endpoint & { _link(p: Endpoint): void } = {
    _link(p: Endpoint) {
      peer = p
    },
    send(data: Uint8Array) {
      if (closed || peer === null) return
      peer._deliver(new Uint8Array(data)) // copy: never alias the sender's buffer
    },
    close(_code?: number) {
      if (closed) return
      closed = true
      onClose?.()
      peer?._closed()
    },
    onMessage(handler) {
      onMessage = handler
    },
    onClose(handler) {
      onClose = handler
    },
    _deliver(data: Uint8Array) {
      inbox.push(data)
      if (draining) return
      draining = true
      try {
        while (inbox.length > 0) {
          const next = inbox.shift()!
          onMessage?.(next)
        }
      } finally {
        draining = false
      }
    },
    _closed() {
      if (closed) return
      closed = true
      onClose?.()
    },
  }
  return ep as Endpoint & { _link(p: Endpoint): void }
}

/** An in-process duplex `WebSocketLike` pair: `a.send` → `b`'s message handler and vice versa. */
export function makeSocketPair(): { a: WebSocketLike; b: WebSocketLike } {
  const a = makeEndpoint() as Endpoint & { _link(p: Endpoint): void }
  const b = makeEndpoint() as Endpoint & { _link(p: Endpoint): void }
  a._link(b)
  b._link(a)
  return { a, b }
}

function toUint8(data: unknown): Uint8Array | null {
  if (data instanceof Uint8Array) return data
  if (data instanceof ArrayBuffer) return new Uint8Array(data)
  if (Array.isArray(data)) return Buffer.concat(data.map((d) => (d instanceof Uint8Array ? d : Buffer.from(d))))
  if (Buffer.isBuffer(data)) return new Uint8Array(data)
  return null
}

/**
 * Adapt a live `ws` WebSocket to `WebSocketLike` (binary frames only).
 *
 * The `ws.on('message'|'close')` listeners are attached NOW (at construction), not when the consumer
 * later calls `onMessage`/`onClose`. The composition root builds this synchronously on the socket's
 * 'connection' event, but the mux consumer only wires its handler AFTER an async mTLS-registry lookup
 * (the bridge). Any frame that arrives in that gap — notably the agent's FIRST heartbeat ping, sent
 * immediately on open — would otherwise be dropped by `ws` (no listener yet), starving the heartbeat
 * and flapping the tunnel every 15 s. We buffer early frames (and an early close) and flush on wire-up.
 */
export function wsToWebSocketLike(ws: WsWebSocket): WebSocketLike {
  let onMessage: ((data: Uint8Array) => void) | null = null
  let onClose: (() => void) | null = null
  const backlog: Uint8Array[] = []
  let closedEarly = false

  ws.on('message', (data: unknown) => {
    const bytes = toUint8(data)
    if (bytes === null) return
    if (onMessage === null) backlog.push(bytes)
    else onMessage(bytes)
  })
  ws.on('close', () => {
    if (onClose === null) closedEarly = true
    else onClose()
  })

  return {
    send(data: Uint8Array) {
      ws.send(data, { binary: true })
    },
    close(code?: number) {
      ws.close(code)
    },
    onMessage(handler) {
      onMessage = handler
      while (backlog.length > 0) handler(backlog.shift()!)
    },
    onClose(handler) {
      onClose = handler
      if (closedEarly) handler()
    },
  }
}
