/**
 * T4 · Credit-based per-stream flow control (§4.1) — pure, in-memory, deny-by-default.
 *
 * Each stream has an independent send window: exhausting stream A's credit MUST NOT affect
 * stream B (heavy `vim`/`top` redraw can't starve another stream). Unknown/released streams
 * carry no credit (deny-by-default: prevents send-after-close write amplification).
 * Immutable-style updates (replace the record, never mutate in place).
 */
export const DEFAULT_INITIAL_WINDOW = 256 * 1024
export const WINDOW_REPLENISH_THRESHOLD = 0.5 // emit WINDOW_UPDATE when half consumed

interface StreamCredit {
  readonly window: number // remaining SEND credit
  readonly initial: number // initial window (for replenish threshold)
  readonly delivered: number // RECEIVED bytes since last replenish
}

export interface FlowController {
  registerStream(streamId: number, initialWindow: number): void
  releaseStream(streamId: number): void
  canSend(streamId: number, bytes: number): boolean
  consumeSendCredit(streamId: number, bytes: number): void
  grantCredit(streamId: number, credit: number): void
  creditFor(streamId: number): number
  onDelivered(streamId: number, bytes: number): number
}

export function createFlowController(): FlowController {
  const streams = new Map<number, StreamCredit>()

  return {
    registerStream(streamId, initialWindow) {
      streams.set(streamId, { window: initialWindow, initial: initialWindow, delivered: 0 })
    },

    releaseStream(streamId) {
      streams.delete(streamId)
    },

    canSend(streamId, bytes) {
      const c = streams.get(streamId)
      if (c === undefined) return false // deny-by-default (unknown/released stream)
      return bytes <= c.window
    },

    consumeSendCredit(streamId, bytes) {
      const c = streams.get(streamId)
      if (c === undefined) return
      const nextWindow = c.window - bytes
      // Never go negative (canSend gates callers; clamp defensively).
      streams.set(streamId, { ...c, window: Math.max(0, nextWindow) })
    },

    grantCredit(streamId, credit) {
      const c = streams.get(streamId)
      if (c === undefined) return
      streams.set(streamId, { ...c, window: c.window + credit })
    },

    creditFor(streamId) {
      return streams.get(streamId)?.window ?? 0
    },

    onDelivered(streamId, bytes) {
      const c = streams.get(streamId)
      if (c === undefined) return 0
      const delivered = c.delivered + bytes
      if (delivered >= c.initial * WINDOW_REPLENISH_THRESHOLD) {
        streams.set(streamId, { ...c, delivered: 0 })
        return delivered // replenish this many bytes of credit back to the peer
      }
      streams.set(streamId, { ...c, delivered })
      return 0
    },
  }
}
