use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionSummary {
    pub session_id: String,
    pub title: String,
    pub channel_name: String,
    pub chat_id: String,
    pub message_count: i64,
    pub last_active_at: i64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub archived_at: Option<i64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlashCommandInfo {
    pub name: String,
    pub description: String,
    pub aliases: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct UploadDescriptor {
    pub upload_id: String,
    pub name: String,
    pub media_type: String,
    pub mime_type: String,
    pub size: u64,
    pub expires_at: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessageAttachment {
    pub index: u32,
    pub name: String,
    pub media_type: String,
    pub mime_type: String,
}

/// Bounded client projection of a durable Agent run.  Sensitive internals
/// (budget, signal contract, delivery context, provider state) never leave
/// the gateway; the full result is available through the HTTP API.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentRunView {
    pub id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_run_id: Option<String>,
    pub agent_id: String,
    pub provider_name: String,
    pub model_id: String,
    pub mode: String,
    pub depth: u16,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub plan_item_id: Option<String>,
    pub status: String,
    /// Task prompt (bounded for client delivery).
    pub task: String,
    /// Bounded result excerpt; full content is loaded on demand.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub result: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    pub tool_calls_count: i64,
    pub iterations: i64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub deadline_at: Option<i64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub started_at: Option<i64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub finished_at: Option<i64>,
    pub created_at: i64,
    pub updated_at: i64,
}

/// Bounded client projection of a durable inbox event.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentEventView {
    pub id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub run_id: Option<String>,
    pub event_type: String,
    pub delivery: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub severity: Option<String>,
    /// Structured payload, bounded; signal summaries and completion status
    /// only.  The client must treat the payload as untrusted data.
    pub payload_json: String,
    pub status: String,
    pub attempt_count: i64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_error: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub consumed_at: Option<i64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub superseded_at: Option<i64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dead_lettered_at: Option<i64>,
    pub created_at: i64,
}

/// Serialized transcript message for a single Agent run, exposed through the
/// HTTP detail endpoint.  `reasoning_content` is client-visible here but
/// `provider_state` is never included.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentTranscriptMessage {
    pub id: String,
    pub run_id: String,
    pub seq: i64,
    pub role: String,
    pub content: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reasoning_content: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_call_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_calls: Option<Vec<crate::providers::ToolCall>>,
    pub created_at: i64,
}

impl From<crate::storage::agent_run::AgentRunMessageRecord> for AgentTranscriptMessage {
    fn from(record: crate::storage::agent_run::AgentRunMessageRecord) -> Self {
        let tool_calls = record
            .tool_calls_json
            .as_deref()
            .and_then(|json| serde_json::from_str(json).ok());
        Self {
            id: record.id,
            run_id: record.run_id,
            seq: record.seq,
            role: record.role,
            content: record.content,
            reasoning_content: record.reasoning_content,
            tool_call_id: record.tool_call_id,
            tool_name: record.tool_name,
            tool_calls,
            created_at: record.created_at,
        }
    }
}

impl AgentRunView {
    pub fn from_record(
        record: &crate::storage::agent_run::AgentRunRecord,
        max_result_chars: usize,
    ) -> Self {
        let task = truncate(&record.task, 2_000);
        let result = record
            .result
            .as_deref()
            .map(|result| truncate(result, max_result_chars));
        Self {
            id: record.id.clone(),
            parent_run_id: record.parent_run_id.clone(),
            agent_id: record.agent_id.clone(),
            provider_name: record.provider_name.clone(),
            model_id: record.model_id.clone(),
            mode: record.mode.as_str().to_string(),
            depth: u16::try_from(record.depth).unwrap_or(u16::MAX),
            plan_item_id: record.plan_item_id.clone(),
            status: record.status.as_str().to_string(),
            task,
            result,
            error: record.error.clone(),
            tool_calls_count: record.tool_calls_count,
            iterations: record.iterations,
            deadline_at: Some(record.deadline_at),
            started_at: record.started_at,
            finished_at: record.finished_at,
            created_at: record.created_at,
            updated_at: record.updated_at,
        }
    }
}

