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

use std::sync::Arc;

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessageAttachment {
    /// File reference with optional truncated tool output (for @ references)
    FileReference {
        path: String,
        /// Original full content (for display purposes)
        content: Arc<String>,
        /// Tool output format with truncation applied (like read tool results)
        tool_output: Option<Arc<String>>,
        /// Whether the content was truncated
        truncated: bool,
    },
    DirectoryReference {
        path: String,
        tree: Arc<String>,
    },
    Image {
        filename: String,
        mime: String,
        data_url: String,
    },
}

impl MessageAttachment {
    pub fn is_image(&self) -> bool {
        matches!(self, Self::Image { .. })
    }

    pub fn summary(&self) -> String {
        match self {
            Self::FileReference {
                path,
                content,
                truncated,
                ..
            } => {
                let preview = content.lines().take(6).collect::<Vec<_>>().join(" ");
                let truncated_hint = if *truncated { " (truncated)" } else { "" };
                if preview.trim().is_empty() {
                    format!("[file:{}{}]", path, truncated_hint)
                } else {
                    format!(
                        "[file:{}{}] {}",
                        path,
                        truncated_hint,
                        truncate_preview(&preview, 120)
                    )
                }
            }
            Self::DirectoryReference { path, tree } => {
                let preview = tree.lines().take(8).collect::<Vec<_>>().join(" ");
                if preview.trim().is_empty() {
                    format!("[dir:{}]", path)
                } else {
                    format!("[dir:{}] {}", path, truncate_preview(&preview, 120))
                }
            }
            Self::Image { filename, mime, .. } => format!("[image:{} {}]", filename, mime),
        }
    }

    /// Returns the prompt text in tool call result format (like opencode).
    /// For FileReference with tool_output, returns read tool result format.
    /// For FileReference without tool_output, returns original format.
    pub fn prompt_text(&self) -> Option<String> {
        match self {
            Self::FileReference {
                path,
                content,
                tool_output,
                truncated,
            } => {
                // If we have tool output (from @ reference), return in tool result format
                if let Some(output) = tool_output {
                    let truncated_hint = if *truncated {
                        "\n\n(The tool call succeeded but the output was truncated.)"
                    } else {
                        ""
                    };
                    return Some(format!(
                        "\n\n{tool_name} Tool: read\n{{\"path\":\"{args}\"}}\n\nOutput:\n{output}{truncated_hint}",
                        tool_name = "read",
                        args = path,
                        output = output.as_ref(),
                        truncated_hint = truncated_hint
                    ));
                }
                // Fall back to original format
                Some(format!(
                    "\n\nReferenced file: {}\n```text\n{}\n```",
                    path, content
                ))
            }
            Self::DirectoryReference { path, tree } => Some(format!(
                "\n\nReferenced directory: {}\n```text\n{}\n```",
                path, tree
            )),
            Self::Image { .. } => None,
        }
    }
}

pub fn tool_output_preview(tool_name: Option<&str>, output: &str) -> String {
    let output_char_count = output.chars().count();
    if output_char_count <= 8_000 {
        return output.to_string();
    }

    let tool_name = tool_name.unwrap_or("tool");
    let head = truncate_preview(output, 3_000);
    let tail = tail_preview(output, 1_000);

    format!(
        "[{tool_name} output truncated: {output_char_count} chars]\n\nFirst excerpt:\n{head}\n\nLast excerpt:\n{tail}"
    )
}

#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ToolMetadata {
    #[serde(default)]
    pub filepath: Option<String>,
    #[serde(default)]
    pub diff: Option<String>,
    #[serde(default)]
    pub truncated: Option<bool>,
    #[serde(default)]
    pub exists: Option<bool>,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ToolExecutionResult {
    pub output: String,
    #[serde(default)]
    pub attachments: Vec<MessageAttachment>,
    #[serde(default)]
    pub metadata: ToolMetadata,
    #[serde(default)]
    pub instruction_sources: Vec<String>,
    #[serde(default)]
    pub rtk_rewritten: bool,
}

