//! Raw XML element types and deserialization from Element trees.

mod parser;

use facet_xml as xml;
use std::collections::HashMap;

pub use parser::{
    ElementParseError, ElementParser, ElementSerializeError, ElementSerializer, from_element,
    to_element,
};

/// Error when navigating to a path in an Element tree.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathError {
    /// Path was empty - cannot navigate to root as Content.
    EmptyPath { path: Vec<usize> },
    /// Index out of bounds.
    IndexOutOfBounds {
        path: Vec<usize>,
        index: usize,
        len: usize,
    },
    /// Tried to navigate through a text node.
    TextNodeHasNoChildren { path: Vec<usize> },
}

impl std::fmt::Display for PathError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PathError::EmptyPath { path } => write!(f, "empty path: {path:?}"),
            PathError::IndexOutOfBounds { path, index, len } => {
                write!(
                    f,
                    "index {index} out of bounds (len={len}) at path {path:?}"
                )
            }
            PathError::TextNodeHasNoChildren { path } => {
                write!(f, "text node has no children at path {path:?}")
            }
        }
    }
}

impl std::error::Error for PathError {}

/// Content that can appear inside an XML element - either child elements or text.
#[derive(Debug, Clone, PartialEq, Eq, facet::Facet)]
#[repr(u8)]
pub enum Content {
    /// Text content.
    #[facet(xml::text)]
    Text(String),
    /// A child element (catch-all for any tag name).
    #[facet(xml::custom_element)]
    Element(Element),
}

impl Content {
    /// Returns `Some(&str)` if this is text content.
    pub fn as_text(&self) -> Option<&str> {
        match self {
            Content::Text(t) => Some(t),
            _ => None,
        }
    }

    /// Returns `Some(&Element)` if this is an element.
    pub fn as_element(&self) -> Option<&Element> {
        match self {
            Content::Element(e) => Some(e),
            _ => None,
        }
    }
}

/// An XML element that captures any tag name, attributes, and children.
///
/// This type can represent arbitrary XML structure without needing
/// a predefined schema.
#[derive(Debug, Clone, PartialEq, Eq, Default, facet::Facet)]
pub struct Element {
    /// The element's tag name (captured dynamically).
    #[facet(xml::tag, default)]
    pub tag: String,

    /// All attributes as key-value pairs.
    #[facet(flatten, default)]
    pub attrs: HashMap<String, String>,

    /// Child content (elements and text).
    #[facet(flatten, default)]
    #[facet(recursive_type)]
    pub children: Vec<Content>,
}

impl Element {
    /// Create a new element with just a tag name.
    pub fn new(tag: impl Into<String>) -> Self {
        Self {
            tag: tag.into(),
            attrs: HashMap::new(),
            children: Vec::new(),
        }
    }

    /// Add an attribute.
    pub fn with_attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
        self.attrs.insert(name.into(), value.into());
        self
    }

    /// Add a child element.
    pub fn with_child(mut self, child: Element) -> Self {
        self.children.push(Content::Element(child));
        self
    }

    /// Add text content.
    pub fn with_text(mut self, text: impl Into<String>) -> Self {
        self.children.push(Content::Text(text.into()));
        self
    }

    /// Get an attribute value by name.
    pub fn get_attr(&self, name: &str) -> Option<&str> {
        self.attrs.get(name).map(|s| s.as_str())
    }

    /// Iterate over child elements (skipping text nodes).
    pub fn child_elements(&self) -> impl Iterator<Item = &Element> {
        self.children.iter().filter_map(|c| c.as_element())
    }

    /// Get the combined text content (concatenated from all text children).
    pub fn text_content(&self) -> String {
        let mut result = String::new();
        for child in &self.children {
            match child {
                Content::Text(t) => result.push_str(t),
                Content::Element(e) => result.push_str(&e.text_content()),
            }
        }
        result
    }

    /// Get a mutable reference to content at a path.
    /// Path is a sequence of child indices.
    pub fn get_content_mut(&mut self, path: &[usize]) -> Result<&mut Content, PathError> {
        if path.is_empty() {
            return Err(PathError::EmptyPath { path: vec![] });
        }

        let idx = path[0];
        let len = self.children.len();
        let child = self
            .children
            .get_mut(idx)
            .ok_or_else(|| PathError::IndexOutOfBounds {
                path: path.to_vec(),
                index: idx,
                len,
            })?;

        if path.len() == 1 {
            return Ok(child);
        }

        match child {
            Content::Element(e) => e.get_content_mut(&path[1..]),
            Content::Text(_) => Err(PathError::TextNodeHasNoChildren {
                path: path.to_vec(),
            }),
        }
    }

    /// Get a mutable reference to the children vec at a path.
    pub fn children_mut(&mut self, path: &[usize]) -> Result<&mut Vec<Content>, PathError> {
        if path.is_empty() {
            return Ok(&mut self.children);
        }
        match self.get_content_mut(path)? {
            Content::Element(e) => Ok(&mut e.children),
            Content::Text(_) => Err(PathError::TextNodeHasNoChildren {
                path: path.to_vec(),
            }),
        }
    }

    /// Get a mutable reference to the attrs at a path.
    pub fn attrs_mut(&mut self, path: &[usize]) -> Result<&mut HashMap<String, String>, PathError> {
        if path.is_empty() {
            return Ok(&mut self.attrs);
        }
        match self.get_content_mut(path)? {
            Content::Element(e) => Ok(&mut e.attrs),
            Content::Text(_) => Err(PathError::TextNodeHasNoChildren {
                path: path.to_vec(),
            }),
        }
    }

    /// Serialize to HTML string.
    pub fn to_html(&self) -> String {
        let mut out = String::new();
        self.write_html(&mut out);
        out
    }

    /// Write HTML to a string buffer.
    pub fn write_html(&self, out: &mut String) {
        out.push('<');
        out.push_str(&self.tag);
        // Sort attrs for deterministic output
        let mut attr_list: Vec<_> = self.attrs.iter().collect();
        attr_list.sort_by_key(|(k, _)| *k);
        for (k, v) in attr_list {
            out.push(' ');
            out.push_str(k);
            out.push_str("=\"");
            out.push_str(&html_escape(v));
            out.push('"');
        }
        out.push('>');
        for child in &self.children {
            match child {
                Content::Text(s) => out.push_str(s),
                Content::Element(e) => e.write_html(out),
            }
        }
        out.push_str("</");
        out.push_str(&self.tag);
        out.push('>');
    }
}

