///|
/// Manifest aggregation — merges `Array[&Extension]` into a single
/// `AggregatedPorts` value for the agent constructor.

///|
/// A registered Lifecycle contributor paired with its declared capability
/// requirements. The pairing survives aggregation so the composition can
/// gate each contributor's `CompositionView` on its own declared `requires`
/// at `Lifecycle::on_compose` delivery time. The manifest id also binds
/// Agent-owned capabilities to their declaring extension.
priv struct LifecycleEntry {
  id : String
  requires : Array[@port.Capability]
  port : &@port.Lifecycle
}

///|
/// Flattened port bundle ready for the agent constructor. The `model` field
/// is a single value (multi-model routing is solved in-extension), and `ui`
/// is wrapped into one UiPort reference (the agent never sees a bare array).
priv struct AggregatedPorts {
  model : &@port.ModelPort
  decision : &@port.DecisionPort?
  log : &@port.LogPort?
  tools : Array[ToolEntry]
  tool_call_owners : Map[String, Array[String]]
  sessions : Array[&@port.SessionPort]
  observers : Array[&@port.Observer]
  hooks : Array[&@port.PipelineHook]
  /// Memory sources paired with their manifest id — the id routes
  /// `memory_add`'s optional `source` argument.
  memory : Array[(String, &@port.MemoryPort)]
  lifecycle : Array[LifecycleEntry]
  commands : Array[&@port.CommandPort]
  ui : &@port.UiPort
  /// Contributor paired with its manifest id (used as the prompt section
  /// header so the assembled prompt names its source).
  prompt_contributors : Array[(String, &@port.SystemPromptContributor)]
}

///|
/// Order follows extension array order; collisions fail fast.
fn aggregate_extensions(
  exts : Array[&@port.Extension],
) -> AggregatedPorts raise @error.CompositionError {
  if exts.length() == 0 {
    raise EmptyManifests
  }
  let models : Array[&@port.ModelPort] = []
  let model_manifests : Array[String] = []
  let decisions : Array[&@port.DecisionPort] = []
  let decision_manifests : Array[String] = []
  let logs : Array[&@port.LogPort] = []
  let log_manifests : Array[String] = []
  let tools : Array[ToolEntry] = []
  let tool_call_owners : Map[String, Array[String]] = Map::from_array([])
  let tool_index = ToolNameIndex::new()
  let sessions : Array[&@port.SessionPort] = []
  let observers : Array[&@port.Observer] = []
  let hooks : Array[&@port.PipelineHook] = []
  let memory : Array[(String, &@port.MemoryPort)] = []
  let lifecycle : Array[LifecycleEntry] = []
  let commands : Array[&@port.CommandPort] = []
  let command_manifests : Map[String, Array[String]] = Map::from_array([])
  let ui : Array[&@port.UiPort] = []
  let prompt_contributors : Array[(String, &@port.SystemPromptContributor)] = []
  for ext in exts {
    let manifest = ext.manifest()
    let mid = manifest.id
    if mid == "" {
      raise ManifestSchemaError(
        manifest_id="",
        detail="extension returned a manifest with empty id",
      )
    }
    if tool_call_owners.get(mid) is Some(existing) &&
      existing != manifest.tool_call_owners {
      raise ManifestSchemaError(
        manifest_id=mid,
        detail="conflicting tool_call_owners declarations for the same owner",
      )
    }
    tool_call_owners[mid] = manifest.tool_call_owners.copy()
    for m in manifest.models {
      models.push(m)
      model_manifests.push(mid)
    }
    for d in manifest.decisions {
      decisions.push(d)
      decision_manifests.push(mid)
    }
    for log in manifest.logs {
      logs.push(log)
      log_manifests.push(mid)
    }
    for provider in manifest.tools {
      for tool_def in provider.list_tools() {
        let name = tool_def.name.to_string()
        let source_label = match nonempty_provenance(tool_def) {
          Some(s) => s
          None =>
            "ToolDef.provenance is empty for tool '" +
            name +
            "' in manifest '" +
            mid +
            "' (set ToolDef.provenance to a stable provider id to disambiguate)"
        }
        match tool_index.record(name, source_label) {
          Some(labels) => {
            let manifests = labels.copy()
            manifests.push(mid)
            raise ToolCollision(
              name,
              tool_collision_message(labels[0], source_label),
              manifests~,
            )
          }
          None => ()
        }
        tools.push(ToolEntry::bound(tool_def, mid, provider))
      }
    }
    for cmd in manifest.commands {
      for def in cmd.commands() {
        let cid = def.id
        if command_manifests.contains(cid) {
          let existing = command_manifests[cid]
          existing.push(mid)
          raise CommandCollision(cid, manifests=existing)
        } else {
          command_manifests[cid] = [mid]
        }
      }
      commands.push(cmd)
    }
    for s in manifest.sessions {
      sessions.push(s)
    }
    for o in manifest.observers {
      observers.push(o)
    }
    for h in manifest.hooks {
      hooks.push(h)
    }
    for m in manifest.memory {
      memory.push((mid, m))
    }
    for l in manifest.lifecycle {
      lifecycle.push({ id: mid, requires: manifest.requires.copy(), port: l, })
    }
    for u in manifest.ui {
      ui.push(u)
    }
    for p in manifest.prompt_contributors {
      prompt_contributors.push((mid, p))
    }
  }
  match models.length() {
    0 => raise MissingModel
    1 => ()
    _ => raise MultipleModels(manifests=model_manifests)
  }
  // Decision cardinality: optional singleton. Multi-provider routing belongs
  // inside one DecisionPort meta-extension.
  let decision : &@port.DecisionPort? = match decisions.length() {
    0 => None
    1 => Some(decisions[0])
    _ => raise MultipleDecisions(manifests=decision_manifests)
  }
  // Log cardinality: optional singleton. Multi-sink fan-out belongs inside
  // one LogPort meta-extension so write/durability failure semantics are local.
  let log : &@port.LogPort? = match logs.length() {
    0 => None
    1 => Some(logs[0])
    _ => raise MultipleLogs(manifests=log_manifests)
  }
  // UI cardinality: 0 → NoopUiPort (unwrapped: a UI-less composition never
  // waits for a user, so no request events), 1 → wrapped passthrough,
  // 2+ → wrapped CompositeUiPort.
  let ui_ref : &@port.UiPort = match ui.length() {
    0 => (NoopUiPort() : &@port.UiPort)
    1 =>
      (
        UserRequestObservingUiPort::UserRequestObservingUiPort(ui[0], observers) :
        &@port.UiPort)
    _ =>
      (
        UserRequestObservingUiPort::UserRequestObservingUiPort(
          CompositeUiPort(ui.copy()),
          observers,
        ) : &@port.UiPort)
  }
  {
    model: models[0],
    decision,
    log,
    tools,
    tool_call_owners,
    sessions,
    observers,
    hooks,
    memory,
    lifecycle,
    commands,
    ui: ui_ref,
    prompt_contributors,
  }
}

