#![allow(dead_code)]
use anyhow::Result;
use base64::Engine;
use mongodb::{
    bson::{doc, oid::ObjectId, DateTime},
    Collection,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

/// A persisted, rotated refresh token. Only the SHA-256 hash of the raw JWT
/// refresh token is ever stored — the plaintext lives only inside the signed
/// JWT handed to the client.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefreshToken {
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    pub id: Option<ObjectId>,
    #[serde(rename = "tokenHash")]
    pub token_hash: String,
    #[serde(rename = "userId")]
    pub user_id: String,
    #[serde(rename = "expiresAt")]
    pub expires_at: DateTime,
    #[serde(rename = "createdAt")]
    pub created_at: DateTime,
    #[serde(rename = "revoked")]
    pub revoked: bool,
    #[serde(rename = "revokedAt")]
    pub revoked_at: Option<DateTime>,
}

/// SHA-256 the raw refresh token and base64-encode the digest. Two equal
/// tokens produce equal hashes, so lookups work; the plaintext is never stored.
pub fn hash_token(token: &str) -> String {
    let digest = Sha256::digest(token.as_bytes());
    base64::engine::general_purpose::STANDARD.encode(digest)
}

#[derive(Clone)]
pub struct RefreshTokenRepository {
    collection: Collection<RefreshToken>,
}

impl RefreshTokenRepository {
    pub fn new(db: &mongodb::Database) -> Self {
        Self {
            collection: db.collection("refresh_tokens"),
        }
    }

    /// Persist a refresh token record from its raw JWT form. Hashes the token
    /// before storage.
    pub async fn create(
        &self,
        user_id: &str,
        raw_token: &str,
        expires_at: DateTime,
    ) -> Result<ObjectId> {
        let now = DateTime::now();
        let record = RefreshToken {
            id: None,
            token_hash: hash_token(raw_token),
            user_id: user_id.to_string(),
            expires_at,
            created_at: now,
            revoked: false,
            revoked_at: None,
        };

        let id = self
            .collection
            .insert_one(record, None)
            .await?
            .inserted_id
            .as_object_id()
            .ok_or_else(|| anyhow::anyhow!("Failed to get inserted refresh token id"))?;
        Ok(id)
    }

    /// Look up a token record by its raw JWT form. Returns `None` if not found.
    /// Callers must still check `revoked` and `expires_at`.
    pub async fn find_by_raw_token(&self, raw_token: &str) -> Result<Option<RefreshToken>> {
        let hash = hash_token(raw_token);
        let record = self
            .collection
            .find_one(doc! { "tokenHash": hash }, None)
            .await?;
        Ok(record)
    }

    /// Mark a single token record (by id) as revoked.
    pub async fn revoke(&self, id: &ObjectId) -> Result<()> {
        self.collection
            .update_one(
                doc! { "_id": id },
                doc! {
                    "$set": {
                        "revoked": true,
                        "revokedAt": DateTime::now()
                    }
                },
                None,
            )
            .await?;
        Ok(())
    }

    /// Revoke every active refresh token for a user. Called on credential
    /// changes (password change / recovery) and explicit "logout everywhere".
    pub async fn revoke_all_by_user(&self, user_id: &str) -> Result<()> {
        self.collection
            .update_many(
                doc! { "userId": user_id, "revoked": false },
                doc! {
                    "$set": {
                        "revoked": true,
                        "revokedAt": DateTime::now()
                    }
                },
                None,
            )
            .await?;
        Ok(())
    }
}

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

    #[test]
    fn hash_token_is_deterministic() {
        assert_eq!(hash_token("abc"), hash_token("abc"));
    }

    #[test]
    fn hash_token_differs_for_different_input() {
        assert_ne!(hash_token("abc"), hash_token("abd"));
    }

    #[test]
    fn hash_token_is_base64_sha256_length() {
        // SHA-256 = 32 bytes -> 44 base64 chars (standard alphabet, padded).
        assert_eq!(hash_token("anything").len(), 44);
    }
}