fn html_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

impl From<Element> for Content {
    fn from(e: Element) -> Self {
        Content::Element(e)
    }
}

impl From<String> for Content {
    fn from(s: String) -> Self {
        Content::Text(s)
    }
}

impl From<&str> for Content {
    fn from(s: &str) -> Self {
        Content::Text(s.to_owned())
    }
}

#[cfg(test)]
mod tests {
    use std::{fmt::Display, str::FromStr};

    use super::*;
    use facet::Facet;
    use facet_testhelpers::test;

    #[test]
    fn element_builder_api() {
        let elem = Element::new("root")
            .with_attr("id", "123")
            .with_child(Element::new("child").with_text("hello world"));

        assert_eq!(elem.tag, "root");
        assert_eq!(elem.get_attr("id"), Some("123"));
        assert_eq!(elem.children.len(), 1);

        let child = elem.child_elements().next().unwrap();
        assert_eq!(child.tag, "child");
        assert_eq!(child.text_content(), "hello world");
    }

    #[test]
    fn parse_simple_xml() {
        let xml = r#"<root><child>hello</child></root>"#;
        let elem: Element = facet_xml::from_str(xml).unwrap();

        assert_eq!(elem.tag, "root");
        assert_eq!(elem.children.len(), 1);

        let child = elem.child_elements().next().unwrap();
        assert_eq!(child.tag, "child");
        assert_eq!(child.text_content(), "hello");
    }

    #[test]
    fn parse_with_attributes() {
        let xml = r#"<root id="123" class="test"><child name="foo">bar</child></root>"#;
        let elem: Element = facet_xml::from_str(xml).unwrap();

        assert_eq!(elem.tag, "root");
        assert_eq!(elem.get_attr("id"), Some("123"));
        assert_eq!(elem.get_attr("class"), Some("test"));

        let child = elem.child_elements().next().unwrap();
        assert_eq!(child.get_attr("name"), Some("foo"));
        assert_eq!(child.text_content(), "bar");
    }

    #[test]
    fn parse_mixed_content() {
        let xml = r#"<p>Hello <b>world</b>!</p>"#;
        let elem: Element = facet_xml::from_str(xml).unwrap();

        assert_eq!(elem.tag, "p");
        assert_eq!(elem.children.len(), 3);
        // Note: trailing whitespace is trimmed by XML parser
        assert_eq!(elem.children[0].as_text(), Some("Hello"));
        assert_eq!(elem.children[1].as_element().unwrap().tag, "b");
        assert_eq!(elem.children[2].as_text(), Some("!"));
        assert_eq!(elem.text_content(), "Helloworld!");
    }

    #[test]
    fn from_element_to_struct() {
        #[derive(facet::Facet, Debug, PartialEq)]
        struct Person {
            name: String,
            age: u32,
        }

        let elem = Element::new("person")
            .with_child(Element::new("name").with_text("Alice"))
            .with_child(Element::new("age").with_text("30"));

        let person: Person = from_element(&elem).unwrap();
        assert_eq!(person.name, "Alice");
        assert_eq!(person.age, 30);
    }

