//! Core session types.
//!
//! This module contains the fundamental session types:
//! - [`SessionStatus`]: Current state of the agent (idle, busy, wait, done)
//! - [`Session`]: Aggregated session state derived from events

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

use crate::context::is_process_alive;

/// Session status indicating the current state of the agent.
///
/// Status is updated based on events:
/// - `Idle`: Agent ready, waiting for input (SessionStart, Stop)
/// - `Busy`: Agent actively processing (UserPromptSubmit, PostToolUse)
/// - `Wait`: Agent waiting for user permission (PermissionRequest)
/// - `Done`: Session has terminated (SessionEnd)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SessionStatus {
    /// Agent ready, waiting for user input
    Idle,
    /// Agent actively processing
    Busy,
    /// Agent waiting for user permission
    Wait,
    /// Session has terminated
    Done,
}

impl std::fmt::Display for SessionStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SessionStatus::Idle => write!(f, "idle"),
            SessionStatus::Busy => write!(f, "busy"),
            SessionStatus::Wait => write!(f, "wait"),
            SessionStatus::Done => write!(f, "done"),
        }
    }
}

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

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "idle" => Ok(SessionStatus::Idle),
            "busy" => Ok(SessionStatus::Busy),
            "wait" => Ok(SessionStatus::Wait),
            "done" => Ok(SessionStatus::Done),
            _ => Err(format!("unknown session status: {}", s)),
        }
    }
}

/// A Claude Code session with aggregated metadata.
///
/// Sessions are derived from events but stored separately for efficient querying.
/// All aggregate fields are updated incrementally when events are inserted.
///
/// # Schema v2
///
/// This struct implements Sessions Schema v2 which uses unified columns with
/// explicit source tracking instead of source-specific columns (e.g., `tokens_input`
/// instead of `tokens_input_otel` and `tokens_input_transcript`).
///
/// Source priority is: transcript > otel > hook
/// - Transcript is preferred as the authoritative source (includes is_sidechain info)
/// - OTEL is used as fallback for token/API data
/// - Hook provides real-time status and prompts
///
/// # Session Revival
///
/// Sessions can be revived after ending. The schema tracks:
/// - `first_started_at` / `last_started_at`: Original and most recent start times
/// - `first_ended_at` / `last_ended_at`: Original and most recent end times
/// - `revival_count`: Number of times the session was revived
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
    // --- Identity ---
    /// Unique session identifier
    pub session_id: String,

    /// Machine identifier for multi-machine sync
    pub machine_id: String,

    /// Source framework (claude_code, gemini, codex, etc.)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub framework: Option<String>,

    // --- Lifecycle ---
    /// Original session start time (immutable)
    pub first_started_at: DateTime<Utc>,

    /// Most recent start/revival time
    pub last_started_at: DateTime<Utc>,

    /// First time session ended (immutable historical record)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub first_ended_at: Option<DateTime<Utc>>,

    /// Most recent end time (None = running)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_ended_at: Option<DateTime<Utc>>,

    /// Timestamp of most recent activity
    pub last_activity: DateTime<Utc>,

    /// Number of times the session was revived after ending
    pub revival_count: u32,

    /// Current session status (idle, busy, wait, done)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<SessionStatus>,

    /// Timestamp when status was last updated (prevents stale updates)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status_updated_at: Option<DateTime<Utc>>,

    // --- Process context ---
    /// Process ID (NULL when process ended, can change on revival)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pid: Option<i32>,

    /// Process start time in seconds (Unix timestamp).
    /// Used for deterministic PID identity validation - (pid, start_time) is globally unique.
    /// This prevents false positives from PID reuse and handles late mi6 installation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub process_start_time: Option<i64>,

    /// Working directory
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,

    // --- Git context (async detection with eventual consistency) ---
    /// Absolute path to .git directory
    #[serde(skip_serializing_if = "Option::is_none")]
    pub local_git_dir: Option<String>,

    /// Current git branch name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub git_branch: Option<String>,

    /// Worktree root if in a worktree
    #[serde(skip_serializing_if = "Option::is_none")]
    pub git_worktree_path: Option<String>,

    // --- GitHub context (updated throughout session via async parsing) ---
    /// Repository in owner/repo format (e.g., "paradigmxyz/mi6")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub github_repo: Option<String>,

    /// GitHub issue number from gh commands (most recent)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub github_issue: Option<i32>,

    /// GitHub PR number from gh commands (most recent)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub github_pr: Option<i32>,

    /// Lines added (from git diff vs main/master or gh pr view)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub git_additions: Option<i32>,

    /// Lines deleted (from git diff vs main/master or gh pr view)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub git_deletions: Option<i32>,

    // --- Conversation ---
    /// First user prompt (transcript can correct hook's initial value)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub first_user_message: Option<String>,

    /// Most recent user message text
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_user_message: Option<String>,

    /// Model used in most recent API request
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,

    /// Number of context compactions
    pub compaction_count: u32,

    // --- Metrics (unified, accumulate across revivals) ---
    /// Total user message count
    pub message_count: u32,

    /// Total tool call count
    pub tool_call_count: u32,

    /// Total API request count
    pub api_request_count: u32,

    /// Total failed API request count
    pub api_error_count: u32,

    // --- Token usage (unified, accumulates across revivals) ---
    /// Total input tokens
    pub tokens_input: i64,

    /// Total output tokens
    pub tokens_output: i64,

    /// Total cache read tokens
    pub tokens_cache_read: i64,

    /// Total cache write tokens
    pub tokens_cache_write: i64,

    /// Total cost in USD
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cost_usd: Option<f64>,

    // --- Context window ---
    /// Current context window usage (raw token count)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub context: Option<i64>,

    // --- Data source tracking ---
    /// Cumulative count of hook events (enables priority logic)
    pub n_hook_events: u32,

    /// Cumulative count of OTEL events (enables priority logic)
    pub n_otel_events: u32,

    /// Whether transcript has been successfully parsed
    pub transcript_available: bool,

    /// Full absolute path to transcript file
    #[serde(skip_serializing_if = "Option::is_none")]
    pub transcript_path: Option<String>,
}

