use chacha20poly1305::XNonce;
use sqlx::SqlitePool;

use crate::errors::*;
use crate::kv;
use super::Crypto;


#[derive(Clone, Debug)]
pub enum AppSession {
    Unlocked {
        salt: [u8; 32],
        crypto: Crypto,
    },
    Locked {
        salt: [u8; 32],
        verify_nonce: XNonce,
        verify_blob: Vec<u8>
    },
    Empty,
}

impl AppSession {
    pub fn new(passphrase: &str) -> Result<Self, CryptoError> {
        let salt = Crypto::salt();
        let crypto = Crypto::new(passphrase, &salt)?;
        Ok(Self::Unlocked {salt, crypto})
    }

    pub fn unlock(&mut self, passphrase: &str) -> Result<(), UnlockError> {
        let (salt, nonce, blob) = match self {
            Self::Empty => return Err(UnlockError::NoCredentials),
            Self::Unlocked {..} => return Err(UnlockError::NotLocked),
            Self::Locked {salt, verify_nonce, verify_blob} => (salt, verify_nonce, verify_blob),
        };

        let crypto = Crypto::new(passphrase, salt)
            .map_err(|e| CryptoError::Argon2(e))?;

        // if passphrase is incorrect, this will fail
        let _verify = crypto.decrypt(&nonce, &blob)?;

        *self = Self::Unlocked {crypto, salt: *salt};
        Ok(())
    }

    pub async fn load(pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
        match kv::load_bytes_multi!(pool, "salt", "verify_nonce", "verify_blob").await? {
            Some((salt, nonce, blob)) => {

                Ok(Self::Locked {
                    salt: salt.try_into().map_err(|_| LoadCredentialsError::InvalidData)?,
                    // note: replace this with try_from at some point
                    verify_nonce: XNonce::clone_from_slice(&nonce),
                    verify_blob: blob,
                })
            },
            None => Ok(Self::Empty),
        }
    }

    pub async fn save(&self, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
        match self {
            Self::Unlocked {salt, crypto} => {
                let (nonce, blob) = crypto.encrypt(b"correct horse battery staple")?;
                kv::save_bytes(pool, "salt", salt).await?;
                kv::save_bytes(pool, "verify_nonce", &nonce.as_slice()).await?;
                kv::save_bytes(pool, "verify_blob", &blob).await?;
            },
            Self::Locked {salt, verify_nonce, verify_blob} => {
                kv::save_bytes(pool, "salt", salt).await?;
                kv::save_bytes(pool, "verify_nonce", &verify_nonce.as_slice()).await?;
                kv::save_bytes(pool, "verify_blob", verify_blob).await?;
            },
            // "saving" an empty session just means doing nothing
            Self::Empty => (),
        };

        Ok(())
    }

    pub async fn reset(&mut self, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
        match self {            
            Self::Unlocked {..} | Self::Locked {..} => {
                kv::delete_multi(pool, &["salt", "verify_nonce", "verify_blob"]).await?;
                *self = Self::Empty;
            },
            Self::Empty => (),
        }
        Ok(())
    }

    pub fn try_get_crypto(&self) -> Result<&Crypto, GetCredentialsError> {
        match self {
            Self::Empty => Err(GetCredentialsError::Empty),
            Self::Locked {..} => Err(GetCredentialsError::Locked),
            Self::Unlocked {crypto, ..} => Ok(crypto),
        }
    }
}