//! Encrypted session middleware for handling encrypted user sessions
//!
//! This module provides middleware for managing encrypted user sessions using
//! the crypto service to encrypt/decrypt session data containing user
//! information.

use std::sync::Arc;

use axum::{
    extract::{Request, State},
    http::StatusCode,
    middleware::Next,
    response::Response,
};
use chrono::{Duration, Utc};
use rustelo_core_lib::auth::{Role, User, UserProfile};
use time::OffsetDateTime;
use tower_cookies::{Cookie, Cookies};
use tower_sessions::cookie::SameSite;
use uuid::Uuid;

use crate::crypto::{CryptoError, CryptoService};

/// Configuration for encrypted sessions
#[derive(Debug, Clone)]
pub struct EncryptedSessionConfig {
    /// Cookie name for the encrypted session
    pub cookie_name: String,
    /// Session lifetime in seconds
    pub session_lifetime: i64,
    /// Whether to use secure cookies (HTTPS only)
    pub secure: bool,
    /// Whether to use HTTP-only cookies
    pub http_only: bool,
    /// Cookie path
    pub path: String,
    /// Cookie domain
    pub domain: Option<String>,
    /// Cookie same site policy
    pub same_site: SameSite,
}

impl Default for EncryptedSessionConfig {
    fn default() -> Self {
        Self {
            cookie_name: "encrypted_session".to_string(),
            session_lifetime: 3600, // 1 hour
            secure: false,          // Set to true in production with HTTPS
            http_only: true,
            path: "/".to_string(),
            domain: None,
            same_site: SameSite::Lax,
        }
    }
}

/// Encrypted session store
#[derive(Debug, Clone)]
pub struct EncryptedSessionStore {
    crypto: Arc<CryptoService>,
    config: EncryptedSessionConfig,
}

impl EncryptedSessionStore {
    /// Create a new encrypted session store
    pub fn new(crypto: Arc<CryptoService>, config: EncryptedSessionConfig) -> Self {
        Self { crypto, config }
    }

    /// Get the session configuration
    pub fn config(&self) -> &EncryptedSessionConfig {
        &self.config
    }

    /// Create a new encrypted session store with default config
    pub fn with_default_config(crypto: Arc<CryptoService>) -> Self {
        Self::new(crypto, EncryptedSessionConfig::default())
    }

    /// Create an encrypted session for a user
    pub fn create_session(&self, user: &User) -> Result<String, CryptoError> {
        let session_data =
            crate::crypto::utils::user_to_session_data(user, self.config.session_lifetime);
        self.crypto.encrypt_json(&session_data)
    }

    /// Get user session data from encrypted session
    pub fn get_session(
        &self,
        encrypted_session: &str,
    ) -> Result<crate::crypto::EncryptedSessionData, CryptoError> {
        crate::crypto::utils::decrypt_user_session(&self.crypto, encrypted_session)
    }

    /// Set session cookie
    pub fn set_session_cookie(&self, cookies: &Cookies, encrypted_session: &str) {
        let mut cookie = Cookie::new(
            self.config.cookie_name.clone(),
            encrypted_session.to_string(),
        );
        cookie.set_path(self.config.path.clone());
        cookie.set_http_only(self.config.http_only);
        cookie.set_secure(self.config.secure);
        cookie.set_same_site(self.config.same_site);

        if let Some(domain) = &self.config.domain {
            cookie.set_domain(domain.clone());
        }

        let expires = Utc::now() + Duration::seconds(self.config.session_lifetime);
        let expires_time = OffsetDateTime::from_unix_timestamp(expires.timestamp())
            .unwrap_or_else(|_| OffsetDateTime::now_utc());
        cookie.set_expires(Some(expires_time));

        cookies.add(cookie);
    }

    /// Get session cookie value
    pub fn get_session_cookie(&self, cookies: &Cookies) -> Option<String> {
        cookies
            .get(&self.config.cookie_name)
            .map(|c| c.value().to_string())
    }

    /// Remove session cookie
    pub fn remove_session_cookie(&self, cookies: &Cookies) {
        cookies.remove(Cookie::from(self.config.cookie_name.clone()));
    }

    /// Refresh session (extend expiry)
    pub fn refresh_session(&self, user: &User) -> Result<String, CryptoError> {
        self.create_session(user)
    }
}

/// Extension trait for request to get encrypted session data
pub trait EncryptedSessionExt {
    /// Get the current encrypted session data
    fn encrypted_session(&self) -> Option<&crate::crypto::EncryptedSessionData>;

    /// Get the current user from encrypted session
    fn current_user(&self) -> Option<User>;

    /// Check if user has a specific role
    fn has_role(&self, role: &Role) -> bool;

    /// Check if user has any of the specified roles
    fn has_any_role(&self, roles: &[Role]) -> bool;

