//! WebAuthn / passkey credential persistence layer.
//!
//! rok-auth provides the database CRUD operations for passkey credentials and
//! stays free of OpenSSL dependencies.  The WebAuthn ceremony (challenge
//! generation, attestation verification, assertion verification) is the
//! caller's responsibility using their chosen library, for example
//! [`webauthn-rs`](https://docs.rs/webauthn-rs).
//!
//! # Typical integration
//!
//! ```text
//! // Registration
//! let (ccr, reg_state) = webauthn.start_passkey_registration(...)?;
//! // → cache reg_state as JSON, send ccr to browser
//!
//! // Browser submits RegisterPublicKeyCredential
//! let passkey = webauthn.finish_passkey_registration(&reg_response, &reg_state)?;
//! PasskeyManager::store(pool, user_id, key_name, passkey.cred_id_b64(), passkey_json, 0).await?;
//!
//! // Authentication
//! let creds = PasskeyManager::load_json(pool, user_id).await?;
//! // Deserialize creds, run ceremony, then:
//! PasskeyManager::update_sign_count(pool, user_id, &cred_id_b64, new_count).await?;
//! ```

use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use chrono::{DateTime, Utc};
use rand::RngCore;
use sqlx::PgPool;

use crate::error::AuthError;

// ── DB row ────────────────────────────────────────────────────────────────────

/// A row from the `passkey_credentials` table.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct PasskeyCredential {
    pub id: i64,
    pub user_id: i64,
    /// Base64url-encoded WebAuthn credential ID bytes.
    pub credential_id: String,
    /// JSON blob of the full credential (caller's format — e.g. `webauthn_rs::Passkey`).
    pub passkey_json: String,
    pub sign_count: i64,
    pub name: String,
    pub created_at: DateTime<Utc>,
}

// ── helper ────────────────────────────────────────────────────────────────────

/// Encode raw credential-ID bytes to the base64url representation used in the DB.
pub fn encode_cred_id(bytes: &[u8]) -> String {
    URL_SAFE_NO_PAD.encode(bytes)
}

/// Decode a base64url credential ID back to bytes.
pub fn decode_cred_id(s: &str) -> Result<Vec<u8>, AuthError> {
    URL_SAFE_NO_PAD
        .decode(s)
        .map_err(|e| AuthError::Internal(format!("invalid credential_id: {e}")))
}

/// Generate a random 32-byte session ID for keying the WebAuthn challenge state
/// in a cache (e.g. `Cache::set("webauthn:challenge:{id}", state, 5min)`).
pub fn new_session_id() -> String {
    let mut bytes = [0u8; 32];
    rand::thread_rng().fill_bytes(&mut bytes);
    URL_SAFE_NO_PAD.encode(bytes)
}

// ── PasskeyManager ────────────────────────────────────────────────────────────

/// Database operations for WebAuthn passkey credentials.
pub struct PasskeyManager;

impl PasskeyManager {
    /// Persist a newly registered credential.
    ///
    /// `credential_id` is the base64url-encoded credential ID bytes returned
    /// by the WebAuthn registration ceremony.
    /// `passkey_json` is the serialised passkey object from your WebAuthn
    /// library — rok-auth stores it opaquely.
    pub async fn store(
        pool: &PgPool,
        user_id: i64,
        name: &str,
        credential_id: &str,
        passkey_json: &str,
        sign_count: i64,
    ) -> Result<PasskeyCredential, AuthError> {
        sqlx::query_as::<_, PasskeyCredential>(
            "INSERT INTO passkey_credentials
                (user_id, credential_id, passkey_json, sign_count, name)
             VALUES ($1, $2, $3, $4, $5)
             RETURNING *",
        )
        .bind(user_id)
        .bind(credential_id)
        .bind(passkey_json)
        .bind(sign_count)
        .bind(name)
        .fetch_one(pool)
        .await
        .map_err(|e| AuthError::Internal(e.to_string()))
    }

