//! Data models for Everruns API
//!
//! These types represent the request and response objects used by the API.

use serde::{Deserialize, Serialize};

/// Per-agent capability configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AgentCapabilityConfig {
    /// Reference to the capability ID
    #[serde(rename = "ref")]
    pub capability_ref: String,
    /// Per-agent configuration for this capability (capability-specific)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub config: Option<serde_json::Value>,
}

impl AgentCapabilityConfig {
    /// Create a new capability config with just a ref
    pub fn new(capability_ref: impl Into<String>) -> Self {
        Self {
            capability_ref: capability_ref.into(),
            config: None,
        }
    }

    /// Set the config
    pub fn config(mut self, config: serde_json::Value) -> Self {
        self.config = Some(config);
        self
    }
}

/// Public capability information
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CapabilityInfo {
    pub id: String,
    pub name: String,
    pub description: String,
    pub status: String,
    #[serde(default)]
    pub category: Option<String>,
    #[serde(default)]
    pub dependencies: Vec<String>,
    #[serde(default)]
    pub icon: Option<String>,
    #[serde(default)]
    pub is_mcp: bool,
    /// Human-readable display name for UI rendering
    #[serde(default)]
    pub display_name: Option<String>,
    /// UI feature strings this capability contributes to
    #[serde(default)]
    pub features: Vec<String>,
    /// Whether this is an Agent Skill capability
    #[serde(default)]
    pub is_skill: bool,
    /// Risk level for approval requirements (TM-AGENT-005)
    #[serde(default)]
    pub risk_level: Option<String>,
}

/// Agent configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Agent {
    pub id: String,
    pub name: String,
    #[serde(default)]
    pub description: Option<String>,
    pub system_prompt: String,
    #[serde(default)]
    pub default_model_id: Option<String>,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub capabilities: Vec<AgentCapabilityConfig>,
    #[serde(default)]
    pub initial_files: Vec<InitialFile>,
    pub status: AgentStatus,
    pub created_at: String,
    pub updated_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentStatus {
    Active,
    Archived,
}

/// Request to create an agent
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct CreateAgentRequest {
    /// Client-supplied agent ID (format: agent_{32-hex}).
    /// If not provided, one is auto-generated by the server.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    pub name: String,
    pub system_prompt: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default_model_id: Option<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub capabilities: Vec<AgentCapabilityConfig>,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub initial_files: Vec<InitialFile>,
}

impl CreateAgentRequest {
    /// Create a new request with required fields
    pub fn new(name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
        Self {
            id: None,
            name: name.into(),
            system_prompt: system_prompt.into(),
            description: None,
            default_model_id: None,
            tags: vec![],
            capabilities: vec![],
            initial_files: vec![],
        }
    }

    /// Set a client-supplied agent ID
    pub fn id(mut self, id: impl Into<String>) -> Self {
        self.id = Some(id.into());
        self
    }

    /// Set the description
    pub fn description(mut self, description: impl Into<String>) -> Self {
        self.description = Some(description.into());
        self
    }

    /// Set the default model ID
    pub fn default_model_id(mut self, model_id: impl Into<String>) -> Self {
        self.default_model_id = Some(model_id.into());
        self
    }

    /// Set the tags
    pub fn tags(mut self, tags: Vec<String>) -> Self {
        self.tags = tags;
        self
    }

    /// Set the capabilities
    pub fn capabilities(mut self, capabilities: Vec<AgentCapabilityConfig>) -> Self {
        self.capabilities = capabilities;
        self
    }

    /// Set the starter files copied into each new session for this agent
    pub fn initial_files(mut self, initial_files: Vec<InitialFile>) -> Self {
        self.initial_files = initial_files;
        self
    }
}

/// Generate a random agent ID in the format `agent_<32-hex>`.
pub fn generate_agent_id() -> String {
    let mut bytes = [0u8; 16];
    getrandom::getrandom(&mut bytes).expect("failed to generate random bytes");
    let hex: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
    format!("agent_{}", hex)
}

/// Generate a random harness ID in the format `harness_<32-hex>`.
pub fn generate_harness_id() -> String {
    let mut bytes = [0u8; 16];
    getrandom::getrandom(&mut bytes).expect("failed to generate random bytes");
    let hex: String = bytes.iter().map(|b| format!("{:02x}", b)).collect();
    format!("harness_{}", hex)
}

