//! Manual serialization for ConversationEvent to/from storage format.
//!
//! The storage format is decoupled from the Rust enum so the two can evolve
//! independently. Each event is stored as an `(event_type, event_data)` pair
//! where `event_data` is a JSON string.

use eyre::{Result, eyre};
use serde_json::Value;

use crate::tui::ConversationEvent;

/// Serialize a ConversationEvent into an (event_type, event_data_json) pair
/// suitable for database storage.
pub fn serialize_event(event: &ConversationEvent) -> (String, String) {
    match event {
        ConversationEvent::UserMessage { content } => {
            ("user_message".to_string(), serde_json::json!({ "content": content }).to_string())
        }
        ConversationEvent::Text { content } => {
            ("text".to_string(), serde_json::json!({ "content": content }).to_string())
        }
        ConversationEvent::ToolCall { id, name, input } => (
            "tool_call".to_string(),
            serde_json::json!({
                "id": id,
                "name": name,
                "input": input,
            })
            .to_string(),
        ),
        ConversationEvent::ToolResult {
            tool_use_id,
            content,
            is_error,
            remote,
            content_length,
        } => (
            "tool_result".to_string(),
            serde_json::json!({
                "tool_use_id": tool_use_id,
                "content": content,
                "is_error": is_error,
                "remote": remote,
                "content_length": content_length,
            })
            .to_string(),
        ),
        ConversationEvent::OutOfBandOutput {
            name,
            command,
            content,
        } => (
            "out_of_band_output".to_string(),
            serde_json::json!({
                "name": name,
                "command": command,
                "content": content,
            })
            .to_string(),
        ),
        ConversationEvent::SystemContext { content } => {
            ("system_context".to_string(), serde_json::json!({ "content": content }).to_string())
        }
        ConversationEvent::SkillInvocation {
            name,
            arguments,
            content,
        } => (
            "skill_invocation".to_string(),
            serde_json::json!({
                "name": name,
                "arguments": arguments,
                "content": content,
            })
            .to_string(),
        ),
    }
}

/// Deserialize an (event_type, event_data_json) pair from storage back into a
/// ConversationEvent.
pub fn deserialize_event(event_type: &str, event_data: &str) -> Result<ConversationEvent> {
    let data: Value = serde_json::from_str(event_data)
        .map_err(|e| eyre!("failed to parse event_data JSON: {e}"))?;

    match event_type {
        "user_message" => Ok(ConversationEvent::UserMessage {
            content: json_string(&data, "content")?,
        }),
        "text" => Ok(ConversationEvent::Text {
            content: json_string(&data, "content")?,
        }),
        "tool_call" => Ok(ConversationEvent::ToolCall {
            id: json_string(&data, "id")?,
            name: json_string(&data, "name")?,
            input: data
                .get("input")
                .cloned()
                .ok_or_else(|| eyre!("tool_call missing 'input' field"))?,
        }),
        "tool_result" => Ok(ConversationEvent::ToolResult {
            tool_use_id: json_string(&data, "tool_use_id")?,
            content: json_string(&data, "content")?,
            is_error: data
                .get("is_error")
                .and_then(Value::as_bool)
                .ok_or_else(|| eyre!("tool_result missing 'is_error' field"))?,
            remote: data.get("remote").and_then(Value::as_bool).unwrap_or(false),
            content_length: data.get("content_length").and_then(Value::as_u64).map(|v| v as usize),
        }),
        "out_of_band_output" => Ok(ConversationEvent::OutOfBandOutput {
            name: json_string(&data, "name")?,
            command: data
                .get("command")
                .and_then(|v| {
                    if v.is_null() {
                        None
                    } else {
                        v.as_str()
                    }
                })
                .map(String::from),
            content: json_string(&data, "content")?,
        }),
        "system_context" => Ok(ConversationEvent::SystemContext {
            content: json_string(&data, "content")?,
        }),
        "skill_invocation" => Ok(ConversationEvent::SkillInvocation {
            name: json_string(&data, "name")?,
            arguments: data
                .get("arguments")
                .and_then(|v| {
                    if v.is_null() {
                        None
                    } else {
                        v.as_str()
                    }
                })
                .map(String::from),
            content: json_string(&data, "content")?,
        }),
        other => Err(eyre!("unknown event type: {other}")),
    }
}

