///|
pub struct QueuedMessage {
  id : @uuid.Uuid
  message : @ai.Message
  web_search : Bool
}

///|
pub impl ToJson for QueuedMessage with fn to_json(self : QueuedMessage) -> Json {
  {
    "id": self.id.to_string(),
    "message": self.message.to_openai().to_json(),
    "web_search": self.web_search,
  }
}

///|
pub impl @json.FromJson for QueuedMessage with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> QueuedMessage raise @json.JsonDecodeError {
  let object = @jsonx.as_object(json, path~)
  let id : @uuid.Uuid = object.required("id", path~)
  let message : @openai.ChatCompletionMessageParam = object.required(
    "message",
    path~,
  )
  let message = @ai.Message::from_openai(message)
  let web_search : Bool = object.optional("web_search", path~).unwrap_or(false)
  { id, message, web_search }
}

///|
/// Represents an AI agent that interacts with language models and executes tools.
///
/// The `Agent` struct encapsulates the complete state and behavior of an AI agent,
/// including conversation history, available tools, model configuration, and event
/// handling. It manages the conversation loop, token counting, context pruning,
/// and tool execution.
pub(all) struct Agent {
  /// UUID generator for creating unique identifiers.
  uuid : @uuid.Generator
  /// Current working directory used as the base path for tool operations.
  cwd : String
  model : @model.Model
  logger : @pino.Logger
  priv tools : Map[String, Tool]
  /// Conversation history containing all messages exchanged.
  priv history : @conversation.Conversation
  /// Pending messages to be sent in the next API call.
  priv mut input_queue : Array[@ai.Message]
  priv pending_queue : @deque.Deque[QueuedMessage]
  event_target : @broadcast.Broadcast[@event.Event]
  priv token_counter : @token_counter.Counter
  /// Pruner for managing context size within token budgets.
  priv context_pruner : @context_pruner.Pruner
  priv session_manager : @conversation.Manager
  priv rules : @rules.Loader
  priv skills : @skills.Loader
  mut web_search : Bool
  request_timeout_ms : Int?
  /// Queue for receiving external events from the environment.
  priv external_events : @event.ExternalEventQueue
}

///|
/// Error raised when the API response contains no choices.
///
/// This error occurs when the chat completion response from the OpenAI API
/// returns an empty choices array, which indicates an unexpected API response
/// format or a failure in the model to generate a response.
priv suberror EmptyChoices

///|
/// Queues a message to be sent to the AI model in the next round of
/// conversation.
pub fn Agent::queue_message(
  agent : Agent,
  message : @ai.Message,
  web_search? : Bool = agent.web_search,
) -> @uuid.Uuid {
  let id = agent.uuid.v4()
  agent.pending_queue.push_back({ id, message, web_search })
  agent.emit(MessageQueued(id~))
  id
}

///|
pub fn Agent::queued_messages(self : Agent) -> Array[QueuedMessage] {
  self.pending_queue.to_array()
}

///|
/// Closes the agent and performs any necessary cleanup.
///
/// This function is currently a placeholder for future cleanup operations
/// such as closing connections, flushing logs, or releasing resources.
///
/// Parameters:
///
/// * `agent` : The agent instance to close (currently unused).
pub fn Agent::close(_ : Agent) -> Unit {

}

///|
/// Prepares messages for an API request by counting tokens, pruning if needed, and applying caching.
///
/// This function performs the following operations:
/// 1. Calculates which events need to be pruned to fit within token budget
/// 2. Emits `Pruned` events for each event to prune (making pruning persistent)
/// 3. Emits `TokenCounted` and `ContextPruned` events for monitoring
/// 4. Gets messages from conversation (respecting pruned events)
/// 5. Applies prompt caching to optimize API performance
///
/// Parameters:
///
/// * `agent` : The agent instance containing token counter and context pruner.
/// * `tools` : The tools array for the API call (used for token counting).
///
/// Returns a tuple of (cached_messages, estimated_token_count) ready to be sent to the API.
async fn Agent::prepare_messages_for_request(
  agent : Agent,
  tools~ : Array[@tool.ToolDesc],
) -> (Array[@openai.ChatCompletionMessageParam], Int) {
  let tools_openai = tools.map(x => x.to_openai())

  // Calculate which events need to be pruned (does not modify conversation)
  let prune_result = agent.context_pruner.calculate_pruning(
    agent.history,
    tools=tools_openai,
  )

  // Emit TokenCounted event with original token count for monitoring
  agent.emit(TokenCounted(prune_result.origin_token_count))

  // Emit Pruned events for each event to prune (makes pruning persistent)
  for event_id in prune_result.pruned_ids {
    agent.emit(Pruned(id=event_id))
  }

  // Emit ContextPruned event with before/after token counts
  agent.emit(
    ContextPruned(
      origin_token_count=prune_result.origin_token_count,
      pruned_token_count=prune_result.pruned_token_count,
    ),
  )

  // Get messages from conversation (respects newly emitted Pruned events)
  let messages = agent.history.messages(include_system=true)
  agent.skills.load_and_apply(messages)
  agent.rules.apply(messages)
  let openai_messages = messages.map(fn(msg) { msg.to_openai() })

  // Apply prompt caching to messages to improve API performance
  let cached_messages = @cache.cache_messages(openai_messages)
  (cached_messages, prune_result.pruned_token_count)
}

