///|
pub fn build_tool_result_message(
  call : @kernel.ToolCall,
  result : @kernel.ToolOutcome,
) -> @kernel.Message {
  let content : String = match result {
    @kernel.Success(content~, ..) => content
    @kernel.ToolReportedError(content~, ..) => content
    @kernel.RuntimeFailure(message~, ..) => message
    @kernel.NotExecuted(reason~, ..) => reason.to_string()
  }
  @kernel.ToolMessage(
    call_id=call.call_id,
    tool_name=call.name,
    outcome=@kernel.Success(content~, structured=None),
  )
}

///|
pub fn build_error_message(
  call : @kernel.ToolCall,
  error : @error.RuntimeError,
) -> @kernel.Message {
  @kernel.ToolMessage(
    call_id=call.call_id,
    tool_name=call.name,
    outcome=@kernel.RuntimeFailure(
      error_category="RuntimeError",
      message=error.to_string(),
    ),
  )
}

///|
/// Internal: merges multiple providers' tool lists. Agent routes via ToolRouting instead.
pub(all) struct CompositeToolProvider {
  providers : Array[&@port.ToolProvider]
}

///|
pub impl @port.ToolProvider for CompositeToolProvider with fn list_tools(self) {
  let all : Array[@kernel.ToolDef] = []
  for p in self.providers {
    for t in p.list_tools() {
      all.push(t)
    }
  }
  all
}

///|
pub impl @port.ToolProvider for CompositeToolProvider with fn execute(
  _self,
  name : String,
  _call : @kernel.ToolCall,
) -> @kernel.ToolOutcome raise @error.RuntimeError {
  raise @error.RuntimeError::UnknownTool(
    "CompositeToolProvider does not route execute(); Agent uses ToolRouting instead. Tool: '\{name}'",
  )
}

///|
/// Dynamic runtime tool registration. Implements ToolProvider.
pub(all) struct ToolRegistry {
  mut tools : Map[String, @kernel.ToolDef]
  mut executors : Map[String, (@kernel.ToolCall) -> @kernel.ToolOutcome]
}

///|
pub fn ToolRegistry::ToolRegistry() -> ToolRegistry {
  { tools: Map::from_array([]), executors: Map::from_array([]) }
}

///|
/// Register (or deliberately replace) a tool at runtime. Re-registering an
/// existing name overwrites both the definition and the executor — that is
/// intentional hot-replacement semantics for dynamic registries. Callers
/// that did NOT mean to replace should use `register_strict`, which fails
/// fast on a name collision instead of silently shadowing the prior tool.
pub fn ToolRegistry::register(
  self : ToolRegistry,
  tool : @kernel.ToolDef,
  executor : (@kernel.ToolCall) -> @kernel.ToolOutcome,
) -> Unit {
  self.tools[tool.name.to_string()] = tool
  self.executors[tool.name.to_string()] = executor
}

///|
/// Register a tool, failing fast with `RuntimeError::ToolAlreadyRegistered`
/// when the name is already taken. This mirrors the composition-time rule
/// (M0-T06-B): a name collision is never silently resolved by last-wins.
pub fn ToolRegistry::register_strict(
  self : ToolRegistry,
  tool : @kernel.ToolDef,
  executor : (@kernel.ToolCall) -> @kernel.ToolOutcome,
) -> Unit raise @error.RuntimeError {
  let key = tool.name.to_string()
  if self.tools.contains(key) {
    raise @error.RuntimeError::ToolAlreadyRegistered(
      "tool '\{key}' is already registered",
    )
  }
  self.tools[key] = tool
  self.executors[key] = executor
}

///|
pub fn ToolRegistry::unregister(self : ToolRegistry, name : String) -> Unit {
  self.tools.remove(name)
  self.executors.remove(name)
}

///|
pub impl @port.ToolProvider for ToolRegistry with fn list_tools(self) {
  let all : Array[@kernel.ToolDef] = []
  for v in self.tools.values() {
    all.push(v)
  }
  all
}

///|
pub impl @port.ToolProvider for ToolRegistry with fn execute(
  self,
  name : String,
  call : @kernel.ToolCall,
) -> @kernel.ToolOutcome raise @error.RuntimeError {
  match self.executors.get(name) {
    Some(exec) => exec(call)
    None =>
      raise @error.RuntimeError::UnknownTool(
        "No executor registered for tool '\{name}'",
      )
  }
}

///|
pub(all) struct NoopMemoryPort {}

///|
pub impl @port.MemoryPort for NoopMemoryPort with fn store(_self, _entry) -> String raise @error.MemoryError {
  raise @error.MemoryError::Store("not implemented")
}

///|
pub impl @port.MemoryPort for NoopMemoryPort with fn search(_self, _query) -> Array[
  @types.MemoryEntry,
] raise @error.MemoryError {
  raise @error.MemoryError::Search("not implemented")
}

///|
pub impl @port.MemoryPort for NoopMemoryPort with fn delete(_self, _id) -> Unit raise @error.MemoryError {
  raise @error.MemoryError::Delete("not implemented")
}

///|
pub(all) struct NoopLifecycle {}