///|
/// Core-owned wrapper around the aggregated `UiPort`. Every interactive user
/// request (permission confirm, plan review, ask_question) entering the port
/// broadcasts `UserRequestStarted` / `UserRequestFinished` to the observer bus,
/// so presence-style observers can surface "waiting for user" without coupling
/// to a single caller. It only sees requests resolved from a `CompositionView`
/// (requires `Capability::Ui`); raw contributor holders bypass it.
priv struct UserRequestObservingUiPort {
  inner : &@port.UiPort
  observers : Array[&@port.Observer]
}

///|
fn UserRequestObservingUiPort::UserRequestObservingUiPort(
  inner : &@port.UiPort,
  observers : Array[&@port.Observer],
) -> UserRequestObservingUiPort {
  { inner, observers, }
}

///|
fn user_request_label(req : @port.UiRequest) -> (String, String) {
  match req {
    Input(prompt~, ..) => ("input", prompt)
    Confirm(prompt~) => ("confirm", prompt)
    Select(prompt~, ..) => ("select", prompt)
  }
}

///|
fn UserRequestObservingUiPort::emit_user_request_event(
  self : UserRequestObservingUiPort,
  event : @types.TurnEvent,
) -> Unit {
  let snapshot = @event_snapshot.snapshot_turn_event(event)
  for observer in self.observers {
    // Scope is None: the wrapper sits outside any run identity.
    observer.on_event(None, snapshot)
  }
}

///|
impl @port.UiPort for UserRequestObservingUiPort with fn ui_descriptor(self) -> @port.UiDescriptor {
  self.inner.ui_descriptor()
}

///|
impl @port.UiPort for UserRequestObservingUiPort with fn render(
  self,
  intent : @port.UiRender,
) -> Unit {
  self.inner.render(intent)
}

///|
impl @port.UiPort for UserRequestObservingUiPort with fn request(
  self,
  req : @port.UiRequest,
) -> @port.UiResponse raise @error.UiError {
  let (kind, prompt) = user_request_label(req)
  self.emit_user_request_event(UserRequestStarted(kind~, prompt~))
  let result : Result[@port.UiResponse, @error.UiError] = Ok(
    self.inner.request(req),
  ) catch {
    e => Err(e)
  }
  let outcome : String = match result {
    Ok(_) => "answered"
    Err(Unsupported(..)) => "unsupported"
    Err(Cancelled) => "cancelled"
    Err(_) => "failed"
  }
  self.emit_user_request_event(UserRequestFinished(kind~, outcome~))
  match result {
    Ok(response) => response
    Err(error) => raise error
  }
}