//! Bounded LLSD XML parsing and serialization without entity expansion.

#![allow(clippy::missing_errors_doc)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::unused_self)]

use crate::{Error, OSD};
use base64::Engine as _;
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{ReadWrite, Uri};
use std::collections::HashMap;
use std::io::Read as _;
use std::time::UNIX_EPOCH;

const MAX_BYTES: usize = OSD::DEFAULT_MAX_BINARY_BYTES;

pub(crate) fn deserialize_bytes(data: Vec<u8>) -> Result<OSD, Error> {
    if data.len() > MAX_BYTES {
        return Err(parse_error(0, "XML LLSD input exceeds allocation limit"));
    }
    let text = std::str::from_utf8(data.strip_prefix(b"\xef\xbb\xbf").unwrap_or(&data))
        .map_err(|_| parse_error(0, "XML LLSD input is not valid UTF-8"))?;
    deserialize_text(text)
}

pub(crate) fn deserialize_string(data: String) -> Result<OSD, Error> {
    if data.len() > MAX_BYTES {
        return Err(parse_error(0, "XML LLSD input exceeds allocation limit"));
    }
    deserialize_text(data.strip_prefix('\u{feff}').unwrap_or(&data))
}

pub(crate) fn deserialize_stream(mut stream: Box<dyn ReadWrite + Send>) -> Result<OSD, Error> {
    let mut data = Vec::new();
    (&mut *stream)
        .take((MAX_BYTES + 1) as u64)
        .read_to_end(&mut data)
        .map_err(|_| Error::InvalidOperation)?;
    deserialize_bytes(data)
}

pub(crate) fn deserialize_reader(reader: crate::xml::Reader) -> Result<OSD, Error> {
    deserialize_string(reader.0)
}

fn deserialize_text(text: &str) -> Result<OSD, Error> {
    let mut trimmed = text.trim_start();
    if trimmed.starts_with("<?") {
        let end = trimmed.find("?>").ok_or_else(|| {
            parse_error(
                text.len() - trimmed.len(),
                "unterminated XML processing instruction",
            )
        })?;
        trimmed = trimmed[end + 2..].trim_start();
    }
    let first = trimmed
        .find('<')
        .ok_or_else(|| parse_error(0, "missing XML LLSD element"))?;
    let origin = text.len() - trimmed.len() + first;
    let mut parser = Parser::new(&trimmed[first..], origin);
    parser.skip_misc()?;
    let start = parser.read_start()?;
    let value = if local_name(&start.name) == "llsd" {
        if start.empty {
            OSD::Undefined
        } else {
            parser.skip_misc()?;
            if parser.at_end("llsd") {
                parser.read_end("llsd")?;
                OSD::Undefined
            } else {
                let value = parser.parse_value(0)?;
                parser.skip_misc()?;
                parser.read_end("llsd")?;
                value
            }
        }
    } else {
        parser.parse_started(start, 0)?
    };
    value.validate_limits(
        OSD::DEFAULT_MAX_DEPTH,
        OSD::DEFAULT_MAX_NODES,
        OSD::DEFAULT_MAX_BINARY_BYTES,
    )?;
    Ok(value)
}

pub(crate) fn serialize_bytes(value: OSD) -> Result<Vec<u8>, Error> {
    Ok(serialize_string(value)?.into_bytes())
}

pub(crate) fn serialize_string(value: OSD) -> Result<String, Error> {
    let inner = serialize_inner(value)?;
    bounded_join("<llsd>", &inner, "</llsd>")
}

pub(crate) fn serialize_inner(value: OSD) -> Result<String, Error> {
    value.validate_limits(
        OSD::DEFAULT_MAX_DEPTH,
        OSD::DEFAULT_MAX_NODES,
        OSD::DEFAULT_MAX_BINARY_BYTES,
    )?;
    let mut encoder = Encoder::new();
    encoder.write_value(&value, 0)?;
    Ok(encoder.output)
}

pub(crate) fn serialize_element(writer: crate::xml::Writer, value: OSD) -> Result<(), Error> {
    writer.append(&serialize_inner(value)?)
}

struct StartTag {
    name: String,
    attributes: HashMap<String, String>,
    empty: bool,
    position: usize,
}