impl ToolExecutionResult {
    pub fn new(output: impl Into<String>) -> Self {
        Self {
            output: output.into(),
            attachments: Vec::new(),
            metadata: ToolMetadata::default(),
            instruction_sources: Vec::new(),
            rtk_rewritten: false,
        }
    }

    pub fn with_rtk_rewritten(mut self, rewritten: bool) -> Self {
        self.rtk_rewritten = rewritten;
        self
    }

    pub fn preview_for_storage(&self, tool_name: Option<&str>) -> Self {
        let output = tool_output_preview(tool_name, &self.output);
        if output == self.output {
            return self.clone();
        }

        Self {
            output,
            attachments: self.attachments.clone(),
            metadata: self.metadata.clone(),
            instruction_sources: self.instruction_sources.clone(),
            rtk_rewritten: self.rtk_rewritten,
        }
    }
}

fn truncate_preview(value: &str, max_chars: usize) -> String {
    let count = value.chars().count();
    if count <= max_chars {
        return value.to_string();
    }

    let mut shortened = value.chars().take(max_chars).collect::<String>();
    shortened.push_str("...");
    shortened
}

fn tail_preview(value: &str, max_chars: usize) -> String {
    let count = value.chars().count();
    if count <= max_chars {
        return value.to_string();
    }

    let mut shortened = value.chars().rev().take(max_chars).collect::<String>();
    shortened = shortened.chars().rev().collect();
    let mut preview = String::from("...");
    preview.push_str(&shortened);
    preview
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MessageRole {
    System,
    User,
    Assistant,
    Tool,
    Error,
}

impl MessageRole {
    pub fn label(&self) -> &'static str {
        match self {
            Self::System => "system",
            Self::User => "user",
            Self::Assistant => "assistant",
            Self::Tool => "tool",
            Self::Error => "error",
        }
    }

    pub fn db_value(&self) -> &'static str {
        self.label()
    }

    pub fn from_db_value(value: &str) -> Self {
        match value {
            "system" => Self::System,
            "user" => Self::User,
            "assistant" => Self::Assistant,
            "tool" => Self::Tool,
            "error" => Self::Error,
            _ => Self::System,
        }
    }
}

pub const COMPACTION_MESSAGE_LABEL: &str = "Session compacted";

#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct ToolCall {
    pub id: String,
    pub name: String,
    pub arguments: String,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct AssistantTurn {
    #[serde(default)]
    pub content: String,
    #[serde(default)]
    pub reasoning: String,
    #[serde(default)]
    pub tool_calls: Vec<ToolCall>,
    #[serde(default)]
    pub finish_reason: Option<String>,
}

