/// Lightweight DOM built from quick-xml events.
/// Provides lxml-like navigation: find, find_all, text, attr, inner_text.
use quick_xml::events::{BytesStart, Event};
use quick_xml::Reader;
use std::io::BufRead;

#[derive(Debug, Default)]
pub struct Element {
    pub tag: String,
    pub attrs: Vec<(String, String)>,
    pub children: Vec<Node>,
}

#[derive(Debug)]
pub enum Node {
    Elem(Element),
    Text(String),
}

impl Element {
    fn from_start(e: &BytesStart) -> Self {
        let tag = String::from_utf8_lossy(e.name().as_ref()).into_owned();
        let attrs = e
            .attributes()
            .filter_map(|a| {
                let a = a.ok()?;
                let k = String::from_utf8_lossy(a.key.as_ref()).into_owned();
                let v = String::from_utf8_lossy(&a.value).into_owned();
                Some((k, v))
            })
            .collect();
        Element { tag, attrs, children: Vec::new() }
    }

    pub fn find(&self, tag: &str) -> Option<&Element> {
        self.children.iter().find_map(|n| match n {
            Node::Elem(e) if e.tag == tag => Some(e),
            _ => None,
        })
    }

    /// Iterate over direct children matching `tag`. Zero allocation.
    pub fn find_all<'a>(&'a self, tag: &'a str) -> impl Iterator<Item = &'a Element> + 'a {
        self.children.iter().filter_map(move |n| match n {
            Node::Elem(e) if e.tag == tag => Some(e),
            _ => None,
        })
    }

    pub fn attr(&self, name: &str) -> Option<&str> {
        self.attrs.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())
    }

    pub fn text(&self) -> Option<&str> {
        self.children.iter().find_map(|n| match n {
            Node::Text(s) if !s.is_empty() => Some(s.as_str()),
            _ => None,
        })
    }

    /// Concatenated text of all descendants (allocates).
    pub fn inner_text(&self) -> Option<String> {
        let mut buf = String::new();
        collect_text(&self.children, &mut buf);
        let t = buf.trim().to_owned();
        if t.is_empty() { None } else { Some(t) }
    }
}

fn collect_text(nodes: &[Node], buf: &mut String) {
    for n in nodes {
        match n {
            Node::Text(s) => buf.push_str(s),
            Node::Elem(e) => collect_text(&e.children, buf),
        }
    }
}

fn io_err(msg: &str) -> quick_xml::Error {
    quick_xml::Error::from(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, msg))
}

/// Build a complete element subtree. Caller consumed the Start event already.
pub fn build_subtree<R: BufRead>(
    reader: &mut Reader<R>,
    start: &BytesStart,
) -> std::result::Result<Element, quick_xml::Error> {
    let mut stack: Vec<Element> = vec![Element::from_start(start)];
    let mut buf = Vec::with_capacity(512);

    loop {
        buf.clear();
        match reader.read_event_into(&mut buf)? {
            Event::Start(ref e) => {
                stack.push(Element::from_start(e));
            }
            Event::Empty(ref e) => {
                let elem = Element::from_start(e);
                stack.last_mut().expect("non-empty stack").children.push(Node::Elem(elem));
            }
            Event::Text(ref e) => {
                let text = e.unescape()?.into_owned();
                if !text.trim().is_empty() {
                    stack.last_mut().expect("non-empty stack").children.push(Node::Text(text));
                }
            }
            Event::CData(ref e) => {
                let text = String::from_utf8_lossy(e.as_ref()).into_owned();
                if !text.is_empty() {
                    stack.last_mut().expect("non-empty stack").children.push(Node::Text(text));
                }
            }
            Event::End(_) => {
                if stack.len() == 1 {
                    return stack.pop().ok_or_else(|| io_err("empty stack"));
                }
                let child = stack.pop().ok_or_else(|| io_err("empty stack"))?;
                stack.last_mut().expect("non-empty stack").children.push(Node::Elem(child));
            }
            Event::Eof => return Err(io_err("unexpected EOF inside subtree")),
            _ => {}
        }
    }
}
