///|
/// Router-level transient-failure retry policy. Applied by
/// `RouterModelPort::from_catalogs` to every ingested slot port so all
/// providers inherit the same exception-retry semantics (sudden 5xx, mid-stream
/// resets, truncated SSE) without per-provider wrappers. Rate-limit (429)
/// verdicts never retry here — they belong to the rate-limit resume domain.
pub(all) struct TransientRetryPolicy {
  /// Retries granted after the first failure (total attempts = 1 + this).
  max_retries : Int
  initial_delay_ms : Int
} derive(Debug)

///|
/// Mirrors the OpenCode session retry: 3 retries, 2s initial doubling delay.
pub fn TransientRetryPolicy::default() -> TransientRetryPolicy {
  { max_retries: 3, initial_delay_ms: 2000, }
}

///|
pub extend TransientRetryPolicy with @debug.Debug::{to_repr}

///|
struct TransientRetryModelPort {
  inner : &@posoco.ModelPort
  policy : TransientRetryPolicy
}

///|
pub fn transient_retry_port(
  inner~ : &@posoco.ModelPort,
  policy~ : TransientRetryPolicy,
) -> &@posoco.ModelPort {
  TransientRetryModelPort::{ inner, policy, }
}

///|
/// Retryable classes, all leaving the request half-processed on a stateless
/// completion POST (streamed chunks already delivered to the host/UI are not
/// transcript fact, so repeating the request from scratch is safe):
/// classified by `@chatkit.is_transient_model_error` over the shared
/// provider message shapes.
pub impl @posoco.ModelPort for TransientRetryModelPort with fn chat(
  self : TransientRetryModelPort,
  scope : @posoco.InvocationScope,
  messages : ArrayView[@posoco.Message],
  tools : Array[@posoco.ToolDef],
  options : @posoco.ChatOptions,
  stream : @posoco.StreamMode,
) -> @posoco.ModelCallResult raise @posoco.ModelError {
  let mut retries_left = self.policy.max_retries
  let mut delay_ms = self.policy.initial_delay_ms
  while true {
    let result = Ok(self.inner.chat(scope, messages, tools, options, stream)) catch {
      error => Err(error)
    }
    match result {
      Ok(value) => return value
      Err(error) =>
        if retries_left > 0 && @chatkit.is_transient_model_error(error) {
          @async.sleep(delay_ms)
          retries_left -= 1
          delay_ms *= 2
        } else {
          raise error
        }
    }
  }
  abort("transient retry loop exits via return or raise")
}

///|
/// Compact retries like any other completion POST: transient failures while
/// summarizing replay the summary request from scratch.
pub impl @posoco.ModelPort for TransientRetryModelPort with fn compact(
  self : TransientRetryModelPort,
  scope : @posoco.InvocationScope,
  messages : ArrayView[@posoco.Message],
  options : @posoco.ChatOptions,
  trigger : @posoco.CompactTrigger,
) -> @posoco.CompactResult raise @posoco.ModelError {
  let mut retries_left = self.policy.max_retries
  let mut delay_ms = self.policy.initial_delay_ms
  while true {
    let result = Ok(self.inner.compact(scope, messages, options, trigger)) catch {
      error => Err(error)
    }
    match result {
      Ok(value) => return value
      Err(error) =>
        if retries_left > 0 && @chatkit.is_transient_model_error(error) {
          @async.sleep(delay_ms)
          retries_left -= 1
          delay_ms *= 2
        } else {
          raise error
        }
    }
  }
  abort("transient retry loop exits via return or raise")
}

///|
pub impl @posoco.ModelPort for TransientRetryModelPort with fn provider_config(
  self : TransientRetryModelPort,
) -> @posoco.ProviderConfig {
  self.inner.provider_config()
}

///|
pub extend TransientRetryModelPort with @posoco.ModelPort::{
  provider_config,
  chat,
  compact,
}