//! Session management — trait definition and helper functions.
#[cfg(test)]
use std::sync::Arc;

use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

#[cfg(test)]
use crate::error::AuthError;
use crate::error::Result;

/// Session data stored in the backend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionData {
    /// User ID (unique per user)
    pub user_id:            String,
    /// Session issued timestamp (Unix seconds)
    pub issued_at:          u64,
    /// Session expiration timestamp (Unix seconds)
    pub expires_at:         u64,
    /// Hash of the refresh token (stored securely)
    pub refresh_token_hash: String,
}

impl SessionData {
    /// Check if session is expired
    pub fn is_expired(&self) -> bool {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        self.expires_at <= now
    }
}

/// Token pair returned after successful authentication
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenPair {
    /// JWT access token (short-lived, typically 15 min - 1 hour)
    pub access_token:  String,
    /// Refresh token (long-lived, typically 7-30 days)
    pub refresh_token: String,
    /// Time in seconds until access token expires
    pub expires_in:    u64,
}

/// SessionStore trait - implement this for your storage backend
///
/// # Examples
///
/// Implement for PostgreSQL:
/// ```no_run
/// // Requires: sqlx PgPool and live PostgreSQL connection.
/// use async_trait::async_trait;
/// use fraiseql_auth::session::{SessionStore, SessionData, TokenPair};
/// use fraiseql_auth::error::Result;
///
/// pub struct PostgresSessionStore {
///     // pool: sqlx::PgPool,
/// }
///
/// #[async_trait]
/// impl SessionStore for PostgresSessionStore {
///     async fn create_session(&self, _user_id: &str, _expires_at: u64) -> Result<TokenPair> {
///         panic!("example stub")
///     }
///     async fn get_session(&self, _refresh_token_hash: &str) -> Result<SessionData> {
///         panic!("example stub")
///     }
///     async fn revoke_session(&self, _refresh_token_hash: &str) -> Result<()> {
///         panic!("example stub")
///     }
///     async fn revoke_all_sessions(&self, _user_id: &str) -> Result<()> {
///         panic!("example stub")
///     }
/// }
/// ```
///
/// Implement for Redis:
/// ```no_run
/// // Requires: redis crate and live Redis connection.
/// use async_trait::async_trait;
/// use fraiseql_auth::session::{SessionStore, SessionData, TokenPair};
/// use fraiseql_auth::error::Result;
///
/// pub struct RedisSessionStore {
///     // client: redis::Client,
/// }
///
/// #[async_trait]
/// impl SessionStore for RedisSessionStore {
///     async fn create_session(&self, _user_id: &str, _expires_at: u64) -> Result<TokenPair> {
///         panic!("example stub")
///     }
///     async fn get_session(&self, _refresh_token_hash: &str) -> Result<SessionData> {
///         panic!("example stub")
///     }
///     async fn revoke_session(&self, _refresh_token_hash: &str) -> Result<()> {
///         panic!("example stub")
///     }
///     async fn revoke_all_sessions(&self, _user_id: &str) -> Result<()> {
///         panic!("example stub")
///     }
/// }
/// ```
// Reason: used as dyn Trait (Arc<dyn SessionStore>); async_trait ensures Send bounds and
// dyn-compatibility async_trait: dyn-dispatch required; remove when RTN + Send is stable (RFC 3425)
#[async_trait]
pub trait SessionStore: Send + Sync {
    /// Create a new session and return token pair
    ///
    /// # Arguments
    /// * `user_id` - The user identifier
    /// * `expires_at` - When the session should expire (Unix seconds)
    ///
    /// # Returns
    /// TokenPair with access_token and refresh_token
    ///
    /// # Errors
    /// Returns error if session creation fails
    async fn create_session(&self, user_id: &str, expires_at: u64) -> Result<TokenPair>;

    /// Get session data by refresh token hash
    ///
    /// # Arguments
    /// * `refresh_token_hash` - Hash of the refresh token
    ///
    /// # Returns
    /// SessionData if session exists and is not revoked
    ///
    /// # Errors
    /// Returns SessionError if session not found or revoked
    async fn get_session(&self, refresh_token_hash: &str) -> Result<SessionData>;