    #[test]
    fn from_element_with_attrs() {
        #[derive(facet::Facet, Debug, PartialEq)]
        struct Item {
            #[facet(xml::attribute)]
            id: String,
            value: String,
        }

        let elem = Element::new("item")
            .with_attr("id", "123")
            .with_child(Element::new("value").with_text("hello"));

        let item: Item = from_element(&elem).unwrap();
        assert_eq!(item.id, "123");
        assert_eq!(item.value, "hello");
    }

    #[test]
    fn to_element_simple() {
        #[derive(facet::Facet, Debug, PartialEq)]
        struct Person {
            name: String,
            age: u32,
        }

        let person = Person {
            name: "Alice".to_string(),
            age: 30,
        };

        let elem = to_element(&person).unwrap();
        assert_eq!(elem.tag, "person");
        assert_eq!(elem.children.len(), 2);

        let name_child = elem.child_elements().find(|e| e.tag == "name").unwrap();
        assert_eq!(name_child.text_content(), "Alice");

        let age_child = elem.child_elements().find(|e| e.tag == "age").unwrap();
        assert_eq!(age_child.text_content(), "30");
    }

    #[test]
    fn to_element_with_attrs() {
        #[derive(facet::Facet, Debug, PartialEq)]
        struct Item {
            #[facet(xml::attribute)]
            id: String,
            value: String,
        }

        let item = Item {
            id: "123".to_string(),
            value: "hello".to_string(),
        };

        let elem = to_element(&item).unwrap();
        assert_eq!(elem.tag, "item");
        assert_eq!(elem.get_attr("id"), Some("123"));

        let value_child = elem.child_elements().find(|e| e.tag == "value").unwrap();
        assert_eq!(value_child.text_content(), "hello");
    }

    #[test]
    fn roundtrip_simple() {
        #[derive(facet::Facet, Debug, PartialEq)]
        struct Person {
            name: String,
            age: u32,
        }

        let original = Person {
            name: "Bob".to_string(),
            age: 42,
        };

        let elem = to_element(&original).unwrap();
        let roundtripped: Person = from_element(&elem).unwrap();

        assert_eq!(original, roundtripped);
    }

    #[test]
    fn roundtrip_with_attrs() {
        #[derive(facet::Facet, Debug, PartialEq)]
        struct Item {
            #[facet(xml::attribute)]
            id: String,
            #[facet(xml::attribute)]
            version: u32,
            value: String,
        }

        let original = Item {
            id: "test-123".to_string(),
            version: 5,
            value: "content".to_string(),
        };

        let elem = to_element(&original).unwrap();
        let roundtripped: Item = from_element(&elem).unwrap();

        assert_eq!(original, roundtripped);
    }

    /// Reproduction test for issue #10:
    /// `Vec<Element>` does not match any tag, although it should match every tag
    #[test]
    fn vec_element_matches_any_tag() {
        #[derive(facet::Facet, Debug)]
        #[facet(rename = "any")]
        struct AnyContainer {
            #[facet(xml::elements)]
            elements: Vec<Element>,
        }

        let xml = r#"<any><foo a="b" /><bar c="d" /></any>"#;
        let result: AnyContainer = facet_xml::from_str(xml).unwrap();

        assert_eq!(result.elements.len(), 2);
        assert_eq!(result.elements[0].tag, "foo");
        assert_eq!(result.elements[0].get_attr("a"), Some("b"));
        assert_eq!(result.elements[1].tag, "bar");
        assert_eq!(result.elements[1].get_attr("c"), Some("d"));
    }

    /// Edge case: specific fields should take precedence over catch-all Vec<Element>
    #[test]
    fn vec_element_catch_all_with_specific_field() {
        #[derive(facet::Facet, Debug)]
        #[facet(rename = "container")]
        struct MixedContainer {
            // Specific field - should match <name> elements
            name: String,
            // Catch-all - should get everything else
            #[facet(xml::elements)]
            others: Vec<Element>,
        }

        let xml = r#"<container><name>test</name><foo>a</foo><bar>b</bar></container>"#;
        let result: MixedContainer = facet_xml::from_str(xml).unwrap();

        assert_eq!(result.name, "test");
        assert_eq!(result.others.len(), 2);
        assert_eq!(result.others[0].tag, "foo");
        assert_eq!(result.others[1].tag, "bar");
    }

