///|
/// Structured quota/rate-limit verdict returned by a provider (the HTTP 429
/// family). Raised as `ModelError::RateLimited` instead of `Transport` so
/// hosts can schedule recovery from typed data instead of parsing strings.
///
/// `reset_at_ms` is the Unix epoch in milliseconds at which the provider
/// stated the quota window frees; `None` means no explicit reset time was
/// stated, so waiting is not schedulable and the caller should treat the
/// verdict as terminal for this turn. `provider_code` is the provider's own
/// machine code for the verdict when available (for example z.ai `"1308"`,
/// OpenAI `"usage_limit_reached"`). `message` is a bounded excerpt of the
/// provider message, safe to display.
pub(all) struct RateLimitInfo {
  status : Int
  message : String
  reset_at_ms : Int64?
  provider_code : String?
} derive(Eq, Debug)

///|
fn bounded_rate_limit_message(message : String) -> String {
  let chars : Array[Char] = []
  let mut truncated = false
  for char in message {
    if chars.length() >= 120 {
      truncated = true
      break
    }
    match char {
      '\n' | '\r' => chars.push(' ')
      other => chars.push(other)
    }
  }
  let label = String::from_array(chars)
  if truncated {
    label + "…"
  } else {
    label
  }
}

///|
pub(all) suberror ModelError {
  RequestBuild(String)
  Transport(String)
  ResponseParse(String)
  /// The provider answered with a quota/rate-limit verdict instead of
  /// serving the request. Distinct from `Transport` throttles so a host can
  /// decide between waiting for `reset_at_ms` and failing fast.
  RateLimited(RateLimitInfo)
} derive(Debug)

///|
pub impl Show for ModelError with fn to_string(self) -> String {
  match self {
    RequestBuild(msg) => "ModelError::RequestBuild(\{msg})"
    Transport(msg) => "ModelError::Transport(\{msg})"
    ResponseParse(msg) => "ModelError::ResponseParse(\{msg})"
    RateLimited(info) => {
      let code_label = match info.provider_code {
        Some(code) => code
        None => "none"
      }
      let reset_label = match info.reset_at_ms {
        Some(at) => "\{at}"
        None => "none"
      }
      "ModelError::RateLimited(status=\{info.status}, code=\{code_label}, reset_at_ms=\{reset_label}, message=\{bounded_rate_limit_message(info.message)})"
    }
  }
}

///|
pub(all) suberror SessionError {
  Load(String)
  Save(String)
} derive(Debug)

///|
pub impl Show for SessionError with fn to_string(self) -> String {
  match self {
    Load(msg) => "SessionError::Load(\{msg})"
    Save(msg) => "SessionError::Save(\{msg})"
  }
}

///|
pub(all) suberror RuntimeError {
  UnknownTool(String)
  InvocationFailed(String)
  Transport(String)
  /// `ToolRegistry::register_strict` rejected a duplicate tool name.
  ToolAlreadyRegistered(String)
} derive(Debug)

///|
pub impl Show for RuntimeError with fn to_string(self) -> String {
  match self {
    UnknownTool(msg) => "RuntimeError::UnknownTool(\{msg})"
    InvocationFailed(msg) => "RuntimeError::InvocationFailed(\{msg})"
    Transport(msg) => "RuntimeError::Transport(\{msg})"
    ToolAlreadyRegistered(msg) => "RuntimeError::ToolAlreadyRegistered(\{msg})"
  }
}

///|
pub(all) suberror AgentError {
  Model(String)
  Session(SessionError)
  Runtime(RuntimeError)
  /// The kernel tool-round budget rejected a batch. `consumed` is the round
  /// count at rejection (`limit + 1`); `limit` is the configured bound.
  ToolLoopExceeded(consumed~ : Int, limit~ : Int)
  PipelineAborted(String)
} derive(Debug)

///|
pub impl Show for AgentError with fn to_string(self) -> String {
  match self {
    Model(msg) => "AgentError::Model(\{msg})"
    Session(err) => "AgentError::Session(\{err.to_string()})"
    Runtime(err) => "AgentError::Runtime(\{err.to_string()})"
    ToolLoopExceeded(consumed~, limit~) =>
      "AgentError::ToolLoopExceeded(consumed=\{consumed}, limit=\{limit}; raise AgentConfig.max_tool_rounds or set None for unbounded)"
    PipelineAborted(msg) => "AgentError::PipelineAborted(\{msg})"
  }
}

///|
pub(all) suberror MemoryError {
  Store(String)
  Search(String)
  Delete(String)
} derive(Debug)

