//! Fallback parser for models that emit tool calls as *text* instead of
//! structured `tool_calls` — required for Baidu ERNIE (the `ernie_x1` parser
//! format) and seen from some Qwen3-Coder serving stacks.
//!
//! Supported formats:
//!   1. Hermes/ernie_x1 style:  <tool_call>{"name": "...", "arguments": {...}}</tool_call>
//!   2. Qwen XML style:         <function=name><parameter=key>value</parameter>...</function>
//!   3. <think>…</think> blocks are extracted into reasoning.

use crate::llm::ToolCall;
use regex::Regex;
use serde_json::{json, Map, Value};
use std::sync::OnceLock;

macro_rules! re {
    ($name:ident, $pat:expr) => {
        fn $name() -> &'static Regex {
            static RE: OnceLock<Regex> = OnceLock::new();
            RE.get_or_init(|| Regex::new($pat).expect("valid toolparse regex"))
        }
    };
}

re!(think_re, r"(?s)<think>(.*?)</think>");
re!(tool_call_re, r"(?s)<tool_call>(.*?)</tool_call>");
re!(function_re, r"(?s)<function=([\w.-]+)>(.*?)</function>");
re!(parameter_re, r"(?s)<parameter=([\w.-]+)>\s*(.*?)\s*</parameter>");
// ernie_x1 also wraps responses: <response>…</response>
re!(response_re, r"(?s)</?response>");

pub struct ParsedOutput {
    pub content: String,
    pub reasoning: String,
    pub tool_calls: Vec<ToolCall>,
}

/// Returns true when the text contains tool-call-like markup worth parsing.
pub fn looks_like_text_tool_call(content: &str) -> bool {
    content.contains("<tool_call>") || content.contains("<function=")
}

/// Extract text-format tool calls (and `<think>` reasoning) from content.
pub fn parse(content: &str) -> ParsedOutput {
    let mut reasoning = String::new();
    let mut tool_calls: Vec<ToolCall> = Vec::new();

    // 1. Pull out <think> blocks.
    let mut remaining = think_re()
        .replace_all(content, |caps: &regex::Captures| {
            reasoning.push_str(caps[1].trim());
            reasoning.push('\n');
            String::new()
        })
        .into_owned();

    // 2. Hermes / ernie_x1 JSON tool calls.
    remaining = tool_call_re()
        .replace_all(&remaining, |caps: &regex::Captures| {
            let body = caps[1].trim();
            // Body may itself be the Qwen XML format wrapped in <tool_call>.
            if body.contains("<function=") {
                for tc in parse_xml_functions(body) {
                    tool_calls.push(tc);
                }
            } else if let Some(tc) = parse_json_tool_call(body) {
                tool_calls.push(tc);
            }
            String::new()
        })
        .into_owned();

    // 3. Bare Qwen XML function calls (no <tool_call> wrapper).
    if remaining.contains("<function=") {
        let calls = parse_xml_functions(&remaining);
        if !calls.is_empty() {
            tool_calls.extend(calls);
            remaining = function_re().replace_all(&remaining, "").into_owned();
        }
    }

    remaining = response_re().replace_all(&remaining, "").into_owned();
    // Stray closing tag some models emit after the XML block.
    remaining = remaining.replace("</tool_call>", "");

    // Assign ids.
    for (i, tc) in tool_calls.iter_mut().enumerate() {
        tc.id = format!("textcall_{i}");
    }

    ParsedOutput {
        content: remaining.trim().to_string(),
        reasoning: reasoning.trim().to_string(),
        tool_calls,
    }
}

fn parse_json_tool_call(body: &str) -> Option<ToolCall> {
    let v: Value = serde_json::from_str(body).ok()?;
    let name = v["name"].as_str()?.to_string();
    let arguments = match &v["arguments"] {
        Value::String(s) => s.clone(),
        Value::Null => "{}".to_string(),
        other => other.to_string(),
    };
    Some(ToolCall { id: String::new(), name, arguments })
}

fn parse_xml_functions(text: &str) -> Vec<ToolCall> {
    let mut out = Vec::new();
    for caps in function_re().captures_iter(text) {
        let name = caps[1].to_string();
        let mut args = Map::new();
        for p in parameter_re().captures_iter(&caps[2]) {
            let key = p[1].to_string();
            let raw = p[2].trim();
            args.insert(key, coerce_value(raw));
        }
        out.push(ToolCall {
            id: String::new(),
            name,
            arguments: Value::Object(args).to_string(),
        });
    }
    out
}

/// XML parameter values are strings; coerce obvious bools/ints so tools like
/// `is_background` and `timeout_ms` get proper types.
fn coerce_value(raw: &str) -> Value {
    match raw {
        "true" | "True" => json!(true),
        "false" | "False" => json!(false),
        _ => {
            if let Ok(n) = raw.parse::<i64>() {
                json!(n)
            } else {
                json!(raw)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_ernie_x1_json_tool_call() {
        let content = r#"<think>I should check the file listing first.</think>
I'll list the files.
<tool_call>
{"name": "run_shell_command", "arguments": {"command": "ls -la", "description": "list files"}}
</tool_call>"#;
        assert!(looks_like_text_tool_call(content));
        let parsed = parse(content);
        assert_eq!(parsed.tool_calls.len(), 1);
        assert_eq!(parsed.tool_calls[0].name, "run_shell_command");
        let args: Value = serde_json::from_str(&parsed.tool_calls[0].arguments).unwrap();
        assert_eq!(args["command"], "ls -la");
        assert!(parsed.reasoning.contains("file listing"));
        assert_eq!(parsed.content, "I'll list the files.");
    }

    #[test]
    fn parses_qwen_xml_function_call() {
        let content = r#"<function=run_shell_command>
<parameter=command>
echo cloud-ok > cloud.txt && cat cloud.txt
</parameter>
<parameter=description>
Create a file
</parameter>
<parameter=is_background>
false
</parameter>
<parameter=timeout_ms>
60000
</parameter>
</function>
</tool_call>"#;
        let parsed = parse(content);
        assert_eq!(parsed.tool_calls.len(), 1);
        let args: Value = serde_json::from_str(&parsed.tool_calls[0].arguments).unwrap();
        assert_eq!(args["command"], "echo cloud-ok > cloud.txt && cat cloud.txt");
        assert_eq!(args["is_background"], json!(false));
        assert_eq!(args["timeout_ms"], json!(60000));
        assert!(parsed.content.is_empty(), "content: {:?}", parsed.content);
    }

    #[test]
    fn xml_inside_tool_call_wrapper() {
        let content = "<tool_call><function=web_search>\n<parameter=query>\nrust 2026\n</parameter>\n</function></tool_call>";
        let parsed = parse(content);
        assert_eq!(parsed.tool_calls.len(), 1);
        assert_eq!(parsed.tool_calls[0].name, "web_search");
    }

    #[test]
    fn plain_text_untouched() {
        let content = "Just a normal answer with no markup.";
        assert!(!looks_like_text_tool_call(content));
        let parsed = parse(content);
        assert!(parsed.tool_calls.is_empty());
        assert_eq!(parsed.content, content);
    }

    #[test]
    fn multiple_json_calls() {
        let content = "<tool_call>{\"name\":\"a\",\"arguments\":{}}</tool_call><tool_call>{\"name\":\"b\",\"arguments\":{\"x\":1}}</tool_call>";
        let parsed = parse(content);
        assert_eq!(parsed.tool_calls.len(), 2);
        assert_eq!(parsed.tool_calls[0].name, "a");
        assert_eq!(parsed.tool_calls[1].id, "textcall_1");
    }
}