fn json_string(data: &Value, field: &str) -> Result<String> {
    data.get(field)
        .and_then(Value::as_str)
        .map(String::from)
        .ok_or_else(|| eyre!("missing or non-string field '{field}'"))
}

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

    use super::*;

    fn round_trip(event: &ConversationEvent) -> ConversationEvent {
        let (event_type, event_data) = serialize_event(event);
        deserialize_event(&event_type, &event_data).unwrap()
    }

    #[test]
    fn test_user_message() {
        let event = ConversationEvent::UserMessage {
            content: "hello world".to_string(),
        };
        let result = round_trip(&event);
        assert!(
            matches!(result, ConversationEvent::UserMessage { content } if content == "hello world")
        );
    }

    #[rstest]
    #[case::plain("response text")]
    #[case::special_chars("line1\nline2\ttab \"quotes\" \\backslash 🎉")]
    fn text_round_trips(#[case] content: &str) {
        let event = ConversationEvent::Text {
            content: content.to_string(),
        };
        assert!(
            matches!(round_trip(&event), ConversationEvent::Text { content: c } if c == content)
        );
    }

    #[test]
    fn test_tool_call() {
        let input = serde_json::json!({"command": "ls -la", "danger": "low"});
        let event = ConversationEvent::ToolCall {
            id: "tc_123".to_string(),
            name: "suggest_command".to_string(),
            input: input.clone(),
        };
        let result = round_trip(&event);
        match result {
            ConversationEvent::ToolCall {
                id,
                name,
                input: result_input,
            } => {
                assert_eq!(id, "tc_123");
                assert_eq!(name, "suggest_command");
                assert_eq!(result_input, input);
            }
            _ => panic!("expected ToolCall"),
        }
    }

    #[test]
    fn test_tool_result() {
        let event = ConversationEvent::ToolResult {
            tool_use_id: "tc_123".to_string(),
            content: "file contents here".to_string(),
            is_error: false,
            remote: false,
            content_length: None,
        };
        let result = round_trip(&event);
        match result {
            ConversationEvent::ToolResult {
                tool_use_id,
                content,
                is_error,
                remote,
                content_length,
            } => {
                assert_eq!(tool_use_id, "tc_123");
                assert_eq!(content, "file contents here");
                assert!(!is_error);
                assert!(!remote);
                assert!(content_length.is_none());
            }
            _ => panic!("expected ToolResult"),
        }
    }

    #[test]
    fn test_tool_result_error() {
        let event = ConversationEvent::ToolResult {
            tool_use_id: "tc_456".to_string(),
            content: "permission denied".to_string(),
            is_error: true,
            remote: false,
            content_length: None,
        };
        let result = round_trip(&event);
        match result {
            ConversationEvent::ToolResult { is_error, .. } => assert!(is_error),
            _ => panic!("expected ToolResult"),
        }
    }

    #[test]
    fn test_tool_result_remote() {
        let event = ConversationEvent::ToolResult {
            tool_use_id: "tc_789".to_string(),
            content: "ref:abc123".to_string(),
            is_error: false,
            remote: true,
            content_length: Some(4096),
        };
        let result = round_trip(&event);
        match result {
            ConversationEvent::ToolResult {
                remote,
                content_length,
                ..
            } => {
                assert!(remote);
                assert_eq!(content_length, Some(4096));
            }
            _ => panic!("expected ToolResult"),
        }
    }

    #[test]
    fn test_tool_result_backwards_compat() {
        // Old stored data without remote/content_length fields should deserialize
        // with defaults (remote=false, content_length=None)
        let event = deserialize_event(
            "tool_result",
            r#"{"tool_use_id":"tc_old","content":"old result","is_error":false}"#,
        )
        .unwrap();
        match event {
            ConversationEvent::ToolResult {
                remote,
                content_length,
                ..
            } => {
                assert!(!remote);
                assert!(content_length.is_none());
            }
            _ => panic!("expected ToolResult"),
        }
    }

    #[test]
    fn test_out_of_band_with_command() {
        let event = ConversationEvent::OutOfBandOutput {
            name: "System".to_string(),
            command: Some("/help".to_string()),
            content: "help text".to_string(),
        };
        let result = round_trip(&event);
        match result {
            ConversationEvent::OutOfBandOutput {
                name,
                command,
                content,
            } => {
                assert_eq!(name, "System");
                assert_eq!(command.as_deref(), Some("/help"));
                assert_eq!(content, "help text");
            }
            _ => panic!("expected OutOfBandOutput"),
        }
    }

    #[test]
    fn test_out_of_band_without_command() {
        let event = ConversationEvent::OutOfBandOutput {
            name: "System".to_string(),
            command: None,
            content: "some output".to_string(),
        };
        let result = round_trip(&event);
        match result {
            ConversationEvent::OutOfBandOutput { command, .. } => {
                assert!(command.is_none());
            }
            _ => panic!("expected OutOfBandOutput"),
        }
    }

    #[rstest]
    #[case::unknown_type("banana", "{}", Some("unknown event type"))]
    #[case::invalid_json("text", "not json", None)]
    #[case::missing_field("text", "{}", Some("content"))]
    fn deserialize_errors(
        #[case] event_type: &str,
        #[case] event_data: &str,
        #[case] expected_substr: Option<&str>,
    ) {
        let result = deserialize_event(event_type, event_data);
        assert!(result.is_err());
        if let Some(s) = expected_substr {
            assert!(result.unwrap_err().to_string().contains(s));
        }
    }

    #[test]
    fn test_tool_call_with_nested_input() {
        let input = serde_json::json!({
            "command": "echo 'hello'",
            "nested": { "a": [1, 2, 3], "b": null }
        });
        let event = ConversationEvent::ToolCall {
            id: "tc_1".to_string(),
            name: "execute_shell_command".to_string(),
            input: input.clone(),
        };
        let result = round_trip(&event);
        match result {
            ConversationEvent::ToolCall {
                input: result_input,
                ..
            } => {
                assert_eq!(result_input, input);
            }
            _ => panic!("expected ToolCall"),
        }
    }

    #[test]
    fn test_system_context() {
        let event = ConversationEvent::SystemContext {
            content: "[system: new invocation started]".to_string(),
        };
        let result = round_trip(&event);
        assert!(
            matches!(result, ConversationEvent::SystemContext { content } if content == "[system: new invocation started]")
        );
    }
}