/// Session representing an active conversation
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Session {
    pub id: String,
    pub organization_id: String,
    pub harness_id: String,
    #[serde(default)]
    pub agent_id: Option<String>,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub locale: Option<String>,
    #[serde(default)]
    pub model_id: Option<String>,
    #[serde(default)]
    pub capabilities: Vec<AgentCapabilityConfig>,
    pub status: SessionStatus,
    pub created_at: String,
    pub updated_at: String,
    #[serde(default)]
    pub usage: Option<TokenUsage>,
    /// Number of active (enabled) schedules for this session
    #[serde(default)]
    pub active_schedule_count: Option<i32>,
    /// Aggregated UI features from all active capabilities
    #[serde(default)]
    pub features: Vec<String>,
    /// Whether this session is pinned by the current user
    #[serde(default)]
    pub is_pinned: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionStatus {
    Started,
    Active,
    Idle,
    #[serde(rename = "waitingfortoolresults")]
    WaitingForToolResults,
}

/// Token usage statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TokenUsage {
    #[serde(default)]
    pub input_tokens: u64,
    #[serde(default)]
    pub output_tokens: u64,
    #[serde(default)]
    pub cache_read_tokens: u64,
}

/// Starter file copied into a new session workspace
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct InitialFile {
    pub path: String,
    pub content: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub encoding: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_readonly: Option<bool>,
}

impl InitialFile {
    /// Create a new initial file with required fields
    pub fn new(path: impl Into<String>, content: impl Into<String>) -> Self {
        Self {
            path: path.into(),
            content: content.into(),
            encoding: None,
            is_readonly: None,
        }
    }

    /// Set the content encoding
    pub fn encoding(mut self, encoding: impl Into<String>) -> Self {
        self.encoding = Some(encoding.into());
        self
    }

    /// Set the readonly flag
    pub fn is_readonly(mut self, is_readonly: bool) -> Self {
        self.is_readonly = Some(is_readonly);
        self
    }
}

/// Request to create a session
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct CreateSessionRequest {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub harness_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub agent_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub locale: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model_id: Option<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub capabilities: Vec<AgentCapabilityConfig>,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub initial_files: Vec<InitialFile>,
}

impl Default for CreateSessionRequest {
    fn default() -> Self {
        Self::new()
    }
}

impl CreateSessionRequest {
    /// Create a new request (server defaults to Generic harness)
    pub fn new() -> Self {
        Self {
            harness_id: None,
            agent_id: None,
            title: None,
            locale: None,
            model_id: None,
            tags: vec![],
            capabilities: vec![],
            initial_files: vec![],
        }
    }

    /// Set the harness ID
    pub fn harness_id(mut self, harness_id: impl Into<String>) -> Self {
        self.harness_id = Some(harness_id.into());
        self
    }

    /// Set the agent ID
    pub fn agent_id(mut self, agent_id: impl Into<String>) -> Self {
        self.agent_id = Some(agent_id.into());
        self
    }

    /// Set the session title
    pub fn title(mut self, title: impl Into<String>) -> Self {
        self.title = Some(title.into());
        self
    }

    /// Set the session locale
    pub fn locale(mut self, locale: impl Into<String>) -> Self {
        self.locale = Some(locale.into());
        self
    }

    /// Set the model ID
    pub fn model_id(mut self, model_id: impl Into<String>) -> Self {
        self.model_id = Some(model_id.into());
        self
    }

    /// Set the tags
    pub fn tags(mut self, tags: Vec<String>) -> Self {
        self.tags = tags;
        self
    }

    /// Set the capabilities
    pub fn capabilities(mut self, capabilities: Vec<AgentCapabilityConfig>) -> Self {
        self.capabilities = capabilities;
        self
    }

    /// Set the initial files copied into the session workspace
    pub fn initial_files(mut self, initial_files: Vec<InitialFile>) -> Self {
        self.initial_files = initial_files;
        self
    }
}

/// External actor identity for messages from external channels (Slack, Discord, etc.)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ExternalActor {
    /// Opaque actor identifier from the source channel
    pub actor_id: String,
    /// Source channel identifier (e.g. "slack", "discord")
    pub source: String,
    /// Resolved display name (falls back to actor_id if absent)
    #[serde(default)]
    pub actor_name: Option<String>,
    /// Channel-specific metadata
    #[serde(default)]
    pub metadata: Option<std::collections::HashMap<String, String>>,
}

