//! Deterministic, reflection-free packet and compatibility-object diagnostics.

use std::fmt::Write as _;

use libremetaverse_structured_data::OSD;
use libremetaverse_types::Error;
use libremetaverse_types::compat::Object;

use crate::packets::{Header, Packet};

#[must_use]
pub(crate) fn interpret_options(header: &Header) -> String {
    format!(
        "[{} {} {} {}]",
        if header.appended_acks { "Ack" } else { "   " },
        if header.resent { "Res" } else { "   " },
        if header.reliable { "Rel" } else { "   " },
        if header.zerocoded { "Zer" } else { "   " },
    )
}

pub(crate) fn packet_to_string(packet: &Packet) -> Result<String, Error> {
    let descriptor =
        crate::packet_catalog::descriptor_by_type(packet.type_).ok_or(Error::InvalidOperation)?;
    let mut result = String::new();
    writeln!(
        result,
        "Packet Type: {0} http://lib.openmetaverse.co/wiki/{0} http://wiki.secondlife.com/wiki/{0}",
        descriptor.name,
    )
    .map_err(|_| Error::InvalidOperation)?;
    writeln!(result, "[Packet Header]").map_err(|_| Error::InvalidOperation)?;
    writeln!(result, "Sequence: {}", packet.header.sequence)
        .map_err(|_| Error::InvalidOperation)?;
    writeln!(result, " Options: {}", interpret_options(&packet.header))
        .map_err(|_| Error::InvalidOperation)?;
    writeln!(result, "\n[Packet Payload]").map_err(|_| Error::InvalidOperation)?;
    let body = packet.packet_to_osd()?;
    format_osd_map(&body.snapshot(), 0, &mut result)?;
    Ok(result)
}

pub(crate) fn message_to_string(message: &Object, recurse_level: i32) -> Result<String, Error> {
    if recurse_level < 0 || recurse_level > 64 {
        return Err(Error::Argument);
    }
    let mut result = String::new();
    format_object(
        message,
        usize::try_from(recurse_level).map_err(|_| Error::Argument)?,
        &mut result,
    )?;
    Ok(result)
}

fn format_object(value: &Object, depth: usize, output: &mut String) -> Result<(), Error> {
    let indent = "  ".repeat(depth);
    match value {
        Object::Undefined => output.push_str(&format!("{indent}undefined\n")),
        Object::Boolean(value) => output.push_str(&format!("{indent}{value} [Boolean]\n")),
        Object::Integer(value) => output.push_str(&format!("{indent}{value} [Int32]\n")),
        Object::UInteger(value) => output.push_str(&format!("{indent}{value} [UInt32]\n")),
        Object::Long(value) => output.push_str(&format!("{indent}{value} [Int64]\n")),
        Object::ULong(value) => output.push_str(&format!("{indent}{value} [UInt64]\n")),
        Object::Real(value) => output.push_str(&format!("{indent}{value} [Double]\n")),
        Object::String(value) => output.push_str(&format!("{indent}{value} [String]\n")),
        Object::Bytes(value) => {
            output.push_str(&format!("{indent}[Byte[{0}]]\n", value.len()));
            format_hex(value, depth + 1, output)?;
        }
        Object::Array(values) => {
            output.push_str(&format!("{indent}[Array {0}]\n", values.len()));
            for (index, value) in values.iter().enumerate() {
                output.push_str(&format!("{indent}  [{index}]\n"));
                format_object(value, depth + 2, output)?;
            }
        }
        Object::Map(values) => {
            output.push_str(&format!("{indent}[Map {0}]\n", values.len()));
            let mut entries = values.iter().collect::<Vec<_>>();
            entries.sort_by(|left, right| left.0.cmp(right.0));
            for (key, value) in entries {
                output.push_str(&format!("{indent}  {key}:\n"));
                format_object(value, depth + 2, output)?;
            }
        }
        other => output.push_str(&format!("{indent}{other:?}\n")),
    }
    Ok(())
}

fn format_osd_map(
    values: &std::collections::HashMap<String, OSD>,
    depth: usize,
    output: &mut String,
) -> Result<(), Error> {
    let mut entries = values.iter().collect::<Vec<_>>();
    entries.sort_by(|left, right| left.0.cmp(right.0));
    for (key, value) in entries {
        writeln!(output, "{}{key}:", "  ".repeat(depth)).map_err(|_| Error::InvalidOperation)?;
        format_osd(value, depth + 1, output)?;
    }
    Ok(())
}

fn format_osd(value: &OSD, depth: usize, output: &mut String) -> Result<(), Error> {
    let indent = "  ".repeat(depth);
    match value {
        OSD::Map(values) => format_osd_map(values, depth, output),
        OSD::Array(values) => {
            for (index, value) in values.iter().enumerate() {
                writeln!(output, "{indent}[{index}]").map_err(|_| Error::InvalidOperation)?;
                format_osd(value, depth + 1, output)?;
            }
            Ok(())
        }
        OSD::Binary(bytes) => format_hex(bytes, depth, output),
        other => writeln!(output, "{indent}{other:?}").map_err(|_| Error::InvalidOperation),
    }
}

fn format_hex(bytes: &[u8], depth: usize, output: &mut String) -> Result<(), Error> {
    let indent = "  ".repeat(depth);
    for chunk in bytes.chunks(16) {
        write!(output, "{indent}").map_err(|_| Error::InvalidOperation)?;
        for byte in chunk {
            write!(output, "{byte:02X} ").map_err(|_| Error::InvalidOperation)?;
        }
        output.push('\n');
    }
    Ok(())
}

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

    #[test]
    fn options_match_reference_layout() {
        let mut header = crate::packet_catalog::new_header(crate::PacketFrequency::High, 1, false);
        header.reliable = true;
        header.resent = true;
        assert_eq!(interpret_options(&header), "[    Res Rel    ]");
    }

    #[test]
    fn packet_dump_contains_header_and_sorted_payload() {
        let packet =
            crate::packet_catalog::build_packet(crate::packet_catalog::PacketType::CloseCircuit)
                .unwrap();
        let dump = packet_to_string(&packet).unwrap();
        assert!(dump.contains("Packet Type: CloseCircuit"));
        assert!(dump.contains("[Packet Header]"));
        assert!(dump.contains("[Packet Payload]"));
    }

    #[test]
    fn message_dump_formats_nested_values_and_bytes() {
        let value = Object::Map(std::collections::HashMap::from([
            ("z".to_owned(), Object::Bytes(vec![0xab, 0xcd])),
            ("a".to_owned(), Object::Integer(7)),
        ]));
        let text = message_to_string(&value, 0).unwrap();
        assert!(text.find("a:").unwrap() < text.find("z:").unwrap());
        assert!(text.contains("AB CD"));
    }
}
