///|
pub struct Policy {
  enter_percent : Double
  exit_percent : Double
  enter_intervals : Int32
  exit_intervals : Int32
  min_headroom_bytes : Int64
} derive(Debug, Eq)

///|
/// Percent thresholds apply independently to PSI some and throttled periods.
/// These defaults are heuristics; callers tune for their workload.
pub fn Policy::new(
  enter_percent~ : Double = 20.0,
  exit_percent~ : Double = 5.0,
  enter_intervals~ : Int32 = 2,
  exit_intervals~ : Int32 = 3,
  min_headroom_bytes~ : Int64 = 67108864L,
) -> Policy raise ParseError {
  guard exit_percent >= 0.0 &&
    enter_percent <= 100.0 &&
    exit_percent < enter_percent &&
    enter_intervals >= 1 &&
    enter_intervals <= 1000 &&
    exit_intervals >= 1 &&
    exit_intervals <= 1000 &&
    min_headroom_bytes >= 0L else {
    raise Invalid("invalid policy thresholds")
  }
  {
    enter_percent,
    exit_percent,
    enter_intervals,
    exit_intervals,
    min_headroom_bytes,
  }
}

///|
pub struct Controller {
  priv policy : Policy
  priv mut state : Admission
  priv mut high_streak : Int32
  priv mut low_streak : Int32
  priv mut identity : String?
  priv mut last_end_us : Int64?
}

///|
pub fn Controller::new(policy : Policy) -> Controller {
  {
    policy,
    state: Ready,
    high_streak: 0,
    low_streak: 0,
    identity: None,
    last_end_us: None,
  }
}

///|
/// Explicitly reset after a source/generation change. No invalid update mutates state.
pub fn Controller::reset(self : Controller) -> Unit {
  self.state = Ready
  self.high_streak = 0
  self.low_streak = 0
  self.identity = None
  self.last_end_us = None
}

///|
/// Consume contiguous intervals exactly once. Missing PSI prevents automatic recovery.
pub fn Controller::update(
  self : Controller,
  sample : Interval,
  limits : Budget,
) -> Advice raise DeltaError {
  if self.identity is Some(old) && old != sample.identity {
    raise IdentityChanged
  }
  if self.last_end_us is Some(old) && sample.end_us - sample.elapsed_us != old {
    raise InvalidInterval
  }
  let p = self.policy
  let reasons : Array[Reason] = []
  let mut low = true
  let mut missing = false
  let streams = [
    (sample.cpu_stall, CpuStall),
    (sample.memory_stall, MemoryStall),
    (sample.io_stall, IoStall),
  ]
  for item in streams {
    let (stall, reason) = item
    match stall {
      Some(value) => {
        if value.some_percent >= p.enter_percent {
          reasons.push(reason)
        }
        if value.some_percent > p.exit_percent {
          low = false
        }
      }
      None => {
        missing = true
        low = false
      }
    }
  }
  if sample.throttled_period_percent is Some(value) {
    if value >= p.enter_percent {
      reasons.push(CpuThrottled)
    }
    if value > p.exit_percent {
      low = false
    }
  }
  if limits.hard_headroom_bytes is Some(free) && free <= p.min_headroom_bytes {
    reasons.push(LowMemoryHeadroom)
    low = false
  }
  if limits.high_headroom_bytes is Some(0L) || sample.memory_high_events > 0L {
    reasons.push(MemoryHigh)
    low = false
  }
  if sample.memory_max_events > 0L {
    reasons.push(MemoryMax)
    low = false
  }
  let emergency = sample.oom_events > 0L || sample.oom_kills > 0L
  if emergency {
    reasons.push(OutOfMemory)
    low = false
  }
  let high = !reasons.is_empty()
  self.high_streak = if high {
    Int32::min(self.high_streak + 1, p.enter_intervals)
  } else {
    0
  }
  self.low_streak = if low {
    Int32::min(self.low_streak + 1, p.exit_intervals)
  } else {
    0
  }
  if emergency || self.high_streak >= p.enter_intervals {
    self.state = Shed
  }
  if self.state == Shed && self.low_streak >= p.exit_intervals {
    self.state = Ready
  }
  if self.state == Shed && !high {
    reasons.push(Recovering)
  }
  if missing {
    reasons.push(MissingPressure)
  }
  self.identity = Some(sample.identity)
  self.last_end_us = Some(sample.end_us)
  {
    state: self.state,
    reasons,
    high_streak: self.high_streak,
    low_streak: self.low_streak,
  }
}

///|
pub extend Policy with Debug::{to_repr}

///|
pub extend Policy with Eq::{equal, not_equal}