impl ExternalActor {
    /// Create a new ExternalActor with required fields
    pub fn new(actor_id: impl Into<String>, source: impl Into<String>) -> Self {
        Self {
            actor_id: actor_id.into(),
            source: source.into(),
            actor_name: None,
            metadata: None,
        }
    }

    /// Set the display name
    pub fn actor_name(mut self, name: impl Into<String>) -> Self {
        self.actor_name = Some(name.into());
        self
    }

    /// Set metadata
    pub fn metadata(mut self, metadata: std::collections::HashMap<String, String>) -> Self {
        self.metadata = Some(metadata);
        self
    }
}

/// Message in a session
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Message {
    pub id: String,
    pub session_id: String,
    pub sequence: u64,
    pub role: MessageRole,
    pub content: Vec<ContentPart>,
    #[serde(default)]
    pub thinking: Option<String>,
    #[serde(default)]
    pub tags: Vec<String>,
    pub created_at: String,
    /// External actor identity (for messages from external channels)
    #[serde(default)]
    pub external_actor: Option<ExternalActor>,
    /// Execution phase for multi-step tool-calling flows
    #[serde(default)]
    pub phase: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageRole {
    User,
    Agent,
    ToolResult,
}

/// Content part within a message
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
    Text {
        text: String,
    },
    Image {
        url: Option<String>,
        base64: Option<String>,
    },
    ImageFile {
        image_id: String,
    },
    ToolCall {
        id: String,
        name: String,
        arguments: serde_json::Value,
    },
    ToolResult {
        tool_call_id: String,
        result: Option<serde_json::Value>,
        error: Option<String>,
    },
}

impl ContentPart {
    /// Create a text content part
    pub fn text(text: impl Into<String>) -> Self {
        Self::Text { text: text.into() }
    }

    /// Create a tool result content part with a successful result
    pub fn tool_result(tool_call_id: impl Into<String>, result: serde_json::Value) -> Self {
        Self::ToolResult {
            tool_call_id: tool_call_id.into(),
            result: Some(result),
            error: None,
        }
    }

    /// Create a tool result content part with an error
    pub fn tool_error(tool_call_id: impl Into<String>, error: impl Into<String>) -> Self {
        Self::ToolResult {
            tool_call_id: tool_call_id.into(),
            result: None,
            error: Some(error.into()),
        }
    }

    /// Returns true if this is a tool call content part
    pub fn is_tool_call(&self) -> bool {
        matches!(self, Self::ToolCall { .. })
    }

    /// Extract tool call info if this is a tool call content part
    pub fn as_tool_call(&self) -> Option<ToolCallInfo<'_>> {
        match self {
            Self::ToolCall {
                id,
                name,
                arguments,
            } => Some(ToolCallInfo {
                id,
                name,
                arguments,
            }),
            _ => None,
        }
    }
}

/// Borrowed view of a tool call content part
#[derive(Debug, Clone)]
pub struct ToolCallInfo<'a> {
    pub id: &'a str,
    pub name: &'a str,
    pub arguments: &'a serde_json::Value,
}

/// Request to create a message
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct CreateMessageRequest {
    pub message: MessageInput,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub controls: Option<Controls>,
    /// External actor identity (for messages from external channels like Slack)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub external_actor: Option<ExternalActor>,
}

impl CreateMessageRequest {
    /// Create a new request with message input
    pub fn new(message: MessageInput) -> Self {
        Self {
            message,
            controls: None,
            external_actor: None,
        }
    }

    /// Create a user text message
    pub fn user_text(text: impl Into<String>) -> Self {
        Self::new(MessageInput::user_text(text))
    }

    /// Create a tool result message containing one or more tool results
    pub fn tool_results(results: Vec<ContentPart>) -> Self {
        Self::new(MessageInput::tool_results(results))
    }

    /// Set the controls
    pub fn controls(mut self, controls: Controls) -> Self {
        self.controls = Some(controls);
        self
    }

    /// Set the external actor identity
    pub fn external_actor(mut self, actor: ExternalActor) -> Self {
        self.external_actor = Some(actor);
        self
    }
}

/// Input for creating a message
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct MessageInput {
    pub role: MessageRole,
    pub content: Vec<ContentPart>,
}

impl MessageInput {
    /// Create a new message input
    pub fn new(role: MessageRole, content: Vec<ContentPart>) -> Self {
        Self { role, content }
    }

    /// Create a user text message
    pub fn user_text(text: impl Into<String>) -> Self {
        Self::new(
            MessageRole::User,
            vec![ContentPart::Text { text: text.into() }],
        )
    }

