// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// Address validation tokens (RFC 9000 §8.1.2, §17.2.5): what a server gives a client in a
// Retry, and what the client hands back on every Initial after it. A token is the server's own
// bytes — no state is kept between the two packets — and it carries what the server has to know
// when the second one arrives: who it was talking to, which connection id the first Initial
// named, and when. The tag makes the rest unforgeable, and the time bound stops a token being
// used long after it was issued.

///|
/// How long a token stays good, in microseconds. Long enough for a client's second flight,
/// short enough that a recorded token is of little use (RFC 9000 §8.1.2 asks for a bound).
let token_life : Int64 = 10_000_000L

///|
/// The token's first byte, so a later change of layout can be told from this one.
let token_format : Byte = b'\x01'

///|
/// What a token says: the address it was issued for, and the connection id the first Initial
/// named (the Retry's tag and the Initial keys are bound to it).
pub struct Token {
  address : Bytes
  odcid : Bytes
}

///|
/// The token for a client at `address` whose first Initial named `odcid`, stamped with `now`.
/// An endpoint that cannot name an address passes an empty one, and the token then binds the
/// connection id and the time alone.
pub fn issue_token(
  key : Bytes,
  address : Bytes,
  odcid : Bytes,
  now : Int64,
) -> Bytes {
  let body = Buffer()
  body.write_byte(token_format)
  body.write_byte(address.length().to_byte())
  body.write_bytes(address)
  body.write_byte(odcid.length().to_byte())
  body.write_bytes(odcid)
  @fixed.write_u64(body, now.reinterpret_as_uint64())
  let pre = body.to_bytes()
  let tag = @crypto.token_tag(key[:], pre[:])
  let out = Buffer()
  out.write_bytes(pre)
  out.write_bytes(tag)
  out.to_bytes()
}

///|
/// What a token says, or `None` when it is short, of another format, tagged with another key, or
/// older than `token_life`. `now` is the same clock the token was stamped from.
pub fn read_token(
  key : Bytes,
  token : Bytes,
  now : Int64,
  address : Bytes,
) -> Token? {
  guard token.length() >= 2 + 1 + 8 + 16 else { return None }
  guard @crypto.token_ok(key[:], token[:]) else { return None }
  let body = token.length() - 16
  guard token[0] == token_format else { return None }
  let addr_len = token[1].to_int()
  let mut at = 2
  guard body >= at + addr_len + 1 else { return None }
  let bound = token[at:at + addr_len].to_owned()
  at = at + addr_len
  let odcid_len = token[at].to_int()
  at = at + 1
  guard body >= at + odcid_len + 8 else { return None }
  let odcid = token[at:at + odcid_len].to_owned()
  at = at + odcid_len
  let stamped = match @fixed.read_u64(token[:], at~) {
    Some(v) => v.reinterpret_as_int64()
    None => return None
  }
  if now - stamped > token_life || stamped > now {
    return None
  }
  // An endpoint given no way to name its peers issues tokens with an empty address, and those
  // are good for any address: nothing was bound.
  if bound.length() > 0 && bound != address {
    return None
  }
  Some({ address: bound, odcid, })
}