use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Represents a message from Claude Code JSONL session
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct Message {
    pub session_id: String,
    pub role: String,
    pub content: String,
    #[serde(default)]
    pub text_content: String,
    pub timestamp: DateTime<Utc>,
    pub branch: Option<String>,
    /// Transcript path the line was parsed from. This stays on the message even
    /// when search results are grouped under a resolved parent session path.
    pub file_path: Option<String>,
    pub line_number: usize,
    pub uuid: Option<String>,
    pub parent_uuid: Option<String>,
}

impl Message {
    /// Parse a JSONL line into a Message
    /// Supports both Claude Code CLI format (sessionId, timestamp) and
    /// Claude Desktop format (session_id, _audit_timestamp)
    pub fn from_jsonl(line: &str, line_number: usize) -> Option<Self> {
        Self::from_jsonl_with_path(line, line_number, None)
    }

    /// Parse a JSONL line with optional transcript path context.
    ///
    /// Codex `response_item` records do not repeat the thread id on every line,
    /// so callers that have the file path can recover it from the rollout
    /// filename.
    pub(crate) fn from_jsonl_with_path(
        line: &str,
        line_number: usize,
        file_path: Option<&str>,
    ) -> Option<Self> {
        use crate::session;
        use crate::session::record::{ContentMode, MessageRole, SessionRecord};

        let json: serde_json::Value = serde_json::from_str(line).ok()?;

        // Use SessionRecord for type classification and content parsing
        let record = SessionRecord::from_value(&json)?;
        let (role, content_blocks) = match record {
            SessionRecord::Message {
                role,
                content_blocks,
                ..
            } => {
                let role_str = match role {
                    MessageRole::User => "user",
                    MessageRole::Assistant => "assistant",
                };
                (role_str.to_string(), content_blocks)
            }
            _ => return None,
        };

        let content = SessionRecord::render_content(&content_blocks, &ContentMode::Full);
        let text_content = SessionRecord::render_content(&content_blocks, &ContentMode::TextOnly);

        // Skip empty content
        if content.trim().is_empty() {
            return None;
        }

        let session_id = session::extract_session_id(&json)
            .or_else(|| file_path.and_then(session::extract_codex_session_id_from_path))?;
        let timestamp = session::extract_timestamp(&json)?;

        // Branch is CLI-only, Desktop doesn't have it
        let branch = session::extract_branch(&json);

        let uuid = session::extract_uuid(&json);
        let parent_uuid = session::extract_parent_uuid(&json);

        Some(Message {
            session_id,
            role,
            content,
            text_content,
            timestamp,
            branch,
            file_path: file_path.map(str::to_string),
            line_number,
            uuid,
            parent_uuid,
        })
    }

