//! The raw-model agent loop (plan §10): a provider-neutral tool-calling loop.
//!
//! Providers implement [`ChatModel::step`] (one request → text and/or tool
//! calls) in their own wire format; this loop drives the conversation, executes
//! tools, and feeds results back until the model stops calling tools.

use crate::agent::tools::{self, ToolCtx, ToolSchema};
use crate::agent::AgentOutcome;
use crate::progress::{Event, Progress};
use anyhow::Result;
use async_trait::async_trait;
use serde_json::Value;

#[derive(Debug, Clone)]
pub struct ToolCall {
    pub id: String,
    pub name: String,
    pub input: Value,
}

#[derive(Debug, Clone)]
pub struct ToolResult {
    pub id: String,
    pub content: String,
}

/// Provider-neutral conversation message.
#[derive(Debug, Clone)]
pub enum Msg {
    User(String),
    Assistant {
        text: Option<String>,
        tool_calls: Vec<ToolCall>,
    },
    ToolResults(Vec<ToolResult>),
}

/// One model turn: any assistant text plus any tool calls it requested.
#[derive(Debug, Clone, Default)]
pub struct ModelStep {
    pub text: Option<String>,
    pub tool_calls: Vec<ToolCall>,
}

#[async_trait]
pub trait ChatModel: Send + Sync {
    async fn step(&self, system: &str, messages: &[Msg], tools: &[ToolSchema])
        -> Result<ModelStep>;
}

/// Drive the tool-calling loop to completion (or the iteration cap).
pub async fn run_loop(
    model: &dyn ChatModel,
    system: &str,
    initial_user: String,
    ctx: &ToolCtx,
    max_iters: usize,
    progress: &dyn Progress,
) -> Result<AgentOutcome> {
    let schemas = tools::schemas(ctx.read_only);
    let mut messages = vec![Msg::User(initial_user)];
    let mut total_calls = 0usize;
    let mut last_text = String::new();

    for _ in 0..max_iters {
        let step = model.step(system, &messages, &schemas).await?;
        if let Some(t) = &step.text {
            if !t.trim().is_empty() {
                last_text = t.clone();
                progress.event(Event::note(t.clone()));
            }
        }

        if step.tool_calls.is_empty() {
            return Ok(AgentOutcome {
                final_text: last_text,
                tool_calls: total_calls,
                cost_usd: None,
            });
        }

        total_calls += step.tool_calls.len();
        let calls = step.tool_calls.clone();
        messages.push(Msg::Assistant {
            text: step.text,
            tool_calls: step.tool_calls,
        });

        let mut results = Vec::with_capacity(calls.len());
        for c in calls {
            progress.event(Event::tool(c.name.clone(), tool_target(&c.input)));
            let content = tools::execute(ctx, &c.name, &c.input);
            if c.name == "write_page" {
                if let Some(p) = c.input.get("path").and_then(|v| v.as_str()) {
                    progress.event(Event::page(p));
                }
            }
            results.push(ToolResult { id: c.id, content });
        }
        messages.push(Msg::ToolResults(results));
    }

    Ok(AgentOutcome {
        final_text: if last_text.is_empty() {
            format!("(stopped after {max_iters} tool iterations without a final answer)")
        } else {
            last_text
        },
        tool_calls: total_calls,
        cost_usd: None,
    })
}

/// A short human-readable target for a tool call (path/pattern/title).
fn tool_target(input: &Value) -> String {
    for key in ["path", "pattern", "title"] {
        if let Some(s) = input.get(key).and_then(|v| v.as_str()) {
            return s.to_string();
        }
    }
    String::new()
}