struct Parser<'a> {
    input: &'a str,
    position: usize,
    origin: usize,
    nodes: usize,
    allocated: usize,
}

impl<'a> Parser<'a> {
    const fn new(input: &'a str, origin: usize) -> Self {
        Self {
            input,
            position: 0,
            origin,
            nodes: 0,
            allocated: 0,
        }
    }

    fn parse_value(&mut self, depth: usize) -> Result<OSD, Error> {
        let start = self.read_start()?;
        self.parse_started(start, depth)
    }

    fn parse_started(&mut self, start: StartTag, depth: usize) -> Result<OSD, Error> {
        if depth > OSD::DEFAULT_MAX_DEPTH {
            return Err(self.error("XML LLSD nesting depth exceeded"));
        }
        self.nodes = self
            .nodes
            .checked_add(1)
            .ok_or_else(|| self.error("XML LLSD node overflow"))?;
        if self.nodes > OSD::DEFAULT_MAX_NODES {
            return Err(self.error("XML LLSD node limit exceeded"));
        }
        let name = local_name(&start.name);
        match name {
            "map" => self.parse_map(start, depth),
            "array" => self.parse_array(start, depth),
            "undef" => {
                self.finish_empty_or_text(&start, false)?;
                Ok(OSD::Undefined)
            }
            "boolean" => {
                let text = self.finish_empty_or_text(&start, true)?;
                Ok(OSD::Boolean(matches!(text.trim(), "1" | "true")))
            }
            "integer" => {
                let text = self.finish_empty_or_text(&start, true)?;
                Ok(OSD::Integer(text.trim().parse().unwrap_or(0)))
            }
            "real" => {
                let text = self.finish_empty_or_text(&start, true)?;
                let value = if text.trim().eq_ignore_ascii_case("nan") {
                    f64::NAN
                } else {
                    text.trim().parse().unwrap_or(0.0)
                };
                Ok(OSD::Real(value))
            }
            "string" => Ok(OSD::String(self.finish_empty_or_text(&start, true)?)),
            "uuid" => {
                let text = self.finish_empty_or_text(&start, true)?;
                Ok(OSD::UUID(
                    UUID::new_with_string(text.trim().into()).unwrap_or_else(|_| UUID::zero()),
                ))
            }
            "date" => {
                let text = self.finish_empty_or_text(&start, true)?;
                Ok(OSD::Date(
                    crate::model::parse_system_time_for_codec(text.trim()).unwrap_or(UNIX_EPOCH),
                ))
            }
            "uri" => Ok(OSD::Uri(Uri(self.finish_empty_or_text(&start, true)?))),
            "binary" => {
                if !start.empty
                    && start
                        .attributes
                        .get("encoding")
                        .is_some_and(|value| value != "base64")
                {
                    return Err(
                        self.error_at(start.position, "unsupported XML LLSD binary encoding")
                    );
                }
                let text = self.finish_empty_or_text(&start, true)?;
                let compact: Vec<_> = text
                    .bytes()
                    .filter(|byte| !byte.is_ascii_whitespace())
                    .collect();
                let bytes = base64::engine::general_purpose::STANDARD
                    .decode(compact)
                    .map_err(|_| self.error_at(start.position, "invalid XML LLSD base64 value"))?;
                self.add_allocation(bytes.len(), "XML LLSD binary allocation limit exceeded")?;
                Ok(OSD::Binary(bytes))
            }
            _ => Err(self.error_at(start.position, "unknown XML LLSD element")),
        }
    }

    fn parse_array(&mut self, start: StartTag, depth: usize) -> Result<OSD, Error> {
        if start.empty {
            return Ok(OSD::Array(Vec::new()));
        }
        let mut values = Vec::new();
        loop {
            self.skip_misc()?;
            if self.at_end("array") {
                self.read_end("array")?;
                break;
            }
            values.push(self.parse_value(depth + 1)?);
            self.add_allocation(
                std::mem::size_of::<OSD>(),
                "XML LLSD array allocation limit exceeded",
            )?;
            if values.len() > OSD::DEFAULT_MAX_NODES {
                return Err(self.error("XML LLSD array node limit exceeded"));
            }
        }
        Ok(OSD::Array(values))
    }

