use winnow::ascii::{multispace0 as ws0, multispace1 as ws1};
use winnow::combinator::{alt, delimited, eof, repeat, seq, terminated};
use winnow::prelude::*;
use winnow::stream::Partial;
use winnow::token::{literal, rest, take_until};

use super::parameters::ToolSchemas;
use super::utils::{MarkerScanState, parse_buffered_event, safe_text_len, take_until_marker};
use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
use crate::tool::{StructuralTagModel, Tool};

const TOOL_CALL_START: &str = "<minimax:tool_call>";
const TOOL_CALL_END: &str = "</minimax:tool_call>";
const INVOKE_START: &str = "<invoke";
const INVOKE_END: &str = "</invoke>";
const PARAMETER_START: &str = "<parameter";
const PARAMETER_END: &str = "</parameter>";

type MinimaxM2Input<'i> = Partial<&'i str>;

#[derive(Debug, Clone, PartialEq, Eq)]
enum MinimaxM2Mode {
    Text,
    ToolBlock { invoke_end_scan: MarkerScanState },
    Done,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum MinimaxM2Event {
    Text {
        len: usize,
    },
    ToolBlockStart,
    Invoke {
        name: String,
        raw_params: Vec<(String, String)>,
    },
    ToolBlockEnd,
    IgnoredRest,
}

/// Tool parser for MiniMax M2 XML-style tool calls.
///
/// Example tool call content:
///
/// ```text
/// <minimax:tool_call><invoke name="get_weather">
/// <parameter name="city">Seattle</parameter>
/// </invoke></minimax:tool_call>
/// ```
///
/// Arguments are emitted only after a full `<invoke>` block is parsed.
pub struct MinimaxM2ToolParser {
    buffer: String,
    mode: MinimaxM2Mode,
    emitted_tool_count: usize,
    tool_parameters: ToolSchemas,
}

impl MinimaxM2ToolParser {
    /// Create a MiniMax M2 tool parser.
    fn new(tools: &[Tool]) -> Self {
        Self {
            buffer: String::new(),
            mode: MinimaxM2Mode::Text,
            emitted_tool_count: 0,
            tool_parameters: ToolSchemas::from_tools(tools),
        }
    }

    /// Apply one parsed MiniMax M2 event to parser state and output.
    fn apply_event(&mut self, event: MinimaxM2Event, output: &mut ToolParserOutput) -> Result<()> {
        match event {
            MinimaxM2Event::Text { len: consumed_len } => {
                output.push_text(&self.buffer[..consumed_len]);
            }
            MinimaxM2Event::ToolBlockStart => {
                self.mode = MinimaxM2Mode::ToolBlock {
                    invoke_end_scan: MarkerScanState::default(),
                };
            }
            MinimaxM2Event::Invoke { name, raw_params } => {
                let arguments = self.tool_parameters.convert_params_with_schema(&name, raw_params);
                let arguments = serde_json::to_string(&arguments)
                    .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;

                output.push_call(ToolCallDelta {
                    tool_index: self.emitted_tool_count,
                    name: Some(name),
                    arguments,
                });
                self.emitted_tool_count += 1;
            }
            MinimaxM2Event::ToolBlockEnd => self.mode = MinimaxM2Mode::Done,
            MinimaxM2Event::IgnoredRest => {}
        }
        Ok(())
    }

    fn reset(&mut self) -> String {
        self.mode = MinimaxM2Mode::Text;
        self.emitted_tool_count = 0;
        std::mem::take(&mut self.buffer)
    }
}

impl ToolParser for MinimaxM2ToolParser {
    fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
    where
        Self: Sized + 'static,
    {
        Ok(Box::new(Self::new(tools)))
    }

    fn structural_tag_model(&self) -> Option<StructuralTagModel> {
        Some(StructuralTagModel::Minimax)
    }

    fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
        self.buffer.push_str(chunk);

