///|
/// Guest handle for calling host capabilities. Constructed by `serve` with
/// the host-declared capability list; policy denials come back as `Failure`
/// values, transport loss raises.
pub struct Ears {
  priv wire : &Wire
  priv lock : @async.Mutex
  priv mut next_id : Int32
  priv allowed : Array[String]
  priv mut closed : Bool
}

///|
pub fn Ears::Ears(wire~ : &Wire, allowed~ : Array[String]) -> Ears {
  {
    wire,
    lock: @async.Mutex(),
    next_id: 0,
    allowed: allowed.copy(),
    closed: false,
  }
}

///|
/// Call a host capability by name. `CapabilityDenied` and `ScopeClosed` are
/// local `Failure` values; a disconnected or malformed reply raises.
pub async fn Ears::call(
  self : Ears,
  name : String,
  arguments : Json,
) -> Outcome {
  if self.closed {
    return Failure(
      code="ScopeClosed",
      content="ears scope is closed",
      structured=None,
    )
  }
  if !self.allowed.contains(name) {
    return Failure(
      code="CapabilityDenied",
      content="capability is outside this invocation: " + name,
      structured=None,
    )
  }
  // One request on the wire at a time: concurrent branches may compute in
  // parallel, but reply correlation needs the request/reply round trip
  // serialized until a governed scheduler bridge exists.
  self.lock.acquire()
  defer self.lock.release()
  self.next_id += 1
  let id = self.next_id
  let request : Json = {
    "type": "request",
    "id": id,
    "name": name,
    "arguments": arguments,
  }
  self.wire.send(request.stringify() + "\n")
  guard self.wire.receive() is Some(line) else {
    fail("ears: host disconnected while waiting for a reply")
  }
  let frame = @json.parse(line) catch {
    _ => fail("ears: reply frame is not valid JSON")
  }
  guard frame is Object(fields) else {
    fail("ears: reply frame must be an object")
  }
  guard fields.get("type") is Some(String("reply")) else {
    fail("ears: reply frame has the wrong type")
  }
  guard fields.get("outcome") is Some(outcome_value) else {
    fail("ears: reply frame is missing its outcome")
  }
  guard fields.get("id") is Some(Number(value, ..)) && value == id.to_double() else {
    fail("ears: reply frame does not answer request \{id}")
  }
  Outcome::from_json(outcome_value)
}

///|
/// Emit a diagnostic to the host. The host decides visibility; this is not
/// output returned to the model. Ignored after `close`.
pub async fn Ears::log(
  self : Ears,
  level~ : String = "info",
  content : String,
) -> Unit {
  if self.closed {
    return
  }
  let frame : Json = { "type": "log", "level": level, "content": content }
  self.wire.send(frame.stringify() + "\n")
}

///|
pub fn Ears::close(self : Ears) -> Unit {
  self.closed = true
}