//! Human-readable dumps for debugging and capture review.
//!
//! Diagnostics only: nothing here is part of the wire contract, and no output
//! format should ever be parsed back.

use crate::fire2::{Frame, Header};
use crate::heat2::{Struct, Value};
use std::fmt::Write as _;

/// Render a TDF struct as an indented tree, mirroring `heat2.py::dump`.
pub fn dump_struct(s: &Struct) -> String {
    let mut out = String::new();
    write_struct(s, 0, &mut out);
    out
}

fn write_struct(s: &Struct, depth: usize, out: &mut String) {
    for (tag, value) in s.iter() {
        let pad = "  ".repeat(depth);
        let _ = write!(out, "{pad}{tag} ({})", value.type_id().name());
        match value {
            Value::Struct(inner) => {
                let _ = writeln!(out, " {{");
                write_struct(inner, depth + 1, out);
                let _ = writeln!(out, "{pad}}}");
            }
            Value::List { elem, items } => {
                let _ = writeln!(out, " [{} x {}]", elem.name(), items.len());
                for item in items {
                    write_value(item, depth + 1, out);
                }
            }
            Value::Map { key, val, entries } => {
                let _ = writeln!(
                    out,
                    " {{{} -> {} x {}}}",
                    key.name(),
                    val.name(),
                    entries.len()
                );
                for (k, v) in entries {
                    write_value(k, depth + 1, out);
                    write_value(v, depth + 2, out);
                }
            }
            other => {
                let _ = writeln!(out, " = {}", scalar(other));
            }
        }
    }
}

fn write_value(value: &Value, depth: usize, out: &mut String) {
    let pad = "  ".repeat(depth);
    match value {
        Value::Struct(inner) => {
            let _ = writeln!(out, "{pad}{{");
            write_struct(inner, depth + 1, out);
            let _ = writeln!(out, "{pad}}}");
        }
        other => {
            let _ = writeln!(out, "{pad}{}", scalar(other));
        }
    }
}

fn scalar(value: &Value) -> String {
    match value {
        Value::Int(v) => format!("{v}"),
        Value::String(s) => format!("{s:?}"),
        Value::Blob(b) => format!("<{} bytes> {}", b.len(), hex(&b[..b.len().min(32)])),
        Value::VarList(v) => format!("{v:?}"),
        Value::ObjType { component, ty } => format!("({component}, {ty})"),
        Value::ObjId { component, ty, id } => format!("({component}, {ty}, {id})"),
        Value::Float(f) => format!("{f}"),
        Value::Union { key, member } => match member {
            Some(m) => format!("union[{key}] {} = {}", m.0, scalar(&m.1)),
            None => format!("union[{key}] unset"),
        },
        Value::Struct(_) | Value::List { .. } | Value::Map { .. } => String::from("..."),
    }
}

/// One-line summary of a Fire2 header.
pub fn describe_header(h: &Header) -> String {
    format!(
        "component=0x{:04x} command=0x{:04x} {} msgNum={} userIdx={} opts=0x{:02x} \
         payload={}B metadata={}B",
        h.component,
        h.command,
        h.msg_type.name(),
        h.msg_num,
        h.user_index,
        h.options,
        h.payload_len,
        h.metadata_len
    )
}

/// Header summary plus a decoded body, falling back to hex when it will not
/// parse. A capture is most valuable exactly when the body is malformed, so
/// this must never fail.
pub fn describe_frame(frame: &Frame) -> String {
    let mut out = describe_header(&frame.header);
    out.push('\n');
    if frame.payload.is_empty() {
        out.push_str("(empty payload)\n");
        return out;
    }
    match crate::heat2::decode(&frame.payload) {
        Ok(body) => out.push_str(&dump_struct(&body)),
        Err(e) => {
            let _ = writeln!(out, "(TDF decode failed: {e})");
            out.push_str(&hexdump(&frame.payload, 256));
        }
    }
    out
}

pub fn hex(bytes: &[u8]) -> String {
    let mut s = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        let _ = write!(s, "{b:02x}");
    }
    s
}

/// Classic offset / hex / ASCII dump, truncated to `limit` bytes.
pub fn hexdump(bytes: &[u8], limit: usize) -> String {
    let shown = &bytes[..bytes.len().min(limit)];
    let mut out = String::new();
    for (row, chunk) in shown.chunks(16).enumerate() {
        let _ = write!(out, "{:08x}  ", row * 16);
        for i in 0..16 {
            match chunk.get(i) {
                Some(b) => {
                    let _ = write!(out, "{b:02x} ");
                }
                None => out.push_str("   "),
            }
            if i == 7 {
                out.push(' ');
            }
        }
        out.push_str(" |");
        for b in chunk {
            out.push(if (0x20..0x7F).contains(b) {
                *b as char
            } else {
                '.'
            });
        }
        out.push_str("|\n");
    }
    if bytes.len() > limit {
        let _ = writeln!(out, "... {} more bytes", bytes.len() - limit);
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::fire2::MsgType;
    use crate::heat2;

    #[test]
    fn dumps_nested_structures() {
        let s = Struct::new().with("PID", Value::Int(33068179)).with(
            "CINF",
            Value::Struct(Struct::new().with("ENV", Value::String("prod".into()))),
        );
        let text = dump_struct(&s);
        assert!(text.contains("PID (int) = 33068179"), "{text}");
        assert!(text.contains("ENV (string) = \"prod\""), "{text}");
    }

    #[test]
    fn describes_a_frame_with_an_undecodable_body() {
        // 0xFF is not a TDF type byte, so this must fall back to hex.
        let frame = Frame::new(9, 7, 1, MsgType::Reply, vec![0xFF; 8]);
        let text = describe_frame(&frame);
        assert!(text.contains("decode failed"), "{text}");
        assert!(text.contains("ff ff ff"), "{text}");
    }

    #[test]
    fn describes_a_frame_with_a_good_body() {
        let body = Struct::new().with("A", Value::Int(1));
        let frame = Frame::new(9, 7, 1, MsgType::Reply, heat2::encode(&body));
        let text = describe_frame(&frame);
        assert!(text.contains("REPLY"), "{text}");
        assert!(text.contains("A (int) = 1"), "{text}");
    }

    #[test]
    fn hexdump_truncates_and_says_so() {
        let text = hexdump(&[0x41; 100], 32);
        assert!(text.contains("68 more bytes"), "{text}");
        assert!(text.contains("AAAA"), "{text}");
    }
}
