///|
/// Runtime-facing records and guards for serving probabilistic predictions.
/// The package remains model-agnostic: inference is performed by the caller,
/// while this layer handles validation, interval policy, rollout accounting,
/// and auditable service health signals.
pub struct ServingRequest {
  request_id : String
  entity_id : String
  features : Array[Double]
  received_at : Int
  schema_version : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn ServingRequest::new(
  request_id : String,
  entity_id : String,
  features : Array[Double],
  received_at : Int,
) -> ServingRequest {
  if request_id == "" ||
    entity_id == "" ||
    features.length() == 0 ||
    received_at < 0 {
    abort("ServingRequest: invalid request identity, features, or timestamp")
  }
  for value in features.iter() {
    if value != value || value.is_inf() {
      abort("ServingRequest: features must be finite")
    }
  }
  { request_id, entity_id, features, received_at, schema_version: "" }
}

///|
pub fn ServingRequest::with_schema(
  self : ServingRequest,
  schema_version : String,
) -> ServingRequest {
  { ..self, schema_version, }
}

///|
pub struct ServingPrediction {
  request_id : String
  value : Double
  lower : Double
  upper : Double
  model_name : String
  model_version : String
  latency_ms : Int
  produced_at : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn ServingPrediction::new(
  request_id : String,
  value : Double,
  model_name : String,
  model_version : String,
  latency_ms : Int,
  produced_at : Int,
) -> ServingPrediction {
  if request_id == "" ||
    model_name == "" ||
    model_version == "" ||
    latency_ms < 0 ||
    produced_at < 0 {
    abort("ServingPrediction: invalid identity, latency, or timestamp")
  }
  if value != value || value.is_inf() {
    abort("ServingPrediction: value must be finite")
  }
  {
    request_id,
    value,
    lower: value,
    upper: value,
    model_name,
    model_version,
    latency_ms,
    produced_at,
  }
}

///|
pub fn ServingPrediction::with_interval(
  self : ServingPrediction,
  lower : Double,
  upper : Double,
) -> ServingPrediction {
  if lower != lower ||
    upper != upper ||
    lower.is_inf() ||
    upper.is_inf() ||
    lower > upper ||
    self.value < lower ||
    self.value > upper {
    abort("ServingPrediction: interval must contain the prediction")
  }
  { ..self, lower, upper }
}

///|
pub fn ServingPrediction::interval_width(self : ServingPrediction) -> Double {
  self.upper - self.lower
}

///|
pub struct ServingFailure {
  request_id : String
  code : String
  message : String
  retryable : Bool
  at : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn ServingFailure::new(
  request_id : String,
  code : String,
  message : String,
  retryable : Bool,
  at : Int,
) -> ServingFailure {
  if request_id == "" || code == "" || at < 0 {
    abort("ServingFailure: request, code and timestamp are required")
  }
  { request_id, code, message, retryable, at }
}

///|
pub struct PredictionBounds {
  minimum : Double
  maximum : Double
  reject_outside : Bool
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn PredictionBounds::new(
  minimum : Double,
  maximum : Double,
) -> PredictionBounds {
  if minimum != minimum ||
    maximum != maximum ||
    minimum.is_inf() ||
    maximum.is_inf() ||
    minimum > maximum {
    abort("PredictionBounds: bounds must be finite and ordered")
  }
  { minimum, maximum, reject_outside: false }
}

///|
pub fn PredictionBounds::reject(
  self : PredictionBounds,
  value : Bool,
) -> PredictionBounds {
  { ..self, reject_outside: value }
}

///|
pub fn PredictionBounds::apply(
  self : PredictionBounds,
  prediction : ServingPrediction,
) -> ServingPrediction? {
  if prediction.value < self.minimum || prediction.value > self.maximum {
    if self.reject_outside {
      None
    } else {
      let clipped_lower = prediction.lower.max(self.minimum).min(self.maximum)
      let clipped_upper = prediction.upper.max(self.minimum).min(self.maximum)
      Some({
        ..prediction,
        value: prediction.value.max(self.minimum).min(self.maximum),
        lower: clipped_lower,
        upper: clipped_upper,
      })
    }
  } else {
    Some(prediction)
  }
}

///|
pub struct ServingBatchReport {
  predictions : Array[ServingPrediction]
  failures : Array[ServingFailure]
  total : Int
  success_rate : Double
  mean_latency_ms : Double
  p95_latency_ms : Double
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn build_batch_report(
  predictions : Array[ServingPrediction],
  failures : Array[ServingFailure],
  total : Int,
) -> ServingBatchReport {
  if total < predictions.length() + failures.length() {
    abort("build_batch_report: total cannot be smaller than observed outcomes")
  }
  let latencies = predictions.map(fn(item) { item.latency_ms.to_double() })
  let mean_latency_ms = if latencies.length() == 0 {
    0.0
  } else {
    mean(latencies)
  }
  let p95_latency_ms = if latencies.length() == 0 {
    0.0
  } else {
    quantile(latencies, 0.95)
  }
  let success_rate = if total == 0 {
    0.0
  } else {
    predictions.length().to_double() / total.to_double()
  }
  {
    predictions,
    failures,
    total,
    success_rate,
    mean_latency_ms,
    p95_latency_ms,
  }
}

///|
pub struct ModelRoute {
  model_name : String
  stable_version : String
  canary_version : String
  canary_percent : Int
  enabled : Bool
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn ModelRoute::new(
  model_name : String,
  stable_version : String,
) -> ModelRoute {
  if model_name == "" || stable_version == "" {
    abort("ModelRoute: model and stable version are required")
  }
  {
    model_name,
    stable_version,
    canary_version: "",
    canary_percent: 0,
    enabled: true,
  }
}

///|
pub fn ModelRoute::canary(
  self : ModelRoute,
  version : String,
  percent : Int,
) -> ModelRoute {
  if version == "" || percent < 0 || percent > 100 {
    abort("ModelRoute: invalid canary configuration")
  }
  { ..self, canary_version: version, canary_percent: percent }
}

///|
pub fn ModelRoute::disable(self : ModelRoute) -> ModelRoute {
  { ..self, enabled: false }
}

///|
pub fn ModelRoute::enable(self : ModelRoute) -> ModelRoute {
  { ..self, enabled: true }
}

///|
pub fn ModelRoute::select(self : ModelRoute, request_id : String) -> String {
  if !self.enabled {
    abort("ModelRoute::select: route is disabled")
  }
  if request_id == "" {
    abort("ModelRoute::select: request id is required")
  }
  if self.canary_version == "" || self.canary_percent == 0 {
    return self.stable_version
  }
  let mut hash = 17
  for index in 0.. RolloutCounter {
  { stable_requests: 0, canary_requests: 0, stable_errors: 0, canary_errors: 0 }
}

///|
pub fn RolloutCounter::record(
  self : RolloutCounter,
  version : String,
  error : Bool,
  canary_version : String,
) -> RolloutCounter {
  let is_canary = version == canary_version && canary_version != ""
  let error_count = if error { 1 } else { 0 }
  if is_canary {
    {
      ..self,
      canary_requests: self.canary_requests + 1,
      canary_errors: self.canary_errors + error_count,
    }
  } else {
    {
      ..self,
      stable_requests: self.stable_requests + 1,
      stable_errors: self.stable_errors + error_count,
    }
  }
}

///|
pub fn RolloutCounter::stable_error_rate(self : RolloutCounter) -> Double {
  if self.stable_requests == 0 {
    0.0
  } else {
    self.stable_errors.to_double() / self.stable_requests.to_double()
  }
}

///|
pub fn RolloutCounter::canary_error_rate(self : RolloutCounter) -> Double {
  if self.canary_requests == 0 {
    0.0
  } else {
    self.canary_errors.to_double() / self.canary_requests.to_double()
  }
}

///|
pub fn RolloutCounter::canary_is_healthy(
  self : RolloutCounter,
  maximum_error_rate : Double,
  minimum_requests : Int,
) -> Bool {
  if maximum_error_rate < 0.0 ||
    maximum_error_rate > 1.0 ||
    minimum_requests < 0 {
    abort("RolloutCounter::canary_is_healthy: invalid policy")
  }
  self.canary_requests >= minimum_requests &&
  self.canary_error_rate() <= maximum_error_rate
}

///|
pub struct TokenBudget {
  capacity : Double
  refill_per_second : Double
  tokens : Double
  last_at : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn TokenBudget::new(
  capacity : Double,
  refill_per_second : Double,
  at : Int,
) -> TokenBudget {
  if capacity <= 0.0 || refill_per_second < 0.0 || at < 0 {
    abort("TokenBudget: invalid capacity, refill, or timestamp")
  }
  { capacity, refill_per_second, tokens: capacity, last_at: at }
}

///|
pub fn TokenBudget::try_acquire(
  self : TokenBudget,
  amount : Double,
  at : Int,
) -> (TokenBudget, Bool) {
  if amount <= 0.0 || amount > self.capacity || at < self.last_at {
    abort("TokenBudget::try_acquire: invalid amount or timestamp")
  }
  let elapsed = (at - self.last_at).to_double()
  let available = (self.tokens + elapsed * self.refill_per_second).min(
    self.capacity,
  )
  if available < amount {
    ({ ..self, tokens: available, last_at: at }, false)
  } else {
    ({ ..self, tokens: available - amount, last_at: at }, true)
  }
}

///|
pub fn TokenBudget::remaining(self : TokenBudget, at : Int) -> Double {
  if at < self.last_at {
    abort("TokenBudget::remaining: timestamp moved backwards")
  }
  (self.tokens + (at - self.last_at).to_double() * self.refill_per_second).min(
    self.capacity,
  )
}

///|
pub struct ServiceHealth {
  window_start : Int
  window_end : Int
  requests : Int
  successes : Int
  errors : Int
  latency_ms : Array[Double]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn ServiceHealth::new(start : Int) -> ServiceHealth {
  if start < 0 {
    abort("ServiceHealth: start must be non-negative")
  }
  {
    window_start: start,
    window_end: start,
    requests: 0,
    successes: 0,
    errors: 0,
    latency_ms: [],
  }
}

///|
pub fn ServiceHealth::record(
  self : ServiceHealth,
  at : Int,
  latency_ms : Double,
  success : Bool,
) -> ServiceHealth {
  if at < self.window_end || latency_ms < 0.0 {
    abort("ServiceHealth::record: invalid time or latency")
  }
  let latencies = self.latency_ms.copy()
  latencies.push(latency_ms)
  let success_count = if success { 1 } else { 0 }
  let error_count = if success { 0 } else { 1 }
  {
    ..self,
    window_end: at,
    requests: self.requests + 1,
    successes: self.successes + success_count,
    errors: self.errors + error_count,
    latency_ms: latencies,
  }
}

///|
pub fn ServiceHealth::availability(self : ServiceHealth) -> Double {
  if self.requests == 0 {
    1.0
  } else {
    self.successes.to_double() / self.requests.to_double()
  }
}

///|
pub fn ServiceHealth::error_rate(self : ServiceHealth) -> Double {
  if self.requests == 0 {
    0.0
  } else {
    self.errors.to_double() / self.requests.to_double()
  }
}

///|
pub fn ServiceHealth::p95_latency(self : ServiceHealth) -> Double {
  if self.latency_ms.length() == 0 {
    0.0
  } else {
    quantile(self.latency_ms, 0.95)
  }
}

///|
pub fn ServiceHealth::meets_slo(
  self : ServiceHealth,
  minimum_availability : Double,
  maximum_p95_ms : Double,
) -> Bool {
  if minimum_availability < 0.0 ||
    minimum_availability > 1.0 ||
    maximum_p95_ms < 0.0 {
    abort("ServiceHealth::meets_slo: invalid SLO")
  }
  self.availability() >= minimum_availability &&
  self.p95_latency() <= maximum_p95_ms
}

///|
pub struct PredictionAuditEntry {
  request_id : String
  entity_id : String
  model_version : String
  value : Double
  interval_width : Double
  latency_ms : Int
  at : Int
  outcome : Double?
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct PredictionAuditLog {
  capacity : Int
  entries : Array[PredictionAuditEntry]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn PredictionAuditLog::new(capacity : Int) -> PredictionAuditLog {
  if capacity <= 0 {
    abort("PredictionAuditLog: capacity must be positive")
  }
  { capacity, entries: [] }
}

///|
pub fn PredictionAuditLog::append(
  self : PredictionAuditLog,
  request : ServingRequest,
  prediction : ServingPrediction,
) -> PredictionAuditLog {
  let entries = self.entries.copy()
  entries.push({
    request_id: request.request_id,
    entity_id: request.entity_id,
    model_version: prediction.model_version,
    value: prediction.value,
    interval_width: prediction.interval_width(),
    latency_ms: prediction.latency_ms,
    at: prediction.produced_at,
    outcome: None,
  })
  if entries.length() > self.capacity {
    ignore(entries.remove(0))
  }
  { ..self, entries, }
}

///|
pub fn PredictionAuditLog::attach_outcome(
  self : PredictionAuditLog,
  request_id : String,
  outcome : Double,
) -> PredictionAuditLog {
  let entries = self.entries.copy()
  for index in 0.. Int {
  self.entries.length()
}

///|
pub fn PredictionAuditLog::observed_error(
  self : PredictionAuditLog,
  absolute_threshold : Double,
) -> Double {
  if absolute_threshold < 0.0 {
    abort("PredictionAuditLog::observed_error: threshold must be non-negative")
  }
  let observed = self.entries.filter(fn(entry) { entry.outcome is Some(_) })
  if observed.length() == 0 {
    return 0.0
  }
  let mut errors = 0.0
  for entry in observed.iter() {
    match entry.outcome {
      Some(outcome) =>
        if (entry.value - outcome).abs() > absolute_threshold {
          errors = errors + 1.0
        }
      None => ()
    }
  }
  errors / observed.length().to_double()
}