        while let Some((event, consumed_len)) = parse_buffered_event(&self.buffer, |input| {
            parse_next_minimax_m2_event(input, &mut self.mode)
        })? {
            self.apply_event(event, output)?;
            self.buffer.drain(..consumed_len);
        }

        Ok(())
    }

    fn finish(&mut self) -> Result<ToolParserOutput> {
        let mut output = ToolParserOutput::default();
        match self.mode {
            MinimaxM2Mode::Text => {
                output.push_text(&self.buffer);
            }
            MinimaxM2Mode::ToolBlock { .. } => {
                return Err(parsing_failed!("incomplete MiniMax M2 tool call"));
            }
            MinimaxM2Mode::Done => {}
        }
        let _ = self.reset();
        Ok(output)
    }

    fn reset(&mut self) -> String {
        MinimaxM2ToolParser::reset(self)
    }
}

/// Parse a MiniMax M2 event for the current parser mode.
fn parse_next_minimax_m2_event(
    input: &mut MinimaxM2Input<'_>,
    mode: &mut MinimaxM2Mode,
) -> ModalResult<MinimaxM2Event> {
    match mode {
        MinimaxM2Mode::Text => parse_text_event(input),
        MinimaxM2Mode::ToolBlock { invoke_end_scan } => {
            parse_tool_block_event(input, invoke_end_scan)
        }
        MinimaxM2Mode::Done => ignored_rest_event(input),
    }
}

/// Parse a text-mode MiniMax M2 event.
fn parse_text_event(input: &mut MinimaxM2Input<'_>) -> ModalResult<MinimaxM2Event> {
    alt((tool_block_start_event, safe_text_event)).parse_next(input)
}

/// Parse a MiniMax M2 tool-block start marker.
fn tool_block_start_event(input: &mut MinimaxM2Input<'_>) -> ModalResult<MinimaxM2Event> {
    literal(TOOL_CALL_START).value(MinimaxM2Event::ToolBlockStart).parse_next(input)
}

/// Parse a safe text run before the next MiniMax M2 marker.
fn safe_text_event(input: &mut MinimaxM2Input<'_>) -> ModalResult<MinimaxM2Event> {
    safe_text_len(input, TOOL_CALL_START).map(|len| MinimaxM2Event::Text { len })
}

/// Parse one event inside a MiniMax M2 tool block.
fn parse_tool_block_event(
    input: &mut MinimaxM2Input<'_>,
    invoke_end_scan: &mut MarkerScanState,
) -> ModalResult<MinimaxM2Event> {
    alt((tool_block_end_event, |input: &mut MinimaxM2Input<'_>| {
        invoke_event(input, invoke_end_scan)
    }))
    .parse_next(input)
}

/// Parse a MiniMax M2 tool-block end marker.
fn tool_block_end_event(input: &mut MinimaxM2Input<'_>) -> ModalResult<MinimaxM2Event> {
    (ws0, literal(TOOL_CALL_END))
        .value(MinimaxM2Event::ToolBlockEnd)
        .parse_next(input)
}

/// Parse a complete MiniMax M2 invoke block.
fn invoke_event(
    input: &mut MinimaxM2Input<'_>,
    invoke_end_scan: &mut MarkerScanState,
) -> ModalResult<MinimaxM2Event> {
    let (name, body) = seq!(
        _: ws0,
        _: literal(INVOKE_START),
        _: (ws1, literal("name=")),
        partial_attr_value,
        _: literal(">"),
        take_until_marker(INVOKE_END, invoke_end_scan),
        _: literal(INVOKE_END),
    )
    .parse_next(input)?;
    let raw_params = parse_invoke_params(body)?;

    Ok(MinimaxM2Event::Invoke {
        name: name.trim().to_string(),
        raw_params,
    })
}

/// Parse all parameter blocks inside a complete MiniMax M2 invoke body.
fn parse_invoke_params(invoke_body: &str) -> ModalResult<Vec<(String, String)>> {
    let mut input = invoke_body;
    delimited(ws0, repeat(0.., terminated(parameter, ws0)), eof).parse_next(&mut input)
}