    /// Revoke a single session
    ///
    /// # Arguments
    /// * `refresh_token_hash` - Hash of the refresh token to revoke
    ///
    /// # Errors
    /// Returns error if revocation fails
    async fn revoke_session(&self, refresh_token_hash: &str) -> Result<()>;

    /// Revoke all sessions for a user
    ///
    /// # Arguments
    /// * `user_id` - The user identifier
    ///
    /// # Errors
    /// Returns error if revocation fails
    async fn revoke_all_sessions(&self, user_id: &str) -> Result<()>;
}

/// Compute a SHA-256 hex digest of a refresh token for secure storage.
///
/// Refresh tokens are stored only as their SHA-256 hash so that a database
/// breach cannot be used to replay sessions.  The original token is returned
/// to the client and never persisted.
pub fn hash_token(token: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(token.as_bytes());
    format!("{:x}", hasher.finalize())
}

/// Generate a cryptographically secure refresh token.
///
/// Returns 32 random bytes from [`rand::rngs::OsRng`] encoded as standard Base64.
/// The resulting token is 44 characters long and has approximately 256 bits of entropy.
pub fn generate_refresh_token() -> String {
    use base64::Engine;
    use rand::{Rng, rngs::OsRng};
    // SECURITY: OsRng ensures OS-level entropy for refresh tokens.
    let random_bytes: Vec<u8> = (0..32).map(|_| OsRng.gen()).collect();
    base64::engine::general_purpose::STANDARD.encode(&random_bytes)
}

/// In-memory session store for testing
#[cfg(test)]
pub struct InMemorySessionStore {
    sessions: Arc<dashmap::DashMap<String, SessionData>>,
}

#[cfg(test)]
impl InMemorySessionStore {
    /// Create a new in-memory session store
    pub fn new() -> Self {
        Self {
            sessions: Arc::new(dashmap::DashMap::new()),
        }
    }

    /// Clear all sessions (useful for tests)
    pub fn clear(&self) {
        self.sessions.clear();
    }

    /// Get number of sessions (useful for tests)
    pub fn len(&self) -> usize {
        self.sessions.len()
    }

    /// Check if there are no sessions
    pub fn is_empty(&self) -> bool {
        self.sessions.is_empty()
    }
}

#[cfg(test)]
impl Default for InMemorySessionStore {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
// Reason: SessionStore is defined with #[async_trait]; all implementations must match
// its transformed method signatures to satisfy the trait contract
// async_trait: dyn-dispatch required; remove when RTN + Send is stable (RFC 3425)
#[async_trait]
impl SessionStore for InMemorySessionStore {
    async fn create_session(&self, user_id: &str, expires_at: u64) -> Result<TokenPair> {
        let refresh_token = generate_refresh_token();
        let refresh_token_hash = hash_token(&refresh_token);

        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        let session = SessionData {
            user_id: user_id.to_string(),
            issued_at: now,
            expires_at,
            refresh_token_hash: refresh_token_hash.clone(),
        };

        self.sessions.insert(refresh_token_hash, session);

        let expires_in = expires_at.saturating_sub(now);

        // For testing, generate a dummy JWT (in real impl, would come from claims)
        let access_token = format!("access_token_{}", refresh_token);

        Ok(TokenPair {
            access_token,
            refresh_token,
            expires_in,
        })
    }

    async fn get_session(&self, refresh_token_hash: &str) -> Result<SessionData> {
        self.sessions
            .get(refresh_token_hash)
            .map(|entry| entry.clone())
            .ok_or(AuthError::TokenNotFound)
    }

    async fn revoke_session(&self, refresh_token_hash: &str) -> Result<()> {
        self.sessions.remove(refresh_token_hash).ok_or(AuthError::SessionError {
            message: "Session not found".to_string(),
        })?;
        Ok(())
    }

    async fn revoke_all_sessions(&self, user_id: &str) -> Result<()> {
        let mut to_remove = Vec::new();
        for entry in self.sessions.iter() {
            if entry.user_id == user_id {
                to_remove.push(entry.key().clone());
            }
        }

        for key in to_remove {
            self.sessions.remove(&key);
        }

        Ok(())
    }
}

#[allow(clippy::unwrap_used)] // Reason: test code, panics are acceptable
#[cfg(test)]
mod tests {
    #[allow(clippy::wildcard_imports)]
    // Reason: test module — wildcard keeps test boilerplate minimal
    use super::*;

