/**
 * T7 — sequence guard (anti-replay/injection, INV13).
 *
 * The transport (§4.1 mux over an ordered WS/TCP stream) delivers per-stream in order, so INV13 is
 * enforced as STRICT SUCCESSOR: recv requires `seq === last + 1` (from 0). A duplicate, reorder,
 * gap, or rewind all raise `ReplayError`. There is deliberately NO sliding acceptance window.
 */
import { ReplayError } from './errors.js'

const U64_MAX = 2n ** 64n - 1n

export class SequenceGuard {
  readonly role: 'send' | 'recv'
  #next: bigint = 0n
  #lastAccepted: bigint | null = null

  constructor(role: 'send' | 'recv') {
    this.role = role
  }

  /** send-side: return the current seq, then increment (strictly monotonic per direction). */
  next(): bigint {
    if (this.role !== 'send') {
      throw new ReplayError('next() called on a recv guard')
    }
    const seq = this.#next
    if (seq > U64_MAX) {
      throw new ReplayError('sequence exhausted u64 — re-key required')
    }
    this.#next += 1n
    return seq
  }

  /** recv-side: require the strict successor; else throw ReplayError (dup/reorder/gap/rewind). */
  accept(seq: bigint): void {
    if (this.role !== 'recv') {
      throw new ReplayError('accept() called on a send guard')
    }
    if (seq < 0n || seq > U64_MAX) {
      throw new ReplayError(`sequence out of u64 range: ${seq}`)
    }
    const expected = this.#lastAccepted === null ? 0n : this.#lastAccepted + 1n
    if (seq !== expected) {
      throw new ReplayError(`out-of-order sequence: expected ${expected}, got ${seq}`)
    }
    this.#lastAccepted = seq
  }

  get lastAccepted(): bigint | null {
    return this.#lastAccepted
  }

  /** Next expected recv seq (send guards report their next send seq). */
  get expected(): bigint {
    return this.role === 'send'
      ? this.#next
      : this.#lastAccepted === null
        ? 0n
        : this.#lastAccepted + 1n
  }
}