/// Parse a MiniMax M2 parameter block.
fn parameter(input: &mut &str) -> ModalResult<(String, String)> {
    let (name, value) = seq!(
        _: literal(PARAMETER_START),
        _: (ws1, literal("name=")),
        attr_value,
        _: literal(">"),
        take_until(0.., PARAMETER_END),
        _: literal(PARAMETER_END),
    )
    .parse_next(input)?;

    Ok((name.trim().to_string(), value.to_string()))
}

/// Parse a quoted or unquoted XML attribute value.
fn attr_value<'i>(input: &mut &'i str) -> ModalResult<&'i str> {
    alt((
        delimited(literal("\""), take_until(1.., "\""), literal("\"")),
        delimited(literal("'"), take_until(1.., "'"), literal("'")),
        take_until(1.., ">"),
    ))
    .parse_next(input)
}

/// Parse a quoted or unquoted XML attribute value from partial streaming input.
fn partial_attr_value<'i>(input: &mut MinimaxM2Input<'i>) -> ModalResult<&'i str> {
    alt((
        delimited(literal("\""), take_until(1.., "\""), literal("\"")),
        delimited(literal("'"), take_until(1.., "'"), literal("'")),
        take_until(1.., ">"),
    ))
    .parse_next(input)
}

/// Parse ignored rest after the MiniMax M2 tool block ends.
fn ignored_rest_event(input: &mut MinimaxM2Input<'_>) -> ModalResult<MinimaxM2Event> {
    rest.value(MinimaxM2Event::IgnoredRest).parse_next(input)
}

#[cfg(test)]
mod tests {
    use expect_test::expect;
    use serde_json::{Value, json};
    use thiserror_ext::AsReport;

