//! Backup encryption primitives for mnemonic export/import.
//!
//! Uses PBKDF2-SHA256 (600,000 iterations) + AES-256-GCM.
//! The output format is: salt (32 B) || nonce (12 B) || ciphertext || tag (16 B).
//!
//! The host app handles transport (iCloud, Google Drive, file download).
//! This module provides only the encryption/decryption.
//!
//! # iCloud sync
//! On macOS/iOS, iCloud Keychain sync is controlled by `kSecAttrSynchronizable`
//! via `crate::keystore::store_mnemonic_for_id(id, phrase, sync: true)`.
//! For cross-device cloud export/import the host app should use this module:
//! encrypt the mnemonic with `encrypt_for_backup`, transport the blob via
//! CloudKit or Google Drive, then decrypt with `decrypt_from_backup`.

use aes_gcm::aead::Aead;
use aes_gcm::{Aes256Gcm, KeyInit};
use rand_core::{OsRng, RngCore};

const PBKDF2_ITERATIONS: u32 = 600_000;
const SALT_LEN: usize = 32;
const NONCE_LEN: usize = 12;
/// Minimum valid blob: salt + nonce + AES-GCM tag (no plaintext).
const MIN_BLOB_LEN: usize = SALT_LEN + NONCE_LEN + 16;

/// Encrypt a mnemonic phrase for backup using a user-supplied password.
///
/// Returns the encrypted blob: `salt (32 B) || nonce (12 B) || ciphertext+tag`.
/// Each call produces a unique output because salt and nonce are randomly generated.
pub fn encrypt_for_backup(phrase: &str, password: &str) -> Result<Vec<u8>, String> {
    let mut salt = [0u8; SALT_LEN];
    OsRng.fill_bytes(&mut salt);
    let mut nonce_bytes = [0u8; NONCE_LEN];
    OsRng.fill_bytes(&mut nonce_bytes);

    let mut key = [0u8; 32];
    pbkdf2_sha256(password.as_bytes(), &salt, PBKDF2_ITERATIONS, &mut key);

    let cipher = Aes256Gcm::new_from_slice(&key).map_err(|e| format!("cipher init: {e}"))?;
    let nonce = aes_gcm::Nonce::from_slice(&nonce_bytes);
    let ciphertext = cipher
        .encrypt(nonce, phrase.as_bytes())
        .map_err(|e| format!("encrypt: {e}"))?;

    // Zeroize the derived key using volatile writes to prevent compiler elision.
    zeroize::Zeroize::zeroize(&mut key);

    let mut out = Vec::with_capacity(SALT_LEN + NONCE_LEN + ciphertext.len());
    out.extend_from_slice(&salt);
    out.extend_from_slice(&nonce_bytes);
    out.extend_from_slice(&ciphertext);
    Ok(out)
}

/// Decrypt a backup blob using the password.
///
/// Returns the mnemonic phrase wrapped in [`zeroize::Zeroizing`] so the bytes
/// are overwritten when the caller drops the value.
pub fn decrypt_from_backup(
    blob: &[u8],
    password: &str,
) -> Result<zeroize::Zeroizing<String>, String> {
    if blob.len() < MIN_BLOB_LEN {
        return Err("backup blob too short".into());
    }

    let salt = &blob[..SALT_LEN];
    let nonce_bytes = &blob[SALT_LEN..SALT_LEN + NONCE_LEN];
    let ciphertext = &blob[SALT_LEN + NONCE_LEN..];

    let mut key = [0u8; 32];
    pbkdf2_sha256(password.as_bytes(), salt, PBKDF2_ITERATIONS, &mut key);

    let cipher = Aes256Gcm::new_from_slice(&key).map_err(|e| format!("cipher init: {e}"))?;
    let nonce = aes_gcm::Nonce::from_slice(nonce_bytes);

    // Do not reveal *why* decryption failed — wrong password and bit corruption
    // are indistinguishable to the caller and we must not leak oracle information.
    let plaintext = cipher
        .decrypt(nonce, ciphertext)
        .map_err(|_| "decryption failed — wrong password or corrupted backup".to_string())?;

    // Zeroize the derived key using volatile writes to prevent compiler elision.
    zeroize::Zeroize::zeroize(&mut key);

    let phrase = String::from_utf8(plaintext)
        .map_err(|_| "decrypted data is not valid UTF-8".to_string())?;
    Ok(zeroize::Zeroizing::new(phrase))
}

