///|
/// Resource limits prevent malformed peers from exhausting a gateway.
pub struct ResourceLimits {
  max_apdu : Int
  max_asdu : Int
  max_objects : Int
  max_history : Int
  max_commands : Int
  max_trace_events : Int
} derive(Eq, Debug)

///|
pub fn ResourceLimits::default() -> ResourceLimits {
  {
    max_apdu: 255,
    max_asdu: 249,
    max_objects: 127,
    max_history: 4096,
    max_commands: 256,
    max_trace_events: 4096,
  }
}

///|
pub fn ResourceLimits::new(
  max_apdu : Int,
  max_asdu : Int,
  max_objects : Int,
  max_history : Int,
  max_commands : Int,
  max_trace_events : Int,
) -> Result[ResourceLimits, String] {
  if max_apdu < 6 || max_apdu > 255 {
    Err("APDU resource limit is outside 6..255")
  } else if max_asdu < 1 || max_asdu > 249 {
    Err("ASDU resource limit is outside 1..249")
  } else if max_objects < 1 || max_objects > 127 {
    Err("object resource limit is outside 1..127")
  } else if max_history < 1 || max_commands < 1 || max_trace_events < 1 {
    Err("resource limits must be positive")
  } else {
    Ok({
      max_apdu,
      max_asdu,
      max_objects,
      max_history,
      max_commands,
      max_trace_events,
    })
  }
}

///|
pub fn ResourceLimits::max_apdu(self : ResourceLimits) -> Int {
  self.max_apdu
}

///|
pub fn ResourceLimits::max_asdu(self : ResourceLimits) -> Int {
  self.max_asdu
}

///|
pub fn ResourceLimits::max_objects(self : ResourceLimits) -> Int {
  self.max_objects
}

///|
pub fn ResourceLimits::max_history(self : ResourceLimits) -> Int {
  self.max_history
}

///|
pub fn ResourceLimits::max_commands(self : ResourceLimits) -> Int {
  self.max_commands
}

///|
pub fn ResourceLimits::max_trace_events(self : ResourceLimits) -> Int {
  self.max_trace_events
}

///|
pub enum AdmissionDecision {
  AcceptedAdmission
  RejectedAdmission(Diagnostic)
} derive(Debug)

///|
pub fn admit_frame(frame : Frame, limits : ResourceLimits) -> AdmissionDecision {
  if encoded_frame_size(frame) > limits.max_apdu() {
    RejectedAdmission(
      Diagnostic::new(MalformedFrame, "APDU exceeds configured resource limit"),
    )
  } else if frame.payload.length() > limits.max_asdu() {
    RejectedAdmission(
      Diagnostic::new(MalformedFrame, "ASDU exceeds configured resource limit"),
    )
  } else {
    AcceptedAdmission
  }
}

///|
pub fn admit_envelope(
  envelope : AsduEnvelope,
  limits : ResourceLimits,
) -> AdmissionDecision {
  if envelope.count() > limits.max_objects() {
    RejectedAdmission(
      Diagnostic::new(
        InvalidQualifier,
        "ASDU object count exceeds configured limit",
      ),
    )
  } else {
    AcceptedAdmission
  }
}

///|
/// A fixed-window rate limiter for commands and diagnostics.
pub struct RateLimiter {
  mut window_start : Int
  window_size : Int
  limit : Int
  mut used : Int
} derive(Eq, Debug)

///|
pub fn RateLimiter::new(
  window_size : Int,
  limit : Int,
) -> Result[RateLimiter, String] {
  if window_size < 1 || limit < 1 {
    Err("rate limiter parameters must be positive")
  } else {
    Ok({ window_start: 0, window_size, limit, used: 0 })
  }
}

///|
pub fn RateLimiter::allow(self : RateLimiter, now : Int) -> Bool {
  if now < self.window_start {
    false
  } else {
    if now - self.window_start >= self.window_size {
      self.window_start = now
      self.used = 0
    }
    if self.used >= self.limit {
      false
    } else {
      self.used += 1
      true
    }
  }
}

///|
pub fn RateLimiter::used(self : RateLimiter) -> Int {
  self.used
}

///|
pub fn RateLimiter::remaining(self : RateLimiter) -> Int {
  self.limit - self.used
}

///|
/// Replay guard keyed by APDU checksum and a bounded time interval.
pub struct ReplayGuard {
  seen : Map[UInt, Int]
  ttl : Int
  limit : Int
} derive(Debug)

///|
pub fn ReplayGuard::new(ttl : Int, limit : Int) -> Result[ReplayGuard, String] {
  if ttl < 1 || limit < 1 {
    Err("replay guard parameters must be positive")
  } else {
    Ok({ seen: {}, ttl, limit })
  }
}

///|
pub fn ReplayGuard::is_replay(
  self : ReplayGuard,
  frame : Bytes,
  now : Int,
) -> Bool {
  let fingerprint = crc32_ieee(frame)
  match self.seen.get(fingerprint) {
    Some(previous) => now - previous <= self.ttl
    None => false
  }
}

///|
pub fn ReplayGuard::remember(
  self : ReplayGuard,
  frame : Bytes,
  now : Int,
) -> Result[Unit, String] {
  if self.seen.length() >= self.limit &&
    self.seen.get(crc32_ieee(frame)) is None {
    Err("replay guard capacity is full")
  } else {
    self.seen[crc32_ieee(frame)] = now
    Ok(())
  }
}

///|
pub fn ReplayGuard::purge(self : ReplayGuard, now : Int) -> Int {
  let expired : Array[UInt] = []
  for fingerprint, timestamp in self.seen {
    if now - timestamp > self.ttl {
      expired.push(fingerprint)
    }
  }
  for fingerprint in expired {
    self.seen.remove(fingerprint)
  }
  expired.length()
}

///|
pub fn ReplayGuard::len(self : ReplayGuard) -> Int {
  self.seen.length()
}

///|
/// Admission pipeline for an inbound APDU.
pub fn admit_inbound(
  data : Bytes,
  limits : ResourceLimits,
  replay : ReplayGuard,
  now : Int,
) -> Result[Frame, Diagnostic] {
  if replay.is_replay(data, now) {
    Err(
      Diagnostic::new(MalformedFrame, "duplicate APDU rejected by replay guard"),
    )
  } else if data.length() > limits.max_apdu() {
    Err(Diagnostic::new(MalformedFrame, "APDU exceeds inbound limit"))
  } else {
    match decode_frame(data) {
      Err(message) => Err(Diagnostic::new(MalformedFrame, message))
      Ok(frame) =>
        match admit_frame(frame, limits) {
          AcceptedAdmission => {
            let _ = replay.remember(data, now)
            Ok(frame)
          }
          RejectedAdmission(error) => Err(error)
        }
    }
  }
}

///|
pub fn security_examples() -> Array[AdmissionDecision] {
  let limits = ResourceLimits::default()
  [
    admit_frame(supervisory_frame(0), limits),
    admit_frame(information_frame(0, 0, b"x"), limits),
  ]
}