impl AssistantTurn {
    pub fn upsert_tool_call(&mut self, tool_call: ToolCall) {
        if let Some(existing) = self
            .tool_calls
            .iter_mut()
            .find(|existing| existing.id == tool_call.id)
        {
            *existing = tool_call;
        } else {
            self.tool_calls.push(tool_call);
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message {
    pub id: Uuid,
    pub role: MessageRole,
    pub content: String,
    #[serde(default)]
    pub attachments: Vec<MessageAttachment>,
    #[serde(default)]
    pub reasoning: String,
    #[serde(default)]
    pub tool_calls: Vec<ToolCall>,
    #[serde(default)]
    pub tool_call_id: Option<String>,
    #[serde(default)]
    pub tool_name: Option<String>,
    #[serde(default)]
    pub metadata: ToolMetadata,
    pub created_at: DateTime<Utc>,
    pub completed_at: Option<DateTime<Utc>>,
    pub streaming: bool,
    #[serde(default)]
    pub input_tokens: Option<u32>,
    #[serde(default)]
    pub output_tokens: Option<u32>,
    #[serde(default)]
    pub total_tokens: Option<u32>,
    #[serde(default)]
    pub cache_read_tokens: Option<u32>,
    #[serde(default)]
    pub cache_write_tokens: Option<u32>,
    #[serde(default)]
    pub model_id: Option<String>,
    #[serde(default)]
    pub tokens_per_second: Option<f32>,
    #[serde(default)]
    pub snapshot_hash: Option<String>,
    #[serde(default)]
    pub patch_files: Option<String>,
    #[serde(default)]
    pub file_diffs: Option<String>,
    #[serde(default)]
    pub mode: Option<crate::prompts::SessionMode>,
    #[serde(default)]
    pub rtk_rewritten: bool,
    #[serde(default)]
    pub thinking_level: Option<crate::config::reasoning::ThinkingLevelType>,
}

impl Message {
    pub fn new(role: MessageRole, content: impl Into<String>) -> Self {
        Self {
            id: Uuid::new_v4(),
            role,
            content: content.into(),
            attachments: Vec::new(),
            reasoning: String::new(),
            tool_calls: Vec::new(),
            tool_call_id: None,
            tool_name: None,
            metadata: ToolMetadata::default(),
            created_at: Utc::now(),
            completed_at: None,
            streaming: false,
            input_tokens: None,
            output_tokens: None,
            total_tokens: None,
            cache_read_tokens: None,
            cache_write_tokens: None,
            model_id: None,
            tokens_per_second: None,
            snapshot_hash: None,
            patch_files: None,
            file_diffs: None,
            mode: None,
            rtk_rewritten: false,
            thinking_level: None,
        }
    }

    pub fn compaction(summary: impl Into<String>) -> Self {
        Self::new(
            MessageRole::System,
            format!("{COMPACTION_MESSAGE_LABEL}\n\n{}", summary.into()),
        )
    }

    pub fn streaming(role: MessageRole, content: impl Into<String>) -> Self {
        Self {
            id: Uuid::new_v4(),
            role,
            content: content.into(),
            attachments: Vec::new(),
            reasoning: String::new(),
            tool_calls: Vec::new(),
            tool_call_id: None,
            tool_name: None,
            metadata: ToolMetadata::default(),
            created_at: Utc::now(),
            completed_at: None,
            streaming: true,
            input_tokens: None,
            output_tokens: None,
            total_tokens: None,
            cache_read_tokens: None,
            cache_write_tokens: None,
            model_id: None,
            tokens_per_second: None,
            snapshot_hash: None,
            patch_files: None,
            file_diffs: None,
            mode: None,
            rtk_rewritten: false,
            thinking_level: None,
        }
    }

    pub fn persisted(
        id: Uuid,
        role: MessageRole,
        content: impl Into<String>,
        created_at: DateTime<Utc>,
        streaming: bool,
    ) -> Self {
        Self {
            id,
            role,
            content: content.into(),
            attachments: Vec::new(),
            reasoning: String::new(),
            tool_calls: Vec::new(),
            tool_call_id: None,
            tool_name: None,
            metadata: ToolMetadata::default(),
            created_at,
            completed_at: None,
            streaming,
            input_tokens: None,
            output_tokens: None,
            total_tokens: None,
            cache_read_tokens: None,
            cache_write_tokens: None,
            model_id: None,
            tokens_per_second: None,
            snapshot_hash: None,
            patch_files: None,
            file_diffs: None,
            mode: None,
            rtk_rewritten: false,
            thinking_level: None,
        }
    }

    pub fn tool_result(
        tool_call_id: impl Into<String>,
        tool_name: impl Into<String>,
        result: ToolExecutionResult,
    ) -> Self {
        Self {
            id: Uuid::new_v4(),
            role: MessageRole::Tool,
            content: result.output,
            attachments: result.attachments,
            reasoning: String::new(),
            tool_calls: Vec::new(),
            tool_call_id: Some(tool_call_id.into()),
            tool_name: Some(tool_name.into()),
            metadata: result.metadata,
            created_at: Utc::now(),
            completed_at: None,
            streaming: false,
            input_tokens: None,
            output_tokens: None,
            total_tokens: None,
            cache_read_tokens: None,
            cache_write_tokens: None,
            model_id: None,
            tokens_per_second: None,
            snapshot_hash: None,
            patch_files: None,
            file_diffs: None,
            mode: None,
            rtk_rewritten: result.rtk_rewritten,
            thinking_level: None,
        }
    }

    pub fn upsert_tool_call(&mut self, tool_call: ToolCall) {
        if let Some(existing) = self
            .tool_calls
            .iter_mut()
            .find(|existing| existing.id == tool_call.id)
        {
            *existing = tool_call;
        } else {
            self.tool_calls.push(tool_call);
        }
    }

    /// Get token usage from this message's token fields.
    pub fn token_usage(&self) -> crate::utils::TokenUsage {
        crate::utils::TokenUsage::new(
            self.input_tokens.unwrap_or(0),
            self.output_tokens.unwrap_or(0),
            self.cache_read_tokens.unwrap_or(0),
            self.cache_write_tokens.unwrap_or(0),
        )
    }
}

#[derive(Clone, Debug)]
pub struct Conversation {
    pub session_id: Uuid,
    pub parent_session_id: Option<Uuid>,
    pub workspace_root: String,
    pub provider_id: String,
    pub provider_display_name: String,
    pub model_id: String,
    pub model_display_name: String,
    pub title: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub context_summary: Option<String>,
    pub context_retained_from: usize,
    pub messages: Vec<Message>,
    pub revert_message_id: Option<Uuid>,
}

impl Conversation {
    pub fn new(
        session_id: Uuid,
        workspace_root: impl Into<String>,
        provider_id: impl Into<String>,
        provider_display_name: impl Into<String>,
        model_id: impl Into<String>,
        model_display_name: impl Into<String>,
        title: impl Into<String>,
    ) -> Self {
        let now = Utc::now();
        Self {
            session_id,
            parent_session_id: None,
            workspace_root: workspace_root.into(),
            provider_id: provider_id.into(),
            provider_display_name: provider_display_name.into(),
            model_id: model_id.into(),
            model_display_name: model_display_name.into(),
            title: title.into(),
            created_at: now,
            updated_at: now,
            context_summary: None,
            context_retained_from: 0,
            messages: Vec::new(),
            revert_message_id: None,
        }
    }

    pub fn set_context_state(&mut self, summary: Option<String>, retained_from: usize) {
        self.context_summary = summary;
        self.context_retained_from = retained_from;
    }

    pub fn clear_context_state(&mut self) {
        self.set_context_state(None, 0);
    }

    pub fn set_model(
        &mut self,
        provider_id: impl Into<String>,
        provider_display_name: impl Into<String>,
        model_id: impl Into<String>,
        model_display_name: impl Into<String>,
    ) {
        self.provider_id = provider_id.into();
        self.provider_display_name = provider_display_name.into();
        self.model_id = model_id.into();
        self.model_display_name = model_display_name.into();
    }

    pub fn model_label(&self) -> String {
        format!(
            "{} / {}",
            self.provider_display_name, self.model_display_name
        )
    }

    pub fn push(&mut self, message: Message) {
        self.updated_at = Utc::now();
        self.messages.push(message);
    }

    pub fn clear_messages(&mut self) {
        self.messages.clear();
        self.revert_message_id = None;
        self.clear_context_state();
        self.updated_at = Utc::now();
    }

    pub fn message_index(&self, message_id: Uuid) -> Option<usize> {
        self.messages
            .iter()
            .position(|message| message.id == message_id)
    }

    pub fn visible_message_count(&self) -> usize {
        self.revert_message_id
            .and_then(|message_id| self.message_index(message_id))
            .unwrap_or(self.messages.len())
    }

    pub fn visible_messages(&self) -> &[Message] {
        let visible_count = self.visible_message_count();
        &self.messages[..visible_count]
    }

    pub fn take_hidden_messages(&mut self) -> Vec<Message> {
        let visible_count = self.visible_message_count();
        if visible_count >= self.messages.len() {
            return Vec::new();
        }

        self.messages.split_off(visible_count)
    }

    pub fn is_reverted(&self) -> bool {
        self.revert_message_id.is_some()
    }

    pub fn last_visible_user_message(&self) -> Option<&Message> {
        self.visible_messages()
            .iter()
            .rev()
            .find(|message| matches!(message.role, MessageRole::User))
    }

    pub fn prev_user_message_before(&self, message_id: Uuid) -> Option<&Message> {
        let end_index = self.message_index(message_id)?;
        self.messages
            .iter()
            .take(end_index)
            .rev()
            .find(|message| matches!(message.role, MessageRole::User))
    }

    pub fn next_user_message_after(&self, message_id: Uuid) -> Option<&Message> {
        let start_index = self.message_index(message_id)?;
        self.messages
            .iter()
            .skip(start_index.saturating_add(1))
            .find(|message| matches!(message.role, MessageRole::User))
    }

    pub fn title_from_prompt(prompt: &str) -> String {
        let first_line = prompt.lines().next().unwrap_or("Untitled session").trim();

        if first_line.is_empty() {
            return "Untitled session".to_string();
        }

        let mut title = first_line.chars().take(48).collect::<String>();
        if first_line.chars().count() > 48 {
            title.push_str("...");
        }

        title
    }

    pub fn update_title_from_prompt(&mut self, prompt: &str) {
        self.title = Self::title_from_prompt(prompt);
    }
}

#[derive(Clone, Debug)]
pub enum BackendEvent {
    Delta {
        session_id: Uuid,
        request_id: u64,
        content: String,
    },
    ReasoningDelta {
        session_id: Uuid,
        request_id: u64,
        content: String,
    },
    ToolCallUpdated {
        session_id: Uuid,
        request_id: u64,
        tool_call: ToolCall,
    },
    Finished {
        session_id: Uuid,
        request_id: u64,
        turn: AssistantTurn,
    },
    Failed {
        session_id: Uuid,
        request_id: u64,
        error: String,
    },
    Retrying {
        session_id: Uuid,
        request_id: u64,
        attempt: u32,
        max_attempts: u32,
        reason: String,
        retry_after_secs: Option<u32>,
    },
    InstructionsLoaded {
        session_id: Uuid,
        sources: Vec<String>,
    },
    ToolCompleted {
        session_id: Uuid,
        request_id: u64,
        tool_call: ToolCall,
        result: ToolExecutionResult,
    },
    SubagentStatus {
        session_id: Uuid,
        request_id: u64,
        child_session_id: Uuid,
        status_text: String,
        current_tool_call: Option<ToolCall>,
        assistant_message: Option<Message>,
        content_delta: Option<String>,
        reasoning_delta: Option<String>,
    },
    SubagentToolResult {
        session_id: Uuid,
        request_id: u64,
        child_session_id: Uuid,
        message: Message,
    },
    SubagentCompleted {
        session_id: Uuid,
        request_id: u64,
        tool_call: ToolCall,
        child_session_id: Uuid,
        result: ToolExecutionResult,
    },
    UsageStats {
        session_id: Uuid,
        request_id: u64,
        input_tokens: u32,
        output_tokens: u32,
        total_tokens: u32,
        cache_read_tokens: u32,
        cache_write_tokens: u32,
        model_id: String,
        duration_ms: Option<u64>,
    },
    ContextCompacted {
        session_id: Uuid,
        compacted: bool,
        manual: bool,
        summary: Option<String>,
        retained_from: usize,
        error: Option<String>,
    },
    /// Async sidebar snapshot computation completed.
    /// Contains full FileDiff data (with patches) computed off the main thread.
    SidebarSnapshotReady {
        session_id: Uuid,
        request_id: u64,
        message_id: Uuid,
        file_diffs_json: String,
    },
}

impl BackendEvent {
    pub fn session_id(&self) -> Uuid {
        match self {
            Self::Delta { session_id, .. }
            | Self::ReasoningDelta { session_id, .. }
            | Self::ToolCallUpdated { session_id, .. }
            | Self::Finished { session_id, .. }
            | Self::Failed { session_id, .. }
            | Self::Retrying { session_id, .. }
            | Self::ToolCompleted { session_id, .. }
            | Self::SubagentStatus { session_id, .. }
            | Self::SubagentToolResult { session_id, .. }
            | Self::SubagentCompleted { session_id, .. }
            | Self::UsageStats { session_id, .. }
            | Self::InstructionsLoaded { session_id, .. }
            | Self::ContextCompacted { session_id, .. }
            | Self::SidebarSnapshotReady { session_id, .. } => *session_id,
        }
    }

    pub fn request_id(&self) -> Option<u64> {
        match self {
            Self::Delta { request_id, .. }
            | Self::ReasoningDelta { request_id, .. }
            | Self::ToolCallUpdated { request_id, .. }
            | Self::Finished { request_id, .. }
            | Self::Failed { request_id, .. }
            | Self::Retrying { request_id, .. }
            | Self::ToolCompleted { request_id, .. }
            | Self::SubagentStatus { request_id, .. }
            | Self::SubagentToolResult { request_id, .. }
            | Self::SubagentCompleted { request_id, .. }
            | Self::UsageStats { request_id, .. }
            | Self::SidebarSnapshotReady { request_id, .. } => Some(*request_id),
            Self::InstructionsLoaded { .. } | Self::ContextCompacted { .. } => None,
        }
    }
}

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

    #[test]
    fn visible_messages_stop_at_revert_marker() {
        let mut conversation = Conversation::new(
            Uuid::new_v4(),
            "/workspace",
            "provider",
            "Provider",
            "model",
            "Model",
            "Untitled session",
        );

        let first_user = Message::new(MessageRole::User, "first prompt");
        let first_assistant = Message::new(MessageRole::Assistant, "first answer");
        let second_user = Message::new(MessageRole::User, "second prompt");
        let second_assistant = Message::new(MessageRole::Assistant, "second answer");

        conversation.push(first_user.clone());
        conversation.push(first_assistant);
        conversation.push(second_user.clone());
        conversation.push(second_assistant);
        conversation.revert_message_id = Some(second_user.id);

        assert_eq!(conversation.visible_messages().len(), 2);
        assert_eq!(
            conversation
                .last_visible_user_message()
                .map(|message| message.id),
            Some(first_user.id)
        );
        assert_eq!(conversation.visible_messages()[0].id, first_user.id);
        assert_eq!(conversation.visible_messages()[1].content, "first answer");
    }

    #[test]
    fn tool_output_preview_truncates_large_outputs() {
        let output = "abcdef\n".repeat(2_000);

        let preview = tool_output_preview(Some("grep"), &output);

        assert!(preview.contains("grep output truncated"));
        assert!(preview.contains("First excerpt"));
        assert!(preview.contains("Last excerpt"));
        assert!(preview.len() < output.len());
    }

    #[test]
    fn backend_event_exposes_session_id() {
        let session_id = Uuid::new_v4();
        let event = BackendEvent::Failed {
            session_id,
            request_id: 7,
            error: "boom".to_string(),
        };

        assert_eq!(event.session_id(), session_id);
    }

    #[test]
    fn assistant_turn_upserts_tool_calls_by_id() {
        let mut turn = AssistantTurn::default();

        turn.upsert_tool_call(ToolCall {
            id: "tool-call-1".to_string(),
            name: "bash".to_string(),
            arguments: "{\"command\":\"ls\"}".to_string(),
        });
        turn.upsert_tool_call(ToolCall {
            id: "tool-call-1".to_string(),
            name: "bash".to_string(),
            arguments: "{\"command\":\"ls -la\"}".to_string(),
        });
        turn.upsert_tool_call(ToolCall {
            id: "tool-call-2".to_string(),
            name: "read".to_string(),
            arguments: "{\"path\":\"README.md\"}".to_string(),
        });

        assert_eq!(turn.tool_calls.len(), 2);
        assert_eq!(turn.tool_calls[0].arguments, "{\"command\":\"ls -la\"}");
        assert_eq!(turn.tool_calls[1].name, "read");
    }
}