/// PBKDF2-SHA256 key derivation (600,000 iterations, MetaMask-compatible).
fn pbkdf2_sha256(password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
    use hmac::Hmac;
    use sha2::Sha256;
    pbkdf2::pbkdf2::<Hmac<Sha256>>(password, salt, iterations, output)
        .expect("PBKDF2 output length is always valid for a 32-byte key");
}

// ---------------------------------------------------------------------------
// v2 format: version(1) || salt(32) || nonce(24) || ciphertext+tag
// ---------------------------------------------------------------------------

/// Version byte for v2 backup format.
const FORMAT_VERSION_V2: u8 = 0x02;

// scrypt parameters matching OWASP recommended interactive login (2^17, r=8, p=1).
const SCRYPT_LOG_N: u8 = 17; // N = 2^17 = 131072
const SCRYPT_R: u32 = 8;
const SCRYPT_P: u32 = 1;

/// XSalsa20-Poly1305 uses a 24-byte nonce.
const XSALSA20_NONCE_LEN: usize = 24;

/// Minimum valid v2 blob: version(1) + salt(32) + nonce(24) + tag(16).
const MIN_V2_BLOB_LEN: usize = 1 + SALT_LEN + XSALSA20_NONCE_LEN + 16;

/// Encrypt a mnemonic using scrypt + XSalsa20-Poly1305 (v2 format).
///
/// Blob format: `version(1) || salt(32) || nonce(24) || ciphertext+tag`.
/// Each call produces unique output because salt and nonce are randomly generated.
pub fn encrypt_for_backup_v2(phrase: &str, password: &str) -> Result<Vec<u8>, String> {
    use xsalsa20poly1305::aead::Aead;
    use xsalsa20poly1305::{KeyInit, XSalsa20Poly1305};

    let mut salt = [0u8; SALT_LEN];
    OsRng.fill_bytes(&mut salt);
    let mut nonce_bytes = [0u8; XSALSA20_NONCE_LEN];
    OsRng.fill_bytes(&mut nonce_bytes);

    let mut key = [0u8; 32];
    scrypt_derive(password.as_bytes(), &salt, &mut key)?;

    let cipher = XSalsa20Poly1305::new_from_slice(&key).map_err(|e| format!("cipher init: {e}"))?;
    let nonce = xsalsa20poly1305::Nonce::from_slice(&nonce_bytes);
    let ciphertext = cipher
        .encrypt(nonce, phrase.as_bytes())
        .map_err(|e| format!("encrypt: {e}"))?;

    // Zeroize the derived key using volatile writes to prevent compiler elision.
    zeroize::Zeroize::zeroize(&mut key);

    let mut out = Vec::with_capacity(1 + SALT_LEN + XSALSA20_NONCE_LEN + ciphertext.len());
    out.push(FORMAT_VERSION_V2);
    out.extend_from_slice(&salt);
    out.extend_from_slice(&nonce_bytes);
    out.extend_from_slice(&ciphertext);
    Ok(out)
}

/// Decrypt a v2 backup blob (scrypt + XSalsa20-Poly1305).
///
/// Returns the mnemonic phrase wrapped in [`zeroize::Zeroizing`] so the bytes
/// are overwritten when the caller drops the value.
pub fn decrypt_from_backup_v2(
    blob: &[u8],
    password: &str,
) -> Result<zeroize::Zeroizing<String>, String> {
    use xsalsa20poly1305::aead::Aead;
    use xsalsa20poly1305::{KeyInit, XSalsa20Poly1305};

    if blob.len() < MIN_V2_BLOB_LEN {
        return Err("backup blob too short".into());
    }
    if blob[0] != FORMAT_VERSION_V2 {
        return Err("unsupported backup format version".into());
    }

    let salt = &blob[1..1 + SALT_LEN];
    let nonce_bytes = &blob[1 + SALT_LEN..1 + SALT_LEN + XSALSA20_NONCE_LEN];
    let ciphertext = &blob[1 + SALT_LEN + XSALSA20_NONCE_LEN..];

    let mut key = [0u8; 32];
    scrypt_derive(password.as_bytes(), salt, &mut key)?;

    let cipher = XSalsa20Poly1305::new_from_slice(&key).map_err(|e| format!("cipher init: {e}"))?;
    let nonce = xsalsa20poly1305::Nonce::from_slice(nonce_bytes);

    // Do not reveal *why* decryption failed — wrong password and bit corruption
    // are indistinguishable to the caller and we must not leak oracle information.
    let plaintext = cipher
        .decrypt(nonce, ciphertext)
        .map_err(|_| "decryption failed — wrong password or corrupted backup".to_string())?;

    // Zeroize the derived key using volatile writes to prevent compiler elision.
    zeroize::Zeroize::zeroize(&mut key);

    let phrase = String::from_utf8(plaintext)
        .map_err(|_| "decrypted data is not valid UTF-8".to_string())?;
    Ok(zeroize::Zeroizing::new(phrase))
}

