//! Tool execution, permission checking, and result storage
//!
//! This module handles the complete lifecycle of tool calls: execution, permission
//! checking, snapshotting, and storing results back into conversation history.

use crate::acp::client_bridge::ClientBridgeSender;
use crate::agent::agent_config::AgentConfig;
use crate::agent::core::SnapshotPolicy;
use crate::agent::execution_context::ExecutionContext;
use crate::agent::snapshots::{SnapshotState, snapshot_metadata};
use crate::events::AgentEventKind;
use crate::hooks::{
    PermissionRequestDecision, PostToolUseRequest, PreDelegationRequest, PreToolUseRequest,
    UpdatedDelegation,
};
use crate::middleware::ToolCall as MiddlewareToolCall;
use crate::middleware::{ExecutionState, ToolResult, WaitCondition};
use crate::model::{AgentMessage, MessagePart};
use crate::session::domain::TaskStatus;
use log::debug;
use querymt::chat::{ChatRole, Content};
use std::sync::Arc;
use tracing::{Instrument, Span, info_span, instrument};
use uuid::Uuid;

/// Execute a single tool call.
///
/// This function:
/// 1. Emits tool call start event
/// 2. Creates a snapshot if configured
/// 3. Records progress
/// 4. Checks permissions (if required)
/// 5. Executes the tool
/// 6. Truncates output if needed
/// 7. Creates snapshot diff/metadata
/// 8. Returns the tool result
#[instrument(
    name = "agent.tool.execute",
    skip(config, exec_ctx, bridge),
    fields(
        session_id = %exec_ctx.session_id,
        tool_name = %call.function.name,
        tool_call_id = %call.id,
        tool_source = tracing::field::Empty,
        is_error = tracing::field::Empty,
        has_snapshot = tracing::field::Empty
    )
)]
pub(super) async fn execute_tool_call(
    config: &AgentConfig,
    call: &MiddlewareToolCall,
    exec_ctx: &ExecutionContext,
    bridge: Option<&ClientBridgeSender>,
) -> Result<ToolResult, anyhow::Error> {
    debug!(
        "Executing tool: session={}, tool={}",
        exec_ctx.session_id, call.function.name
    );

    let mut args: serde_json::Value =
        serde_json::from_str(&call.function.arguments).unwrap_or_else(|_| serde_json::json!({}));
    let hook_result = config
        .hooks
        .run_pre_tool_use(PreToolUseRequest {
            session_id: exec_ctx.session_id.clone(),
            turn_id: exec_ctx.turn_id().unwrap_or_default().to_string(),
            cwd: exec_ctx.cwd().map(|path| path.to_path_buf()),
            model: exec_ctx
                .llm_config()
                .map(|cfg| cfg.model.clone())
                .unwrap_or_default(),
            permission_mode: exec_ctx.permission_mode().to_string(),
            tool_name: call.function.name.clone(),
            tool_input: args.clone(),
            tool_use_id: call.id.clone(),
        })
        .await?;
    for notice in hook_result.notices {
        config.emit_event(
            &exec_ctx.session_id,
            AgentEventKind::HookNotice {
                event_name: notice.event_name,
                message: notice.message,
                is_error: notice.is_error,
            },
        );
    }
    if let Some(updated_input) = hook_result.updated_input {
        args = updated_input;
    }
    if !hook_result.additional_contexts.is_empty() {
        log::debug!(
            "Session {}: ignoring {} pre-tool-use hook additional_context item(s) for MVP",
            exec_ctx.session_id,
            hook_result.additional_contexts.len()
        );
    }
    if hook_result.should_block {
        let reason = hook_result
            .block_reason
            .unwrap_or_else(|| "tool blocked by hook".to_string());
        return Ok(ToolResult::new(
            call.id.clone(),
            vec![Content::text(format!("Error: {}", reason))],
            true,
            Some(call.function.name.clone()),
            Some(serde_json::to_string(&args).unwrap_or_else(|_| call.function.arguments.clone())),
        ));
    }

    let arguments_json =
        serde_json::to_string(&args).unwrap_or_else(|_| call.function.arguments.clone());

    config.emit_event(
        &exec_ctx.session_id,
        AgentEventKind::ToolCallStart {
            tool_call_id: call.id.clone(),
            tool_name: call.function.name.clone(),
            arguments: arguments_json.clone(),
        },
    );

    let snapshot = if config.should_snapshot_tool(&call.function.name) {
        info_span!(
            "agent.tool.snapshot.prepare",
            session_id = %exec_ctx.session_id,
            tool_name = %call.function.name,
            tool_call_id = %call.id,
        )
        .in_scope(|| {
            config
                .prepare_snapshot(exec_ctx.cwd())
                .map(|(root, policy)| {
                    config.emit_event(
                        &exec_ctx.session_id,
                        AgentEventKind::SnapshotStart {
                            policy: policy.to_string(),
                        },
                    );
                    match policy {
                        SnapshotPolicy::Diff => {
                            let pre_tree = crate::index::merkle::MerkleTree::scan(root.as_path());
                            SnapshotState::Diff { pre_tree, root }
                        }
                        SnapshotPolicy::Metadata => SnapshotState::Metadata { root },
                        SnapshotPolicy::None => SnapshotState::None,
                    }
                })
                .unwrap_or(SnapshotState::None)
        })
    } else {
        SnapshotState::None
    };

    // Progress recording is observational — a failure here must not abort the
    // tool call itself, otherwise the tool_use block ends up without a matching
    // tool_result and the session becomes permanently broken.
    match exec_ctx
        .state
        .record_progress(
            crate::session::domain::ProgressKind::ToolCall,
            format!("Calling tool: {}", call.function.name),
            Some(args.clone()),
        )
        .await
    {
        Ok(progress_entry) => {
            config.emit_event(
                &exec_ctx.session_id,
                AgentEventKind::ProgressRecorded { progress_entry },
            );
        }
        Err(e) => {
            log::warn!(
                "Failed to record progress for tool {} (session {}): {}",
                call.function.name,
                exec_ctx.session_id,
                e
            );
        }
    }

    // Set up elicitation channel for this tool call
    let (elicitation_tx, mut elicitation_rx) =
        tokio::sync::mpsc::channel::<crate::tools::ElicitationRequest>(1);

    let event_sink = config.event_sink.clone();
    let session_id_clone = exec_ctx.session_id.clone();
    let pending_elicitations = config.pending_elicitations.clone();
    tokio::spawn(async move {
        while let Some(request) = elicitation_rx.recv().await {
            let elicitation_id = request.elicitation_id.clone();
            {
                let mut pending = pending_elicitations.lock().await;
                pending.insert(elicitation_id.clone(), request.response_tx);
            }
            // Durable: elicitation must be visible in UI replay.
            if let Err(err) = event_sink
                .emit_durable(
                    &session_id_clone,
                    crate::events::AgentEventKind::ElicitationRequested {
                        elicitation_id,
                        session_id: session_id_clone.clone(),
                        message: request.message,
                        requested_schema: request.requested_schema,
                        source: request.source,
                    },
                )
                .await
            {
                log::warn!("failed to emit ElicitationRequested: {}", err);
            }
        }
    });

    let tool_context = exec_ctx.tool_context(config.agent_registry.clone(), Some(elicitation_tx));

    // Check tool permission using the per-session tool config so that runtime
    // mutations (SetAllowedTools / SetDeniedTools) are respected. Also look up
    // the MCP server name so that "servername.*" wildcard entries in the
    // allowlist correctly permit MCP tools (e.g. "context7.*" → "resolve-library-id").
    //
    // Load a lock-free snapshot of the MCP tool state. The `Guard` is
    // dropped before any `.await` (same discipline as the old RwLock).
    let (mcp_server_name, mcp_tool) = {
        let snap = exec_ctx.runtime.mcp_tool_state.load();
        let server_name = snap
            .tools
            .get(&call.function.name)
            .map(|a| a.server_name().to_owned());
        let tool = snap.tools.get(&call.function.name).cloned();
        (server_name, tool)
    };

    let (raw_result_blocks, is_error, tool_source) =
        if !crate::agent::tools::is_mcp_tool_allowed_with(
            &exec_ctx.tool_config,
            &call.function.name,
            mcp_server_name.as_deref(),
        ) {
            (
                vec![Content::text(format!(
                    "Error: tool '{}' is not allowed",
                    call.function.name
                ))],
                true,
                "blocked",
            )
        } else if let Some(tool) = config.tool_registry.find(&call.function.name) {
            match tool
                .call(args.clone(), &tool_context)
                .instrument(info_span!(
                    "agent.tool.invoke",
                    source = "builtin",
                    tool_name = %call.function.name,
                    tool_call_id = %call.id,
                ))
                .await
            {
                Ok(res) => (res, false, "builtin"),
                Err(e) => (
                    vec![Content::text(format!("Error: {}", e))],
                    true,
                    "builtin",
                ),
            }
        } else if let Some(tool) = mcp_tool {
            use querymt::tool_decorator::CallFunctionTool;
            match tool
                .call(args.clone())
                .instrument(info_span!(
                    "agent.tool.invoke",
                    source = "mcp",
                    tool_name = %call.function.name,
                    tool_call_id = %call.id,
                ))
                .await
            {
                Ok(res) => (res, false, "mcp"),
                Err(e) => (vec![Content::text(format!("Error: {}", e))], true, "mcp"),
            }
        } else if !ensure_tool_permission(
            config,
            exec_ctx,
            &call.id,
            &call.function.name,
            &args,
            bridge,
            exec_ctx.turn_id().unwrap_or_default(),
        )
        .instrument(info_span!(
            "agent.tool.permission_wait",
            tool_name = %call.function.name,
            tool_call_id = %call.id,
            session_id = %exec_ctx.session_id,
        ))
        .await
        .map_err(|e| anyhow::anyhow!("Permission check failed: {}", e))?
        {
            (
                vec![Content::text("Error: permission denied")],
                true,
                "provider",
            )
        } else {
            match exec_ctx
                .session_handle
                .call_tool(&call.function.name, args.clone())
                .instrument(info_span!(
                    "agent.tool.invoke",
                    source = "provider",
                    tool_name = %call.function.name,
                    tool_call_id = %call.id,
                ))
                .await
            {
                Ok(res) => (res, false, "provider"),
                Err(e) => (
                    vec![Content::text(format!("Error: {}", e))],
                    true,
                    "provider",
                ),
            }
        };

    let span = Span::current();
    span.record("tool_source", tool_source);
    span.record("is_error", is_error);

    // Apply Layer 1 truncation to text content blocks.
    let result_blocks = if !is_error {
        use crate::tools::builtins::helpers::{
            TruncationDirection, format_truncation_message_with_overflow, save_overflow_output,
            truncate_output,
        };
        let tc = &config.execution_policy.tool_output;

        let raw_text: String = raw_result_blocks
            .iter()
            .filter_map(|b| b.as_text())
            .collect::<Vec<_>>()
            .join("\n");

        let truncation = truncate_output(
            &raw_text,
            tc.max_lines,
            tc.max_bytes,
            TruncationDirection::Head,
        );

        if truncation.was_truncated {
            let overflow = save_overflow_output(
                &raw_text,
                &tc.overflow_storage,
                &exec_ctx.session_id,
                &call.id,
                None,
            );

            let tool_hint = config
                .tool_registry
                .find(&call.function.name)
                .and_then(|t| t.truncation_hint());

            let suffix = format_truncation_message_with_overflow(
                &truncation,
                TruncationDirection::Head,
                Some(&overflow),
                tool_hint,
            );

            let mut blocks: Vec<Content> = raw_result_blocks
                .into_iter()
                .filter(|b| b.as_text().is_none())
                .collect();
            blocks.insert(
                0,
                Content::text(format!("{}{}", truncation.content, suffix)),
            );
            blocks
        } else {
            raw_result_blocks
        }
    } else {
        raw_result_blocks
    };

    let mut result_blocks = result_blocks;
    let mut is_error = is_error;
    let post_hook = config
        .hooks
        .run_post_tool_use(PostToolUseRequest {
            session_id: exec_ctx.session_id.clone(),
            turn_id: exec_ctx.turn_id().unwrap_or_default().to_string(),
            cwd: exec_ctx.cwd().map(|path| path.to_path_buf()),
            model: exec_ctx
                .llm_config()
                .map(|cfg| cfg.model.clone())
                .unwrap_or_default(),
            permission_mode: exec_ctx.permission_mode().to_string(),
            tool_name: call.function.name.clone(),
            tool_input: args.clone(),
            tool_response: serde_json::Value::String(
                result_blocks
                    .iter()
                    .filter_map(|b| b.as_text())
                    .collect::<Vec<_>>()
                    .join("\n"),
            ),
            tool_use_id: call.id.clone(),
        })
        .await?;
    for notice in post_hook.notices {
        config.emit_event(
            &exec_ctx.session_id,
            AgentEventKind::HookNotice {
                event_name: notice.event_name,
                message: notice.message,
                is_error: notice.is_error,
            },
        );
    }
    if !post_hook.additional_contexts.is_empty() {
        log::debug!(
            "Session {}: ignoring {} post-tool-use hook additional_context item(s) for MVP",
            exec_ctx.session_id,
            post_hook.additional_contexts.len()
        );
    }
    if post_hook.should_block {
        is_error = true;
        result_blocks = vec![Content::text(format!(
            "Error: {}",
            post_hook
                .block_reason
                .unwrap_or_else(|| "tool result blocked by hook".to_string())
        ))];
    }

    // Extract text summary for event (display purposes)
    let result_text: String = result_blocks
        .iter()
        .filter_map(|b| b.as_text())
        .collect::<Vec<_>>()
        .join("\n");

    config.emit_event(
        &exec_ctx.session_id,
        AgentEventKind::ToolCallEnd {
            tool_call_id: call.id.clone(),
            tool_name: call.function.name.clone(),
            is_error,
            result: result_text,
        },
    );

    let snapshot_part = match snapshot {
        SnapshotState::Diff { pre_tree, root } => {
            let (post_tree, changed_paths) = info_span!(
                "agent.tool.snapshot.diff",
                session_id = %exec_ctx.session_id,
                tool_name = %call.function.name,
                tool_call_id = %call.id,
            )
            .in_scope(|| {
                let post_tree = crate::index::merkle::MerkleTree::scan_with_previous(
                    root.as_path(),
                    Some(&pre_tree),
                );
                let changed_paths = post_tree.diff_paths(&pre_tree);
                (post_tree, changed_paths)
            });
            config.emit_event(
                &exec_ctx.session_id,
                AgentEventKind::SnapshotEnd {
                    summary: Some(changed_paths.summary()),
                },
            );
            Some(MessagePart::Snapshot {
                root_hash: post_tree.root_hash,
                changed_paths,
            })
        }
        SnapshotState::Metadata { root } => {
            let (part, summary) = info_span!(
                "agent.tool.snapshot.metadata",
                session_id = %exec_ctx.session_id,
                tool_name = %call.function.name,
                tool_call_id = %call.id,
            )
            .in_scope(|| snapshot_metadata(root.as_path()));
            config.emit_event(
                &exec_ctx.session_id,
                AgentEventKind::SnapshotEnd { summary },
            );
            Some(part)
        }
        SnapshotState::None => None,
    };

    let mut tool_result = ToolResult::new(
        call.id.clone(),
        result_blocks,
        is_error,
        Some(call.function.name.clone()),
        Some(arguments_json.clone()),
    );
    if let Some(part) = snapshot_part {
        tool_result = tool_result.with_snapshot(part);
    }

    Span::current().record("has_snapshot", tool_result.snapshot_part.is_some());

    Ok(tool_result)
}

