use serde::Serialize;

use crate::model::workspace::AgentStatus;

#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum SessionStatus {
    Active,
    Archived,
}

impl SessionStatus {
    pub fn as_str(&self) -> &str {
        match self {
            Self::Active => "active",
            Self::Archived => "archived",
        }
    }
}

/// Returned when a string doesn't correspond to any known [`SessionStatus`].
/// Surfacing the unknown value (instead of silently coercing) lets callers
/// detect corrupted DB rows or values written by a forward-version of the app.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseSessionStatusError(pub String);

impl std::fmt::Display for ParseSessionStatusError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "unknown SessionStatus value: {:?}", self.0)
    }
}

impl std::error::Error for ParseSessionStatusError {}

impl std::str::FromStr for SessionStatus {
    type Err = ParseSessionStatusError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "active" => Ok(Self::Active),
            "archived" => Ok(Self::Archived),
            other => Err(ParseSessionStatusError(other.to_string())),
        }
    }
}

/// Kind of input the agent is waiting for. Mirrors `AttentionKind` in
/// `src-tauri/src/state.rs` but as an owned string so the lib crate stays
/// free of Tauri deps.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum AttentionKind {
    Ask,
    Plan,
}

impl AttentionKind {
    pub fn as_str(&self) -> &str {
        match self {
            Self::Ask => "ask",
            Self::Plan => "plan",
        }
    }
}

/// Maximum length of a session name in characters. Matches the cap the
/// Haiku auto-namer applies so user-entered names and auto-generated names
/// share the same bound.
pub const SESSION_NAME_MAX_CHARS: usize = 60;

/// Normalize a user-supplied session name. Trims surrounding whitespace,
/// rejects the empty string, and caps at `SESSION_NAME_MAX_CHARS`
/// characters (not bytes) so we can't split a multi-byte codepoint.
pub fn validate_session_name(name: &str) -> Result<String, &'static str> {
    let trimmed = name.trim();
    if trimmed.is_empty() {
        return Err("Name cannot be empty");
    }
    Ok(trimmed.chars().take(SESSION_NAME_MAX_CHARS).collect())
}

/// A conversation within a workspace. Each session has its own Claude CLI
/// subprocess, its own message history, and its own checkpoint timeline.
/// A workspace always has at least one active session.
#[derive(Debug, Clone, Serialize)]
pub struct ChatSession {
    pub id: String,
    pub workspace_id: String,
    /// Claude CLI `--resume` UUID. `None` until the first turn completes.
    pub session_id: Option<String>,
    pub name: String,
    /// `true` once the user renames the session — Haiku auto-naming never
    /// overwrites a user-edited name.
    pub name_edited: bool,
    pub turn_count: u32,
    pub sort_order: i32,
    pub status: SessionStatus,
    pub created_at: String,
    pub archived_at: Option<String>,
    /// Redacted, shell-quoted `claude` invocation captured the first time
    /// the agent process spawns for this session. `None` for sessions that
    /// pre-date this feature, or whose first spawn raced with a write
    /// failure. UI renders a banner only when `Some`.
    pub cli_invocation: Option<String>,
    /// Runtime agent status — defaults to `Idle` when loaded from DB; the
    /// command layer overlays the live `AppState.agents` view on top.
    pub agent_status: AgentStatus,
    /// Runtime attention flag — defaults to `false` from DB.
    pub needs_attention: bool,
    /// Runtime attention kind — defaults to `None` from DB.
    pub attention_kind: Option<AttentionKind>,
}