///|
pub impl Show for MemoryError with fn to_string(self) -> String {
  match self {
    Store(msg) => "MemoryError::Store(\{msg})"
    Search(msg) => "MemoryError::Search(\{msg})"
    Delete(msg) => "MemoryError::Delete(\{msg})"
  }
}

///|
/// Composition errors raised during Agent::new (manifest aggregation failures).
///
/// Every variant that involves a specific extension carries `manifest_id` so
/// callers can locate the offending extension without guessing. Multi-party
/// collisions carry the list of involved manifest ids.
pub(all) suberror CompositionError {
  /// Two extensions declared a tool with the same name. Fail-fast; no
  /// last-wins routing. First arg is the tool name, second is a diagnostic
  /// message, third is the list of manifest ids that declared it.
  ToolCollision(String, String, manifests~ : Array[String])
  /// Two extensions declared a slash command with the same id.
  CommandCollision(String, manifests~ : Array[String])
  /// No extension contributed a ModelPort. Exactly one is required.
  MissingModel
  /// More than one extension contributed a ModelPort. Multi-model routing
  /// must be solved inside a meta-extension (e.g. posoco-ext-llm), not by
  /// declaring multiple top-level models.
  MultipleModels(manifests~ : Array[String])
  /// Empty extension list passed to Agent::new.
  EmptyManifests
  /// An extension manifest was malformed (e.g. empty id, structural issue).
  ManifestSchemaError(manifest_id~ : String, detail~ : String)
  /// Legacy: a required port had zero contributors. Kept for compatibility
  /// with older composition paths that do not use manifests.
  EmptyPort(String)
  /// An extension's `Lifecycle::on_compose` raised, failing the composition
  /// loudly. No partial Agent is produced. Extensions raise this themselves
  /// when a capability they need reads `None` in the `CompositionView`
  /// (typically because it was not declared in `requires`).
  ExtensionComposeFailed(manifest_id~ : String, detail~ : String)
} derive(Debug)

///|
pub impl Show for CompositionError with fn to_string(self) -> String {
  match self {
    ToolCollision(name, msg, manifests~) =>
      "CompositionError::ToolCollision(\{name}: \{msg}; manifests=\{manifests.join(",")})"
    CommandCollision(name, manifests~) =>
      "CompositionError::CommandCollision(\{name}; manifests=\{manifests.join(",")})"
    MissingModel => "CompositionError::MissingModel"
    MultipleModels(manifests~) =>
      "CompositionError::MultipleModels(manifests=\{manifests.join(",")})"
    EmptyManifests => "CompositionError::EmptyManifests"
    ManifestSchemaError(manifest_id~, detail~) =>
      "CompositionError::ManifestSchemaError(manifest=\{manifest_id}, detail=\{detail})"
    EmptyPort(port) => "CompositionError::EmptyPort(\{port})"
    ExtensionComposeFailed(manifest_id~, detail~) =>
      "CompositionError::ExtensionComposeFailed(manifest=\{manifest_id}, detail=\{detail})"
  }
}

///|
/// Errors raised by UiPort implementations. `UiPort::request` MUST raise one
/// of these typed variants instead of an untyped error, so that callers can
/// distinguish "user cancelled" from "this UI doesn't support requests".
pub(all) suberror UiError {
  /// The UiPort does not support interactive requests at all (e.g. NoopUiPort,
  /// or a headless host). Distinct from Cancelled (which means the request
  /// was presented to the user and they dismissed it).
  Unsupported(detail~ : String)
  /// User saw the request and explicitly cancelled it.
  Cancelled
  /// The request payload was structurally invalid (e.g. Select with empty
  /// options). Raised by the UiPort before any user interaction.
  InvalidRequest(detail~ : String)
  /// The host reported a transport/IO failure while servicing the request.
  IoFailure(detail~ : String)
} derive(Debug)

///|
pub impl Show for UiError with fn to_string(self) -> String {
  match self {
    Unsupported(detail~) => "UiError::Unsupported(\{detail})"
    Cancelled => "UiError::Cancelled"
    InvalidRequest(detail~) => "UiError::InvalidRequest(\{detail})"
    IoFailure(detail~) => "UiError::IoFailure(\{detail})"
  }
}

///|
/// Errors raised by CommandPort.invoke: unknown command id, invalid args,
/// or execution failure.
pub(all) suberror CommandError {
  NotFound(String)
  InvalidArgs(reason~ : String)
  ExecutionFailed(String)
} derive(Debug)

///|
pub impl Show for CommandError with fn to_string(self) -> String {
  match self {
    NotFound(id) => "CommandError::NotFound(\{id})"
    InvalidArgs(reason~) => "CommandError::InvalidArgs(\{reason})"
    ExecutionFailed(msg) => "CommandError::ExecutionFailed(\{msg})"
  }
}