    fn parse_map(&mut self, start: StartTag, depth: usize) -> Result<OSD, Error> {
        if start.empty {
            return Ok(OSD::Map(HashMap::new()));
        }
        let mut values = HashMap::new();
        loop {
            self.skip_misc()?;
            if self.at_end("map") {
                self.read_end("map")?;
                break;
            }
            let key = self.read_start()?;
            if local_name(&key.name) != "key" {
                return Err(self.error_at(key.position, "expected XML LLSD map key"));
            }
            let key = self.finish_empty_or_text(&key, true)?;
            self.skip_misc()?;
            let value = self.parse_value(depth + 1)?;
            self.add_allocation(
                std::mem::size_of::<(String, OSD)>(),
                "XML LLSD map allocation limit exceeded",
            )?;
            values.insert(key, value);
            if values.len() > OSD::DEFAULT_MAX_NODES {
                return Err(self.error("XML LLSD map node limit exceeded"));
            }
        }
        Ok(OSD::Map(values))
    }

    fn finish_empty_or_text(
        &mut self,
        start: &StartTag,
        allow_text: bool,
    ) -> Result<String, Error> {
        if start.empty {
            return Ok(String::new());
        }
        let text = if allow_text {
            self.read_text(&start.name)?
        } else {
            self.skip_misc()?;
            String::new()
        };
        self.read_end(&start.name)?;
        self.add_allocation(text.len(), "XML LLSD text allocation limit exceeded")?;
        Ok(text)
    }

    fn read_text(&mut self, name: &str) -> Result<String, Error> {
        let mut output = String::new();
        loop {
            if self.at_end(name) {
                break;
            }
            if self.rest().is_empty() {
                return Err(self.error("unterminated XML LLSD scalar"));
            }
            if self.rest().starts_with("<!--") {
                self.skip_comment()?;
                continue;
            }
            if self.rest().starts_with("<?") {
                self.skip_pi()?;
                continue;
            }
            if self.rest().starts_with("<![CDATA[") {
                self.position += 9;
                let end = self
                    .rest()
                    .find("]]>")
                    .ok_or_else(|| self.error("unterminated XML CDATA"))?;
                output.push_str(&self.rest()[..end]);
                self.position += end + 3;
                continue;
            }
            if self.rest().starts_with('<') {
                return Err(self.error("nested element in XML LLSD scalar"));
            }
            let end = self.rest().find('<').unwrap_or(self.rest().len());
            output.push_str(&decode_entities(
                &self.rest()[..end],
                self.absolute_position(),
            )?);
            self.position += end;
            if output.len() > MAX_BYTES {
                return Err(self.error("XML LLSD text allocation limit exceeded"));
            }
        }
        Ok(output)
    }

    fn read_start(&mut self) -> Result<StartTag, Error> {
        self.skip_misc()?;
        let position = self.absolute_position();
        self.expect("<", "expected XML start element")?;
        if self.rest().starts_with('/')
            || self.rest().starts_with('!')
            || self.rest().starts_with('?')
        {
            return Err(self.error_at(position, "expected XML start element"));
        }
        let name = self.read_name()?;
        let mut attributes = HashMap::new();
        loop {
            self.skip_ascii_whitespace();
            if self.rest().starts_with("/>") {
                self.position += 2;
                return Ok(StartTag {
                    name,
                    attributes,
                    empty: true,
                    position,
                });
            }
            if self.rest().starts_with('>') {
                self.position += 1;
                return Ok(StartTag {
                    name,
                    attributes,
                    empty: false,
                    position,
                });
            }
            let attribute = self.read_name()?;
            self.skip_ascii_whitespace();
            self.expect("=", "expected XML attribute equals")?;
            self.skip_ascii_whitespace();
            let quote = self
                .rest()
                .as_bytes()
                .first()
                .copied()
                .filter(|byte| matches!(byte, b'\'' | b'"'))
                .ok_or_else(|| self.error("expected quoted XML attribute"))?;
            self.position += 1;
            let end = self
                .rest()
                .find(char::from(quote))
                .ok_or_else(|| self.error("unterminated XML attribute"))?;
            let value = decode_entities(&self.rest()[..end], self.absolute_position())?;
            self.position += end + 1;
            attributes.insert(local_name(&attribute).to_owned(), value);
        }
    }