    /// Update the sign counter for a credential after successful authentication.
    ///
    /// Protects against cloned authenticators (monotonically increasing counter).
    pub async fn update_sign_count(
        pool: &PgPool,
        user_id: i64,
        credential_id: &str,
        new_count: i64,
    ) -> Result<(), AuthError> {
        sqlx::query(
            "UPDATE passkey_credentials
             SET sign_count = $1
             WHERE user_id = $2 AND credential_id = $3",
        )
        .bind(new_count)
        .bind(user_id)
        .bind(credential_id)
        .execute(pool)
        .await
        .map_err(|e| AuthError::Internal(e.to_string()))?;
        Ok(())
    }

    /// Return the raw `passkey_json` strings for all credentials owned by
    /// `user_id`.  Deserialise them in your WebAuthn library before starting
    /// an authentication ceremony.
    pub async fn load_json(
        pool: &PgPool,
        user_id: i64,
    ) -> Result<Vec<(String, String)>, AuthError> {
        sqlx::query_as::<_, (String, String)>(
            "SELECT credential_id, passkey_json
             FROM passkey_credentials
             WHERE user_id = $1
             ORDER BY created_at ASC",
        )
        .bind(user_id)
        .fetch_all(pool)
        .await
        .map_err(|e| AuthError::Internal(e.to_string()))
    }

    /// List all credentials for a user (includes metadata).
    pub async fn list(pool: &PgPool, user_id: i64) -> Result<Vec<PasskeyCredential>, AuthError> {
        sqlx::query_as::<_, PasskeyCredential>(
            "SELECT * FROM passkey_credentials
             WHERE user_id = $1
             ORDER BY created_at ASC",
        )
        .bind(user_id)
        .fetch_all(pool)
        .await
        .map_err(|e| AuthError::Internal(e.to_string()))
    }

    /// Delete a credential by `credential_id`, scoped to `user_id`.
    pub async fn delete(pool: &PgPool, user_id: i64, credential_id: &str) -> Result<(), AuthError> {
        sqlx::query(
            "DELETE FROM passkey_credentials
             WHERE user_id = $1 AND credential_id = $2",
        )
        .bind(user_id)
        .bind(credential_id)
        .execute(pool)
        .await
        .map_err(|e| AuthError::Internal(e.to_string()))?;
        Ok(())
    }

    /// Rename a credential (e.g. "MacBook Touch ID").
    pub async fn rename(
        pool: &PgPool,
        user_id: i64,
        credential_id: &str,
        new_name: &str,
    ) -> Result<(), AuthError> {
        sqlx::query(
            "UPDATE passkey_credentials SET name = $1
             WHERE user_id = $2 AND credential_id = $3",
        )
        .bind(new_name)
        .bind(user_id)
        .bind(credential_id)
        .execute(pool)
        .await
        .map_err(|e| AuthError::Internal(e.to_string()))?;
        Ok(())
    }

    /// Return `true` if `user_id` has at least one registered passkey.
    pub async fn has_passkey(pool: &PgPool, user_id: i64) -> Result<bool, AuthError> {
        sqlx::query_scalar::<_, bool>(
            "SELECT EXISTS(SELECT 1 FROM passkey_credentials WHERE user_id = $1)",
        )
        .bind(user_id)
        .fetch_one(pool)
        .await
        .map_err(|e| AuthError::Internal(e.to_string()))
    }
}

// ── tests ─────────────────────────────────────────────────────────────────────

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

    #[test]
    fn encode_decode_cred_id_roundtrip() {
        let bytes = b"credential-bytes-here";
        let encoded = encode_cred_id(bytes);
        let decoded = decode_cred_id(&encoded).unwrap();
        assert_eq!(decoded, bytes);
    }

    #[test]
    fn decode_invalid_base64_is_error() {
        let result = decode_cred_id("not valid base64!!!");
        assert!(result.is_err());
    }

    #[test]
    fn new_session_id_is_unique() {
        let a = new_session_id();
        let b = new_session_id();
        assert_ne!(a, b);
        assert!(!a.is_empty());
    }

    #[test]
    fn new_session_id_is_base64url() {
        let id = new_session_id();
        assert!(URL_SAFE_NO_PAD.decode(&id).is_ok());
    }
}