    use super::{MinimaxM2ToolParser, TOOL_CALL_END, TOOL_CALL_START, ToolParser};
    use crate::tool::ToolParserTestExt as _;
    use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};

    fn build_tool_block(invokes: &[(&str, Vec<(&str, &str)>)]) -> String {
        let invokes = invokes
            .iter()
            .map(|(function_name, params)| {
                let params = params
                    .iter()
                    .map(|(name, value)| format!(r#"<parameter name="{name}">{value}</parameter>"#))
                    .collect::<Vec<_>>()
                    .join("");
                format!(r#"<invoke name="{function_name}">{params}</invoke>"#)
            })
            .collect::<String>();
        format!("{TOOL_CALL_START}{invokes}{TOOL_CALL_END}")
    }

    #[test]
    fn minimax_m2_parse_complete_without_tool_call_keeps_text() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = parser.parse_complete("Hello, world!").unwrap();

        assert_eq!(output.normal_text(), "Hello, world!");
        assert!(output.calls().is_empty());
    }

    #[test]
    fn minimax_m2_parse_complete_extracts_single_tool_call() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = parser
            .parse_complete(&build_tool_block(&[(
                "get_weather",
                vec![("city", "Seattle"), ("days", "5")],
            )]))
            .unwrap();

        assert!(output.normal_text().is_empty());
        assert_eq!(output.calls().len(), 1);
        assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
        assert_eq!(
            serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
            json!({ "city": "Seattle", "days": 5 })
        );
    }

    #[test]
    fn minimax_m2_parse_complete_preserves_prefix_and_ignores_trailing_text() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = format!(
            "Let me check. {} This trailing text is ignored.",
            build_tool_block(&[("get_weather", vec![("city", "Seattle")])])
        );
        let output = parser.parse_complete(&output).unwrap();

        assert_eq!(output.normal_text(), "Let me check. ");
        assert_eq!(output.calls().len(), 1);
    }

    #[test]
    fn minimax_m2_parse_complete_extracts_multiple_invokes() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = parser
            .parse_complete(&build_tool_block(&[
                ("get_weather", vec![("city", "Seattle")]),
                ("get_weather", vec![("city", "NYC")]),
            ]))
            .unwrap();

        assert_eq!(output.calls().len(), 2);
        assert_eq!(output.calls()[0].tool_index, 0);
        assert_eq!(output.calls()[1].tool_index, 1);
        assert_eq!(
            serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
            json!({ "city": "Seattle" })
        );
        assert_eq!(
            serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
            json!({ "city": "NYC" })
        );
    }

    #[test]
    fn minimax_m2_parse_complete_converts_schema_types() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = parser
            .parse_complete(&build_tool_block(&[(
                "convert",
                vec![
                    ("whole", "5.0"),
                    ("flag", "true"),
                    ("payload", r#"{"nested":true}"#),
                    ("items", "[1,2]"),
                    ("empty", "42"),
                ],
            )]))
            .unwrap();

        assert_eq!(
            serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
            json!({
                "whole": 5.0,
                "flag": true,
                "payload": { "nested": true },
                "items": [1, 2],
                "empty": "42",
            })
        );
    }

    #[test]
    fn minimax_m2_parse_complete_preserves_raw_entities_in_parameter_value() {
        // The MiniMax-M2 chat template renders string parameter values RAW (no
        // XML escaping), so a value the user wants to be the literal text
        // "Tom &amp; Jerry &lt;3" is emitted verbatim. The parser must preserve
        // it; xml_unescape currently decodes it, corrupting the bytes.
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = parser
            .parse_complete(&build_tool_block(&[(
                "get_weather",
                vec![("city", "Tom &amp; Jerry &lt;3")],
            )]))
            .unwrap();
        let args: Value = serde_json::from_str(&output.calls()[0].arguments).unwrap();
        assert_eq!(args["city"], json!("Tom &amp; Jerry &lt;3"));
    }

    #[test]
    fn minimax_m2_parse_complete_preserves_raw_closing_tag_text_in_parameter_value() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = parser
            .parse_complete(&build_tool_block(&[(
                "get_weather",
                vec![
                    (
                        "city",
                        "Seattle &lt;/parameter&gt;&lt;/invoke&gt;&lt;/minimax:tool_call&gt;",
                    ),
                    ("days", "5"),
                ],
            )]))
            .unwrap();

        assert_eq!(
            serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
            json!({
                "city": "Seattle &lt;/parameter&gt;&lt;/invoke&gt;&lt;/minimax:tool_call&gt;",
                "days": 5,
            })
        );
    }

    #[test]
    fn minimax_m2_parse_complete_handles_multiline_parameters() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = parser
            .parse_complete(
                "<minimax:tool_call>\
                 <invoke name=\"calculate_area\">\
                 <parameter name=\"shape\">\nrectangle\n</parameter>\
                 <parameter name=\"dimensions\">{\"width\":10,\n\"height\":20}</parameter>\
                 <parameter name=\"precision\">2</parameter>\
                 </invoke>\
                 </minimax:tool_call>",
            )
            .unwrap();

        assert_eq!(
            serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
            json!({
                "shape": "\nrectangle\n",
                "dimensions": { "width": 10, "height": 20 },
                "precision": 2,
            })
        );
    }

    #[test]
    fn minimax_m2_streaming_extracts_single_tool_call() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = collect_stream(
            &mut parser,
            &[
                "<minimax:tool_call>",
                r#"<invoke name="get_weather">"#,
                r#"<parameter name="city">Seattle</parameter>"#,
                "</invoke></minimax:tool_call>",
            ],
        );

        assert!(output.normal_text().is_empty());
        assert_eq!(output.calls().len(), 1);
        assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
        assert_eq!(
            serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
            json!({ "city": "Seattle" })
        );
    }

    #[test]
    fn minimax_m2_streaming_preserves_prefix_text() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = collect_stream(
            &mut parser,
            &[
                "Let me check. ",
                "<minimax:tool_call>",
                r#"<invoke name="get_weather"><parameter name="city">Seattle</parameter></invoke>"#,
                "</minimax:tool_call>",
            ],
        );

        assert_eq!(output.normal_text(), "Let me check. ");
        assert_eq!(output.calls().len(), 1);
    }

    #[test]
    fn minimax_m2_streaming_without_tool_call_emits_text_incrementally() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = collect_stream(&mut parser, &["Hello, ", "world!"]);

        assert_eq!(output.normal_text(), "Hello, world!");
        assert!(output.calls().is_empty());
    }

    #[test]
    fn minimax_m2_streaming_handles_marker_split_across_chunks() {
        let text = build_tool_block(&[("get_weather", vec![("city", "Seattle")])]);
        let chunks = split_by_chars(&text, 3);
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = collect_stream(&mut parser, &chunks);

        assert_eq!(output.calls().len(), 1);
        assert!(output.normal_text().is_empty());
    }

    #[test]
    fn minimax_m2_streaming_extracts_multiple_invokes_in_order() {
        let text = build_tool_block(&[
            ("get_weather", vec![("city", "Seattle")]),
            ("get_weather", vec![("city", "NYC")]),
        ]);
        let chunks = split_by_chars(&text, 7);
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = collect_stream(&mut parser, &chunks);

        assert_eq!(output.calls().len(), 2);
        assert_eq!(output.calls()[0].tool_index, 0);
        assert_eq!(output.calls()[1].tool_index, 1);
    }

    #[test]
    fn minimax_m2_streaming_handles_template_whitespace_and_split_parameters() {
        let text = concat!(
            "I will call the tools.\n",
            "<minimax:tool_call>\n",
            "<invoke name=\"get_weather\">\n",
            "<parameter name=\"city\">Seattle</parameter>\n",
            "</invoke>\n",
            "<invoke name=\"get_weather\">\n",
            "<parameter name=\"city\">NYC</parameter>\n",
            "</invoke>\n",
            "</minimax:tool_call>",
        );
        let chunks = split_by_chars(text, 7);
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let result = collect_stream(&mut parser, &chunks);

        assert_eq!(result.normal_text(), "I will call the tools.\n");
        assert_eq!(result.calls().len(), 2);
        assert_eq!(result.calls()[0].tool_index, 0);
        assert_eq!(result.calls()[0].name.as_deref(), Some("get_weather"));
        assert_eq!(result.calls()[1].tool_index, 1);
        assert_eq!(result.calls()[1].name.as_deref(), Some("get_weather"));
    }

    #[test]
    fn minimax_m2_streaming_ignores_text_after_tool_block() {
        let text = format!(
            "{} ignored",
            build_tool_block(&[("get_weather", vec![("city", "Seattle")])])
        );
        let chunks = split_by_chars(&text, 5);
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output = collect_stream(&mut parser, &chunks);

        assert!(output.normal_text().is_empty());
        assert_eq!(output.calls().len(), 1);
    }

    #[test]
    fn minimax_m2_streaming_does_not_emit_incomplete_tool_call() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let output =
            parser.parse_chunk(r#"<minimax:tool_call><invoke name="get_weather">"#).unwrap();

        assert!(output.normal_text().is_empty());
        assert!(output.calls().is_empty());
    }

    #[test]
    fn minimax_m2_finish_fails_incomplete_tool_call() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        parser.parse_chunk(r#"<minimax:tool_call><invoke name="get_weather">"#).unwrap();

        assert!(parser.finish().is_err());
    }

    #[test]
    fn minimax_m2_finish_fails_after_bare_tool_block_start() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        parser.parse_chunk("<minimax:tool_call>").unwrap();

        assert!(parser.finish().is_err());
    }

    #[test]
    fn minimax_m2_malformed_tool_call_fails_fast() {
        let mut parser = MinimaxM2ToolParser::new(&test_tools());
        let error = parser.parse_chunk("<minimax:tool_call><bad></minimax:tool_call>").unwrap_err();

        expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
    }
}