/// Decrypt a backup blob, auto-detecting the format version.
///
/// - First byte `0x02` → v2 (scrypt + XSalsa20-Poly1305).
/// - Any other first byte → v1 (PBKDF2-SHA256 + AES-256-GCM).
pub fn decrypt_backup(blob: &[u8], password: &str) -> Result<zeroize::Zeroizing<String>, String> {
    if blob.first() == Some(&FORMAT_VERSION_V2) {
        decrypt_from_backup_v2(blob, password)
    } else {
        decrypt_from_backup(blob, password)
    }
}

/// Derive a 32-byte key from `password` and `salt` using scrypt.
fn scrypt_derive(password: &[u8], salt: &[u8], output: &mut [u8; 32]) -> Result<(), String> {
    let params = scrypt::Params::new(SCRYPT_LOG_N, SCRYPT_R, SCRYPT_P, output.len())
        .map_err(|e| format!("scrypt params: {e}"))?;
    scrypt::scrypt(password, salt, &params, output).map_err(|e| format!("scrypt: {e}"))
}

/// Transport adapter for backup storage.
///
/// The SDK handles encryption via [`encrypt_for_backup`] / [`decrypt_from_backup`].
/// The host app implements this trait for its platform:
/// - iOS: iCloud Keychain sync (`kSecAttrSynchronizable`) or CloudKit
/// - Android: Google Drive API or Firestore
/// - Desktop: file export/import
/// - WASM: download/upload blob
pub trait BackupProvider: Send + Sync {
    /// Store an encrypted backup blob. The blob is already encrypted by
    /// [`encrypt_for_backup`] — the provider just transports it.
    fn store(&self, key_id: &str, encrypted_blob: &[u8]) -> Result<(), String>;

    /// Retrieve a previously stored backup blob.
    fn load(&self, key_id: &str) -> Result<Option<Vec<u8>>, String>;

    /// Delete a stored backup.
    fn delete(&self, key_id: &str) -> Result<(), String>;

    /// List all available backup IDs.
    fn list(&self) -> Result<Vec<String>, String>;
}

/// No-op backup provider for tests and platforms without cloud backup.
#[derive(Debug, Default)]
pub struct NoopBackupProvider;

impl BackupProvider for NoopBackupProvider {
    fn store(&self, _key_id: &str, _encrypted_blob: &[u8]) -> Result<(), String> {
        Ok(())
    }

    fn load(&self, _key_id: &str) -> Result<Option<Vec<u8>>, String> {
        Ok(None)
    }

    fn delete(&self, _key_id: &str) -> Result<(), String> {
        Ok(())
    }