    /// Create a tool result message containing one or more tool results
    pub fn tool_results(results: Vec<ContentPart>) -> Self {
        Self::new(MessageRole::ToolResult, results)
    }
}

/// Controls for message generation
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Controls {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_tokens: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub temperature: Option<f32>,
}

impl Default for Controls {
    fn default() -> Self {
        Self::new()
    }
}

impl Controls {
    /// Create new empty controls
    pub fn new() -> Self {
        Self {
            model_id: None,
            max_tokens: None,
            temperature: None,
        }
    }

    /// Set the model ID
    pub fn model_id(mut self, model_id: impl Into<String>) -> Self {
        self.model_id = Some(model_id.into());
        self
    }

    /// Set the max tokens
    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
        self.max_tokens = Some(max_tokens);
        self
    }

    /// Set the temperature
    pub fn temperature(mut self, temperature: f32) -> Self {
        self.temperature = Some(temperature);
        self
    }
}

/// Paginated list response
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ListResponse<T> {
    pub data: Vec<T>,
    #[serde(default)]
    pub total: u64,
    #[serde(default)]
    pub offset: u64,
    #[serde(default)]
    pub limit: u64,
}

/// SSE Event from the server
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Event {
    pub id: String,
    #[serde(rename = "type")]
    pub event_type: String,
    pub ts: String,
    pub session_id: String,
    pub data: serde_json::Value,
    #[serde(default)]
    pub context: EventContext,
}

impl Event {
    /// Extract tool calls from an `output.message.completed` event's data.
    ///
    /// Returns tool call content parts found in `data.message.content`.
    pub fn tool_calls(&self) -> Vec<ToolCallInfo<'_>> {
        extract_tool_calls(&self.data)
    }
}

/// Extract tool call info from event data (`data.message.content`).
pub fn extract_tool_calls(data: &serde_json::Value) -> Vec<ToolCallInfo<'_>> {
    let Some(content) = data
        .get("message")
        .and_then(|m| m.get("content"))
        .and_then(|c| c.as_array())
    else {
        return vec![];
    };
    content
        .iter()
        .filter_map(|part| {
            if part.get("type")?.as_str()? != "tool_call" {
                return None;
            }
            Some(ToolCallInfo {
                id: part.get("id")?.as_str()?,
                name: part.get("name")?.as_str()?,
                arguments: part.get("arguments")?,
            })
        })
        .collect()
}

/// Context for an event
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub struct EventContext {
    #[serde(default)]
    pub turn_id: Option<String>,
    #[serde(default)]
    pub input_message_id: Option<String>,
}

// --- Session Filesystem Models ---

/// File metadata without content
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FileInfo {
    pub id: String,
    pub session_id: String,
    pub path: String,
    pub name: String,
    pub is_directory: bool,
    pub is_readonly: bool,
    pub size_bytes: i64,
    pub created_at: String,
    pub updated_at: String,
}

/// Complete file with content
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SessionFile {
    pub id: String,
    pub session_id: String,
    pub path: String,
    pub name: String,
    pub is_directory: bool,
    pub is_readonly: bool,
    pub size_bytes: i64,
    pub created_at: String,
    pub updated_at: String,
    #[serde(default)]
    pub content: Option<String>,
    #[serde(default)]
    pub encoding: Option<String>,
}

/// File stat information (without id/session_id)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FileStat {
    pub path: String,
    pub name: String,
    pub is_directory: bool,
    pub is_readonly: bool,
    pub size_bytes: i64,
    pub created_at: String,
    pub updated_at: String,
}

/// Request to create a file or directory
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct CreateFileRequest {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub encoding: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_directory: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_readonly: Option<bool>,
}

impl CreateFileRequest {
    /// Create a request for a new file
    pub fn file(content: impl Into<String>) -> Self {
        Self {
            content: Some(content.into()),
            encoding: None,
            is_directory: None,
            is_readonly: None,
        }
    }

    /// Create a request for a new directory
    pub fn directory() -> Self {
        Self {
            content: None,
            encoding: None,
            is_directory: Some(true),
            is_readonly: None,
        }
    }

    /// Set the content encoding ("text" or "base64")
    pub fn encoding(mut self, encoding: impl Into<String>) -> Self {
        self.encoding = Some(encoding.into());
        self
    }

