//! Streaming update handling for ACP.
//!
//! This module implements session/update notification handling, including:
//! - Session update dispatcher
//! - Message and thought chunk accumulation
//! - Tool call tracking and updates
//! - Plan and command updates
//! - User message chunks (for session replay)

use crate::acp::protocol::prompt::ContentBlock;
use serde::{Deserialize, Serialize};

/// Notification of a session update from the agent.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionUpdateNotification {
    /// Session ID this update belongs to.
    pub session_id: String,
    /// The update itself.
    #[serde(flatten)]
    pub update: SessionUpdate,
}

/// Session update type.
///
/// These are sent from the agent to the client during a prompt turn
/// to provide real-time updates on progress.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "sessionUpdate", rename_all = "snake_case")]
pub enum SessionUpdate {
    /// Agent message chunk (assistant output).
    AgentMessageChunk {
        content: ContentBlock,
    },
    /// Agent thought/reasoning chunk.
    AgentThoughtChunk {
        content: ContentBlock,
    },
    /// User message chunk (for session replay).
    UserMessageChunk {
        content: ContentBlock,
    },
    /// New tool call initiated.
    ToolCall {
        tool_call_id: String,
        title: String,
        kind: ToolKind,
        status: ToolCallStatus,
        #[serde(skip_serializing_if = "Option::is_none")]
        location: Option<ToolCallLocation>,
        #[serde(skip_serializing_if = "Option::is_none")]
        content: Option<Vec<ToolCallContent>>,
    },
    /// Update to existing tool call.
    ToolCallUpdate {
        tool_call_id: String,
        status: ToolCallStatus,
        #[serde(skip_serializing_if = "Option::is_none")]
        content: Option<Vec<ToolCallContent>>,
    },
    /// Agent execution plan.
    Plan {
        entries: Vec<PlanEntry>,
    },
    /// Available commands update.
    AvailableCommandsUpdate {
        commands: Vec<Command>,
    },
    /// Current mode update (agent-initiated mode change).
    ///
    /// Sent when the agent changes its own mode (e.g., exiting plan mode via a tool).
    /// Per ACP spec, the field is `modeId` (camelCase).
    CurrentModeUpdate {
        #[serde(rename = "modeId")]
        mode_id: String,
    },
    /// Current model update (agent-initiated model change).
    ///
    /// UNSTABLE in ACP spec - may be sent when agent changes model.
    /// Included for forward compatibility.
    CurrentModelUpdate {
        #[serde(rename = "modelId")]
        model_id: String,
    },
}

/// Kind of tool being invoked.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ToolKind {
    /// File read operations.
    Read,
    /// File write/modification operations.
    Edit,
    /// Search operations (glob, grep, ls).
    Search,
    /// Terminal/command execution.
    Execute,
    /// Agent internal reasoning.
    Think,
    /// Other/unknown tool type.
    Other,
}

/// Status of a tool call.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ToolCallStatus {
    /// Waiting to start.
    Pending,
    /// Currently executing.
    InProgress,
    /// Completed successfully.
    Completed,
    /// Failed with error.
    Failed,
    /// Cancelled by user.
    Cancelled,
}

/// Location information for a tool call.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ToolCallLocation {
    /// File path.
    pub path: String,
    /// Line number (optional).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub line: Option<u32>,
}

/// Content of a tool call.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolCallContent {
    /// Regular content block.
    Content {
        content: ContentBlock,
    },
    /// File diff.
    Diff {
        path: String,
        diff: String,
    },
    /// Terminal output reference.
    Terminal {
        terminal_id: String,
    },
}

/// Plan entry.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PlanEntry {
    /// Plan step content.
    pub content: String,
    /// Priority level.
    pub priority: String,
    /// Status of this step.
    pub status: String,
}

/// Command available to the agent.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Command {
    /// Command name.
    pub name: String,
    /// Optional description.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
}

/// State for accumulating message chunks.
#[derive(Debug, Clone, Default)]
pub struct MessageAccumulator {
    /// Accumulated content blocks.
    pub blocks: Vec<ContentBlock>,
    /// Whether this is a thought/reasoning block.
    pub is_thought: bool,
}

impl MessageAccumulator {
    /// Create a new message accumulator.
    pub fn new(is_thought: bool) -> Self {
        Self {
            blocks: Vec::new(),
            is_thought,
        }
    }