///|
/// Polls all registered external event sources and handles them.
///
/// This function is called at strategic points in the conversation loop to:
/// 1. Check for user cancellation requests
/// 2. Inject immediate user messages
/// 3. Add diagnostic context from the IDE
///
/// Parameters:
///
/// * `agent` : The agent instance to poll events for.
///
/// Returns `true` if the conversation should continue, `false` if cancelled.
fn Agent::poll_external_events(agent : Agent) -> Bool {
  let events = agent.external_events.poll()
  for event in events {
    agent.emit(event.desc, id=event.id)
    match event.desc {
      Cancelled => {
        agent.cancel()
        return false
      }
      UserMessage(msg) =>
        agent.input_queue.push(
          @ai.user_message(
            content=(
              $|[Immediate User Message]
              $|
              $|\{msg}
              $|
              $|Please address this message immediately and prioritize it over your current task.
            ),
          ),
        )
      _ => ()
    }
  }
  true
}

///|
fn Agent::cancel(agent : Agent) -> Unit {
  agent.input_queue.clear()
  agent.emit(Cancelled)
}

///|
/// Starts the agent's conversation loop and executes tool calls until completion.
///
/// This function implements the main agent execution loop:
/// 1. Spawns the event target in the background to handle event processing
/// 2. Emits a `PreConversation` event to signal the start
/// 3. Repeatedly calls the AI model and executes any requested tool calls
/// 4. Continues until the model returns a response with no tool calls
/// 5. Emits a `PostConversation` event when complete
///
/// The conversation loop automatically handles:
/// * Sending queued messages to the API
/// * Processing tool call requests from the model
/// * Executing tools and returning results
/// * Managing conversation history
///
/// Parameters:
///
/// * `agent` : The agent instance to start.
///
/// The function runs within an async task group to manage concurrent operations.
pub async fn Agent::start(agent : Agent) -> Unit {
  @async.with_task_group(group => {
    // Start the event target in background to handle async event processing
    let session = agent.event_target.spawn_in(group)
    defer session.stop()
    // Signal the start of conversation
    agent.emit(PreConversation)
    // We need to first poll external events before starting the loop,
    // otherwise the loop will exit immediately.
    while true {
      // Poll for external events at the start of each iteration
      // This handles user cancellation, immediate messages, and diagnostics
      guard agent.poll_external_events() else { break }
      let mut web_search = agent.web_search
      // Test if the agent.input_queue is empty, if so, pop from pending_queue
      if agent.input_queue.is_empty() {
        if agent.pending_queue.pop_front() is Some(msg) {
          // For sure to happen
          agent.input_queue.push(msg.message)
          web_search = msg.web_search
          agent.emit(MessageUnqueued(id=msg.id))
          agent.emit(UserMessage(msg.message.content()))
        }
      }
      if agent.input_queue.is_empty() {
        // No messages to send, end the conversation
        break
      }
      let tools = agent.tools_desc()
      // Retrieve and clear the message queue
      // staged contains messages that need to be sent in this call
      // Build the complete message array for the API call
      // Count tokens, prune if needed, and apply caching
      let (cache_messages, estimated_tokens) = agent.prepare_messages_for_request(
        tools~,
      )

      // Enable web search plugin if configured
      let extra_body : Map[String, Json] = {}
      if web_search {
        // OpenRouter supports a native web plugin. Other model backends rely on
        // the generic web_search tool when it is registered by the caller.
        if agent.model.base_url.contains("openrouter.ai/api/v1") {
          extra_body["plugins"] = [{ "id": "web" }]
        }
      }

      // Make the API request to get the model's response
      let response = @openai.chat_with_text_deltas(
        model=agent.model,
        @openai.chat_completion(
          messages=cache_messages,
          model=agent.model.model_name,
          tools=tools.map(x => x.to_openai()),
          stream=true,
        ),
        logger=agent.logger,
        extra_body~,
        on_text_delta=delta => {
          if !delta.is_blank() {
            agent.emit(AssistantMessageDelta(delta))
          }
        },
        timeout_ms=agent.request_timeout_ms,
      )
      // Extract the message from the response, raise error if no choices returned
      guard response is { choices: [{ message, .. }, ..], usage, .. } else {
        raise EmptyChoices
      }

      // Emit event with API usage statistics and the returned message
      agent.emit(
        AssistantMessage(
          usage=usage.map(u => @ai.Usage::from_openai(u)),
          tool_calls=message.tool_calls.map(tc => {
            @ai.ToolCall::from_openai_tool_call(tc)
          }),
          message.content.unwrap_or(""),
        ),
      )

      // Calibrate token counter based on actual usage from model response
      if usage is Some(u) {
        agent.token_counter.calibrate(
          model_name=agent.model.model_name,
          estimated_tokens~,
          actual_tokens=u.prompt_tokens,
        )
      }

      // Add all staged messages to permanent conversation history
      agent.input_queue = []
      // Persist the updated conversation history to disk
      agent.session_manager.save(agent.history)
      // A plain assistant turn with no tool calls is a terminal model response
      // for this loop. Exit immediately instead of relying on the next loop
      // iteration to notice an empty queue.
      if message.tool_calls.is_empty() {
        break
      }
      // Execute each tool call and queue the results for the next iteration
      for call in message.tool_calls {
        let tool_message = agent.execute_tool(
          @ai.ToolCall::from_openai_tool_call(call),
        )
        // Save again after each tool call so a stalled follow-up model round
        // still leaves a durable post-tool state on disk.
        agent.session_manager.save(agent.history)
        agent.input_queue.push(tool_message)
      }
    }
    // Signal the end of conversation
    agent.emit(PostConversation)
    // Persist the final closed conversation state so downstream analysis and
    // reconciliation do not have to infer whether the loop exited cleanly.
    agent.session_manager.save(agent.history)
  })
}