impl AgentEventView {
    pub fn from_record(record: &crate::storage::agent_inbox::AgentInboxEventRecord) -> Self {
        Self {
            id: record.id.clone(),
            run_id: record.run_id.clone(),
            event_type: record.event_type.as_str().to_string(),
            delivery: record.delivery.as_str().to_string(),
            severity: record.severity.clone(),
            payload_json: record.payload_json.clone(),
            status: record.status.as_str().to_string(),
            attempt_count: record.attempt_count,
            last_error: record.last_error.clone(),
            consumed_at: record.consumed_at,
            superseded_at: record.superseded_at,
            dead_lettered_at: record.dead_lettered_at,
            created_at: record.created_at,
        }
    }
}

fn truncate(value: &str, max: usize) -> String {
    if value.chars().count() <= max {
        value.to_string()
    } else {
        let mut truncated: String = value.chars().take(max).collect();
        truncated.push('…');
        truncated
    }
}

impl MessageAttachment {
    pub fn from_media_ref(index: usize, media_ref: &crate::bus::MediaRef) -> Self {
        let name = std::path::Path::new(&media_ref.path)
            .file_name()
            .map(|name| name.to_string_lossy().into_owned())
            .filter(|name| !name.is_empty())
            .unwrap_or_else(|| "attachment".to_string());
        let mime_type = mime_guess::from_path(&name)
            .first_or_octet_stream()
            .to_string();
        Self {
            index: u32::try_from(index).unwrap_or(u32::MAX),
            name,
            media_type: media_ref.media_type.clone(),
            mime_type,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryMessage {
    pub id: String,
    pub seq: i64,
    pub role: String,
    pub content: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reasoning_content: Option<String>,
    #[serde(default)]
    pub completion_status: crate::bus::CompletionStatus,
    pub created_at: i64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_call_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_calls: Option<Vec<crate::providers::ToolCall>>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub attachments: Vec<MessageAttachment>,
    #[serde(default)]
    pub turn_origin: crate::bus::TurnOrigin,
}

impl From<crate::bus::CommittedMessage> for HistoryMessage {
    fn from(message: crate::bus::CommittedMessage) -> Self {
        let attachments = message
            .media_refs
            .iter()
            .enumerate()
            .map(|(index, media_ref)| MessageAttachment::from_media_ref(index, media_ref))
            .collect();
        Self {
            id: message.id,
            seq: message.seq,
            role: message.role,
            content: message.content,
            reasoning_content: message.reasoning_content,
            completion_status: message.completion_status,
            created_at: message.created_at,
            tool_call_id: message.tool_call_id,
            tool_name: message.tool_name,
            tool_calls: message.tool_calls,
            attachments,
            turn_origin: message.turn_origin,
        }
    }
}

impl HistoryMessage {
    pub fn from_message_meta(message: crate::storage::message::MessageMeta) -> Self {
        let attachments = message
            .media_refs
            .as_deref()
            .and_then(|refs| serde_json::from_str::<Vec<crate::bus::MediaRef>>(refs).ok())
            .unwrap_or_default()
            .iter()
            .enumerate()
            .map(|(index, media_ref)| MessageAttachment::from_media_ref(index, media_ref))
            .collect();
        Self {
            id: message.id,
            seq: message.seq,
            role: message.role,
            content: message.content,
            reasoning_content: message.reasoning_content,
            completion_status: message.completion_status,
            created_at: message.created_at,
            tool_call_id: message.tool_call_id,
            tool_name: message.tool_name,
            tool_calls: message
                .tool_calls
                .and_then(|calls| serde_json::from_str(&calls).ok()),
            attachments,
            turn_origin: message.turn_origin,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum WsInbound {
    #[serde(rename = "user_input")]
    UserInput {
        content: String,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        upload_ids: Vec<String>,
        /// Stable id generated by the client for optimistic-message
        /// reconciliation.  It is optional for older clients and channels.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        client_message_id: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        channel: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        chat_id: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        sender_id: Option<String>,
    },
    #[serde(rename = "clear_history")]
    ClearHistory {
        #[serde(default, skip_serializing_if = "Option::is_none")]
        chat_id: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        session_id: Option<String>,
    },
    #[serde(rename = "create_session")]
    CreateSession {
        #[serde(default, skip_serializing_if = "Option::is_none")]
        title: Option<String>,
    },
    #[serde(rename = "list_sessions")]
    ListSessions {
        #[serde(default)]
        include_archived: bool,
    },
    #[serde(rename = "load_session")]
    LoadSession { session_id: String },
    #[serde(rename = "get_session_history")]
    GetSessionHistory {
        session_id: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        limit: Option<u32>,
    },
    #[serde(rename = "get_session_plan")]
    GetSessionPlan { session_id: String },
    #[serde(rename = "get_session_stats")]
    GetSessionStats { session_id: String },
    #[serde(rename = "rename_session")]
    RenameSession {
        #[serde(default, skip_serializing_if = "Option::is_none")]
        session_id: Option<String>,
        title: String,
    },
    #[serde(rename = "archive_session")]
    ArchiveSession {
        #[serde(default, skip_serializing_if = "Option::is_none")]
        session_id: Option<String>,
    },
    #[serde(rename = "delete_session")]
    DeleteSession {
        #[serde(default, skip_serializing_if = "Option::is_none")]
        session_id: Option<String>,
    },
    #[serde(rename = "get_slash_commands")]
    GetSlashCommands,
    #[serde(rename = "get_agent_runs")]
    GetAgentRuns {
        session_id: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        cursor: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        limit: Option<u32>,
    },
    #[serde(rename = "get_agent_run")]
    GetAgentRun { session_id: String, run_id: String },
    #[serde(rename = "ping")]
    Ping,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum WsOutbound {
    #[serde(rename = "turn_updated")]
    TurnUpdated {
        snapshot: crate::session::TurnSnapshot,
    },
    #[serde(rename = "turn_committed")]
    TurnCommitted {
        session_id: String,
        history_revision: i64,
        messages: Vec<HistoryMessage>,
    },
    #[serde(rename = "assistant_response")]
    AssistantResponse {
        id: String,
        content: String,
        role: String,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        attachments: Vec<MessageAttachment>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        session_id: Option<String>,
    },
    #[serde(rename = "error")]
    Error { code: String, message: String },
    #[serde(rename = "session_established")]
    SessionEstablished {
        session_id: String,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        capabilities: Vec<String>,
    },
    #[serde(rename = "session_created")]
    SessionCreated { session_id: String, title: String },
    #[serde(rename = "session_list")]
    SessionList {
        sessions: Vec<SessionSummary>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        current_session_id: Option<String>,
    },
    #[serde(rename = "session_loaded")]
    SessionLoaded {
        session_id: String,
        title: String,
        message_count: i64,
    },
    #[serde(rename = "session_history")]
    SessionHistory {
        session_id: String,
        messages: Vec<HistoryMessage>,
    },
    #[serde(rename = "session_plan")]
    SessionPlan {
        session_id: String,
        plan: Option<crate::work::TaskPlan>,
    },
    #[serde(rename = "session_stats")]
    SessionStats { stats: crate::session::SessionStats },
    #[serde(rename = "plan_updated")]
    PlanUpdated {
        session_id: String,
        reason: String,
        changed_item_ids: Vec<String>,
        plan: Option<crate::work::TaskPlan>,
    },
    #[serde(rename = "session_renamed")]
    SessionRenamed { session_id: String, title: String },
    #[serde(rename = "session_archived")]
    SessionArchived { session_id: String },
    #[serde(rename = "session_deleted")]
    SessionDeleted { session_id: String },
    #[serde(rename = "history_cleared")]
    HistoryCleared { session_id: String },
    #[serde(rename = "slash_commands_list")]
    SlashCommandsList { commands: Vec<SlashCommandInfo> },
    #[serde(rename = "session_agent_runs")]
    SessionAgentRuns {
        session_id: String,
        revision: i64,
        runs: Vec<AgentRunView>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        next_cursor: Option<String>,
    },
    #[serde(rename = "agent_run_updated")]
    AgentRunUpdated {
        session_id: String,
        revision: i64,
        run: AgentRunView,
    },
    #[serde(rename = "agent_event_updated")]
    AgentEventUpdated {
        session_id: String,
        revision: i64,
        event: AgentEventView,
    },
    #[serde(rename = "pong")]
    Pong,
    #[serde(rename = "command_executed")]
    CommandExecuted { message: String },
    #[serde(rename = "system_notification")]
    SystemNotification {
        content: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        session_id: Option<String>,
    },
}

pub fn parse_inbound(raw: &str) -> Result<WsInbound, serde_json::Error> {
    serde_json::from_str(raw)
}

pub fn serialize_inbound(msg: &WsInbound) -> Result<String, serde_json::Error> {
    serde_json::to_string(msg)
}

pub fn serialize_outbound(msg: &WsOutbound) -> Result<String, serde_json::Error> {
    serde_json::to_string(msg)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::session::{TurnId, TurnPhase, TurnState, TurnStatus};

    #[test]
    fn turn_updated_serializes_as_one_complete_snapshot_frame() {
        let frame = WsOutbound::TurnUpdated {
            snapshot: TurnState {
                id: TurnId("turn-1".into()),
                session_id: "cli_chat:client:dialog".into(),
                message_id: "message-1".into(),
                revision: 7,
                status: TurnStatus::Running,
                phase: TurnPhase::Responding,
                blocks: Vec::new(),
                usage: None,
                error: None,
            },
        };

        let json = serialize_outbound(&frame).unwrap();
        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(value["type"], "turn_updated");
        assert_eq!(value["snapshot"]["revision"], 7);
        assert_eq!(value["snapshot"]["status"], "running");
    }

    #[test]
    fn turn_committed_serializes_revision_and_durable_delta() {
        let frame = WsOutbound::TurnCommitted {
            session_id: "session".to_string(),
            history_revision: 7,
            messages: vec![HistoryMessage {
                id: "message".to_string(),
                seq: 7,
                role: "assistant".to_string(),
                content: "done".to_string(),
                reasoning_content: None,
                completion_status: crate::bus::CompletionStatus::Completed,
                created_at: 1,
                tool_call_id: None,
                tool_name: None,
                tool_calls: None,
                attachments: Vec::new(),
                turn_origin: crate::bus::TurnOrigin::User,
            }],
        };
        let value = serde_json::to_value(frame).unwrap();

        assert_eq!(value["type"], "turn_committed");
        assert_eq!(value["history_revision"], 7);
        assert_eq!(value["messages"][0]["id"], "message");
    }

    #[test]
    fn user_input_preserves_optional_client_message_id() {
        let inbound = parse_inbound(
            r#"{"type":"user_input","content":"hello","client_message_id":"client-1"}"#,
        )
        .unwrap();
        match inbound {
            WsInbound::UserInput {
                client_message_id,
                upload_ids,
                ..
            } => {
                assert_eq!(client_message_id.as_deref(), Some("client-1"));
                assert!(upload_ids.is_empty());
            }
            other => panic!("unexpected frame: {other:?}"),
        }

        let serialized = serialize_inbound(&WsInbound::UserInput {
            content: "hello".to_string(),
            upload_ids: Vec::new(),
            client_message_id: Some("client-1".to_string()),
            channel: None,
            chat_id: None,
            sender_id: None,
        })
        .unwrap();
        assert!(serialized.contains(r#""client_message_id":"client-1""#));
    }

    #[test]
    fn history_defaults_new_reasoning_fields_for_old_frames() {
        let message: HistoryMessage = serde_json::from_value(serde_json::json!({
            "id": "message",
            "seq": 1,
            "role": "assistant",
            "content": "answer",
            "created_at": 1,
            "attachments": []
        }))
        .unwrap();

        assert_eq!(message.reasoning_content, None);
        assert_eq!(
            message.completion_status,
            crate::bus::CompletionStatus::Completed
        );
    }

    #[test]
    fn session_stats_request_and_response_use_structured_frames() {
        let inbound =
            parse_inbound(r#"{"type":"get_session_stats","session_id":"cli_chat:client:dialog"}"#)
                .unwrap();
        assert!(matches!(inbound, WsInbound::GetSessionStats { .. }));

        let stats = crate::session::SessionStats {
            session_id: "cli_chat:client:dialog".into(),
            title: "stats".into(),
            provider: "provider".into(),
            model: "model".into(),
            user_message_count: 1,
            history_message_count: 2,
            lifetime_usage: crate::session::LifetimeUsage {
                input_tokens: 100,
                output_tokens: 20,
                total_tokens: 120,
                cached_input_tokens: Some(40),
                request_count: 1,
                turn_count: 1,
                tracked_since: Some(1),
            },
            context: crate::session::ContextUsage {
                configured_window_tokens: 128_000,
                effective_window_tokens: 128_000,
                configured_reserve_tokens: 16_384,
                reserve_tokens: 16_384,
                configured_keep_recent_tokens: 20_000,
                effective_keep_recent_tokens: 20_000,
                used_tokens: 100,
                remaining_tokens: 127_900,
                compression_threshold_tokens: 111_616,
                source: crate::session::ContextUsageSource::Observed,
                last_observed_prompt_tokens: Some(90),
                observed_at: Some(1),
                active_checkpoint_id: None,
                checkpoint_generation: 0,
                checkpoint_tokens_before: None,
                checkpoint_tokens_after: None,
                checkpoint_degraded: false,
            },
            created_at: 1,
            last_active_at: 2,
            updated_at: 3,
        };
        let value = serde_json::to_value(WsOutbound::SessionStats { stats }).unwrap();
        assert_eq!(value["type"], "session_stats");
        assert_eq!(value["stats"]["context"]["source"], "observed");
        assert_eq!(value["stats"]["lifetime_usage"]["input_tokens"], 100);
    }

    #[test]
    fn agent_run_and_event_views_serialize_without_sensitive_fields() {
        let run = crate::storage::agent_run::AgentRunRecord {
            id: "run-1".to_string(),
            root_session_id: "cli:test:d1".to_string(),
            root_turn_id: None,
            parent_run_id: None,
            caller_agent_id: "ROOT".to_string(),
            caller_scope_id: "turn-1".to_string(),
            idempotency_key: None,
            agent_id: "researcher".to_string(),
            definition_hash: "hash".to_string(),
            provider_profile: "research".to_string(),
            provider_name: "openai-test".to_string(),
            model_id: "model-x".to_string(),
            mode: crate::storage::agent_run::AgentRunMode::Background,
            depth: 1,
            plan_item_id: None,
            execution_id: "run-1".to_string(),
            task: "analyze the logs".to_string(),
            context_json: Some("sensitive caller context".to_string()),
            budget_json: r#"{"remaining_runs":3}"#.to_string(),
            signal_contract_json: Some("secret contract".to_string()),
            signal_delivery: None,
            status: crate::storage::agent_run::AgentRunStatus::Completed,
            result: Some("r".repeat(10_000)),
            error: None,
            prompt_tokens: None,
            completion_tokens: None,
            cost: None,
            tool_calls_count: 3,
            iterations: 2,
            runtime_generation: 1,
            attempt: 1,
            completion_slot_reserved: false,
            deadline_at: 1000,
            revision: 7,
            started_at: Some(10),
            finished_at: Some(20),
            created_at: 5,
            updated_at: 20,
        };
        let view = AgentRunView::from_record(&run, 2_000);
        let value = serde_json::to_value(&view).unwrap();
        assert_eq!(value["id"], "run-1");
        assert_eq!(value["status"], "completed");
        assert!(!value.as_object().unwrap().contains_key("context_json"));
        assert!(!value.as_object().unwrap().contains_key("budget_json"));
        assert!(
            !value
                .as_object()
                .unwrap()
                .contains_key("signal_contract_json")
        );
        assert!(!value.as_object().unwrap().contains_key("execution_id"));
        // The result is bounded for client delivery.
        assert!(value["result"].as_str().unwrap().chars().count() <= 2_001);

        let message = WsOutbound::AgentRunUpdated {
            session_id: "cli:test:d1".to_string(),
            revision: 7,
            run: view,
        };
        let value = serde_json::to_value(message).unwrap();
        assert_eq!(value["type"], "agent_run_updated");
        assert_eq!(value["revision"], 7);

        let inbound: WsInbound =
            parse_inbound(r#"{"type":"get_agent_runs","session_id":"cli:test:d1","limit":10}"#)
                .unwrap();
        assert!(matches!(
            inbound,
            WsInbound::GetAgentRuns {
                limit: Some(10),
                ..
            }
        ));
    }
}