    /// Edge case: text nodes should be ignored when using xml::elements
    #[test]
    fn vec_element_ignores_text_nodes() {
        #[derive(facet::Facet, Debug)]
        #[facet(rename = "any")]
        struct AnyContainer {
            #[facet(xml::elements)]
            elements: Vec<Element>,
        }

        // Text nodes between elements should be ignored
        let xml = r#"<any>text before<foo/>middle text<bar/>text after</any>"#;
        let result: AnyContainer = facet_xml::from_str(xml).unwrap();

        assert_eq!(result.elements.len(), 2);
        assert_eq!(result.elements[0].tag, "foo");
        assert_eq!(result.elements[1].tag, "bar");
    }

    /// Edge case: roundtrip serialization of Vec<Element>
    ///
    /// Tests that Element's xml::tag field is used as the element name during
    /// serialization, producing `<foo>...</foo>` instead of `<element><tag>foo</tag>...</element>`.
    #[test]
    fn vec_element_roundtrip() {
        #[derive(facet::Facet, Debug, PartialEq)]
        #[facet(rename = "container")]
        struct Container {
            #[facet(xml::elements)]
            elements: Vec<Element>,
        }

        let original = Container {
            elements: vec![
                Element::new("foo").with_attr("a", "1"),
                Element::new("bar").with_text("hello"),
            ],
        };

        let xml = facet_xml::to_string(&original).unwrap();

        // After fix: tag field becomes the element name
        // <container><foo a="1"/><bar>hello</bar></container>
        assert!(xml.contains("<foo"), "expected <foo>, got: {}", xml);
        assert!(xml.contains("<bar"), "expected <bar>, got: {}", xml);

        // Roundtrip should preserve original tag names
        let roundtripped: Container = facet_xml::from_str(&xml).unwrap();
        assert_eq!(roundtripped.elements.len(), 2);
        assert_eq!(roundtripped.elements[0].tag, "foo");
        assert_eq!(roundtripped.elements[0].get_attr("a"), Some("1"));
        assert_eq!(roundtripped.elements[1].tag, "bar");
        assert_eq!(roundtripped.elements[1].text_content(), "hello");
    }

    /// Edge case: empty container produces empty Vec
    #[test]
    fn vec_element_empty_container() {
        #[derive(facet::Facet, Debug)]
        #[facet(rename = "empty")]
        struct EmptyContainer {
            #[facet(xml::elements)]
            elements: Vec<Element>,
        }

        let xml = r#"<empty></empty>"#;
        let result: EmptyContainer = facet_xml::from_str(xml).unwrap();

        assert!(result.elements.is_empty());
    }

    #[derive(Debug, Facet)]
    #[facet(proxy = StringRepr)]
    struct ConstantName;

    /// A proxy type for Facet that uses the Display/FromStr implementation
    #[derive(Debug, Facet)]
    #[repr(transparent)]
    pub(crate) struct StringRepr(pub String);

    impl Display for ConstantName {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "CONSTANT")
        }
    }

    impl FromStr for ConstantName {
        type Err = &'static str;
        fn from_str(s: &str) -> Result<Self, Self::Err> {
            if s == "CONSTANT" {
                Ok(Self)
            } else {
                Err("expected `CONSTANT`")
            }
        }
    }

    impl From<ConstantName> for StringRepr {
        fn from(value: ConstantName) -> Self {
            Self(value.to_string())
        }
    }
    impl From<&ConstantName> for StringRepr {
        fn from(value: &ConstantName) -> Self {
            Self(value.to_string())
        }
    }
    impl TryFrom<StringRepr> for ConstantName {
        type Error = <ConstantName as core::str::FromStr>::Err;
        fn try_from(value: StringRepr) -> Result<Self, Self::Error> {
            value.0.parse()
        }
    }
    impl TryFrom<&StringRepr> for ConstantName {
        type Error = <ConstantName as core::str::FromStr>::Err;
        fn try_from(value: &StringRepr) -> Result<Self, Self::Error> {
            value.0.parse()
        }
    }

    #[derive(Debug, Facet)]
    #[repr(C)]
    enum Foo {
        #[facet(rename = "foo")]
        Value {
            #[facet(xml::attribute)]
            #[allow(unused)]
            name: ConstantName,
            #[facet(xml::attribute)]
            #[allow(unused)]
            exists: String,
        },
    }

    #[test]
    fn transparent_attribute_not_discarded() {
        let raw_xml = r#"
<foo name="CONSTANT" exists="i do exist and am not discarded"></foo>"#;
        let x: Foo = facet_xml::from_str(raw_xml).unwrap();
        let element = crate::to_element(&x).unwrap();
        let _ = facet_xml::to_string(&x).unwrap();
        let _ = facet_xml::to_string(&element).unwrap();
        assert!(
            element.attrs.contains_key("exists"),
            "this attribute is not discarded"
        );
        assert_eq!(element.attrs["name"], "CONSTANT", "name is not discarded");
    }
}