impl Session {
    /// Check if the session is still active (no end time recorded)
    pub fn is_active(&self) -> bool {
        self.last_ended_at.is_none()
    }

    /// Check if the session's process is still running.
    ///
    /// Returns `true` if the session has a PID and that process is still alive.
    /// Returns `false` if there's no PID or the process is not running.
    pub fn is_process_running(&self) -> bool {
        self.pid.is_some_and(is_process_alive)
    }

    /// Total tokens across all categories
    pub fn total_tokens(&self) -> i64 {
        self.tokens_input + self.tokens_output + self.tokens_cache_read + self.tokens_cache_write
    }

    /// Duration of the session (from first start to last end or last activity)
    pub fn duration(&self) -> chrono::Duration {
        let end = self.last_ended_at.unwrap_or(self.last_activity);
        end - self.first_started_at
    }

    /// Determine which source currently owns the metrics.
    ///
    /// Priority: transcript > otel > hook
    pub fn metrics_source(&self) -> &'static str {
        if self.transcript_available {
            "transcript"
        } else if self.n_otel_events > 0 {
            "otel"
        } else {
            "hook"
        }
    }

    /// Whether OTEL events should update token metrics.
    ///
    /// OTEL can update tokens only when transcript is not available.
    pub fn should_otel_update_tokens(&self) -> bool {
        !self.transcript_available
    }

    /// Whether hook events should update message/tool counts.
    ///
    /// Hooks can update counts only when transcript is not available.
    pub fn should_hook_update_counts(&self) -> bool {
        !self.transcript_available
    }

    /// Check if this session uses a shared process (one process for many sessions).
    ///
    /// For frameworks like Cursor IDE, a single long-running process hosts multiple
    /// sessions. PID-based liveness detection doesn't work reliably for these because
    /// the process stays alive even after individual sessions end.
    ///
    /// Use this to skip PID timestamp validation for shared-process frameworks.
    pub fn uses_shared_process(&self) -> bool {
        self.framework.as_ref().is_some_and(|f| f == "cursor")
    }
}

impl Default for Session {
    fn default() -> Self {
        let now = Utc::now();
        Self {
            session_id: String::new(),
            machine_id: String::new(),
            framework: None,
            first_started_at: now,
            last_started_at: now,
            first_ended_at: None,
            last_ended_at: None,
            last_activity: now,
            revival_count: 0,
            status: None,
            status_updated_at: None,
            pid: None,
            process_start_time: None,
            cwd: None,
            local_git_dir: None,
            git_branch: None,
            git_worktree_path: None,
            github_repo: None,
            github_issue: None,
            github_pr: None,
            git_additions: None,
            git_deletions: None,
            first_user_message: None,
            last_user_message: None,
            model: None,
            compaction_count: 0,
            message_count: 0,
            tool_call_count: 0,
            api_request_count: 0,
            api_error_count: 0,
            tokens_input: 0,
            tokens_output: 0,
            tokens_cache_read: 0,
            tokens_cache_write: 0,
            cost_usd: None,
            context: None,
            n_hook_events: 0,
            n_otel_events: 0,
            transcript_available: false,
            transcript_path: None,
        }
    }
}