    fn list(&self) -> Result<Vec<String>, String> {
        Ok(vec![])
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    const TEST_PHRASE: &str =
        "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";

    #[test]
    fn test_encrypts_and_decrypts_roundtrip() {
        let password = "test-password-123";
        let blob = encrypt_for_backup(TEST_PHRASE, password).unwrap();
        let decrypted = decrypt_from_backup(&blob, password).unwrap();
        assert_eq!(decrypted.as_str(), TEST_PHRASE);
    }

    #[test]
    fn test_decrypt_wrong_password_fails() {
        let blob = encrypt_for_backup("secret phrase", "correct").unwrap();
        assert!(decrypt_from_backup(&blob, "wrong").is_err());
    }

    #[test]
    fn test_decrypt_truncated_blob_fails() {
        assert!(decrypt_from_backup(&[0u8; 10], "password").is_err());
    }

    #[test]
    fn test_decrypt_empty_blob_fails() {
        assert!(decrypt_from_backup(&[], "password").is_err());
    }

    #[test]
    fn test_decrypt_exact_minimum_length_truncated_fails() {
        // MIN_BLOB_LEN - 1 must always fail the length check.
        let short = vec![0u8; MIN_BLOB_LEN - 1];
        assert!(decrypt_from_backup(&short, "password").is_err());
    }

    #[test]
    fn test_encrypt_produces_different_output_each_time() {
        let blob1 = encrypt_for_backup("same phrase", "same password").unwrap();
        let blob2 = encrypt_for_backup("same phrase", "same password").unwrap();
        // Random salt + nonce means two independent encryptions must differ.
        assert_ne!(blob1, blob2, "random salt/nonce must differ between calls");
    }

    #[test]
    fn test_blob_format_has_expected_minimum_length() {
        let blob = encrypt_for_backup("test", "pass").unwrap();
        // salt (32) + nonce (12) + tag (16) + plaintext (4) = 64 minimum
        assert!(
            blob.len() >= SALT_LEN + NONCE_LEN + 16 + 4,
            "blob must contain salt + nonce + ciphertext + tag"
        );
    }

    #[test]
    fn test_blob_salt_and_nonce_differ_between_calls() {
        let blob1 = encrypt_for_backup("phrase", "pw").unwrap();
        let blob2 = encrypt_for_backup("phrase", "pw").unwrap();
        // Salt occupies bytes 0..32, nonce bytes 32..44.
        assert_ne!(&blob1[..SALT_LEN], &blob2[..SALT_LEN], "salts must differ");
        assert_ne!(
            &blob1[SALT_LEN..SALT_LEN + NONCE_LEN],
            &blob2[SALT_LEN..SALT_LEN + NONCE_LEN],
            "nonces must differ"
        );
    }

    #[test]
    fn test_noop_provider_store_succeeds() {
        let p = NoopBackupProvider;
        assert!(p.store("id", b"blob").is_ok());
    }

    #[test]
    fn test_noop_provider_load_returns_none() {
        let p = NoopBackupProvider;
        assert_eq!(p.load("id").unwrap(), None);
    }

    #[test]
    fn test_noop_provider_delete_succeeds() {
        let p = NoopBackupProvider;
        assert!(p.delete("id").is_ok());
    }

    #[test]
    fn test_noop_provider_list_returns_empty() {
        let p = NoopBackupProvider;
        assert_eq!(p.list().unwrap(), Vec::<String>::new());
    }

    // -----------------------------------------------------------------------
    // v2 backup format tests (scrypt + XSalsa20-Poly1305)
    // -----------------------------------------------------------------------

    // NOTE: scrypt with N=2^17 is intentionally expensive (~0.5 s on modern
    // hardware). These tests therefore use --release or accept the slowdown
    // in debug mode; they are not marked #[ignore] because correctness of the
    // crypto path is security-critical and must always run.

    #[test]
    fn test_v2_encrypts_and_decrypts_roundtrip() {
        let password = "test-password-v2";
        let blob = encrypt_for_backup_v2(TEST_PHRASE, password).unwrap();
        let decrypted = decrypt_from_backup_v2(&blob, password).unwrap();
        assert_eq!(decrypted.as_str(), TEST_PHRASE);
    }

    #[test]
    fn test_v2_decrypt_wrong_password_fails() {
        let blob = encrypt_for_backup_v2("secret phrase", "correct-v2").unwrap();
        assert!(decrypt_from_backup_v2(&blob, "wrong-v2").is_err());
    }

    #[test]
    fn test_v2_decrypt_truncated_blob_fails() {
        // A blob shorter than the minimum (1 + 32 + 24 + 16 = 73 bytes) must fail.
        assert!(decrypt_from_backup_v2(&[0x02u8; 10], "password").is_err());
    }

    #[test]
    fn test_v2_encrypt_produces_different_output_each_time() {
        let blob1 = encrypt_for_backup_v2("same phrase", "same password").unwrap();
        let blob2 = encrypt_for_backup_v2("same phrase", "same password").unwrap();
        assert_ne!(blob1, blob2, "random salt/nonce must differ between calls");
    }

    #[test]
    fn test_v2_blob_starts_with_version_byte() {
        let blob = encrypt_for_backup_v2("phrase", "pass").unwrap();
        assert_eq!(blob[0], FORMAT_VERSION_V2);
    }

    #[test]
    fn test_decrypt_backup_dispatcher_handles_v1() {
        // A v1 blob has no leading version byte — first byte is the start of the salt.
        // The dispatcher must recognise it is not 0x02 and fall through to v1.
        let blob = encrypt_for_backup(TEST_PHRASE, "pw-v1").unwrap();
        // Sanity: v1 blobs must not accidentally start with 0x02.
        // If they do (astronomically unlikely with a random salt), skip the assertion.
        if blob[0] != FORMAT_VERSION_V2 {
            let decrypted = decrypt_backup(&blob, "pw-v1").unwrap();
            assert_eq!(decrypted.as_str(), TEST_PHRASE);
        }
    }

    #[test]
    fn test_decrypt_backup_dispatcher_handles_v2() {
        let blob = encrypt_for_backup_v2(TEST_PHRASE, "pw-v2").unwrap();
        assert_eq!(blob[0], FORMAT_VERSION_V2);
        let decrypted = decrypt_backup(&blob, "pw-v2").unwrap();
        assert_eq!(decrypted.as_str(), TEST_PHRASE);
    }
}
