//! Human-readable formatting of raw agent log entries.

use crate::chat::util::truncate_at_char_boundary;

/// Format a single log entry as a human-readable text line.
///
/// `timestamp` is an ISO 8601 string; `event` is the flattened `AgentEvent`
/// value (has `type`, `agent_name`, etc. at the top level).
///
/// Returns `None` for entries that should be skipped (raw streaming noise,
/// trivial status changes, empty output, etc.).
#[allow(clippy::string_slice)] // timestamp[11..19]: ISO 8601 is ASCII-only, these byte offsets are always valid
pub fn format_log_entry_as_text(timestamp: &str, event: &serde_json::Value) -> Option<String> {
    let agent_name = event
        .get("agent_name")
        .and_then(|v| v.as_str())
        .unwrap_or("?");
    // Extract HH:MM:SS from ISO 8601 "2026-04-10T12:48:02.123456789+00:00"
    let ts_short = if timestamp.len() >= 19 {
        &timestamp[11..19]
    } else {
        timestamp
    };
    let pfx = format!("[{ts_short}][{agent_name}]");

    match event.get("type").and_then(|v| v.as_str()) {
        Some("output") => {
            let text = event
                .get("text")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .trim();
            if text.is_empty() {
                None
            } else {
                Some(format!("{pfx} {text}"))
            }
        }
        Some("error") => {
            let msg = event
                .get("message")
                .and_then(|v| v.as_str())
                .unwrap_or("(unknown error)");
            Some(format!("{pfx} ERROR: {msg}"))
        }
        Some("done") => Some(format!("{pfx} DONE")),
        Some("status") => {
            // Skip trivial running/started noise
            let status = event.get("status").and_then(|v| v.as_str()).unwrap_or("?");
            match status {
                "running" | "started" => None,
                _ => Some(format!("{pfx} STATUS: {status}")),
            }
        }
        Some("agent_json") => {
            let data = event.get("data")?;
            match data.get("type").and_then(|v| v.as_str()) {
                Some("assistant") => {
                    let mut parts: Vec<String> = Vec::new();
                    if let Some(arr) = data.pointer("/message/content").and_then(|v| v.as_array()) {
                        for item in arr {
                            match item.get("type").and_then(|v| v.as_str()) {
                                Some("text") => {
                                    let text = item
                                        .get("text")
                                        .and_then(|v| v.as_str())
                                        .unwrap_or("")
                                        .trim();
                                    if !text.is_empty() {
                                        parts.push(format!("{pfx} {text}"));
                                    }
                                }
                                Some("tool_use") => {
                                    let name =
                                        item.get("name").and_then(|v| v.as_str()).unwrap_or("?");
                                    let input = item
                                        .get("input")
                                        .map(|v| serde_json::to_string(v).unwrap_or_default())
                                        .unwrap_or_default();
                                    let display = if input.len() > 200 {
                                        format!("{}...", truncate_at_char_boundary(&input, 200))
                                    } else {
                                        input
                                    };
                                    parts.push(format!("{pfx} TOOL: {name}({display})"));
                                }
                                _ => {}
                            }
                        }
                    }
                    if parts.is_empty() {
                        None
                    } else {
                        Some(parts.join("\n"))
                    }
                }
                Some("user") => {
                    let mut parts: Vec<String> = Vec::new();
                    if let Some(arr) = data.pointer("/message/content").and_then(|v| v.as_array()) {
                        for item in arr {
                            if item.get("type").and_then(|v| v.as_str()) != Some("tool_result") {
                                continue;
                            }
                            let content_str = match item.get("content") {
                                Some(serde_json::Value::String(s)) => s.clone(),
                                Some(v) => v.to_string(),
                                None => String::new(),
                            };
                            let display = if content_str.len() > 500 {
                                // Walk back to the nearest char boundary so we
                                // don't panic when the 500-byte mark lands
                                // inside a multi-byte UTF-8 codepoint (e.g.
                                // box-drawing chars like '─', smart quotes,
                                // emoji). `is_char_boundary(len)` is always
                                // true so the loop terminates.
                                let mut end = 500;
                                while !content_str.is_char_boundary(end) {
                                    end -= 1;
                                }
                                format!(
                                    "{}... [{} chars total]",
                                    &content_str[..end],
                                    content_str.len()
                                )
                            } else {
                                content_str
                            };
                            if !display.trim().is_empty() {
                                parts.push(format!("{pfx} RESULT: {display}"));
                            }
                        }
                    }
                    if parts.is_empty() {
                        None
                    } else {
                        Some(parts.join("\n"))
                    }
                }
                _ => None, // Skip stream_event, system init, etc.
            }
        }
        _ => None,
    }
}

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

    /// Regression: a tool_result whose content is >500 bytes AND has a
    /// multi-byte UTF-8 codepoint straddling byte 500 must not panic.
    /// Previously `&content_str[..500]` would slice mid-codepoint and crash
    /// the get_agent_output MCP tool.
    #[test]
    fn tool_result_truncation_handles_multibyte_at_boundary() {
        // 498 ASCII filler + a 3-byte '─' (U+2500) starting at byte 499 +
        // 100 more ASCII chars.  The naive `..500` slice would land inside
        // the box-drawing char and panic.
        let mut content = "a".repeat(499);
        content.push('─');
        content.push_str(&"b".repeat(100));
        assert!(content.len() > 500);
        assert!(!content.is_char_boundary(500));

        let event = json!({
            "type": "agent_json",
            "agent_name": "coder-1",
            "data": {
                "type": "user",
                "message": {
                    "content": [{ "type": "tool_result", "content": content }]
                }
            }
        });

        let out = format_log_entry_as_text("2026-05-12T15:30:00.000000Z", &event);
        assert!(out.is_some(), "tool_result must format without panicking");
        let s = out.unwrap();
        assert!(s.contains("RESULT:"));
        assert!(s.contains("chars total"));
    }
}