    /// Extract text content from message content blocks
    /// Handles both array format [{"type":"text","text":"..."}] and plain string format
    pub fn extract_content(raw: &serde_json::Value) -> String {
        use crate::session::record::{parse_content_blocks, ContentMode, SessionRecord};
        let blocks = parse_content_blocks(raw);
        SessionRecord::render_content(&blocks, &ContentMode::Full)
    }
}

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

    #[test]
    fn test_parse_user_message() {
        let jsonl = r#"{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Hello Claude"}]},"sessionId":"abc123","timestamp":"2025-01-09T10:00:00Z"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("Should parse user message");

        assert_eq!(msg.session_id, "abc123");
        assert_eq!(msg.role, "user");
        assert_eq!(msg.content, "Hello Claude");
        assert_eq!(msg.text_content, "Hello Claude");
        assert_eq!(msg.line_number, 1);
    }

    #[test]
    fn test_parse_assistant_message() {
        let jsonl = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello! How can I help?"}]},"sessionId":"abc123","timestamp":"2025-01-09T10:01:00Z"}"#;

        let msg = Message::from_jsonl(jsonl, 2).expect("Should parse assistant message");

        assert_eq!(msg.role, "assistant");
        assert_eq!(msg.content, "Hello! How can I help?");
        assert_eq!(msg.text_content, "Hello! How can I help?");
    }

    #[test]
    fn test_parse_message_with_branch() {
        let jsonl = r#"{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Fix bug"}]},"sessionId":"abc123","timestamp":"2025-01-09T10:00:00Z","cwd":"/projects/myapp","branch":"feature/fix-bug"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("Should parse message with branch");

        assert_eq!(msg.branch, Some("feature/fix-bug".to_string()));
    }

    #[test]
    fn test_parse_message_multiple_text_blocks() {
        let jsonl = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Part 1"},{"type":"text","text":"Part 2"}]},"sessionId":"abc123","timestamp":"2025-01-09T10:00:00Z"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("Should parse multiple text blocks");

        assert_eq!(msg.content, "Part 1\nPart 2");
        assert_eq!(msg.text_content, "Part 1 Part 2");
    }

    #[test]
    fn test_parse_tool_use_message() {
        let jsonl = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"Read","input":{"file_path":"/tmp/test.txt"}}]},"sessionId":"abc123","timestamp":"2025-01-09T10:00:00Z"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("Should parse tool_use message");

        assert!(msg.content.contains("file_path"));
        assert!(msg.content.contains("/tmp/test.txt"));
        assert!(
            msg.text_content.is_empty(),
            "TextOnly mode should skip tool_use blocks"
        );
    }

    #[test]
    fn test_skip_summary_type() {
        let jsonl = r#"{"type":"summary","summary":"Session summary text","sessionId":"abc123"}"#;

        let msg = Message::from_jsonl(jsonl, 1);

        assert!(msg.is_none(), "Should skip summary type messages");
    }

    #[test]
    fn test_skip_invalid_json() {
        let jsonl = "not valid json {{{";

        let msg = Message::from_jsonl(jsonl, 1);

        assert!(msg.is_none(), "Should skip invalid JSON");
    }

    #[test]
    fn test_extract_content_from_text_block() {
        let raw: serde_json::Value = serde_json::json!([
            {"type": "text", "text": "Hello world"}
        ]);

        let content = Message::extract_content(&raw);

        assert_eq!(content, "Hello world");
    }

    #[test]
    fn test_extract_content_from_tool_result() {
        let raw: serde_json::Value = serde_json::json!([
            {"type": "tool_result", "content": "File contents here"}
        ]);

        let content = Message::extract_content(&raw);

        assert!(content.contains("File contents here"));
    }

    #[test]
    fn test_extract_content_thinking_block() {
        let raw: serde_json::Value = serde_json::json!([
            {"type": "thinking", "thinking": "The user wants to refactor the auth module"},
            {"type": "text", "text": "I'll help you refactor that."}
        ]);

        let content = Message::extract_content(&raw);

        assert!(content.contains("The user wants to refactor the auth module"));
        assert!(content.contains("I'll help you refactor that."));
    }

    #[test]
    fn test_parse_desktop_format_message() {
        // Desktop format uses session_id (underscore) and _audit_timestamp
        let jsonl = r#"{"type":"assistant","message":{"model":"claude-opus-4-5-20251101","id":"msg_01Q4WpB2jNfsHuijFwLGLFwN","type":"message","role":"assistant","content":[{"type":"text","text":"I'd love to help you analyze how you're spending your time!"}],"stop_reason":null,"stop_sequence":null},"parent_tool_use_id":null,"session_id":"0c2f5015-9457-491f-8f35-218d6c34ff68","uuid":"aa419e76-930a-4970-85c3-b5f03e85f6e0","_audit_timestamp":"2026-01-13T13:31:31.268Z"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("Should parse Desktop format message");

        assert_eq!(msg.session_id, "0c2f5015-9457-491f-8f35-218d6c34ff68");
        assert_eq!(msg.role, "assistant");
        assert!(msg.content.contains("I'd love to help you analyze"));
        assert!(msg.branch.is_none()); // Desktop doesn't have branch
        assert_eq!(
            msg.uuid,
            Some("aa419e76-930a-4970-85c3-b5f03e85f6e0".to_string())
        );
    }

    #[test]
    fn test_parse_desktop_user_message() {
        let jsonl = r#"{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Hello from Desktop"}]},"session_id":"desktop-session-123","_audit_timestamp":"2026-01-13T10:00:00.000Z"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("Should parse Desktop user message");

        assert_eq!(msg.session_id, "desktop-session-123");
        assert_eq!(msg.role, "user");
        assert_eq!(msg.content, "Hello from Desktop");
    }

    #[test]
    fn test_parse_codex_response_item_with_path_session_id() {
        let jsonl = r#"{"timestamp":"2026-05-01T10:00:01.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"Hello from Codex"}]}}"#;
        let path = "/Users/user/.codex/sessions/2026/05/01/rollout-2026-05-01T10-00-00-019f0000-0000-7000-8000-000000000001.jsonl";

        let msg = Message::from_jsonl_with_path(jsonl, 1, Some(path))
            .expect("Should parse Codex response item");

        assert_eq!(msg.session_id, "019f0000-0000-7000-8000-000000000001");
        assert_eq!(msg.role, "user");
        assert_eq!(msg.content, "Hello from Codex");
        assert_eq!(msg.text_content, "Hello from Codex");
    }

    #[test]
    fn test_parse_codex_function_output_with_path_session_id() {
        let jsonl = r#"{"timestamp":"2026-05-01T10:00:03.000Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_1","output":"tool output with searchable text"}}"#;
        let path = "/Users/user/.codex/sessions/2026/05/01/rollout-2026-05-01T10-00-00-019f0000-0000-7000-8000-000000000001.jsonl";

        let msg = Message::from_jsonl_with_path(jsonl, 1, Some(path))
            .expect("Should parse Codex tool output");

        assert_eq!(msg.session_id, "019f0000-0000-7000-8000-000000000001");
        assert_eq!(msg.role, "assistant");
        assert!(msg.content.contains("searchable text"));
    }

    #[test]
    fn test_parse_string_content() {
        // Some user messages have content as a plain string instead of array
        let jsonl = r#"{"type":"user","message":{"role":"user","content":"Hello plain string"},"sessionId":"abc123","timestamp":"2025-01-09T10:00:00Z"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("Should parse plain string content");

        assert_eq!(msg.content, "Hello plain string");
        assert_eq!(msg.text_content, "Hello plain string");
        assert_eq!(msg.role, "user");
    }

    #[test]
    fn test_parse_command_message_as_slash_command() {
        let jsonl = r#"{"type":"user","message":{"role":"user","content":[{"type":"text","text":"<command-name>revdiff</command-name><command-message>revdiff:revdiff</command-message>"}]},"sessionId":"abc123","timestamp":"2025-01-09T10:00:00Z"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("Should parse command message");

        assert_eq!(msg.content, "/revdiff:revdiff");
        assert_eq!(msg.text_content, "/revdiff:revdiff");
    }

    #[test]
    fn test_parse_command_args_as_slash_command_args() {
        let jsonl = r#"{"type":"user","message":{"role":"user","content":[{"type":"text","text":"<command-name>foo</command-name><command-args>bar baz</command-args>"}]},"sessionId":"abc123","timestamp":"2025-01-09T10:00:00Z"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("Should parse command args");

        assert_eq!(msg.content, "/foo bar baz");
        assert_eq!(msg.text_content, "/foo bar baz");
    }

    #[test]
    fn test_parse_uuid_and_parent_uuid() {
        let jsonl = r#"{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Hello"}]},"sessionId":"abc123","timestamp":"2025-01-09T10:00:00Z","uuid":"uuid-111","parentUuid":"uuid-000"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("Should parse message with uuid");

        assert_eq!(msg.uuid, Some("uuid-111".to_string()));
        assert_eq!(msg.parent_uuid, Some("uuid-000".to_string()));
    }

    #[test]
    fn test_parse_message_without_uuid() {
        let jsonl = r#"{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Hello"}]},"sessionId":"abc123","timestamp":"2025-01-09T10:00:00Z"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("Should parse message without uuid");

        assert_eq!(msg.uuid, None);
        assert_eq!(msg.parent_uuid, None);
    }

    #[test]
    fn test_extract_content_string() {
        let raw: serde_json::Value = serde_json::json!("plain text content");

        let content = Message::extract_content(&raw);

        assert_eq!(content, "plain text content");
    }

    #[test]
    fn test_parse_server_tool_use_message_not_dropped() {
        let jsonl = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"server_tool_use","name":"web_search","id":"tu_123"}]},"sessionId":"abc123","timestamp":"2025-01-09T10:00:00Z"}"#;

        let msg = Message::from_jsonl(jsonl, 1).expect("server_tool_use should not be dropped");

        assert_eq!(msg.content, "web_search");
    }

    #[test]
    fn test_parse_tool_result_only_message_not_dropped() {
        let jsonl = r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"file contents here","tool_use_id":"tu_123"}]},"sessionId":"abc123","timestamp":"2025-01-09T10:00:00Z"}"#;

        let msg =
            Message::from_jsonl(jsonl, 1).expect("tool_result-only message should not be dropped");

        assert!(msg.content.contains("file contents here"));
        assert!(
            msg.text_content.is_empty(),
            "TextOnly mode should skip tool_result blocks"
        );
    }
}
