use std::{collections::HashMap, sync::Arc};
use anyhow::Context;
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use futures_util::future::OptionFuture;
use pbkdf2::Pbkdf2;
use rand::{CryptoRng, Rng, RngCore, SeedableRng};
use thiserror::Error;
use zeroize::Zeroizing;
use zxcvbn::zxcvbn;
pub type SchemeVersion = u16;
#[derive(Debug, Error)]
#[error("Password manager is disabled")]
pub struct PasswordManagerDisabledError;
#[derive(Clone)]
pub struct PasswordManager {
    inner: Option<Arc<InnerPasswordManager>>,
}
struct InnerPasswordManager {
    minimum_complexity: u8,
    current_hasher: Hasher,
    current_version: SchemeVersion,
    other_hashers: HashMap<SchemeVersion, Hasher>,
}
impl PasswordManager {
    pub fn new<I: IntoIterator<Item = (SchemeVersion, Hasher)>>(
        minimum_complexity: u8,
        iter: I,
    ) -> Result<Self, anyhow::Error> {
        let mut iter = iter.into_iter();
        let (current_version, current_hasher) = iter
            .next()
            .context("Iterator must have at least one item")?;
        let other_hashers = iter.collect();
        Ok(Self {
            inner: Some(Arc::new(InnerPasswordManager {
                minimum_complexity,
                current_hasher,
                current_version,
                other_hashers,
            })),
        })
    }
    #[must_use]
    pub const fn disabled() -> Self {
        Self { inner: None }
    }
    #[must_use]
    pub const fn is_enabled(&self) -> bool {
        self.inner.is_some()
    }
    fn get_inner(&self) -> Result<Arc<InnerPasswordManager>, PasswordManagerDisabledError> {
        self.inner.clone().ok_or(PasswordManagerDisabledError)
    }
    pub fn is_password_complex_enough(&self, password: &str) -> Result<bool, anyhow::Error> {
        let inner = self.get_inner()?;
        let score = zxcvbn(password, &[]);
        Ok(u8::from(score.score()) >= inner.minimum_complexity)
    }
    #[tracing::instrument(name = "passwords.hash", skip_all)]
    pub async fn hash<R: CryptoRng + RngCore + Send>(
        &self,
        rng: R,
        password: Zeroizing<Vec<u8>>,
    ) -> Result<(SchemeVersion, String), anyhow::Error> {
        let inner = self.get_inner()?;
        let rng = rand_chacha::ChaChaRng::from_rng(rng)?;
        let span = tracing::Span::current();
        let version = inner.current_version;
        let hashed = tokio::task::spawn_blocking(move || {
            span.in_scope(move || inner.current_hasher.hash_blocking(rng, &password))
        })
        .await??;
        Ok((version, hashed))
    }
    #[tracing::instrument(name = "passwords.verify", skip_all, fields(%scheme))]
    pub async fn verify(
        &self,
        scheme: SchemeVersion,
        password: Zeroizing<Vec<u8>>,
        hashed_password: String,
    ) -> Result<(), anyhow::Error> {
        let inner = self.get_inner()?;
        let span = tracing::Span::current();
        tokio::task::spawn_blocking(move || {
            span.in_scope(move || {
                let hasher = if scheme == inner.current_version {
                    &inner.current_hasher
                } else {
                    inner
                        .other_hashers
                        .get(&scheme)
                        .context("Hashing scheme not found")?
                };
                hasher.verify_blocking(&hashed_password, &password)
            })
        })
        .await??;
        Ok(())
    }
    #[tracing::instrument(name = "passwords.verify_and_upgrade", skip_all, fields(%scheme))]
    pub async fn verify_and_upgrade<R: CryptoRng + RngCore + Send>(
        &self,
        rng: R,
        scheme: SchemeVersion,
        password: Zeroizing<Vec<u8>>,
        hashed_password: String,
    ) -> Result<Option<(SchemeVersion, String)>, anyhow::Error> {
        let inner = self.get_inner()?;
        let new_hash_fut: OptionFuture<_> = (scheme != inner.current_version)
            .then(|| self.hash(rng, password.clone()))
            .into();
        let verify_fut = self.verify(scheme, password, hashed_password);
        let (new_hash_res, verify_res) = tokio::join!(new_hash_fut, verify_fut);
        verify_res?;
        let new_hash = new_hash_res.transpose()?;
        Ok(new_hash)
    }
}
pub struct Hasher {
    algorithm: Algorithm,
    pepper: Option<Vec<u8>>,
}
impl Hasher {
    #[must_use]
    pub const fn bcrypt(cost: Option<u32>, pepper: Option<Vec<u8>>) -> Self {
        let algorithm = Algorithm::Bcrypt { cost };
        Self { algorithm, pepper }
    }
    #[must_use]
    pub const fn argon2id(pepper: Option<Vec<u8>>) -> Self {
        let algorithm = Algorithm::Argon2id;
        Self { algorithm, pepper }
    }
    #[must_use]
    pub const fn pbkdf2(pepper: Option<Vec<u8>>) -> Self {
        let algorithm = Algorithm::Pbkdf2;
        Self { algorithm, pepper }
    }
    fn hash_blocking<R: CryptoRng + RngCore>(
        &self,
        rng: R,
        password: &[u8],
    ) -> Result<String, anyhow::Error> {
        self.algorithm
            .hash_blocking(rng, password, self.pepper.as_deref())
    }
    fn verify_blocking(&self, hashed_password: &str, password: &[u8]) -> Result<(), anyhow::Error> {
        self.algorithm
            .verify_blocking(hashed_password, password, self.pepper.as_deref())
    }
}
#[derive(Debug, Clone, Copy)]
enum Algorithm {
    Bcrypt { cost: Option<u32> },
    Argon2id,
    Pbkdf2,
}
impl Algorithm {
    fn hash_blocking<R: CryptoRng + RngCore>(
        self,
        mut rng: R,
        password: &[u8],
        pepper: Option<&[u8]>,
    ) -> Result<String, anyhow::Error> {
        match self {
            Self::Bcrypt { cost } => {
                let mut password = Zeroizing::new(password.to_vec());
                if let Some(pepper) = pepper {
                    password.extend_from_slice(pepper);
                }
                let salt = rng.gen();
                let hashed = bcrypt::hash_with_salt(password, cost.unwrap_or(12), salt)?;
                Ok(hashed.format_for_version(bcrypt::Version::TwoB))
            }
            Self::Argon2id => {
                let algorithm = argon2::Algorithm::default();
                let version = argon2::Version::default();
                let params = argon2::Params::default();
                let phf = if let Some(secret) = pepper {
                    Argon2::new_with_secret(secret, algorithm, version, params)?
                } else {
                    Argon2::new(algorithm, version, params)
                };
                let salt = SaltString::generate(rng);
                let hashed = phf.hash_password(password.as_ref(), &salt)?;
                Ok(hashed.to_string())
            }
            Self::Pbkdf2 => {
                let mut password = Zeroizing::new(password.to_vec());
                if let Some(pepper) = pepper {
                    password.extend_from_slice(pepper);
                }
                let salt = SaltString::generate(rng);
                let hashed = Pbkdf2.hash_password(password.as_ref(), &salt)?;
                Ok(hashed.to_string())
            }
        }
    }
    fn verify_blocking(
        self,
        hashed_password: &str,
        password: &[u8],
        pepper: Option<&[u8]>,
    ) -> Result<(), anyhow::Error> {
        match self {
            Algorithm::Bcrypt { .. } => {
                let mut password = Zeroizing::new(password.to_vec());
                if let Some(pepper) = pepper {
                    password.extend_from_slice(pepper);
                }
                let result = bcrypt::verify(password, hashed_password)?;
                anyhow::ensure!(result, "wrong password");
            }
            Algorithm::Argon2id => {
                let algorithm = argon2::Algorithm::default();
                let version = argon2::Version::default();
                let params = argon2::Params::default();
                let phf = if let Some(secret) = pepper {
                    Argon2::new_with_secret(secret, algorithm, version, params)?
                } else {
                    Argon2::new(algorithm, version, params)
                };
                let hashed_password = PasswordHash::new(hashed_password)?;
                phf.verify_password(password.as_ref(), &hashed_password)?;
            }
            Algorithm::Pbkdf2 => {
                let mut password = Zeroizing::new(password.to_vec());
                if let Some(pepper) = pepper {
                    password.extend_from_slice(pepper);
                }
                let hashed_password = PasswordHash::new(hashed_password)?;
                Pbkdf2.verify_password(password.as_ref(), &hashed_password)?;
            }
        };
        Ok(())
    }
}
#[cfg(test)]
mod tests {
    use rand::SeedableRng;
    use super::*;
    #[test]
    fn hashing_bcrypt() {
        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
        let password = b"hunter2";
        let password2 = b"wrong-password";
        let pepper = b"a-secret-pepper";
        let pepper2 = b"the-wrong-pepper";
        let alg = Algorithm::Bcrypt { cost: Some(10) };
        let hash = alg
            .hash_blocking(&mut rng, password, Some(pepper))
            .expect("Couldn't hash password");
        insta::assert_snapshot!(hash);
        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
        assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
        assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
        assert!(alg.verify_blocking(&hash, password, None).is_err());
        let hash = alg
            .hash_blocking(&mut rng, password, None)
            .expect("Couldn't hash password");
        insta::assert_snapshot!(hash);
        assert!(alg.verify_blocking(&hash, password, None).is_ok());
        assert!(alg.verify_blocking(&hash, password2, None).is_err());
        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
    }
    #[test]
    fn hashing_argon2id() {
        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
        let password = b"hunter2";
        let password2 = b"wrong-password";
        let pepper = b"a-secret-pepper";
        let pepper2 = b"the-wrong-pepper";
        let alg = Algorithm::Argon2id;
        let hash = alg
            .hash_blocking(&mut rng, password, Some(pepper))
            .expect("Couldn't hash password");
        insta::assert_snapshot!(hash);
        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
        assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
        assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
        assert!(alg.verify_blocking(&hash, password, None).is_err());
        let hash = alg
            .hash_blocking(&mut rng, password, None)
            .expect("Couldn't hash password");
        insta::assert_snapshot!(hash);
        assert!(alg.verify_blocking(&hash, password, None).is_ok());
        assert!(alg.verify_blocking(&hash, password2, None).is_err());
        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
    }
    #[test]
    #[ignore = "this is particularly slow (20s+ seconds)"]
    fn hashing_pbkdf2() {
        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
        let password = b"hunter2";
        let password2 = b"wrong-password";
        let pepper = b"a-secret-pepper";
        let pepper2 = b"the-wrong-pepper";
        let alg = Algorithm::Pbkdf2;
        let hash = alg
            .hash_blocking(&mut rng, password, Some(pepper))
            .expect("Couldn't hash password");
        insta::assert_snapshot!(hash);
        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_ok());
        assert!(alg.verify_blocking(&hash, password2, Some(pepper)).is_err());
        assert!(alg.verify_blocking(&hash, password, Some(pepper2)).is_err());
        assert!(alg.verify_blocking(&hash, password, None).is_err());
        let hash = alg
            .hash_blocking(&mut rng, password, None)
            .expect("Couldn't hash password");
        insta::assert_snapshot!(hash);
        assert!(alg.verify_blocking(&hash, password, None).is_ok());
        assert!(alg.verify_blocking(&hash, password2, None).is_err());
        assert!(alg.verify_blocking(&hash, password, Some(pepper)).is_err());
    }
    #[allow(clippy::too_many_lines)]
    #[tokio::test]
    async fn hash_verify_and_upgrade() {
        let mut rng = rand_chacha::ChaChaRng::seed_from_u64(42);
        let password = Zeroizing::new(b"hunter2".to_vec());
        let wrong_password = Zeroizing::new(b"wrong-password".to_vec());
        let manager = PasswordManager::new(
            0,
            [
                (
                    1,
                    Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
                ),
            ],
        )
        .unwrap();
        let (version, hash) = manager
            .hash(&mut rng, password.clone())
            .await
            .expect("Failed to hash");
        assert_eq!(version, 1);
        insta::assert_snapshot!(hash);
        manager
            .verify(version, password.clone(), hash.clone())
            .await
            .expect("Failed to verify");
        manager
            .verify(version, wrong_password.clone(), hash.clone())
            .await
            .expect_err("Verification should have failed");
        manager
            .verify(2, password.clone(), hash.clone())
            .await
            .expect_err("Verification should have failed");
        let res = manager
            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
            .await
            .expect("Failed to verify");
        assert!(res.is_none());
        manager
            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
            .await
            .expect_err("Verification should have failed");
        let manager = PasswordManager::new(
            0,
            [
                (2, Hasher::argon2id(None)),
                (
                    1,
                    Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
                ),
            ],
        )
        .unwrap();
        manager
            .verify(version, password.clone(), hash.clone())
            .await
            .expect("Failed to verify");
        manager
            .verify(version, wrong_password.clone(), hash.clone())
            .await
            .expect_err("Verification should have failed");
        let res = manager
            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
            .await
            .expect("Failed to verify");
        assert!(res.is_some());
        let (version, hash) = res.unwrap();
        assert_eq!(version, 2);
        insta::assert_snapshot!(hash);
        let res = manager
            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
            .await
            .expect("Failed to verify");
        assert!(res.is_none());
        manager
            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
            .await
            .expect_err("Verification should have failed");
        manager
            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
            .await
            .expect_err("Verification should have failed");
        let manager = PasswordManager::new(
            0,
            [
                (3, Hasher::argon2id(Some(b"a-secret-pepper".to_vec()))),
                (2, Hasher::argon2id(None)),
                (
                    1,
                    Hasher::bcrypt(Some(10), Some(b"a-secret-pepper".to_vec())),
                ),
            ],
        )
        .unwrap();
        manager
            .verify(version, password.clone(), hash.clone())
            .await
            .expect("Failed to verify");
        manager
            .verify(version, wrong_password.clone(), hash.clone())
            .await
            .expect_err("Verification should have failed");
        let res = manager
            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
            .await
            .expect("Failed to verify");
        assert!(res.is_some());
        let (version, hash) = res.unwrap();
        assert_eq!(version, 3);
        insta::assert_snapshot!(hash);
        let res = manager
            .verify_and_upgrade(&mut rng, version, password.clone(), hash.clone())
            .await
            .expect("Failed to verify");
        assert!(res.is_none());
        manager
            .verify_and_upgrade(&mut rng, version, wrong_password.clone(), hash.clone())
            .await
            .expect_err("Verification should have failed");
    }
}