/// Adapter that bridges `rustelo_server::auth::OtpService` to the
/// `rustelo_components_leptos::auth::OtpServiceFacade` trait so that Leptos
/// server functions defined in `rustelo_components_leptos` can invoke the
/// concrete OTP backend without a reverse dependency.
#[cfg(all(feature = "auth", feature = "leptos-hydration"))]
pub mod adapter {
    use std::sync::Arc;

    use rustelo_components_leptos::auth::{
        otp_service::{
            server::{BoxFuture, OtpServiceFacade},
            OtpVerifyResult,
        },
        server_fns::{
            BookmarkInfo, CreatedServiceToken, ServiceTokenInfo, UserMessage, UserNote,
            UserResource,
        },
    };
    use rustelo_core_lib::auth::User;
    use rustelo_core_lib::state::UserInfo;
    use uuid::Uuid;

    use crate::{
        auth::OtpService, config::AuthOtpConfig, content::websocket::WebSocketServer,
        database::auth::AuthRepositoryTrait,
    };

    pub struct OtpServiceAdapter {
        pub otp_service: Arc<OtpService>,
        pub config: AuthOtpConfig,
        /// Optional WS server reference for security event notifications.
        /// Set to `Some` in startup to enable cross-device security alerts and
        /// new-device-login notifications.
        pub ws_server: Option<Arc<WebSocketServer>>,
    }

    impl OtpServiceAdapter {
        /// Resolve a session token to a user UUID via JWT validation.
        fn resolve_user_id(svc: &OtpService, session_token: &str) -> Result<Uuid, String> {
            let claims = svc
                .jwt_svc
                .verify_access_token(session_token)
                .map_err(|e| format!("Invalid session: {e}"))?;

            Uuid::parse_str(&claims.sub).map_err(|e| format!("Invalid user ID in token: {e}"))
        }
    }

    impl OtpServiceFacade for OtpServiceAdapter {
        fn request_otp(&self, email: &str, lang: &str) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let email = email.to_string();
            let lang = lang.to_string();
            let code_len = self.config.otp_code_length;
            let expiry = self.config.otp_expiry_minutes;
            Box::pin(async move {
                svc.request_otp(&email, code_len, expiry, &lang)
                    .await
                    .map_err(|e| e.to_string())
            })
        }

        fn verify_otp(
            &self,
            email: &str,
            code: &str,
        ) -> BoxFuture<Result<OtpVerifyResult, String>> {
            let svc = self.otp_service.clone();
            let ws = self.ws_server.clone();
            let email = email.to_string();
            let code = code.to_string();
            let max = self.config.otp_max_attempts;
            Box::pin(async move {
                use rustelo_core_lib::auth::AuthError;

                use crate::auth::otp::OtpVerifyOutcome;
                use crate::content::websocket::ContentNotification;

                let outcome_result = svc.verify_otp(&email, &code, max).await;

                // Security alert: notify existing sessions on brute-force detection.
                if let Err(AuthError::TooMany2FAAttempts) = &outcome_result {
                    if let Some(ref ws_ref) = ws {
                        if let Ok(Some(db_user)) = svc.auth_repo.find_user_by_email(&email).await {
                            ws_ref
                                .notify_user(
                                    db_user.id,
                                    ContentNotification::SecurityAlert {
                                        message: "Too many failed OTP attempts".to_string(),
                                    },
                                )
                                .await;
                        }
                    }
                }

                let outcome = outcome_result.map_err(|e| e.to_string())?;

                Ok(match outcome {
                    OtpVerifyOutcome::Authenticated(resp) => {
                        // New-device-login: notify other active sessions of the same user.
                        if let Some(ref ws_ref) = ws {
                            let session_hint = if resp.access_token.len() >= 8 {
                                format!("session:{}", &resp.access_token[..8])
                            } else {
                                "session:new".to_string()
                            };
                            ws_ref
                                .notify_user(
                                    resp.user.id,
                                    ContentNotification::NewDeviceLogin {
                                        session_id: session_hint,
                                    },
                                )
                                .await;
                        }

                        let user = resp.user;
                        OtpVerifyResult::Authenticated {
                            user: UserInfo {
                                id: user.id.to_string(),
                                email: user.email,
                                name: user.display_name,
                                roles: user.roles.iter().map(|r| format!("{r:?}")).collect(),
                                avatar_url: user.avatar_url,
                            },
                            session_token: resp.access_token,
                        }
                    }
                    OtpVerifyOutcome::NeedsConsent { pending_token } => {
                        OtpVerifyResult::NeedsConsent { pending_token }
                    }
                })
            })
        }