    /// Check if user has a specific category
    fn has_category(&self, category: &str) -> bool;

    /// Check if user has a specific tag
    fn has_tag(&self, tag: &str) -> bool;
}

/// Request extension for encrypted session data
#[derive(Clone)]
pub struct EncryptedSessionData {
    pub session_data: Option<crate::crypto::EncryptedSessionData>,
}

impl EncryptedSessionExt for Request {
    fn encrypted_session(&self) -> Option<&crate::crypto::EncryptedSessionData> {
        self.extensions()
            .get::<EncryptedSessionData>()
            .and_then(|ext| ext.session_data.as_ref())
    }

    fn current_user(&self) -> Option<User> {
        self.encrypted_session().map(|session| {
            // Convert session data back to User struct
            let roles: Vec<Role> = session
                .roles
                .iter()
                .filter_map(|r| match r.as_str() {
                    "Admin" => Some(Role::Admin),
                    "Moderator" => Some(Role::Moderator),
                    "User" => Some(Role::User),
                    "Guest" => Some(Role::Guest),
                    _ => None,
                })
                .collect();

            User {
                id: Uuid::parse_str(&session.user_id).unwrap_or_default(),
                email: session.email.clone(),
                username: session.username.clone(),
                display_name: session.display_name.clone(),
                avatar_url: None,
                roles,
                is_active: true,
                email_verified: true,
                created_at: Utc::now(),
                updated_at: Utc::now(),
                last_login: None,
                two_factor_enabled: false,
                profile: UserProfile {
                    first_name: None,
                    last_name: None,
                    bio: None,
                    timezone: None,
                    locale: None,
                    preferences: session.preferences.clone(),
                    categories: session.categories.clone(),
                    tags: session.tags.clone(),
                },
            }
        })
    }

    fn has_role(&self, role: &Role) -> bool {
        self.current_user()
            .map(|user| user.has_role(role))
            .unwrap_or(false)
    }

    fn has_any_role(&self, roles: &[Role]) -> bool {
        self.current_user()
            .map(|user| roles.iter().any(|r| user.has_role(r)))
            .unwrap_or(false)
    }

    fn has_category(&self, category: &str) -> bool {
        self.encrypted_session()
            .map(|session| session.categories.contains(&category.to_string()))
            .unwrap_or(false)
    }

    fn has_tag(&self, tag: &str) -> bool {
        self.encrypted_session()
            .map(|session| session.tags.contains(&tag.to_string()))
            .unwrap_or(false)
    }
}

/// Middleware for handling encrypted sessions
pub async fn encrypted_session_middleware(
    State(session_store): State<Arc<EncryptedSessionStore>>,
    cookies: Cookies,
    mut request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let session_data = if let Some(encrypted_session) = session_store.get_session_cookie(&cookies) {
        match session_store.get_session(&encrypted_session) {
            Ok(session) => Some(session),
            Err(e) => {
                tracing::debug!("Failed to decrypt session: {}", e);
                // Remove invalid session cookie
                session_store.remove_session_cookie(&cookies);
                None
            }
        }
    } else {
        None
    };

    // Add session data to request extensions
    request
        .extensions_mut()
        .insert(EncryptedSessionData { session_data });

    Ok(next.run(request).await)
}

/// Middleware to require authentication
pub async fn require_auth_middleware(request: Request, next: Next) -> Result<Response, StatusCode> {
    if request.encrypted_session().is_none() {
        return Err(StatusCode::UNAUTHORIZED);
    }

    Ok(next.run(request).await)
}

/// Middleware to require specific role
pub fn require_role_middleware(
    required_role: Role,
) -> impl Fn(
    Request,
    Next,
) -> std::pin::Pin<
    Box<dyn std::future::Future<Output = Result<Response, StatusCode>> + Send>,
> + Clone {
    move |request: Request, next: Next| {
        let required_role = required_role.clone();
        Box::pin(async move {
            if !request.has_role(&required_role) {
                return Err(StatusCode::FORBIDDEN);
            }

            Ok(next.run(request).await)
        })
    }
}

/// Middleware to require any of the specified roles
pub fn require_any_role_middleware(
    required_roles: Vec<Role>,
) -> impl Fn(
    Request,
    Next,
) -> std::pin::Pin<
    Box<dyn std::future::Future<Output = Result<Response, StatusCode>> + Send>,
> + Clone {
    move |request: Request, next: Next| {
        let required_roles = required_roles.clone();
        Box::pin(async move {
            if !request.has_any_role(&required_roles) {
                return Err(StatusCode::FORBIDDEN);
            }

            Ok(next.run(request).await)
        })
    }
}

/// Middleware to require specific category access
pub fn require_category_middleware(
    required_category: String,
) -> impl Fn(
    Request,
    Next,
) -> std::pin::Pin<
    Box<dyn std::future::Future<Output = Result<Response, StatusCode>> + Send>,