    #[test]
    fn test_hash_token() {
        let token = "my_secret_token";
        let hash1 = hash_token(token);
        let hash2 = hash_token(token);

        // Same token should produce same hash
        assert_eq!(hash1, hash2);

        // Different token should produce different hash
        let different_hash = hash_token("different_token");
        assert_ne!(hash1, different_hash);
    }

    #[test]
    fn test_generate_refresh_token() {
        let token1 = generate_refresh_token();
        let token2 = generate_refresh_token();

        // Tokens should be random and different
        assert_ne!(token1, token2);
        // Should be non-empty
        assert!(!token1.is_empty());
        assert!(!token2.is_empty());
    }

    #[test]
    fn test_session_data_not_expired() {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        let session = SessionData {
            user_id:            "user123".to_string(),
            issued_at:          now,
            expires_at:         now + 3600,
            refresh_token_hash: "hash".to_string(),
        };

        assert!(!session.is_expired());
    }

    #[test]
    fn test_session_data_expired() {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        let session = SessionData {
            user_id:            "user123".to_string(),
            issued_at:          now - 3600,
            expires_at:         now - 100,
            refresh_token_hash: "hash".to_string(),
        };

        assert!(session.is_expired());
    }

    #[tokio::test]
    async fn test_in_memory_store_create_session() {
        let store = InMemorySessionStore::new();
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        let result = store.create_session("user123", now + 3600).await;
        let tokens = result.unwrap_or_else(|e| panic!("expected Ok from create_session: {e}"));
        assert!(!tokens.access_token.is_empty());
        assert!(!tokens.refresh_token.is_empty());
        assert!(tokens.expires_in > 0);
    }

    #[tokio::test]
    async fn test_in_memory_store_get_session() {
        let store = InMemorySessionStore::new();
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        let tokens = store.create_session("user123", now + 3600).await.unwrap();
        let refresh_token_hash = hash_token(&tokens.refresh_token);

        let session = store
            .get_session(&refresh_token_hash)
            .await
            .unwrap_or_else(|e| panic!("expected Ok from get_session: {e}"));
        assert_eq!(session.user_id, "user123");
    }

    #[tokio::test]
    async fn test_in_memory_store_revoke_session() {
        let store = InMemorySessionStore::new();
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        let tokens = store.create_session("user123", now + 3600).await.unwrap();
        let refresh_token_hash = hash_token(&tokens.refresh_token);

        store
            .revoke_session(&refresh_token_hash)
            .await
            .unwrap_or_else(|e| panic!("expected Ok from revoke_session: {e}"));

        let session = store.get_session(&refresh_token_hash).await;
        assert!(
            matches!(session, Err(AuthError::TokenNotFound)),
            "expected TokenNotFound after revocation, got: {session:?}"
        );
    }

    #[tokio::test]
    async fn test_in_memory_store_revoke_all_sessions() {
        let store = InMemorySessionStore::new();
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        // Create multiple sessions for same user
        let tokens1 = store.create_session("user123", now + 3600).await.unwrap();
        let tokens2 = store.create_session("user123", now + 3600).await.unwrap();

        // Create session for different user
        let tokens3 = store.create_session("user456", now + 3600).await.unwrap();

        assert_eq!(store.len(), 3);

        // Revoke all for user123
        store
            .revoke_all_sessions("user123")
            .await
            .unwrap_or_else(|e| panic!("expected Ok from revoke_all_sessions: {e}"));

        // user456 session should still exist
        let hash3 = hash_token(&tokens3.refresh_token);
        store
            .get_session(&hash3)
            .await
            .unwrap_or_else(|e| panic!("expected user456 session to still exist: {e}"));

        // user123 sessions should be gone
        let hash1 = hash_token(&tokens1.refresh_token);
        let hash2 = hash_token(&tokens2.refresh_token);
        assert!(
            matches!(store.get_session(&hash1).await, Err(AuthError::TokenNotFound)),
            "expected user123 session 1 to be revoked"
        );
        assert!(
            matches!(store.get_session(&hash2).await, Err(AuthError::TokenNotFound)),
            "expected user123 session 2 to be revoked"
        );
    }
}