    fn read_end(&mut self, expected: &str) -> Result<(), Error> {
        self.expect("</", "expected XML end element")?;
        let name = self.read_name()?;
        self.skip_ascii_whitespace();
        self.expect(">", "unterminated XML end element")?;
        if local_name(&name) != local_name(expected) {
            return Err(self.error("mismatched XML LLSD end element"));
        }
        Ok(())
    }

    fn at_end(&self, name: &str) -> bool {
        self.rest()
            .strip_prefix("</")
            .and_then(|rest| {
                rest.split(|c: char| c == '>' || c.is_ascii_whitespace())
                    .next()
            })
            .is_some_and(|found| local_name(found) == local_name(name))
    }

    fn skip_misc(&mut self) -> Result<(), Error> {
        loop {
            self.skip_ascii_whitespace();
            if self.rest().starts_with("<?") {
                self.skip_pi()?;
            } else if self.rest().starts_with("<!--") {
                self.skip_comment()?;
            } else if self
                .rest()
                .get(..9)
                .is_some_and(|value| value.eq_ignore_ascii_case("<!DOCTYPE"))
            {
                return Err(self.error("XML DTD and entity declarations are disabled"));
            } else {
                break;
            }
        }
        Ok(())
    }

    fn skip_pi(&mut self) -> Result<(), Error> {
        let end = self
            .rest()
            .find("?>")
            .ok_or_else(|| self.error("unterminated XML processing instruction"))?;
        self.position += end + 2;
        Ok(())
    }
    fn skip_comment(&mut self) -> Result<(), Error> {
        let end = self
            .rest()
            .find("-->")
            .ok_or_else(|| self.error("unterminated XML comment"))?;
        self.position += end + 3;
        Ok(())
    }
    fn skip_ascii_whitespace(&mut self) {
        while self
            .rest()
            .as_bytes()
            .first()
            .is_some_and(u8::is_ascii_whitespace)
        {
            self.position += 1;
        }
    }
    fn read_name(&mut self) -> Result<String, Error> {
        let length = self
            .rest()
            .bytes()
            .take_while(|byte| {
                byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b':' | b'.')
            })
            .count();
        if length == 0 {
            return Err(self.error("invalid XML name"));
        }
        let name = self.rest()[..length].to_owned();
        self.position += length;
        Ok(name)
    }
    fn expect(&mut self, text: &str, context: &'static str) -> Result<(), Error> {
        if self.rest().starts_with(text) {
            self.position += text.len();
            Ok(())
        } else {
            Err(self.error(context))
        }
    }
    fn add_allocation(&mut self, amount: usize, context: &'static str) -> Result<(), Error> {
        self.allocated = self
            .allocated
            .checked_add(amount)
            .ok_or_else(|| self.error(context))?;
        if self.allocated > MAX_BYTES {
            Err(self.error(context))
        } else {
            Ok(())
        }
    }
    fn rest(&self) -> &'a str {
        &self.input[self.position..]
    }
    const fn absolute_position(&self) -> usize {
        self.origin + self.position
    }
    const fn error(&self, context: &'static str) -> Error {
        self.error_at(self.absolute_position(), context)
    }
    const fn error_at(&self, position: usize, context: &'static str) -> Error {
        parse_error(position, context)
    }
}