    /// Add a content block to the accumulator.
    pub fn add_block(&mut self, block: ContentBlock) {
        self.blocks.push(block);
    }

    /// Get accumulated text (concatenates all text blocks).
    pub fn get_text(&self) -> String {
        self.blocks
            .iter()
            .filter_map(|block| {
                if let ContentBlock::Text { text, .. } = block {
                    Some(text.as_str())
                } else {
                    None
                }
            })
            .collect::<Vec<_>>()
            .join("")
    }

    /// Clear the accumulator.
    pub fn clear(&mut self) {
        self.blocks.clear();
    }
}

/// Tool call tracking state.
#[derive(Debug, Clone, PartialEq)]
pub struct ToolCallInfo {
    /// Unique ID for this tool call.
    pub tool_call_id: String,
    /// Human-readable title.
    pub title: String,
    /// Kind of tool.
    pub kind: ToolKind,
    /// Current status.
    pub status: ToolCallStatus,
    /// Location (file/line) if applicable.
    pub location: Option<ToolCallLocation>,
    /// Content accumulated so far.
    pub content: Vec<ToolCallContent>,
}

impl ToolCallInfo {
    /// Create a new tool call tracking object.
    pub fn new(
        tool_call_id: String,
        title: String,
        kind: ToolKind,
        status: ToolCallStatus,
        location: Option<ToolCallLocation>,
        content: Option<Vec<ToolCallContent>>,
    ) -> Self {
        Self {
            tool_call_id,
            title,
            kind,
            status,
            location,
            content: content.unwrap_or_default(),
        }
    }

    /// Update the tool call with new status and optionally new content.
    pub fn update(&mut self, status: ToolCallStatus, new_content: Option<Vec<ToolCallContent>>) {
        self.status = status;
        if let Some(content) = new_content {
            self.content.extend(content);
        }
    }