/// Check if a tool call requires permission and request it if needed.
///
/// Returns `true` if permission is granted (or not required), `false` if denied.
#[instrument(
    name = "agent.tool.permission",
    skip(config, exec_ctx, args, bridge),
    fields(
        session_id = %exec_ctx.session_id,
        tool_name = %tool_name,
        tool_call_id = %tool_call_id,
        requires_permission = tracing::field::Empty,
        cache_hit = tracing::field::Empty,
        granted = tracing::field::Empty
    )
)]
pub(super) async fn ensure_tool_permission(
    config: &AgentConfig,
    exec_ctx: &ExecutionContext,
    tool_call_id: &str,
    tool_name: &str,
    args: &serde_json::Value,
    bridge: Option<&ClientBridgeSender>,
    turn_id: &str,
) -> Result<bool, agent_client_protocol::Error> {
    use crate::agent::utils::{extract_locations, tool_kind_for_tool};
    use agent_client_protocol::schema::{
        PermissionOption, PermissionOptionId, PermissionOptionKind, RequestPermissionOutcome,
        RequestPermissionRequest, ToolCallId, ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields,
    };

    let requires_permission = config.requires_permission_for_tool(tool_name);
    Span::current().record("requires_permission", requires_permission);
    if !requires_permission {
        Span::current().record("cache_hit", false);
        Span::current().record("granted", true);
        return Ok(true);
    }

    if let Some(task) = &exec_ctx.state.active_task
        && task.status != TaskStatus::Active
    {
        return Ok(false);
    }

    {
        let cache = exec_ctx.runtime.permission_cache.lock();
        if let Some(cached) = cache.get(tool_name) {
            Span::current().record("cache_hit", true);
            Span::current().record("granted", *cached);
            return Ok(*cached);
        }
    }
    Span::current().record("cache_hit", false);

    let hook_decision = config
        .hooks
        .run_permission_request(crate::hooks::PermissionRequestRequest {
            session_id: exec_ctx.session_id.clone(),
            turn_id: turn_id.to_string(),
            cwd: exec_ctx.cwd().map(|path| path.to_path_buf()),
            model: exec_ctx
                .llm_config()
                .map(|cfg| cfg.model.clone())
                .unwrap_or_default(),
            permission_mode: exec_ctx.permission_mode().to_string(),
            tool_name: tool_name.to_string(),
            tool_input: args.clone(),
        })
        .await
        .map_err(|e| agent_client_protocol::Error::internal_error().data(e.to_string()))?;
    for notice in hook_decision.notices {
        config.emit_event(
            &exec_ctx.session_id,
            AgentEventKind::HookNotice {
                event_name: notice.event_name,
                message: notice.message,
                is_error: notice.is_error,
            },
        );
    }
    match hook_decision.decision {
        Some(PermissionRequestDecision::Allow) => {
            Span::current().record("granted", true);
            return Ok(true);
        }
        Some(PermissionRequestDecision::Deny { message }) => {
            log::debug!(
                "Session {}: permission hook denied {}: {}",
                exec_ctx.session_id,
                tool_name,
                message
            );
            Span::current().record("granted", false);
            return Ok(false);
        }
        None => {}
    }

    let permission_id = Uuid::new_v4().to_string();
    config.emit_event(
        &exec_ctx.session_id,
        AgentEventKind::PermissionRequested {
            permission_id: permission_id.clone(),
            task_id: exec_ctx
                .state
                .active_task
                .as_ref()
                .map(|task| task.public_id.clone()),
            tool_name: tool_name.to_string(),
            reason: format!("Tool {} requires explicit permission", tool_name),
        },
    );

    // In the actor model, only the bridge is available (no client)
    let Some(bridge) = bridge else {
        // No bridge available — auto-grant permission
        config.emit_event(
            &exec_ctx.session_id,
            AgentEventKind::PermissionGranted {
                permission_id,
                granted: true,
            },
        );
        Span::current().record("granted", true);
        return Ok(true);
    };

    let locations = extract_locations(args);
    let tool_update_fields = ToolCallUpdateFields::new()
        .title(format!("Run {}", tool_name))
        .kind(tool_kind_for_tool(tool_name))
        .status(ToolCallStatus::Pending)
        .locations(if locations.is_empty() {
            None
        } else {
            Some(locations)
        })
        .raw_input(args.clone());

    let request = RequestPermissionRequest::new(
        exec_ctx.session_id.clone(),
        ToolCallUpdate::new(
            ToolCallId::from(tool_call_id.to_string()),
            tool_update_fields,
        ),
        vec![
            PermissionOption::new(
                PermissionOptionId::from("allow_once"),
                "Allow once",
                PermissionOptionKind::AllowOnce,
            ),
            PermissionOption::new(
                PermissionOptionId::from("allow_always"),
                "Always allow",
                PermissionOptionKind::AllowAlways,
            ),
            PermissionOption::new(
                PermissionOptionId::from("reject_once"),
                "Reject once",
                PermissionOptionKind::RejectOnce,
            ),
            PermissionOption::new(
                PermissionOptionId::from("reject_always"),
                "Always reject",
                PermissionOptionKind::RejectAlways,
            ),
        ],
    );

    let response = bridge.request_permission(request).await?;
    let granted = match response.outcome {
        RequestPermissionOutcome::Selected(selected) => {
            let option_id = selected.option_id.0.as_ref();
            let allow = option_id == "allow_once" || option_id == "allow_always";
            {
                let mut cache = exec_ctx.runtime.permission_cache.lock();
                if option_id == "allow_always" {
                    cache.insert(tool_name.to_string(), true);
                } else if option_id == "reject_always" {
                    cache.insert(tool_name.to_string(), false);
                }
            }
            allow
        }
        _ => false,
    };

    config.emit_event(
        &exec_ctx.session_id,
        AgentEventKind::PermissionGranted {
            permission_id,
            granted,
        },
    );

    Span::current().record("granted", granted);

    Ok(granted)
}