struct Encoder {
    output: String,
}
impl Encoder {
    fn new() -> Self {
        Self {
            output: String::with_capacity(128),
        }
    }
    fn write_value(&mut self, value: &OSD, depth: usize) -> Result<(), Error> {
        if depth > OSD::DEFAULT_MAX_DEPTH {
            return Err(parse_error(
                self.output.len(),
                "XML LLSD nesting depth exceeded",
            ));
        }
        match value {
            OSD::Undefined => self.push("<undef />"),
            OSD::Boolean(value) => self.scalar("boolean", if *value { "1" } else { "0" }),
            OSD::Integer(value) => self.scalar("integer", &value.to_string()),
            OSD::Real(value) => self.scalar("real", &crate::model::format_real_for_codec(*value)),
            OSD::String(value) => self.scalar("string", value),
            OSD::UUID(value) => self.scalar("uuid", &value.to_string()),
            OSD::Date(value) => {
                self.scalar("date", &crate::model::format_system_time_for_codec(*value))
            }
            OSD::Uri(Uri(value)) => self.scalar("uri", &crate::model::format_uri_for_codec(value)),
            OSD::Binary(value) => self.scalar_with_attribute(
                "binary",
                "encoding",
                "base64",
                &base64::engine::general_purpose::STANDARD.encode(value),
            ),
            OSD::Array(values) => {
                self.push("<array>")?;
                for value in values {
                    self.write_value(value, depth + 1)?;
                }
                self.push("</array>")
            }
            OSD::Map(values) => {
                self.push("<map>")?;
                let mut entries: Vec<_> = values.iter().collect();
                entries.sort_unstable_by_key(|(key, _)| *key);
                for (key, value) in entries {
                    self.scalar("key", key)?;
                    self.write_value(value, depth + 1)?;
                }
                self.push("</map>")
            }
            OSD::LlsdXml(value) => self.push(value),
        }
    }
    fn scalar(&mut self, name: &str, value: &str) -> Result<(), Error> {
        self.push(&format!("<{name}>"))?;
        self.push_escaped(value)?;
        self.push(&format!("</{name}>"))
    }
    fn scalar_with_attribute(
        &mut self,
        name: &str,
        attribute: &str,
        attribute_value: &str,
        value: &str,
    ) -> Result<(), Error> {
        self.push(&format!("<{name} {attribute}=\"{attribute_value}\">"))?;
        self.push(value)?;
        self.push(&format!("</{name}>"))
    }
    fn push(&mut self, value: &str) -> Result<(), Error> {
        let length = self
            .output
            .len()
            .checked_add(value.len())
            .ok_or(Error::Argument)?;
        if length > MAX_BYTES {
            return Err(Error::Argument);
        }
        self.output.push_str(value);
        Ok(())
    }

    fn push_escaped(&mut self, value: &str) -> Result<(), Error> {
        let mut start = 0;
        for (index, character) in value.char_indices() {
            let replacement = match character {
                '&' => "&amp;",
                '<' => "&lt;",
                '>' => "&gt;",
                _ => continue,
            };
            self.push(&value[start..index])?;
            self.push(replacement)?;
            start = index + character.len_utf8();
        }
        self.push(&value[start..])
    }
}