///|
fn default_agent_log_path() -> String {
  @moonsuite.product_artifact_for_workspace_root(
    @os.home() catch {
      _ => "."
    },
    "moonclaw",
    "logs/agent.jsonl",
  )
}

///|
/// Creates a new agent instance with the specified AI model and working directory.
///
/// This function initializes a complete agent with all necessary components:
/// * Random number generator and UUID generator (or uses provided ones)
/// * Logger for recording agent activities (defaults to file logging)
/// * Conversation history manager
/// * Token counter for tracking API usage
/// * Context pruner for managing message history within token budgets
/// * Empty tool collection and message queue
/// * Event target for handling lifecycle events
///
/// Parameters:
///
/// * `model` : The AI model to use for generating responses and handling tool calls.
/// * `rand` : Optional random number generator. If not provided, uses ChaCha8.
/// * `uuid` : Optional UUID generator. If not provided, creates one using the random generator.
/// * `logger` : Optional logger instance. Defaults to the MoonClaw product-home log.
/// * `cwd` : The current working directory that will be used as the base path for tool operations.
/// * `system_message` : Optional system prompt to set at initialization.
/// * `user_message` : Optional initial user message to send immediately after initialization.
/// * `home` : Optional MoonSuite root path. If not provided, uses the OS home
///   as the standalone suite root.
/// * `web_search` : Whether to enable web search functionality. Defaults to `false`.
/// * `external_events` : Optional external event queue for receiving events from the environment.
/// * `history` : Optional existing conversation history to load into the agent.
///
/// Returns a new `Agent` instance initialized with empty conversation history,
/// no tools, and a fresh event target for handling agent lifecycle events.
///
/// The agent's context pruner is configured with the model's `safe_zone_tokens`
/// setting to automatically manage conversation history size.
pub async fn new(
  name? : String,
  model : @model.Model,
  uuid? : @uuid.Generator,
  logger? : @pino.Logger = @pino.logger(
    "agent",
    try! @pino.Transport::parse("file:\{default_agent_log_path()}"),
  ),
  system_message? : String,
  user_message? : String,
  home? : StringView,
  cwd~ : StringView,
  web_search? : Bool = false,
  external_events? : @event.ExternalEventQueue,
  history? : @conversation.Conversation,
  request_timeout_ms? : Int? = None,
) -> Agent {
  let home = match home {
    Some(home) => home.to_owned()
    None => @os.home()
  }
  // FIXME:(upstream) function with error not allowed in optional expression
  let uuid = match uuid {
    Some(uuid) => uuid
    None => @uuid.generator(@rand.chacha8())
  }
  let session_manager = @conversation.Manager::new(uuid~, home~)
  let rules = @rules.Loader::new(cwd.to_owned(), logger~)
  let event_target : @broadcast.Broadcast[@event.Event] = @broadcast.Broadcast::new()
  event_target.add_listener(event => {
    let is_delta = match event.desc {
      AssistantMessageDelta(_) => true
      _ => false
    }
    guard !is_delta else { return }
    logger.info("Received event", data={ "event": event.to_json() })
  })
  event_target.put(
    @event.Event::new(id=uuid.v4(), ModelLoaded(name=model.name)),
  )
  let history = match history {
    Some(history) => {
      for event in history.events() {
        event_target.put(event)
      }
      history
    }
    None =>
      session_manager.new_conversation(
        name=name.unwrap_or("New Conversation"),
        cwd=cwd.to_owned(),
        web_search~,
      )
  }
  let external_events = match external_events {
    Some(queue) => queue
    None => @event.ExternalEventQueue::new()
  }
  let skills = @skills.Loader::new(cwd=cwd.to_owned(), logger~)
  let agent = Agent::{
    logger,
    uuid,
    history,
    cwd: cwd.to_owned(),
    model,
    tools: {},
    input_queue: [],
    pending_queue: Deque([]),
    event_target,
    token_counter: @token_counter.Counter::new(logger~),
    context_pruner: @context_pruner.Pruner::new(
      safe_zone_tokens=model.safe_zone_tokens,
      logger~,
    ),
    session_manager,
    rules,
    skills,
    web_search,
    request_timeout_ms,
    external_events,
  }
  rules.load()
  skills.load()
  agent.set_system_prompt(system_message)
  match user_message {
    Some(message) => agent.external_events.send(UserMessage(message))
    None => ()
  }
  agent
}