/// Record side effects of a tool execution (artifacts, delegations).
///
/// Returns a wait condition if the tool initiated an action that requires waiting
/// (e.g., delegation).
#[instrument(
    name = "agent.tool.side_effects",
    skip(config, result, exec_ctx),
    fields(
        session_id = %exec_ctx.session_id,
        tool_call_id = %result.call_id,
        tool_name = result.tool_name.as_deref().unwrap_or("unknown")
    )
)]
pub(super) async fn record_tool_side_effects(
    config: &AgentConfig,
    result: &mut ToolResult,
    exec_ctx: &ExecutionContext,
) -> Result<Option<WaitCondition>, anyhow::Error> {
    if result.is_error {
        return Ok(None);
    }

    let Some(tool_name) = result.tool_name.as_ref() else {
        return Ok(None);
    };

    if tool_name == "write_file" || tool_name == "apply_patch" {
        let args: serde_json::Value =
            serde_json::from_str(result.tool_arguments.as_deref().unwrap_or("{}"))
                .unwrap_or_default();
        let path = args
            .get("path")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        if let Ok(artifact) = exec_ctx
            .state
            .record_artifact(
                "file".to_string(),
                None,
                path.clone(),
                Some(format!("Produced by {}", tool_name)),
            )
            .await
        {
            config.emit_event(
                &exec_ctx.session_id,
                AgentEventKind::ArtifactRecorded { artifact },
            );
        }
    }

    if tool_name == "delegate" {
        let args: serde_json::Value =
            serde_json::from_str(result.tool_arguments.as_deref().unwrap_or("{}"))
                .unwrap_or_default();
        let target_agent_id = args
            .get("target_agent_id")
            .and_then(|v| v.as_str())
            .unwrap_or_default()
            .to_string();
        let objective = args
            .get("objective")
            .and_then(|v| v.as_str())
            .unwrap_or_default()
            .to_string();
        let context_val = args
            .get("context")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());
        let constraints = args
            .get("constraints")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());
        let expected_output = args
            .get("expected_output")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        let pre_hook = config
            .hooks
            .run_pre_delegation(PreDelegationRequest {
                session_id: exec_ctx.session_id.clone(),
                turn_id: exec_ctx.turn_id().unwrap_or_default().to_string(),
                cwd: exec_ctx.cwd().map(|path| path.to_path_buf()),
                model: exec_ctx
                    .llm_config()
                    .map(|cfg| cfg.model.clone())
                    .unwrap_or_default(),
                permission_mode: exec_ctx.permission_mode().to_string(),
                tool_use_id: result.call_id.clone(),
                target_agent_id: target_agent_id.clone(),
                objective: objective.clone(),
                context: context_val.clone(),
                constraints: constraints.clone(),
                expected_output: expected_output.clone(),
            })
            .await?;
        for notice in pre_hook.notices {
            config.emit_event(
                &exec_ctx.session_id,
                AgentEventKind::HookNotice {
                    event_name: notice.event_name,
                    message: notice.message,
                    is_error: notice.is_error,
                },
            );
        }

        let UpdatedDelegation {
            target_agent_id: updated_target_agent_id,
            objective: updated_objective,
            context: updated_context,
            constraints: updated_constraints,
            expected_output: updated_expected_output,
        } = pre_hook.updated_delegation.unwrap_or_default();

        let target_agent_id = updated_target_agent_id.unwrap_or(target_agent_id);
        let objective = updated_objective.unwrap_or(objective);
        let mut context_val = updated_context.or(context_val);
        let constraints = updated_constraints.or(constraints);
        let expected_output = updated_expected_output.or(expected_output);

        if !pre_hook.additional_contexts.is_empty() {
            let extra_context = pre_hook.additional_contexts.join("\n\n");
            context_val = Some(match context_val {
                Some(existing) if !existing.is_empty() => {
                    format!("{}\n\n{}", existing, extra_context)
                }
                _ => extra_context,
            });
        }

        if pre_hook.should_block {
            let reason = pre_hook
                .block_reason
                .unwrap_or_else(|| "delegation blocked by hook".to_string());
            result.content = vec![Content::text(format!(
                "Delegation blocked by hook: {}",
                reason
            ))];
            result.is_error = false;
            return Ok(None);
        }

        if let Ok(serialized_args) = serde_json::to_string(&serde_json::json!({
            "target_agent_id": target_agent_id,
            "objective": objective,
            "context": context_val,
            "constraints": constraints,
            "expected_output": expected_output,
        })) {
            result.tool_arguments = Some(serialized_args);
        }

        if let Ok(delegation) = exec_ctx
            .state
            .record_delegation(
                target_agent_id.clone(),
                objective.clone(),
                context_val.clone(),
                constraints,
                expected_output,
            )
            .await
        {
            config.emit_event(
                &exec_ctx.session_id,
                AgentEventKind::DelegationRequested {
                    delegation: delegation.clone(),
                },
            );
            return Ok(Some(WaitCondition::delegation(
                delegation.public_id.clone(),
            )));
        }
    }

    Ok(None)
}