///|
pub impl @port.Lifecycle for NoopLifecycle with fn on_shutdown(_self) -> Unit {
  ()
}

///|
/// No-op CommandPort: declares no commands, invoke always raises NotFound.
/// Useful as a default or for testing.
pub(all) struct NoopCommandPort {}

///|
pub impl @port.CommandPort for NoopCommandPort with fn commands(_self) -> Array[
  @port.CommandDef,
] {
  []
}

///|
pub impl @port.CommandPort for NoopCommandPort with fn invoke(
  _self,
  id : String,
  _args : Json,
) -> @port.CommandOutcome raise @error.CommandError {
  raise @error.CommandError::NotFound(id)
}

///|
/// Tokenize a slash argument string, honoring double-quoted spans.
fn tokenize_args(text : String) -> Array[String] {
  let tokens : Array[String] = []
  let mut current = StringBuilder::new()
  let mut in_quote = false
  for ch in text {
    if ch == '"' {
      in_quote = !in_quote
    } else if ch == ' ' && !in_quote {
      if current.to_string() != "" {
        tokens.push(current.to_string())
        current = StringBuilder::new()
      }
    } else {
      current.write_char(ch)
    }
  }
  let last = current.to_string()
  if last != "" {
    tokens.push(last)
  }
  tokens
}

///|
/// Coerce a string token to Json matching the param's ptype.
fn coerce_param(p : @port.CommandParam, raw : String) -> Result[Json, String] {
  match p.ptype {
    Str => Ok(Json::string(raw))
    Int => {
      let n = @string.parse_int(raw) catch {
        _ => return Err("parameter " + p.name + " expects Int, got: " + raw)
      }
      Ok(Json::number(n.to_double()))
    }
    Bool =>
      match raw {
        "true" | "on" | "yes" => Ok(Json::boolean(true))
        "false" | "off" | "no" => Ok(Json::boolean(false))
        _ => Err("parameter " + p.name + " expects Bool, got: " + raw)
      }
    Strs =>
      Ok(
        Json::array(
          raw.split(",").map(fn(s) { Json::string(s.to_owned()) }).collect(),
        ),
      )
  }
}

///|
/// Split a `key=value` token into (key, value). Returns None if no `=`.
fn split_kv(token : String) -> (String, String)? {
  let bytes = token.to_array()
  let mut i = 0
  while i < bytes.length() {
    if bytes[i] == '=' {
      let key = token[0:i].to_owned()
      let val = token[i + 1:].to_owned()
      return Some((key, val))
    }
    i += 1
  }
  None
}

///|
/// Find a CommandParam by name in an array. Returns None if not found.
fn find_param(
  params : Array[@port.CommandParam],
  name : String,
) -> @port.CommandParam? {
  for p in params {
    if p.name == name {
      return Some(p)
    }
  }
  None
}

///|
/// Parse a slash argument string into a JSON object according to CommandParam
/// definitions. Positional params fill in declaration order; non-positional
/// params expect `key=value` form. Applies defaults for missing optional params.
/// Returns Err with a reason string on type conversion failure or missing
/// required params. Pure function (no IO).
pub fn parse_slash_args(
  text : String,
  params : Array[@port.CommandParam],
) -> Result[Json, String] {
  let tokens = tokenize_args(text)
  let positional_params = params.filter(fn(p) { p.positional })
  let map : Map[String, Json] = Map::from_array([])
  let mut positional_idx = 0
  // First pass: positional params consume tokens in order, key=value fills others
  for token in tokens {
    match split_kv(token) {
      Some((key, val)) =>
        match find_param(params, key) {
          Some(p) =>
            match coerce_param(p, val) {
              Ok(v) => map[key] = v
              Err(e) => return Err(e)
            }
          None => return Err("unknown parameter: " + key)
        }
      None =>
        if positional_idx < positional_params.length() {
          let p = positional_params[positional_idx]
          match coerce_param(p, token) {
            Ok(v) => map[p.name] = v
            Err(e) => return Err(e)
          }
          positional_idx = positional_idx + 1
        }
    }
  }
  // Second pass: apply defaults + check required
  for p in params {
    if !map.contains(p.name) {
      match p.default {
        Some(d) => map[p.name] = d
        None =>
          if p.required {
            return Err("missing required parameter: " + p.name)
          }
      }
    }
  }
  Ok(Json::object(map))
}

///|
/// Validate a JSON object of args against a CommandDef's params.
/// Checks required presence; optional params with no value and no default
/// are omitted (not an error). Returns the (possibly defaulted) args on Ok.
/// Pure function (no IO).
pub fn validate_args(
  def : @port.CommandDef,
  args : Json,
) -> Result[Json, String] {
  let incoming : Map[String, Json] = match args {
    Object(m) => m
    _ => Map::from_array([])
  }
  let out : Map[String, Json] = Map::from_array([])
  for p in def.params {
    if incoming.contains(p.name) {
      out[p.name] = incoming[p.name]
    } else {
      match p.default {
        Some(d) => out[p.name] = d
        None =>
          if p.required {
            return Err("missing required parameter: " + p.name)
          }
      }
    }
  }
  Ok(Json::object(out))
}