///|
/// Loads an existing agent from a conversation history with the specified
/// configuration.
///
/// Parameters:
///
/// * `model` : The AI model to use for generating responses and handling tool
///   calls.
/// * `history` : The existing conversation history to load into the agent.
/// * `uuid` : Optional UUID generator for creating unique identifiers. If not
///   provided, uses ChaCha8.
/// * `logger` : Optional logger instance for recording agent activities.
///   Defaults to the MoonClaw product-home log.
/// * `home` : Optional MoonSuite root path. If not provided, uses the OS home
///   as the standalone suite root.
/// * `user_message` : Optional initial user message to send immediately after
///   loading.
/// * `web_search` : Whether to enable web search functionality. Defaults to
///   `false`.
/// * `external_events` : Optional external event queue for receiving events
///   from the environment.
///
/// Returns a new `Agent` instance initialized with the provided conversation
/// history and configuration.
pub async fn load(
  model : @model.Model,
  history : @conversation.Conversation,
  uuid? : @uuid.Generator,
  logger? : @pino.Logger,
  home? : StringView,
  user_message? : String,
  web_search? : Bool = false,
  external_events? : @event.ExternalEventQueue,
  request_timeout_ms? : Int? = None,
) -> Agent {
  new(
    model,
    uuid?,
    logger?,
    home?,
    cwd=history.cwd(),
    user_message?,
    web_search~,
    history~,
    external_events?,
    request_timeout_ms~,
  )
}

///|
pub fn Agent::clear_inputs(self : Agent) -> Array[QueuedMessage] {
  self.input_queue.clear()
  let pms = []
  while self.pending_queue.pop_front() is Some(pm) {
    self.emit(MessageUnqueued(id=pm.id))
    pms.push(pm)
  }
  pms
}

///|
/// Gets the unique identifier of the agent.
///
/// Parameters:
///
/// * `agent` : The agent instance to get the identifier for.
///
/// Returns the UUID that uniquely identifies this agent instance.
pub fn Agent::id(agent : Agent) -> @uuid.Uuid {
  agent.history.id()
}