    /// Check if the tool call is in a terminal state.
    pub fn is_terminal(&self) -> bool {
        matches!(
            self.status,
            ToolCallStatus::Completed | ToolCallStatus::Failed | ToolCallStatus::Cancelled
        )
    }
}

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

    #[test]
    fn test_session_update_agent_message_chunk() {
        let update = SessionUpdate::AgentMessageChunk {
            content: ContentBlock::Text {
                text: "Hello".to_string(),
                annotations: None,
            },
        };

        let json = serde_json::to_string(&update).unwrap();
        assert!(json.contains("\"sessionUpdate\":\"agent_message_chunk\""));
        assert!(json.contains("Hello"));
    }

    #[test]
    fn test_session_update_tool_call() {
        let update = SessionUpdate::ToolCall {
            tool_call_id: "tool-1".to_string(),
            title: "Read file".to_string(),
            kind: ToolKind::Read,
            status: ToolCallStatus::Pending,
            location: Some(ToolCallLocation {
                path: "/path/to/file.txt".to_string(),
                line: Some(42),
            }),
            content: None,
        };

        let json = serde_json::to_string(&update).unwrap();
        assert!(json.contains("\"sessionUpdate\":\"tool_call\""));
        assert!(json.contains("tool-1"));
        assert!(json.contains("Read file"));
    }

    #[test]
    fn test_tool_kind_serialization() {
        let kinds = vec![
            ToolKind::Read,
            ToolKind::Edit,
            ToolKind::Search,
            ToolKind::Execute,
            ToolKind::Think,
            ToolKind::Other,
        ];

        for kind in kinds {
            let json = serde_json::to_string(&kind).unwrap();
            let deserialized: ToolKind = serde_json::from_str(&json).unwrap();
            assert_eq!(kind, deserialized);
        }
    }

    #[test]
    fn test_tool_call_status_serialization() {
        let statuses = vec![
            ToolCallStatus::Pending,
            ToolCallStatus::InProgress,
            ToolCallStatus::Completed,
            ToolCallStatus::Failed,
            ToolCallStatus::Cancelled,
        ];

        for status in statuses {
            let json = serde_json::to_string(&status).unwrap();
            let deserialized: ToolCallStatus = serde_json::from_str(&json).unwrap();
            assert_eq!(status, deserialized);
        }
    }

    #[test]
    fn test_tool_call_content_diff() {
        let content = ToolCallContent::Diff {
            path: "/path/to/file.txt".to_string(),
            diff: "@@ -1,1 +1,1 @@\n-old\n+new".to_string(),
        };

        let json = serde_json::to_string(&content).unwrap();
        assert!(json.contains("\"type\":\"diff\""));
        assert!(json.contains("/path/to/file.txt"));
    }

    #[test]
    fn test_message_accumulator() {
        let mut acc = MessageAccumulator::new(false);

        acc.add_block(ContentBlock::Text {
            text: "Hello ".to_string(),
            annotations: None,
        });
        acc.add_block(ContentBlock::Text {
            text: "world".to_string(),
            annotations: None,
        });

        assert_eq!(acc.get_text(), "Hello world");
        assert!(!acc.is_thought);
    }

    #[test]
    fn test_tool_call_info_update() {
        let mut info = ToolCallInfo::new(
            "tool-1".to_string(),
            "Test".to_string(),
            ToolKind::Read,
            ToolCallStatus::Pending,
            None,
            None,
        );

        assert!(!info.is_terminal());

        info.update(ToolCallStatus::InProgress, None);
        assert_eq!(info.status, ToolCallStatus::InProgress);
        assert!(!info.is_terminal());

        info.update(ToolCallStatus::Completed, None);
        assert_eq!(info.status, ToolCallStatus::Completed);
        assert!(info.is_terminal());
    }

    #[test]
    fn test_plan_entry() {
        let entry = PlanEntry {
            content: "Step 1: Do something".to_string(),
            priority: "high".to_string(),
            status: "pending".to_string(),
        };

        let json = serde_json::to_string(&entry).unwrap();
        let deserialized: PlanEntry = serde_json::from_str(&json).unwrap();
        assert_eq!(entry, deserialized);
    }

    #[test]
    fn test_command() {
        let command = Command {
            name: "/help".to_string(),
            description: Some("Show help".to_string()),
        };

        let json = serde_json::to_string(&command).unwrap();
        let deserialized: Command = serde_json::from_str(&json).unwrap();
        assert_eq!(command, deserialized);
    }

    #[test]
    fn test_session_update_notification_current_mode() {
        let notification = SessionUpdateNotification {
            session_id: "session-123".to_string(),
            update: SessionUpdate::CurrentModeUpdate {
                mode_id: "code".to_string(),
            },
        };

        let json = serde_json::to_string(&notification).unwrap();
        assert!(json.contains("session-123"));
        assert!(json.contains("\"sessionUpdate\":\"current_mode_update\""));
        // Verify camelCase field name per ACP spec
        assert!(json.contains("\"modeId\":\"code\""));
    }

    #[test]
    fn test_session_update_notification_current_model() {
        let notification = SessionUpdateNotification {
            session_id: "session-123".to_string(),
            update: SessionUpdate::CurrentModelUpdate {
                model_id: "sonnet".to_string(),
            },
        };

        let json = serde_json::to_string(&notification).unwrap();
        assert!(json.contains("session-123"));
        assert!(json.contains("\"sessionUpdate\":\"current_model_update\""));
        // Verify camelCase field name per ACP spec
        assert!(json.contains("\"modelId\":\"sonnet\""));
    }

    #[test]
    fn test_current_mode_update_deserialization() {
        // Test deserialization from ACP spec format
        let json = r#"{
            "sessionUpdate": "current_mode_update",
            "modeId": "plan"
        }"#;

        let update: SessionUpdate = serde_json::from_str(json).unwrap();
        match update {
            SessionUpdate::CurrentModeUpdate { mode_id } => {
                assert_eq!(mode_id, "plan");
            }
            _ => panic!("Expected CurrentModeUpdate"),
        }
    }

    #[test]
    fn test_current_model_update_deserialization() {
        // Test deserialization from ACP spec format (forward compatibility)
        let json = r#"{
            "sessionUpdate": "current_model_update",
            "modelId": "haiku"
        }"#;

        let update: SessionUpdate = serde_json::from_str(json).unwrap();
        match update {
            SessionUpdate::CurrentModelUpdate { model_id } => {
                assert_eq!(model_id, "haiku");
            }
            _ => panic!("Expected CurrentModelUpdate"),
        }
    }
}
