///|
/// A conversation session. `messages` is the linear transcript this thread
/// owns; `metadata` is a free-form product-owned bag (used for lineage via
/// `parent_thread_id`, product-specific flags, anything posoco does not
/// interpret).
///
/// R3 M3.7: `messages` carries canonical `@kernel.Message` values directly.
/// There is no longer a legacy `@types.Message` form — the kernel ADT is the
/// single protocol.
pub(all) struct Session {
  messages : Array[@kernel.Message]
  metadata : Map[String, Json]
} derive(Eq, Debug)

///|
/// Metadata key under which a session's parent thread id is stored. Absent
/// for threads created via the `new` command (fresh, no parent). Present
/// when this session was forked from another thread (user-initiated
/// `/fork-here`) or compacted from another thread (modelport-driven compact
/// via `ModelPort::compact` returning `CompactMode::NewThread`).
///
/// Stored in `metadata` rather than a dedicated field so existing Session
/// construction sites continue to compile unchanged. Product code reads it
/// via `Session::parent_thread_id(session)`.
pub const PARENT_THREAD_ID_KEY = "posoco.parent_thread_id"

///|
/// Read the parent thread id from a session's metadata. Returns `None` when
/// the key is absent or not a string.
pub fn Session::parent_thread_id(self : Session) -> String? {
  match self.metadata.get(PARENT_THREAD_ID_KEY) {
    Some(Json::String(s)) => Some(s)
    _ => None
  }
}

///|
/// Construct a Session with the given parent thread id recorded in its
/// metadata. Used by fork/compact handlers.
pub fn Session::with_parent_thread_id(
  self : Session,
  parent_thread_id : String,
) -> Session {
  let new_metadata : Map[String, Json] = self.metadata.copy()
  new_metadata[PARENT_THREAD_ID_KEY] = Json::string(parent_thread_id)
  { messages: self.messages, metadata: new_metadata }
}

///|
/// Turn lifecycle events observed via `Observer::on_event`. R3 M3.7: payload
/// types are now canonical kernel types (`ToolCall`, `ToolOutcome`,
/// `Message`, `Usage`). `is_error` on `ToolCallResult` is preserved for
/// legacy observer compatibility — it is derived from the `ToolOutcome`
/// variant (`is_failure`).
pub(all) enum TurnEvent {
  TurnStarted
  ToolCallPending(@kernel.ToolCall)
  ToolCallResult(
    call~ : @kernel.ToolCall,
    result~ : @kernel.ToolOutcome,
    is_error~ : Bool
  )
  ModelResponseReceived(message~ : @kernel.Message, usage~ : @kernel.Usage?)
  SessionRedirect(
    from~ : String,
    to~ : String,
    messages_before~ : Int,
    messages_after~ : Int
  )
  TurnCompleted
  TurnFailed(String)
  ToolCallDeferred(call~ : @kernel.ToolCall, reason~ : String)
  StreamChunkReceived(chunk~ : StreamChunk)
  /// Configuration warning emitted by an extension or product configuration
  /// flow when a requested setting cannot be applied. This event lets
  /// subscribers surface the warning without coupling to the configuration
  /// source.
  ///
  /// `field` identifies the configuration field, `value` is the requested
  /// value, and `reason` explains why it could not be applied.
  ConfigWarning(field~ : String, value~ : String, reason~ : String)
  /// Configuration change notification emitted by an extension or product
  /// configuration flow. Persistence, UI, and other subscribers use it to
  /// stay in sync without polling or coupling to the configuration source.
  ///
  /// `old_value` is empty when the field was previously unset.
  /// `new_value` is empty when the field was reset to `None`.
  ConfigChanged(field~ : String, old_value~ : String, new_value~ : String)
  Custom(source~ : String, label~ : String, data~ : Json)
} derive(Eq, Debug)

///|
pub impl Show for TurnEvent with fn to_string(self : TurnEvent) -> String {
  match self {
    TurnStarted => "TurnStarted"
    ToolCallPending(call) => "ToolCallPending(\{call.call_id.to_string()})"
    ToolCallResult(call~, is_error~, ..) =>
      "ToolCallResult(\{call.call_id.to_string()}, is_error=\{is_error})"
    ModelResponseReceived(..) => "ModelResponseReceived"
    SessionRedirect(from~, to~, ..) => "SessionRedirect(from:\{from}, to:\{to})"
    TurnCompleted => "TurnCompleted"
    TurnFailed(msg) => "TurnFailed(\{msg})"
    ToolCallDeferred(call~, reason~) =>
      "ToolCallDeferred(\{call.call_id.to_string()}, \{reason})"
    StreamChunkReceived(..) => "StreamChunkReceived"
    ConfigWarning(field~, value~, reason~) =>
      "ConfigWarning(field=\{field}, value=\{value}, reason=\{reason})"
    ConfigChanged(field~, old_value~, new_value~) =>
      "ConfigChanged(field=\{field}, \{old_value} → \{new_value})"
    Custom(source~, label~, ..) => "Custom(source:\{source}, label:\{label})"
  }
}

///|
/// Return value of `Agent::run_turn`. R3 M3.7: payload types are canonical.
pub(all) struct TurnResult {
  message : @kernel.Message
  tool_results : Array[@kernel.ToolOutcome]
  final_session_id : String
} derive(Eq, Debug)