///|
/// Errors raised by a frame admission policy.
pub suberror AdmissionError {
  InvalidRule
  NoMatchingRule
  ProtocolNotAllowed
  PayloadTooLarge
  RemoteNotAllowed
  RateExceeded
  TimestampReversed
} derive(Debug)

///|
/// A named rule for a group of accepted frames.
pub struct AdmissionRule {
  name : String
  filter : Filter
  max_payload : Int
  allow_fd : Bool
  allow_remote : Bool
  min_interval_us : UInt64
  mut last_seen_us : UInt64?
}

///|
/// The result of an admission check.
pub struct AdmissionDecision {
  accepted : Bool
  rule_name : String
  reason : String
}

///|
/// A first-match frame admission policy with counters and timing guards.
pub struct AdmissionPolicy {
  rules : Array[AdmissionRule]
  mut accepted_count : Int
  mut rejected_count : Int
  mut last_timestamp_us : UInt64?
}

///|
/// Construct a policy rule.
pub fn admission_rule(
  name : String,
  filter : Filter,
  max_payload? : Int = 64,
  allow_fd? : Bool = true,
  allow_remote? : Bool = false,
  min_interval_us? : UInt64 = 0,
) -> AdmissionRule raise AdmissionError {
  if name.is_empty() || max_payload < 0 || max_payload > 64 {
    raise InvalidRule
  }
  {
    name,
    filter,
    max_payload,
    allow_fd,
    allow_remote,
    min_interval_us,
    last_seen_us: None,
  }
}

///|
pub fn new_admission_policy() -> AdmissionPolicy {
  { rules: [], accepted_count: 0, rejected_count: 0, last_timestamp_us: None }
}

///|
pub fn AdmissionPolicy::add_rule(
  self : AdmissionPolicy,
  rule : AdmissionRule,
) -> Unit {
  self.rules.push(rule)
}

///|
/// Evaluate the first matching rule and update its rate window on success.
pub fn AdmissionPolicy::evaluate(
  self : AdmissionPolicy,
  frame : Frame,
  timestamp_us : UInt64,
) -> AdmissionDecision raise AdmissionError {
  match self.last_timestamp_us {
    Some(previous) => if timestamp_us < previous { raise TimestampReversed }
    None => ()
  }
  self.last_timestamp_us = Some(timestamp_us)
  for index in 0.. rule.max_payload {
        self.rejected_count += 1
        raise PayloadTooLarge
      }
      match rule.last_seen_us {
        Some(previous) => {
          if timestamp_us < previous {
            self.rejected_count += 1
            raise TimestampReversed
          }
          if rule.min_interval_us > 0 &&
            timestamp_us - previous < rule.min_interval_us {
            self.rejected_count += 1
            raise RateExceeded
          }
        }
        None => ()
      }
      self.rules[index].last_seen_us = Some(timestamp_us)
      self.accepted_count += 1
      return { accepted: true, rule_name: rule.name, reason: "accepted" }
    }
  }
  self.rejected_count += 1
  raise NoMatchingRule
}

///|
/// Return whether a frame is admitted, preserving the policy counters.
pub fn AdmissionPolicy::admit(
  self : AdmissionPolicy,
  frame : Frame,
  timestamp_us : UInt64,
) -> Bool {
  try self.evaluate(frame, timestamp_us) catch {
    _ => false
  } noraise {
    _ => true
  }
}

///|
/// Reset all per-rule timing windows and counters.
pub fn AdmissionPolicy::reset(self : AdmissionPolicy) -> Unit {
  for index in 0.. Int {
  self.rules.length()
}

///|
pub fn AdmissionPolicy::accepted(self : AdmissionPolicy) -> Int {
  self.accepted_count
}

///|
pub fn AdmissionPolicy::rejected(self : AdmissionPolicy) -> Int {
  self.rejected_count
}

///|
pub fn AdmissionDecision::accepted(self : AdmissionDecision) -> Bool {
  self.accepted
}

///|
pub fn AdmissionDecision::rule_name(self : AdmissionDecision) -> String {
  self.rule_name
}

///|
pub fn AdmissionDecision::reason(self : AdmissionDecision) -> String {
  self.reason
}

///|
pub fn AdmissionRule::name(self : AdmissionRule) -> String {
  self.name
}

///|
pub fn AdmissionRule::max_payload(self : AdmissionRule) -> Int {
  self.max_payload
}

///|
pub fn AdmissionRule::allows_fd(self : AdmissionRule) -> Bool {
  self.allow_fd
}

///|
pub fn AdmissionRule::allows_remote(self : AdmissionRule) -> Bool {
  self.allow_remote
}

///|
pub fn AdmissionRule::minimum_interval(self : AdmissionRule) -> UInt64 {
  self.min_interval_us
}