//! Authentication domain logic — OPAQUE registration and login.
//!
//! This module contains the pure business logic for OPAQUE auth,
//! extracted from `node_service/auth_ops.rs`. It operates on domain
//! types and the `Store` trait, with no dependency on Cap'n Proto or Protobuf.

use std::sync::Arc;

use dashmap::DashMap;
use opaque_ke::ServerSetup;
use quicprochat_core::opaque_auth::OpaqueSuite;

use crate::auth::{AuthConfig, PendingLogin, SessionInfo};
use crate::storage::{Store, StorageError};

use super::types::*;

/// Shared state needed by auth operations.
pub struct AuthService {
    pub store: Arc<dyn Store>,
    pub opaque_setup: Arc<ServerSetup<OpaqueSuite>>,
    pub pending_logins: Arc<DashMap<String, PendingLogin>>,
    pub sessions: Arc<DashMap<Vec<u8>, SessionInfo>>,
    pub auth_cfg: Arc<AuthConfig>,
}

impl AuthService {
    /// Validate a session token and return the caller's auth context.
    pub fn validate_session(&self, token: &[u8]) -> Option<CallerAuth> {
        let info = self.sessions.get(token)?;
        let expires_at = info.expires_at;
        let identity_key = info.identity_key.clone();
        drop(info); // release read-lock before potential write
        if expires_at <= crate::auth::current_timestamp() {
            self.sessions.remove(token);
            return None;
        }
        Some(CallerAuth {
            identity_key,
            token: token.to_vec(),
            device_id: None,
        })
    }

    /// Start OPAQUE registration.
    pub fn register_start(&self, req: RegisterStartReq) -> Result<RegisterStartResp, StorageError> {
        use opaque_ke::ServerRegistration;

        let result = ServerRegistration::<OpaqueSuite>::start(
            &self.opaque_setup,
            opaque_ke::RegistrationRequest::deserialize(&req.request_bytes)
                .map_err(|e| StorageError::Io(format!("bad registration request: {e}")))?,
            req.username.as_bytes(),
        )
        .map_err(|e| StorageError::Io(format!("OPAQUE register start: {e}")))?;

        let response_bytes = result.message.serialize().to_vec();
        Ok(RegisterStartResp { response_bytes })
    }

    /// Finish OPAQUE registration — persist user record and identity key.
    pub fn register_finish(&self, req: RegisterFinishReq) -> Result<RegisterFinishResp, StorageError> {
        let upload = opaque_ke::RegistrationUpload::<OpaqueSuite>::deserialize(&req.upload_bytes)
            .map_err(|e| StorageError::Io(format!("bad registration upload: {e}")))?;

        let record = opaque_ke::ServerRegistration::<OpaqueSuite>::finish(upload);
        let serialized = record.serialize().to_vec();

        self.store.store_user_record(&req.username, serialized)?;
        self.store
            .store_user_identity_key(&req.username, req.identity_key)?;

        Ok(RegisterFinishResp { success: true })
    }
}

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

    fn test_service() -> AuthService {
        let dir = tempfile::tempdir().unwrap();
        let store = Arc::new(crate::storage::FileBackedStore::open(dir.path()).unwrap());

        let mut rng = rand::rngs::OsRng;
        let opaque_setup = ServerSetup::<OpaqueSuite>::new(&mut rng);

        AuthService {
            store,
            opaque_setup: Arc::new(opaque_setup),
            pending_logins: Arc::new(DashMap::new()),
            sessions: Arc::new(DashMap::new()),
            auth_cfg: Arc::new(AuthConfig {
                required_token: None,
                allow_insecure_identity_from_request: false,
            }),
        }
    }

    #[test]
    fn validate_session_valid_token() {
        let svc = test_service();
        let token = vec![1u8; 16];
        let ik = vec![2u8; 32];

        svc.sessions.insert(
            token.clone(),
            SessionInfo {
                username: "alice".to_string(),
                identity_key: ik.clone(),
                created_at: crate::auth::current_timestamp(),
                expires_at: crate::auth::current_timestamp() + 3600,
            },
        );

        let auth = svc.validate_session(&token).unwrap();
        assert_eq!(auth.identity_key, ik);
        assert_eq!(auth.token, token);
        assert!(auth.device_id.is_none());
    }

    #[test]
    fn validate_session_expired_token() {
        let svc = test_service();
        let token = vec![3u8; 16];

        svc.sessions.insert(
            token.clone(),
            SessionInfo {
                username: "bob".to_string(),
                identity_key: vec![4u8; 32],
                created_at: 0,
                expires_at: 0, // already expired
            },
        );

        assert!(svc.validate_session(&token).is_none());
        // Expired session should be removed from the map
        assert!(!svc.sessions.contains_key(&token));
    }

    #[test]
    fn validate_session_missing_token() {
        let svc = test_service();
        assert!(svc.validate_session(&[0u8; 16]).is_none());
    }

    #[test]
    fn validate_session_removes_expired_on_check() {
        let svc = test_service();
        let token = vec![5u8; 16];

        svc.sessions.insert(
            token.clone(),
            SessionInfo {
                username: "eve".to_string(),
                identity_key: vec![6u8; 32],
                created_at: 0,
                expires_at: 1, // expired long ago
            },
        );

        // First check: returns None and removes
        assert!(svc.validate_session(&token).is_none());
        // Session should be gone
        assert!(svc.sessions.is_empty());
    }
}