fn decode_entities(value: &str, origin: usize) -> Result<String, Error> {
    let mut output = String::with_capacity(value.len());
    let mut remaining = value;
    while let Some(index) = remaining.find('&') {
        output.push_str(&remaining[..index]);
        let entity_start = origin + value.len() - remaining.len() + index;
        remaining = &remaining[index + 1..];
        let end = remaining
            .find(';')
            .ok_or_else(|| parse_error(entity_start, "unterminated XML entity"))?;
        let entity = &remaining[..end];
        let character = match entity {
            "amp" => '&',
            "lt" => '<',
            "gt" => '>',
            "apos" => '\'',
            "quot" => '"',
            entity if entity.starts_with("#x") => char::from_u32(
                u32::from_str_radix(&entity[2..], 16)
                    .map_err(|_| parse_error(entity_start, "invalid XML character reference"))?,
            )
            .ok_or_else(|| parse_error(entity_start, "invalid XML character reference"))?,
            entity if entity.starts_with('#') => char::from_u32(
                entity[1..]
                    .parse()
                    .map_err(|_| parse_error(entity_start, "invalid XML character reference"))?,
            )
            .ok_or_else(|| parse_error(entity_start, "invalid XML character reference"))?,
            _ => {
                return Err(parse_error(
                    entity_start,
                    "XML entity expansion is disabled",
                ));
            }
        };
        output.push(character);
        remaining = &remaining[end + 1..];
    }
    output.push_str(remaining);
    Ok(output)
}
fn local_name(name: &str) -> &str {
    name.rsplit(':').next().unwrap_or(name)
}
fn bounded_join(prefix: &str, value: &str, suffix: &str) -> Result<String, Error> {
    let length = prefix
        .len()
        .checked_add(value.len())
        .and_then(|length| length.checked_add(suffix.len()))
        .ok_or(Error::Argument)?;
    if length > MAX_BYTES {
        return Err(Error::Argument);
    }
    Ok([prefix, value, suffix].concat())
}
const fn parse_error(position: usize, context: &'static str) -> Error {
    Error::Parse { position, context }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn every_variant_round_trips_and_entities_are_exact() {
        let value = OSD::Map(HashMap::from([
            ("text<&".into(), OSD::String("< > & ' \"".into())),
            (
                "nested".into(),
                OSD::Array(vec![OSD::Boolean(true), OSD::Binary(vec![0, 255])]),
            ),
        ]));
        let xml = serialize_string(value.clone()).unwrap();
        assert_eq!(deserialize_string(xml).unwrap(), value);
        assert_eq!(
            deserialize_string("<llsd><string>&#x10137; &amp;</string></llsd>".into()).unwrap(),
            OSD::String("𐄷 &".into())
        );
    }
    #[test]
    fn empty_binary_ignores_encoding_like_the_reference_reader() {
        assert_eq!(
            deserialize_string("<llsd><binary encoding='legacy' /></llsd>".into()).unwrap(),
            OSD::Binary(Vec::new())
        );
    }

    #[test]
    fn every_serialization_entry_point_is_compact_and_exact() {
        let value = OSD::Map(HashMap::from([
            ("z".into(), OSD::Undefined),
            ("a<&".into(), OSD::Binary(vec![0, 255])),
        ]));
        let inner = "<map><key>a&lt;&amp;</key><binary encoding=\"base64\">AP8=</binary><key>z</key><undef /></map>";
        assert_eq!(serialize_inner(value.clone()).unwrap(), inner);
        assert_eq!(
            serialize_string(value.clone()).unwrap(),
            format!("<llsd>{inner}</llsd>")
        );
        assert_eq!(
            serialize_bytes(value.clone()).unwrap(),
            format!("<llsd>{inner}</llsd>").into_bytes()
        );
        let writer = crate::xml::Writer::default();
        serialize_element(writer.clone(), value).unwrap();
        assert_eq!(writer.contents(), inner);
    }

    #[test]
    fn reader_stream_comments_and_cdata_match_xml_behavior() {
        let input = "<?xml version='1.0'?><llsd><!-- ignored --><string><![CDATA[<raw>]]>&amp;</string></llsd>";
        assert_eq!(
            deserialize_reader(crate::xml::Reader(input.into())).unwrap(),
            OSD::String("<raw>&".into())
        );
        let stream: Box<dyn ReadWrite + Send> =
            Box::new(std::io::Cursor::new(input.as_bytes().to_vec()));
        assert_eq!(
            deserialize_stream(stream).unwrap(),
            OSD::String("<raw>&".into())
        );
    }

    #[test]
    fn hostile_entities_and_malformed_nesting_are_rejected() {
        for input in [
            include_str!("../../../fuzz/corpus/xml_llsd/doctype_entity.xml"),
            include_str!("../../../fuzz/corpus/xml_llsd/external_entity.xml"),
            include_str!("../../../fuzz/corpus/xml_llsd/unknown_entity.xml"),
            "<llsd><array><integer>1</array></llsd>",
            "<llsd><binary encoding='base16'>00</binary></llsd>",
        ] {
            assert!(matches!(
                deserialize_string(input.into()),
                Err(Error::Parse { .. })
            ));
        }

        match deserialize_string("<llsd><integer>1</real></llsd>".into()) {
            Err(Error::Parse { position, context }) => {
                assert!(position > 6);
                assert_eq!(context, "nested element in XML LLSD scalar");
            }
            result => panic!("expected positioned XML parse error, got {result:?}"),
        }

        let mut too_deep = "<llsd>".to_owned();
        too_deep.push_str(&"<array>".repeat(OSD::DEFAULT_MAX_DEPTH + 2));
        too_deep.push_str("<undef />");
        too_deep.push_str(&"</array>".repeat(OSD::DEFAULT_MAX_DEPTH + 2));
        too_deep.push_str("</llsd>");
        assert!(matches!(
            deserialize_string(too_deep),
            Err(Error::Parse {
                context: "XML LLSD nesting depth exceeded",
                ..
            })
        ));
    }
}