    /// Set the readonly flag
    pub fn is_readonly(mut self, is_readonly: bool) -> Self {
        self.is_readonly = Some(is_readonly);
        self
    }
}

/// Request to update a file
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct UpdateFileRequest {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub encoding: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_readonly: Option<bool>,
}

impl UpdateFileRequest {
    /// Create a request to update file content
    pub fn content(content: impl Into<String>) -> Self {
        Self {
            content: Some(content.into()),
            encoding: None,
            is_readonly: None,
        }
    }

    /// Set the content encoding ("text" or "base64")
    pub fn encoding(mut self, encoding: impl Into<String>) -> Self {
        self.encoding = Some(encoding.into());
        self
    }

    /// Set the readonly flag
    pub fn is_readonly(mut self, is_readonly: bool) -> Self {
        self.is_readonly = Some(is_readonly);
        self
    }
}

/// Request to copy a file
#[derive(Debug, Clone, Serialize)]
pub struct CopyFileRequest {
    pub src_path: String,
    pub dst_path: String,
}

impl CopyFileRequest {
    pub fn new(src_path: impl Into<String>, dst_path: impl Into<String>) -> Self {
        Self {
            src_path: src_path.into(),
            dst_path: dst_path.into(),
        }
    }
}

/// Request to move/rename a file
#[derive(Debug, Clone, Serialize)]
pub struct MoveFileRequest {
    pub src_path: String,
    pub dst_path: String,
}

impl MoveFileRequest {
    pub fn new(src_path: impl Into<String>, dst_path: impl Into<String>) -> Self {
        Self {
            src_path: src_path.into(),
            dst_path: dst_path.into(),
        }
    }
}

/// Request to search files with regex
#[derive(Debug, Clone, Serialize)]
pub struct GrepRequest {
    pub pattern: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path_pattern: Option<String>,
}

impl GrepRequest {
    pub fn new(pattern: impl Into<String>) -> Self {
        Self {
            pattern: pattern.into(),
            path_pattern: None,
        }
    }

    /// Set an optional path pattern to filter files
    pub fn path_pattern(mut self, path_pattern: impl Into<String>) -> Self {
        self.path_pattern = Some(path_pattern.into());
        self
    }
}

/// Request to get file stat
#[derive(Debug, Clone, Serialize)]
pub struct StatRequest {
    pub path: String,
}

impl StatRequest {
    pub fn new(path: impl Into<String>) -> Self {
        Self { path: path.into() }
    }
}

/// Single grep match
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GrepMatch {
    pub path: String,
    pub line_number: u64,
    pub line: String,
}

/// Grep result for a file
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GrepResult {
    pub path: String,
    pub matches: Vec<GrepMatch>,
}

/// Response for delete operations
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteResponse {
    pub deleted: bool,
}

// --- Connections Models ---

/// A user connection to an external provider
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Connection {
    pub provider: String,
    pub created_at: String,
    pub updated_at: String,
}

/// Request to set a connection API key
#[derive(Debug, Clone, Serialize)]
pub struct SetConnectionRequest {
    pub api_key: String,
}

impl SetConnectionRequest {
    pub fn new(api_key: impl Into<String>) -> Self {
        Self {
            api_key: api_key.into(),
        }
    }
}

// --- Session Secrets Models ---

/// Request to batch-set session secrets
#[derive(Debug, Clone, Serialize)]
pub struct SetSecretsRequest {
    pub secrets: std::collections::HashMap<String, String>,
}

impl SetSecretsRequest {
    pub fn new(secrets: std::collections::HashMap<String, String>) -> Self {
        Self { secrets }
    }
}

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

    #[test]
    fn list_response_deserializes_without_pagination_fields() {
        let json = r#"{"data": [1, 2, 3]}"#;
        let resp: ListResponse<i32> = serde_json::from_str(json).unwrap();
        assert_eq!(resp.data, vec![1, 2, 3]);
        assert_eq!(resp.total, 0);
        assert_eq!(resp.offset, 0);
        assert_eq!(resp.limit, 0);
    }

    #[test]
    fn list_response_deserializes_with_pagination_fields() {
        let json = r#"{"data": ["a"], "total": 10, "offset": 5, "limit": 25}"#;
        let resp: ListResponse<String> = serde_json::from_str(json).unwrap();
        assert_eq!(resp.data, vec!["a"]);
        assert_eq!(resp.total, 10);
        assert_eq!(resp.offset, 5);
        assert_eq!(resp.limit, 25);
    }
}
