//! Session management for stateful conversations.

mod archive;
pub mod compact;
mod export;
pub mod lock;
pub mod manager;
pub mod memory;
pub mod persistence;
#[cfg(feature = "jsonl")]
pub mod persistence_jsonl;
#[cfg(feature = "postgres")]
pub mod persistence_postgres;
#[cfg(feature = "redis-backend")]
pub mod persistence_redis;
pub mod queue;
mod replay;
pub mod session_state;
pub mod state;
pub mod types;

pub use crate::types::TokenUsage;
pub use archive::{
    ArchivePolicy, RestoreVerificationReport, RestoreVerifier, SessionArchiveBundle,
    SessionArchiveService,
};
pub use compact::recovery::{
    ContextRecovery, RecoveryAction, RecoveryContext, RecoveryErrorKind, RecoveryStrategy,
};
pub use compact::{
    CompactConfig, CompactService, CompactionChain, CompactionChainBuilder, CompactionContext,
    CompactionPlan, CompactionStrategy, ContentOverrideEntry, DEFAULT_COMPACT_THRESHOLD,
    FullCompaction, MicroCompaction, TimeBasedCompaction,
};
pub use export::{AuditBundle, ExportPolicy, SessionExporter};
#[cfg(feature = "postgres")]
pub use lock::PostgresLock;
#[cfg(feature = "redis-backend")]
pub use lock::RedisLock;
pub use lock::{
    DEFAULT_LOCK_TTL_SECS, DEFAULT_MAX_RETRIES, DEFAULT_RETRY_DELAY_MS, DistributedLock,
    LOCK_KEY_PREFIX, LockGuard,
};
pub use manager::{ScopedSessionManager, SessionManager};
pub use memory::{InMemoryStore, MemoryEntry, MemoryStore};
pub use persistence::{MemoryPersistence, Persistence, PersistenceFactory, SessionFilter};
#[cfg(feature = "jsonl")]
pub use persistence_jsonl::{
    JsonlConfig, JsonlConfigBuilder, JsonlEntry, JsonlPersistence, SyncMode,
};
#[cfg(feature = "postgres")]
pub use persistence_postgres::{
    PgPoolConfig, PostgresConfig, PostgresPersistence, PostgresSchema, SchemaIssue,
};
#[cfg(feature = "redis-backend")]
pub use persistence_redis::{RedisConfig, RedisPersistence};
pub use queue::{InputQueue, MergedInput, QueueError, QueuedInput, SharedInputQueue};
pub use replay::ReplayService;
pub use session_state::{ExecutionGuard, ExecutionState, SessionSnapshot, ToolState};
pub use state::{
    ContentOverrides, ExecutionMetadata, MessageId, MessageMetadata, Session, SessionAuthorization,
    SessionConfig, SessionId, SessionMessage, SessionState, SessionToolLimits, SessionType,
    ThinkingMetadata, ToolResultMeta,
};
pub use types::{
    CompactRecord, CompactTrigger, EnvironmentContext, Plan, PlanStatus, QueueItem, QueueOperation,
    QueueStatus, SessionAccessScope, SessionStats, SessionTree, TodoItem, TodoStatus,
    ToolExecution,
};

use thiserror::Error;

#[derive(Error, Debug)]
#[non_exhaustive]
pub enum SessionError {
    #[error("Session not found: {id}")]
    NotFound { id: String },

    #[error("Session expired: {id}")]
    Expired { id: String },

    #[error("Storage error: {message}")]
    Storage { message: String },

    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    #[error("Compact error: {message}")]
    Compact { message: String },

    #[error("Context error: {0}")]
    Context(#[from] crate::context::ContextError),

    #[error("Invalid session ID: {value}")]
    InvalidId { value: String },

    #[error("Queue full: {message}")]
    QueueFull { message: String },

    #[error("Channel closed: {message}")]
    ChannelClosed { message: String },

    #[error("Execution error: {message}")]
    Execution { message: String },
}

pub type SessionResult<T> = std::result::Result<T, SessionError>;

#[cfg(any(feature = "postgres", feature = "redis-backend"))]
pub(crate) trait StorageResultExt<T> {
    fn storage_err(self) -> SessionResult<T>;
}

#[cfg(any(feature = "postgres", feature = "redis-backend"))]
pub(crate) async fn with_retry<F, Fut, T>(
    max_retries: u32,
    initial_backoff: std::time::Duration,
    max_backoff: std::time::Duration,
    is_retryable: impl Fn(&SessionError) -> bool,
    operation: F,
) -> SessionResult<T>
where
    F: Fn() -> Fut,
    Fut: std::future::Future<Output = SessionResult<T>>,
{
    let mut attempt = 0;
    let mut backoff = initial_backoff;

    loop {
        match operation().await {
            Ok(result) => return Ok(result),
            Err(e) if attempt < max_retries && is_retryable(&e) => {
                attempt += 1;
                tracing::warn!(
                    attempt = attempt,
                    error = %e,
                    "Retrying operation after transient failure"
                );
                // Symmetrical 10% jitter to prevent thundering herd
                let jitter_factor = 1.0 + (rand::random::<f64>() * 0.2 - 0.1);
                tokio::time::sleep(backoff.mul_f64(jitter_factor)).await;
                backoff = (backoff * 2).min(max_backoff);
            }
            Err(e) => return Err(e),
        }
    }
}

#[cfg(any(feature = "postgres", feature = "redis-backend"))]
impl<T, E: std::fmt::Display> StorageResultExt<T> for std::result::Result<T, E> {
    fn storage_err(self) -> SessionResult<T> {
        self.map_err(|e| SessionError::Storage {
            message: e.to_string(),
        })
    }
}

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

    #[test]
    fn test_session_error_display() {
        let err = SessionError::NotFound {
            id: "test-123".to_string(),
        };
        assert!(err.to_string().contains("test-123"));
    }

    #[test]
    fn test_session_error_expired() {
        let err = SessionError::Expired {
            id: "sess-456".to_string(),
        };
        assert!(err.to_string().contains("expired"));
    }
}