/// Store all completed tool results back into conversation history.
///
/// This function:
/// 1. Creates user messages with tool results
/// 2. Stores them in the database
/// 3. Records side effects (artifacts, delegations)
/// 4. Aggregates file changes for deduplication
/// 5. Returns either WaitingForEvent (if delegation) or BeforeLlmCall
#[instrument(
    name = "agent.tools.store_all_results",
    skip(config, results, context, exec_ctx),
    fields(session_id = %exec_ctx.session_id, result_count = results.len())
)]
pub(super) async fn store_all_tool_results(
    config: &AgentConfig,
    results: &Arc<[ToolResult]>,
    context: &Arc<crate::middleware::ConversationContext>,
    exec_ctx: &mut ExecutionContext,
) -> Result<ExecutionState, anyhow::Error> {
    debug!(
        "Storing all tool results: session={}, count={}",
        exec_ctx.session_id,
        results.len()
    );

    let mut messages = (*context.messages).to_vec();
    let mut wait_conditions = Vec::new();

    // Record per-result side effects (delegations, artifacts, etc.) before
    // persisting the combined tool_result message so hook-driven rewrites are
    // visible to the model in the stored transcript.
    let mut adjusted_results = Vec::with_capacity(results.len());
    for mut result in results.iter().cloned() {
        if let Some(wait_condition) =
            record_tool_side_effects(config, &mut result, exec_ctx).await?
        {
            wait_conditions.push(wait_condition);
        }
        adjusted_results.push(result);
    }

    // Collect all tool results into a single User message so that the LLM API
    // sees every tool_result block immediately after the assistant's tool_use
    // blocks. Splitting them across multiple consecutive User messages violates
    // the Anthropic API contract (and potentially other providers').
    let mut result_parts: Vec<_> = adjusted_results
        .iter()
        .map(|result| MessagePart::ToolResult {
            call_id: result.call_id.clone(),
            content: result.content.clone(),
            is_error: result.is_error,
            tool_name: result.tool_name.clone(),
            tool_arguments: result.tool_arguments.clone(),
            compacted_at: None,
        })
        .collect();
    let snapshot_parts: Vec<_> = adjusted_results
        .iter()
        .filter_map(|result| result.snapshot_part.clone())
        .collect();
    result_parts.extend(snapshot_parts);

    let result_msg = AgentMessage {
        id: Uuid::new_v4().to_string(),
        session_id: exec_ctx.session_id.clone(),
        role: ChatRole::User,
        parts: result_parts,
        created_at: time::OffsetDateTime::now_utc().unix_timestamp(),
        parent_message_id: None,
        source_provider: None,
        source_model: None,
    };

    exec_ctx
        .add_message(result_msg.clone())
        .await
        .map_err(|e| anyhow::anyhow!("Failed to store tool results: {}", e))?;

    messages.push(result_msg.to_chat_message());

    let new_context = Arc::new(
        crate::middleware::ConversationContext::new(
            context.session_id.clone(),
            Arc::from(messages.into_boxed_slice()),
            context.stats.clone(),
            context.provider.clone(),
            context.model.clone(),
        )
        .with_session_mode(context.session_mode),
    );

    // Aggregate changed file paths from tool results for dedup check
    let mut combined = crate::index::DiffPaths::default();
    for result in results.iter() {
        if let Some(ref snapshot) = result.snapshot_part
            && let Some(paths) = snapshot.changed_paths()
        {
            combined.added.extend(paths.added.iter().cloned());
            combined.modified.extend(paths.modified.iter().cloned());
            combined.removed.extend(paths.removed.iter().cloned());
        }
    }

    combined.added.sort();
    combined.added.dedup();
    combined.modified.sort();
    combined.modified.dedup();
    combined.removed.sort();
    combined.removed.dedup();

    if !combined.is_empty() {
        let mut diffs = exec_ctx.runtime.turn_diffs.lock();
        diffs.added.extend(combined.added);
        diffs.modified.extend(combined.modified);
        diffs.removed.extend(combined.removed);
        diffs.added.sort();
        diffs.added.dedup();
        diffs.modified.sort();
        diffs.modified.dedup();
        diffs.removed.sort();
        diffs.removed.dedup();
    }

    if let Some(wait_condition) = WaitCondition::merge(wait_conditions) {
        return Ok(ExecutionState::WaitingForEvent {
            context: new_context,
            wait: wait_condition,
        });
    }

    Ok(ExecutionState::BeforeLlmCall {
        context: new_context,
    })
}
