//! Bounded wire-format dispatch for the public `OSDParser.Deserialize` overloads.

use crate::{Error, OSD, OSDParser};
use libremetaverse_types::compat::ReadWrite;
use std::io::Read as _;

const PROBE_LENGTH: usize = 20;
const MAX_INPUT_BYTES: usize = OSD::DEFAULT_MAX_BINARY_BYTES;
const BINARY_HEADER: &str = "<? llsd/binary ?>";
const PROTOBUF_HEADER: &str = "<? llsd/protobuf ?>";
const XML_HEADERS: [&str; 3] = ["<llsd>", "<?xml", "<? llsd/xml ?>"];

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum WireFormat {
    Binary,
    Json,
    Protobuf,
    Xml,
}

pub(crate) fn deserialize_bytes(data: Vec<u8>) -> Result<OSD, Error> {
    if data.len() > MAX_INPUT_BYTES {
        return Err(Error::Argument);
    }
    let format = classify_bytes(&data).ok_or(Error::Argument)?;
    let value = match format {
        WireFormat::Binary => OSDParser::deserialize_llsd_binary_with_bytes(data)?,
        WireFormat::Protobuf => OSDParser::deserialize_llsd_protobuf_with_bytes(data)?,
        WireFormat::Xml => OSDParser::deserialize_llsd_xml_with_bytes(data)?,
        WireFormat::Json => OSDParser::deserialize_json_with_string(
            String::from_utf8(data).map_err(|_| Error::Argument)?,
        )?,
    };
    validate_decoded(value)
}

pub(crate) fn deserialize_string(data: String) -> Result<OSD, Error> {
    if data.len() > MAX_INPUT_BYTES {
        return Err(Error::Argument);
    }
    let value = match classify_string(&data) {
        WireFormat::Binary => OSDParser::deserialize_llsd_binary_with_bytes(data.into_bytes())?,
        WireFormat::Protobuf => OSDParser::deserialize_llsd_protobuf_with_bytes(data.into_bytes())?,
        WireFormat::Xml => OSDParser::deserialize_llsd_xml_with_string(data)?,
        WireFormat::Json => OSDParser::deserialize_json_with_string(data)?,
    };
    validate_decoded(value)
}

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

fn validate_decoded(value: OSD) -> Result<OSD, Error> {
    value.validate_limits(
        OSD::DEFAULT_MAX_DEPTH,
        OSD::DEFAULT_MAX_NODES,
        OSD::DEFAULT_MAX_BINARY_BYTES,
    )?;
    Ok(value)
}

fn classify_bytes(data: &[u8]) -> Option<WireFormat> {
    let probe = &data[..data.len().min(PROBE_LENGTH)];
    let ascii = String::from_utf8_lossy(probe);
    let utf8 = ascii.trim_start_matches('\u{feff}').trim_start();
    if starts_with_any_ignore_ascii_case(utf8, &XML_HEADERS) {
        return Some(WireFormat::Xml);
    }
    if starts_with_ignore_ascii_case(&ascii, PROTOBUF_HEADER) {
        return Some(WireFormat::Protobuf);
    }
    if starts_with_ignore_ascii_case(&ascii, BINARY_HEADER) {
        return Some(WireFormat::Binary);
    }
    if starts_with_any_ignore_ascii_case(&ascii, &XML_HEADERS) {
        return Some(WireFormat::Xml);
    }
    let first = utf8.as_bytes().first().copied()?;
    if matches!(first, b'{' | b'[' | b'"' | b'-' | b'0'..=b'9')
        || utf8.starts_with("true")
        || utf8.starts_with("false")
        || utf8.starts_with("null")
    {
        Some(WireFormat::Json)
    } else {
        None
    }
}

fn classify_string(data: &str) -> WireFormat {
    if starts_with_ignore_ascii_case(data, PROTOBUF_HEADER) {
        WireFormat::Protobuf
    } else if starts_with_ignore_ascii_case(data, BINARY_HEADER) {
        WireFormat::Binary
    } else if starts_with_any_ignore_ascii_case(data, &XML_HEADERS) {
        WireFormat::Xml
    } else {
        WireFormat::Json
    }
}

fn starts_with_any_ignore_ascii_case(value: &str, prefixes: &[&str]) -> bool {
    prefixes
        .iter()
        .any(|prefix| starts_with_ignore_ascii_case(value, prefix))
}

fn starts_with_ignore_ascii_case(value: &str, prefix: &str) -> bool {
    value
        .get(..prefix.len())
        .is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix))
}

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

    #[test]
    fn byte_dispatch_recognizes_headers_bom_whitespace_and_json_tokens() {
        assert_eq!(
            classify_bytes(b"<? llsd/binary ?>i"),
            Some(WireFormat::Binary)
        );
        assert_eq!(
            classify_bytes(b"<? LLSD/PROTOBUF ?>"),
            Some(WireFormat::Protobuf)
        );
        assert_eq!(
            classify_bytes(b" \n<?xml version='1.0'"),
            Some(WireFormat::Xml)
        );
        assert_eq!(
            classify_bytes(b"\xef\xbb\xbf <llsd>"),
            Some(WireFormat::Xml)
        );
        assert_eq!(classify_bytes(b"  [1,2]"), Some(WireFormat::Json));
        assert_eq!(classify_bytes(b"Unknown conference"), None);
    }

    #[test]
    fn string_dispatch_matches_reference_fallback_to_json() {
        assert_eq!(classify_string("<? llsd/binary ?>i"), WireFormat::Binary);
        assert_eq!(classify_string("<LLSD>"), WireFormat::Xml);
        assert_eq!(classify_string("plain text"), WireFormat::Json);
    }

    #[test]
    fn public_byte_dispatch_rejects_unrecognized_plain_text() {
        assert_eq!(
            deserialize_bytes(b"Unknown conference".to_vec()),
            Err(Error::Argument)
        );
        assert_eq!(
            deserialize_bytes(b"<? llsd/binary ?>i\0\0\0\0".to_vec()),
            Ok(OSD::Integer(0))
        );
        assert_eq!(
            deserialize_bytes(b"<llsd><undef /></llsd>".to_vec()),
            Ok(OSD::Undefined)
        );
        assert_eq!(
            deserialize_bytes(b"<? llsd/protobuf ?>".to_vec()),
            Ok(OSD::Undefined)
        );
        assert_eq!(
            deserialize_bytes(b"{}".to_vec()),
            Ok(OSD::Map(HashMap::new()))
        );

        let stream = Box::new(std::io::Cursor::new(b"{}".to_vec()));
        assert_eq!(deserialize_stream(stream), Ok(OSD::Map(HashMap::new())));
    }
}
