/**
 * §4.1 tunnel holder — PLAN_RELAY_AGENT T7. Holds ONE physical mux over a WsLike socket:
 * encodes outbound frames, decodes inbound frames (via the FROZEN relay-contracts codec — never
 * re-implemented), and dispatches by type to the router (OPEN/DATA/CLOSE) or heartbeat
 * (PING/PONG) or connection-level control (GOAWAY/WINDOW_UPDATE, streamId 0).
 *
 * INV11: payloads are OPAQUE — no ANSI/terminal parsing here. Malformed frames RST the affected
 * stream (never the whole tunnel). After an inbound GOAWAY the tunnel DRAINS: no new OPEN.
 *
 * Design note (vs plan `holdTunnel(socket, router, heartbeat)`): to keep the construction graph
 * ACYCLIC (router/heartbeat are built WITH the tunnel), wiring is a two-phase `dispatchTo()`
 * call rather than constructor args. Same behavior, no task cycle. Recorded as a deviation.
 */
import {
  decodeGoaway,
  decodeMuxFrame,
  decodeOpen,
  encodeGoaway,
  encodeMuxFrame,
} from 'relay-contracts'
import type { GoAwayReason, MuxFrameHeader, MuxOpen } from 'relay-contracts'
import type { WsLike } from './seams.js'

const EMPTY = new Uint8Array(0)

/** Handlers the tunnel dispatches decoded frames to (wired post-construction). */
export interface StreamHandlers {
  handleOpen(open: MuxOpen): void
  handleData(streamId: number, payload: Uint8Array): void
  handleClose(streamId: number, rst: boolean): void
}
export interface HeartbeatSink {
  onPing(token: Uint8Array): void
  onPong(token: Uint8Array): void
}

export interface Tunnel {
  send(header: MuxFrameHeader, payload: Uint8Array): void
  onFrame(cb: (h: MuxFrameHeader, payload: Uint8Array) => void): void
  onGoAway(cb: (reason: GoAwayReason) => void): void
  goAway(lastStreamId: number, reason: GoAwayReason): void
  sendRst(streamId: number): void
  dispatchTo(router: StreamHandlers, heartbeat: HeartbeatSink): void
  close(): void
}

// --- frame-header builders (shared across the transport layer) --------------------------------

export function dataHeader(streamId: number, payloadLen: number): MuxFrameHeader {
  return { version: 1, type: 'data', fin: false, rst: false, streamId, payloadLen }
}
export function closeHeader(streamId: number, rst: boolean): MuxFrameHeader {
  return { version: 1, type: 'close', fin: !rst, rst, streamId, payloadLen: 0 }
}
export function rstHeader(streamId: number): MuxFrameHeader {
  return closeHeader(streamId, true)
}
export function pingHeader(): MuxFrameHeader {
  return { version: 1, type: 'ping', fin: false, rst: false, streamId: 0, payloadLen: 8 }
}
export function pongHeader(): MuxFrameHeader {
  return { version: 1, type: 'pong', fin: false, rst: false, streamId: 0, payloadLen: 8 }
}

function toU8(data: unknown): Uint8Array | null {
  if (data instanceof Uint8Array) return data
  if (data instanceof ArrayBuffer) return new Uint8Array(data)
  if (Array.isArray(data) && data[0] instanceof Uint8Array) return data[0] as Uint8Array
  return null
}

export function holdTunnel(socket: WsLike): Tunnel {
  let frameCb: ((h: MuxFrameHeader, payload: Uint8Array) => void) | null = null
  let goAwayCb: ((reason: GoAwayReason) => void) | null = null
  let handlers: StreamHandlers | null = null
  let heartbeat: HeartbeatSink | null = null
  let draining = false

  function send(header: MuxFrameHeader, payload: Uint8Array): void {
    socket.send(encodeMuxFrame(header, payload))
  }
  function sendRst(streamId: number): void {
    send(rstHeader(streamId), EMPTY)
  }

  function dispatch(header: MuxFrameHeader, payload: Uint8Array): void {
    frameCb?.(header, payload)
    switch (header.type) {
      case 'ping':
        heartbeat?.onPing(payload)
        return
      case 'pong':
        heartbeat?.onPong(payload)
        return
      case 'goaway': {
        const { reason } = decodeGoaway(payload)
        draining = true
        goAwayCb?.(reason)
        return
      }
      case 'open': {
        if (draining) {
          sendRst(header.streamId) // drain: refuse new streams
          return
        }
        let open: MuxOpen
        try {
          open = decodeOpen(payload)
        } catch {
          sendRst(header.streamId) // malformed OPEN → RST that stream, tunnel stays up
          return
        }
        handlers?.handleOpen(open)
        return
      }
      case 'data':
        handlers?.handleData(header.streamId, payload)
        return
      case 'close':
        handlers?.handleClose(header.streamId, header.rst)
        return
      case 'windowUpdate':
        return // consumed by the flow controller at the wiring layer (T11)
    }
  }

  socket.on('message', (...args: unknown[]) => {
    const bytes = toU8(args[0])
    if (bytes === null) return
    let decoded: { header: MuxFrameHeader; payload: Uint8Array }
    try {
      decoded = decodeMuxFrame(bytes)
    } catch {
      return // malformed framing: drop the frame, keep the tunnel alive (robust framing)
    }
    dispatch(decoded.header, decoded.payload)
  })

  return {
    send,
    sendRst,
    onFrame(cb): void {
      frameCb = cb
    },
    onGoAway(cb): void {
      goAwayCb = cb
    },
    goAway(lastStreamId: number, reason: GoAwayReason): void {
      draining = true
      const payload = encodeGoaway(lastStreamId, reason)
      send({ version: 1, type: 'goaway', fin: false, rst: false, streamId: 0, payloadLen: payload.length }, payload)
    },
    dispatchTo(router: StreamHandlers, hb: HeartbeatSink): void {
      handlers = router
      heartbeat = hb
    },
    close(): void {
      socket.close()
    },
  }
}