        fn accept_gdpr(&self, pending_token: &str) -> BoxFuture<Result<OtpVerifyResult, String>> {
            let svc = self.otp_service.clone();
            let token = pending_token.to_string();
            Box::pin(async move {
                let resp = svc.accept_gdpr(&token).await.map_err(|e| e.to_string())?;

                let user = resp.user;
                Ok(OtpVerifyResult::Authenticated {
                    user: UserInfo {
                        id: user.id.to_string(),
                        email: user.email,
                        name: user.display_name,
                        roles: user.roles.iter().map(|r| format!("{r:?}")).collect(),
                        avatar_url: user.avatar_url,
                    },
                    session_token: resp.access_token,
                })
            })
        }

        fn logout_session(&self, session_token: &str) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let token = session_token.to_string();
            Box::pin(async move { svc.logout_session(&token).await.map_err(|e| e.to_string()) })
        }

        fn get_bookmarks(
            &self,
            session_token: &str,
        ) -> BoxFuture<Result<Vec<BookmarkInfo>, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let rows = svc
                    .auth_repo
                    .get_bookmarks(user_id)
                    .await
                    .map_err(|e| e.to_string())?;

                Ok(rows
                    .into_iter()
                    .map(|r| BookmarkInfo {
                        id: r.id.to_string(),
                        content_type: r.content_type,
                        content_id: r.content_id,
                        content_title: r.content_title,
                        content_url: r.content_url,
                    })
                    .collect())
            })
        }

        fn add_bookmark(
            &self,
            session_token: &str,
            content_type: String,
            content_id: String,
            content_title: Option<String>,
            content_url: Option<String>,
        ) -> BoxFuture<Result<String, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                use crate::database::auth::AddBookmarkRequest;
                let req = AddBookmarkRequest {
                    user_id,
                    content_type,
                    content_id,
                    content_title,
                    content_url,
                };
                let id = svc
                    .auth_repo
                    .add_bookmark(&req)
                    .await
                    .map_err(|e| e.to_string())?;

                Ok(id.to_string())
            })
        }

        fn remove_bookmark(
            &self,
            session_token: &str,
            bookmark_id: String,
        ) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let bm_id = Uuid::parse_str(&bookmark_id)
                    .map_err(|e| format!("Invalid bookmark ID: {e}"))?;

                svc.auth_repo
                    .remove_bookmark(bm_id, user_id)
                    .await
                    .map_err(|e| e.to_string())
            })
        }

        fn is_bookmarked(
            &self,
            session_token: &str,
            content_id: String,
            content_type: String,
        ) -> BoxFuture<Result<bool, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                svc.auth_repo
                    .is_bookmarked(user_id, &content_id, &content_type)
                    .await
                    .map_err(|e| e.to_string())
            })
        }

        fn delete_account(&self, session_token: &str) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                svc.auth_repo
                    .delete_user_account(user_id)
                    .await
                    .map_err(|e| e.to_string())
            })
        }

        fn update_display_name(
            &self,
            session_token: &str,
            name: String,
        ) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let ws = self.ws_server.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                use crate::content::websocket::ContentNotification;
                svc.auth_repo
                    .update_display_name(user_id, name)
                    .await
                    .map_err(|e| e.to_string())?;
                if let Some(ref ws_ref) = ws {
                    ws_ref
                        .notify_user(
                            user_id,
                            ContentNotification::AccountUpdated {
                                field: "display_name".to_string(),
                            },
                        )
                        .await;
                }
                Ok(())
            })
        }

        fn refresh_session(&self, session_token: &str) -> BoxFuture<Result<String, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let db_user = svc
                    .auth_repo
                    .find_user_by_id(&user_id)
                    .await
                    .map_err(|e| e.to_string())?
                    .ok_or_else(|| format!("User {user_id} not found"))?;

                let user: User = db_user.into();
                svc.jwt_svc
                    .generate_access_token(&user)
                    .map_err(|e| e.to_string())
            })
        }

        fn logout_all_sessions(&self, session_token: &str) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let ws = self.ws_server.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                use crate::content::websocket::ContentNotification;
                svc.auth_repo
                    .invalidate_all_user_sessions(user_id)
                    .await
                    .map_err(|e| e.to_string())?;
                if let Some(ref ws_ref) = ws {
                    ws_ref
                        .notify_user(
                            user_id,
                            ContentNotification::SessionRevoked {
                                reason: "logout_all".to_string(),
                            },
                        )
                        .await;
                }
                Ok(())
            })
        }

        fn get_user_id_from_token(
            &self,
            session_token: &str,
        ) -> BoxFuture<Result<uuid::Uuid, String>> {
            let result = Self::resolve_user_id(&self.otp_service, session_token);
            Box::pin(async move { result })
        }

        fn get_session_user(
            &self,
            session_token: &str,
        ) -> BoxFuture<Result<rustelo_core_lib::state::UserInfo, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let db_user = svc
                    .auth_repo
                    .find_user_by_id(&user_id)
                    .await
                    .map_err(|e| e.to_string())?
                    .ok_or_else(|| format!("User {user_id} not found"))?;

                Ok(rustelo_core_lib::state::UserInfo {
                    id: db_user.id.to_string(),
                    email: db_user.email,
                    name: db_user.display_name,
                    roles: db_user.roles.iter().map(|r| format!("{r:?}")).collect(),
                    avatar_url: db_user.avatar_url,
                })
            })
        }

        fn get_messages(&self, session_token: &str) -> BoxFuture<Result<Vec<UserMessage>, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let rows = svc
                    .auth_repo
                    .get_messages(user_id)
                    .await
                    .map_err(|e| e.to_string())?;
                Ok(rows
                    .into_iter()
                    .map(|r| UserMessage {
                        id: r.id.to_string(),
                        sender_email: r.sender_email,
                        subject: r.subject,
                        body: r.body,
                        is_read: r.is_read,
                        status: r.status.as_str().to_owned(),
                        created_at: r.created_at.to_rfc3339(),
                        received_at: r.received_at.map(|d| d.to_rfc3339()),
                    })
                    .collect())
            })
        }

        fn mark_message_read(
            &self,
            session_token: &str,
            message_id: String,
        ) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let mid =
                    Uuid::parse_str(&message_id).map_err(|e| format!("Invalid message ID: {e}"))?;
                svc.auth_repo
                    .mark_message_read(mid, user_id)
                    .await
                    .map_err(|e| e.to_string())
            })
        }

        fn delete_message(
            &self,
            session_token: &str,
            message_id: String,
        ) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let mid =
                    Uuid::parse_str(&message_id).map_err(|e| format!("Invalid message ID: {e}"))?;
                svc.auth_repo
                    .delete_message(mid, user_id)
                    .await
                    .map_err(|e| e.to_string())
            })
        }

        fn get_notes(&self, session_token: &str) -> BoxFuture<Result<Vec<UserNote>, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let rows = svc
                    .auth_repo
                    .get_notes(user_id)
                    .await
                    .map_err(|e| e.to_string())?;
                Ok(rows
                    .into_iter()
                    .map(|r| UserNote {
                        id: r.id.to_string(),
                        title: r.title,
                        content: r.content,
                        created_at: r.created_at.to_rfc3339(),
                        updated_at: r.updated_at.to_rfc3339(),
                    })
                    .collect())
            })
        }

        fn create_note(
            &self,
            session_token: &str,
            title: String,
            content: String,
        ) -> BoxFuture<Result<String, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let id = svc
                    .auth_repo
                    .create_note(user_id, &title, &content)
                    .await
                    .map_err(|e| e.to_string())?;
                Ok(id.to_string())
            })
        }

        fn update_note(
            &self,
            session_token: &str,
            note_id: String,
            title: String,
            content: String,
        ) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let nid = Uuid::parse_str(&note_id).map_err(|e| format!("Invalid note ID: {e}"))?;
                svc.auth_repo
                    .update_note(nid, user_id, &title, &content)
                    .await
                    .map_err(|e| e.to_string())
            })
        }

        fn delete_note(
            &self,
            session_token: &str,
            note_id: String,
        ) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let nid = Uuid::parse_str(&note_id).map_err(|e| format!("Invalid note ID: {e}"))?;
                svc.auth_repo
                    .delete_note(nid, user_id)
                    .await
                    .map_err(|e| e.to_string())
            })
        }

        fn get_resources(
            &self,
            session_token: &str,
        ) -> BoxFuture<Result<Vec<UserResource>, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let db_user = svc
                    .auth_repo
                    .find_user_by_id(&user_id)
                    .await
                    .map_err(|e| e.to_string())?
                    .ok_or_else(|| format!("User {user_id} not found"))?;

                let rows = svc
                    .auth_repo
                    .get_resources_for_roles(&db_user.roles)
                    .await
                    .map_err(|e| e.to_string())?;

                Ok(rows
                    .into_iter()
                    .map(|r| UserResource {
                        id: r.id.to_string(),
                        name: r.name,
                        description: r.description,
                        url: r.url,
                        resource_type: r.resource_type,
                        role_name: r.role_name,
                    })
                    .collect())
            })
        }

        // ── Service token management ──────────────────────────────────────────

        fn list_service_tokens(
            &self,
            session_token: &str,
        ) -> BoxFuture<Result<Vec<ServiceTokenInfo>, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let db_user = svc
                    .auth_repo
                    .find_user_by_id(&user_id)
                    .await
                    .map_err(|e| e.to_string())?
                    .ok_or_else(|| format!("User {user_id} not found"))?;
                if !db_user.roles.iter().any(|r| r == "admin") {
                    return Err("Forbidden: admin role required".to_string());
                }
                let rows = svc
                    .auth_repo
                    .list_service_tokens(user_id)
                    .await
                    .map_err(|e| e.to_string())?;
                Ok(rows
                    .into_iter()
                    .map(|r| ServiceTokenInfo {
                        id: r.id.to_string(),
                        name: r.name,
                        scopes: r.scopes,
                        is_active: r.is_active,
                        last_used_at: r.last_used_at.map(|d| d.to_rfc3339()),
                        expires_at: r.expires_at.map(|d| d.to_rfc3339()),
                        created_at: r.created_at.to_rfc3339(),
                    })
                    .collect())
            })
        }

        fn create_service_token(
            &self,
            session_token: &str,
            name: String,
            scopes: Vec<String>,
            expires_days: Option<u32>,
        ) -> BoxFuture<Result<CreatedServiceToken, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                use sha2::{Digest, Sha256};

                let db_user = svc
                    .auth_repo
                    .find_user_by_id(&user_id)
                    .await
                    .map_err(|e| e.to_string())?
                    .ok_or_else(|| format!("User {user_id} not found"))?;
                if !db_user.roles.iter().any(|r| r == "admin") {
                    return Err("Forbidden: admin role required".to_string());
                }

                // Generate a random 32-byte token, hex-encoded.
                let raw: String = {
                    use std::fmt::Write;
                    let bytes: [u8; 32] = rand::random();
                    let mut s = String::with_capacity(64);
                    for b in bytes {
                        write!(s, "{b:02x}").ok();
                    }
                    s
                };
                let hash = Sha256::digest(raw.as_bytes())
                    .iter()
                    .fold(String::new(), |mut s, b| {
                        use std::fmt::Write;
                        let _ = write!(s, "{b:02x}");
                        s
                    });

                let expires_at =
                    expires_days.map(|d| chrono::Utc::now() + chrono::Duration::days(i64::from(d)));

                let scopes_for_db = if scopes.is_empty() {
                    vec!["notify".to_string()]
                } else {
                    scopes
                };

                let token_id = svc
                    .auth_repo
                    .create_service_token(&name, &hash, user_id, &scopes_for_db, expires_at)
                    .await
                    .map_err(|e| e.to_string())?;

                Ok(CreatedServiceToken {
                    id: token_id.to_string(),
                    name,
                    raw_token: raw,
                })
            })
        }

        fn revoke_service_token(
            &self,
            session_token: &str,
            token_id: String,
        ) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let db_user = svc
                    .auth_repo
                    .find_user_by_id(&user_id)
                    .await
                    .map_err(|e| e.to_string())?
                    .ok_or_else(|| format!("User {user_id} not found"))?;
                if !db_user.roles.iter().any(|r| r == "admin") {
                    return Err("Forbidden: admin role required".to_string());
                }
                let tid = uuid::Uuid::parse_str(&token_id)
                    .map_err(|_| format!("Invalid token_id: {token_id}"))?;
                svc.auth_repo
                    .revoke_service_token(tid, user_id)
                    .await
                    .map_err(|e| e.to_string())
            })
        }

        // ── Activity completions ───────────────────────────────────────────

        fn check_activity_completion(
            &self,
            session_token: &str,
            activity_id: String,
        ) -> BoxFuture<Result<bool, String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                svc.auth_repo
                    .check_activity_completion(user_id, &activity_id)
                    .await
                    .map_err(|e| e.to_string())
            })
        }

        fn record_activity_completion(
            &self,
            session_token: &str,
            activity_id: String,
            questionnaire_type: String,
        ) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                svc.auth_repo
                    .record_activity_completion(user_id, &activity_id, &questionnaire_type)
                    .await
                    .map(|_| ())
                    .map_err(|e| e.to_string())
            })
        }

        fn submit_internal_questionnaire(
            &self,
            session_token: &str,
            activity_id: String,
            responses: Vec<(String, String)>,
        ) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let user_id = match Self::resolve_user_id(&svc, session_token) {
                Ok(id) => id,
                Err(e) => return Box::pin(async move { Err(e) }),
            };
            Box::pin(async move {
                let completion_id = svc
                    .auth_repo
                    .record_activity_completion(user_id, &activity_id, "internal")
                    .await
                    .map_err(|e| e.to_string())?;
                svc.auth_repo
                    .store_activity_responses(completion_id, &responses)
                    .await
                    .map_err(|e| e.to_string())
            })
        }

        fn record_activity_completion_by_user_id(
            &self,
            user_id: &str,
            activity_id: String,
            questionnaire_type: &str,
        ) -> BoxFuture<Result<(), String>> {
            let svc = self.otp_service.clone();
            let uid = match uuid::Uuid::parse_str(user_id) {
                Ok(id) => id,
                Err(e) => {
                    let msg = format!("Invalid user_id in callback: {e}");
                    return Box::pin(async move { Err(msg) });
                }
            };
            let qt = questionnaire_type.to_string();
            Box::pin(async move {
                svc.auth_repo
                    .record_activity_completion(uid, &activity_id, &qt)
                    .await
                    .map(|_| ())
                    .map_err(|e| e.to_string())
            })
        }
    }
}

#[cfg(all(feature = "auth", feature = "leptos-hydration"))]
pub use adapter::OtpServiceAdapter;