> + Clone {
    move |request: Request, next: Next| {
        let required_category = required_category.clone();
        Box::pin(async move {
            if !request.has_category(&required_category) {
                return Err(StatusCode::FORBIDDEN);
            }

            Ok(next.run(request).await)
        })
    }
}

/// Middleware to require specific tag access
pub fn require_tag_middleware(
    required_tag: String,
) -> impl Fn(
    Request,
    Next,
) -> std::pin::Pin<
    Box<dyn std::future::Future<Output = Result<Response, StatusCode>> + Send>,
> + Clone {
    move |request: Request, next: Next| {
        let required_tag = required_tag.clone();
        Box::pin(async move {
            if !request.has_tag(&required_tag) {
                return Err(StatusCode::FORBIDDEN);
            }

            Ok(next.run(request).await)
        })
    }
}

/// Helper functions for session management
pub mod session_helpers {
    use super::*;

    /// Login user and create encrypted session
    pub async fn login_user(
        session_store: &EncryptedSessionStore,
        cookies: &Cookies,
        user: &User,
    ) -> Result<String, CryptoError> {
        let encrypted_session = session_store.create_session(user)?;
        session_store.set_session_cookie(cookies, &encrypted_session);
        Ok(encrypted_session)
    }

    /// Logout user and remove session
    pub fn logout_user(session_store: &EncryptedSessionStore, cookies: &Cookies) {
        session_store.remove_session_cookie(cookies);
    }

    /// Refresh user session
    pub async fn refresh_user_session(
        session_store: &EncryptedSessionStore,
        cookies: &Cookies,
        user: &User,
    ) -> Result<String, CryptoError> {
        let encrypted_session = session_store.refresh_session(user)?;
        session_store.set_session_cookie(cookies, &encrypted_session);
        Ok(encrypted_session)
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use chrono::Utc;
    use uuid::Uuid;

    use super::*;
    use crate::crypto::CryptoService;

    fn create_test_user() -> User {
        User {
            id: Uuid::new_v4(),
            email: "test@example.com".to_string(),
            username: "testuser".to_string(),
            display_name: Some("Test User".to_string()),
            avatar_url: None,
            roles: vec![Role::User],
            is_active: true,
            email_verified: true,
            created_at: Utc::now(),
            updated_at: Utc::now(),
            last_login: None,
            two_factor_enabled: false,
            profile: UserProfile {
                first_name: Some("Test".to_string()),
                last_name: Some("User".to_string()),
                bio: None,
                timezone: Some("UTC".to_string()),
                locale: Some("en".to_string()),
                preferences: [("theme".to_string(), "dark".to_string())].into(),
                categories: vec!["tech".to_string(), "programming".to_string()],
                tags: vec!["rust".to_string(), "web".to_string()],
            },
        }
    }

    #[test]
    fn test_encrypted_session_store_creation() {
        let crypto = Arc::new(CryptoService::new().unwrap());
        let config = EncryptedSessionConfig::default();
        let store = EncryptedSessionStore::new(crypto, config);

        assert_eq!(store.config.cookie_name, "encrypted_session");
        assert_eq!(store.config.session_lifetime, 3600);
    }

    #[test]
    fn test_create_and_get_session() {
        let crypto = Arc::new(CryptoService::new().unwrap());
        let store = EncryptedSessionStore::with_default_config(crypto);
        let user = create_test_user();

        let encrypted_session = store.create_session(&user).unwrap();
        let session_data = store.get_session(&encrypted_session).unwrap();

        assert_eq!(session_data.user_id, user.id.to_string());
        assert_eq!(session_data.email, user.email);
        assert_eq!(session_data.username, user.username);
        assert_eq!(session_data.categories, user.profile.categories);
        assert_eq!(session_data.tags, user.profile.tags);
    }

    #[test]
    fn test_session_expiry() {
        let crypto = Arc::new(CryptoService::new().unwrap());
        let mut config = EncryptedSessionConfig::default();
        config.session_lifetime = -1; // Expired
        let store = EncryptedSessionStore::new(crypto, config);
        let user = create_test_user();

        let encrypted_session = store.create_session(&user).unwrap();
        let result = store.get_session(&encrypted_session);

        assert!(result.is_err());
    }

    #[test]
    fn test_session_refresh() {
        let crypto = Arc::new(CryptoService::new().unwrap());
        let store = EncryptedSessionStore::with_default_config(crypto);
        let user = create_test_user();

        let session1 = store.create_session(&user).unwrap();
        let session2 = store.refresh_session(&user).unwrap();

        // Sessions should be different but both valid
        assert_ne!(session1, session2);
        assert!(store.get_session(&session1).is_ok());
        assert!(store.get_session(&session2).is_ok());
    }
}
