use aes_gcm::{
  aead::{Aead, KeyInit},
  Aes256Gcm, Key,
};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use rand::RngExt;
use std::collections::HashMap;
use std::sync::Mutex;

const E2E_FILE_HEADER: &[u8] = b"DBE2E";
const E2E_FILE_VERSION: u8 = 1;

/// Argon2id is intentionally expensive (~80–150 ms per call). During an
/// encryption rollover, every synced entity (proxy, group, vpn, extension,
/// extension group, profile metadata) goes through `derive_profile_key`,
/// which without caching means hundreds of sequential 100 ms derivations.
///
/// Cache the derived key keyed on (sha256(password), salt). Entries are
/// evicted on `set_e2e_password` / `delete_e2e_password` so a password
/// change cannot use stale keys.
type DerivedKeyCache = HashMap<([u8; 32], String), [u8; 32]>;
static KEY_CACHE: std::sync::LazyLock<Mutex<DerivedKeyCache>> =
  std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));

fn password_fingerprint(pwd: &str) -> [u8; 32] {
  use sha2::{Digest, Sha256};
  let mut hasher = Sha256::new();
  hasher.update(pwd.as_bytes());
  let result = hasher.finalize();
  let mut out = [0u8; 32];
  out.copy_from_slice(&result);
  out
}

fn invalidate_key_cache() {
  if let Ok(mut cache) = KEY_CACHE.lock() {
    cache.clear();
  }
}

fn get_e2e_password_path() -> std::path::PathBuf {
  crate::app_dirs::settings_dir().join("e2e_password.dat")
}

fn get_vault_password() -> String {
  env!("DONUT_BROWSER_VAULT_PASSWORD").to_string()
}

pub fn store_e2e_password(password: &str) -> Result<(), String> {
  invalidate_key_cache();
  let file_path = get_e2e_password_path();

  if let Some(parent) = file_path.parent() {
    std::fs::create_dir_all(parent).map_err(|e| format!("Failed to create directory: {e}"))?;
  }

  let vault_password = get_vault_password();
  let salt_bytes: [u8; 16] = rand::rng().random();
  let salt =
    SaltString::encode_b64(&salt_bytes).map_err(|e| format!("Failed to encode salt: {e}"))?;
  let argon2 = Argon2::default();
  let password_hash = argon2
    .hash_password(vault_password.as_bytes(), &salt)
    .map_err(|e| format!("Argon2 key derivation failed: {e}"))?;
  let hash_value = password_hash.hash.unwrap();
  let hash_bytes = hash_value.as_bytes();

  let key_bytes: [u8; 32] = hash_bytes[..32]
    .try_into()
    .map_err(|_| "Invalid key length")?;
  let key = Key::<Aes256Gcm>::from(key_bytes);
  let cipher = Aes256Gcm::new(&key);
  let nonce_bytes: [u8; 12] = rand::rng().random();
  let nonce = aes_gcm::Nonce::from(nonce_bytes);

  let ciphertext = cipher
    .encrypt(&nonce, password.as_bytes())
    .map_err(|e| format!("Encryption failed: {e}"))?;

  let mut file_data = Vec::new();
  file_data.extend_from_slice(E2E_FILE_HEADER);
  file_data.push(E2E_FILE_VERSION);

  let salt_str = salt.as_str();
  file_data.push(salt_str.len() as u8);
  file_data.extend_from_slice(salt_str.as_bytes());
  file_data.extend_from_slice(&nonce);
  file_data.extend_from_slice(&(ciphertext.len() as u32).to_le_bytes());
  file_data.extend_from_slice(&ciphertext);

  std::fs::write(&file_path, file_data)
    .map_err(|e| format!("Failed to write e2e password file: {e}"))?;
  crate::app_dirs::restrict_to_owner(std::path::Path::new(&file_path));

  Ok(())
}

pub fn load_e2e_password() -> Result<Option<String>, String> {
  let file_path = get_e2e_password_path();
  if !file_path.exists() {
    return Ok(None);
  }

  let file_data =
    std::fs::read(&file_path).map_err(|e| format!("Failed to read e2e password file: {e}"))?;

  if file_data.len() < E2E_FILE_HEADER.len() + 1 {
    return Ok(None);
  }

  if &file_data[..E2E_FILE_HEADER.len()] != E2E_FILE_HEADER {
    return Ok(None);
  }

  let version = file_data[E2E_FILE_HEADER.len()];
  if version != E2E_FILE_VERSION {
    return Ok(None);
  }

  let mut offset = E2E_FILE_HEADER.len() + 1;

  if offset >= file_data.len() {
    return Ok(None);
  }
  let salt_len = file_data[offset] as usize;
  offset += 1;

  if offset + salt_len > file_data.len() {
    return Ok(None);
  }
  let salt_str = std::str::from_utf8(&file_data[offset..offset + salt_len])
    .map_err(|_| "Invalid salt encoding")?;
  offset += salt_len;

  let salt = SaltString::from_b64(salt_str).map_err(|e| format!("Invalid salt: {e}"))?;

  if offset + 12 > file_data.len() {
    return Ok(None);
  }
  let nonce_bytes: [u8; 12] = file_data[offset..offset + 12]
    .try_into()
    .map_err(|_| "Invalid nonce")?;
  let nonce = aes_gcm::Nonce::from(nonce_bytes);
  offset += 12;

  if offset + 4 > file_data.len() {
    return Ok(None);
  }
  let ciphertext_len =
    u32::from_le_bytes(file_data[offset..offset + 4].try_into().unwrap()) as usize;
  offset += 4;

  if offset + ciphertext_len > file_data.len() {
    return Ok(None);
  }
  let ciphertext = &file_data[offset..offset + ciphertext_len];

  let vault_password = get_vault_password();
  let argon2 = Argon2::default();
  let password_hash = argon2
    .hash_password(vault_password.as_bytes(), &salt)
    .map_err(|e| format!("Argon2 key derivation failed: {e}"))?;
  let hash_value = password_hash.hash.unwrap();
  let hash_bytes = hash_value.as_bytes();

  let key_bytes: [u8; 32] = hash_bytes[..32]
    .try_into()
    .map_err(|_| "Invalid key length")?;
  let key = Key::<Aes256Gcm>::from(key_bytes);
  let cipher = Aes256Gcm::new(&key);

  let plaintext = cipher
    .decrypt(&nonce, ciphertext)
    .map_err(|e| format!("Decryption failed: {e}"))?;

  let password =
    String::from_utf8(plaintext).map_err(|e| format!("Invalid UTF-8 in password: {e}"))?;

  Ok(Some(password))
}

pub fn has_e2e_password() -> bool {
  get_e2e_password_path().exists()
}

pub fn remove_e2e_password() -> Result<(), String> {
  invalidate_key_cache();
  let file_path = get_e2e_password_path();
  if file_path.exists() {
    std::fs::remove_file(&file_path)
      .map_err(|e| format!("Failed to remove e2e password file: {e}"))?;
  }
  Ok(())
}

/// Derive a per-profile encryption key using Argon2id, with an in-process
/// cache keyed on `(sha256(password), salt)`. Repeated calls with the same
/// password+salt are O(1); a password change calls `invalidate_key_cache`
/// to drop stale entries.
pub fn derive_profile_key(user_password: &str, profile_salt: &str) -> Result<[u8; 32], String> {
  let pwd_fp = password_fingerprint(user_password);
  let cache_key = (pwd_fp, profile_salt.to_string());

  if let Ok(cache) = KEY_CACHE.lock() {
    if let Some(cached) = cache.get(&cache_key) {
      return Ok(*cached);
    }
  }

  let salt_bytes = BASE64
    .decode(profile_salt)
    .map_err(|e| format!("Invalid salt encoding: {e}"))?;

  let salt = SaltString::encode_b64(&salt_bytes)
    .map_err(|e| format!("Failed to create salt string: {e}"))?;

  let argon2 = Argon2::default();
  let password_hash = argon2
    .hash_password(user_password.as_bytes(), &salt)
    .map_err(|e| format!("Key derivation failed: {e}"))?;
  let hash_value = password_hash.hash.unwrap();
  let hash_bytes = hash_value.as_bytes();

  let mut key = [0u8; 32];
  key.copy_from_slice(&hash_bytes[..32]);

  if let Ok(mut cache) = KEY_CACHE.lock() {
    cache.insert(cache_key, key);
  }

  Ok(key)
}

/// Generate a random 16-byte salt, base64-encoded
pub fn generate_salt() -> String {
  let salt: [u8; 16] = rand::rng().random();
  BASE64.encode(salt)
}

/// Encrypt bytes with AES-256-GCM. Output format: [nonce 12B][ciphertext]
pub fn encrypt_bytes(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>, String> {
  let aes_key = Key::<Aes256Gcm>::from(*key);
  let cipher = Aes256Gcm::new(&aes_key);
  let nonce_bytes: [u8; 12] = rand::rng().random();
  let nonce = aes_gcm::Nonce::from(nonce_bytes);

  let ciphertext = cipher
    .encrypt(&nonce, plaintext)
    .map_err(|e| format!("Encryption failed: {e}"))?;

  let mut output = Vec::with_capacity(12 + ciphertext.len());
  output.extend_from_slice(&nonce);
  output.extend_from_slice(&ciphertext);
  Ok(output)
}

/// Decrypt bytes encrypted with encrypt_bytes. Input format: [nonce 12B][ciphertext]
pub fn decrypt_bytes(key: &[u8; 32], encrypted: &[u8]) -> Result<Vec<u8>, String> {
  if encrypted.len() < 12 {
    return Err("Encrypted data too short".to_string());
  }

  let nonce_bytes: [u8; 12] = encrypted[..12].try_into().map_err(|_| "Invalid nonce")?;
  let nonce = aes_gcm::Nonce::from(nonce_bytes);
  let ciphertext = &encrypted[12..];

  let aes_key = Key::<Aes256Gcm>::from(*key);
  let cipher = Aes256Gcm::new(&aes_key);

  cipher
    .decrypt(&nonce, ciphertext)
    .map_err(|e| format!("Decryption failed: {e}"))
}

/// Versioned encryption envelope used for non-profile entities (proxies,
/// VPNs, groups, extensions, extension groups). Each upload has its own
/// random per-entity salt so the bucket can't be rainbow-table-attacked
/// even with a shared password across many entities.
#[derive(serde::Serialize, serde::Deserialize)]
pub struct EncryptedEnvelope {
  /// Format version. Increment when changing how `ct` is structured.
  pub v: u32,
  /// Base64 of the per-entity salt. Plaintext on the wire — salts are public.
  pub salt: String,
  /// Base64 of `nonce(12B) || AES-256-GCM ciphertext` (output of `encrypt_bytes`).
  pub ct: String,
}

/// Wrap a plaintext JSON byte slice into an encrypted envelope if the user
/// has E2E enabled. Returns `(payload_bytes, content_type)` ready to upload.
/// On no-password, returns the original JSON unchanged.
pub fn maybe_seal_for_upload(json: &[u8]) -> Result<(Vec<u8>, &'static str), String> {
  let pwd = match load_e2e_password()? {
    Some(p) => p,
    None => return Ok((json.to_vec(), "application/json")),
  };
  let salt = generate_salt();
  let key = derive_profile_key(&pwd, &salt)?;
  let ct = encrypt_bytes(&key, json)?;
  let envelope = EncryptedEnvelope {
    v: 1,
    salt,
    ct: BASE64.encode(&ct),
  };
  let payload =
    serde_json::to_vec(&envelope).map_err(|e| format!("Failed to serialize envelope: {e}"))?;
  Ok((payload, "application/json"))
}

/// Reverse of `maybe_seal_for_upload`. Returns the inner plaintext JSON
/// bytes regardless of whether `raw` was an envelope or legacy plaintext.
///
/// Distinguishes three cases:
/// - `raw` is plaintext JSON, no password set → returns `raw` unchanged.
/// - `raw` is an envelope, password set → decrypts and returns plaintext.
/// - `raw` is an envelope, no password set → returns `Err(EncryptedEnvelope)`
///   so callers (subscription / startup probe) can show "enter password to
///   continue syncing" UI.
pub fn maybe_unseal_after_download(raw: &[u8]) -> Result<Vec<u8>, String> {
  // Try parsing as envelope first; envelopes are JSON objects with a "v" field.
  if let Ok(env) = serde_json::from_slice::<EncryptedEnvelope>(raw) {
    if env.v != 1 {
      return Err(format!("Unsupported envelope version: {}", env.v));
    }
    let pwd = load_e2e_password()?.ok_or_else(|| "ENCRYPTION_PASSWORD_REQUIRED".to_string())?;
    let key = derive_profile_key(&pwd, &env.salt)?;
    let ct = BASE64
      .decode(&env.ct)
      .map_err(|e| format!("Invalid envelope ciphertext: {e}"))?;
    return decrypt_bytes(&key, &ct);
  }
  // Not an envelope — legacy plaintext. Caller will JSON-parse it directly.
  Ok(raw.to_vec())
}

// Tauri commands

#[tauri::command]
pub async fn set_e2e_password(password: String) -> Result<(), String> {
  if password.len() < 8 {
    return Err("Password must be at least 8 characters".to_string());
  }
  enforce_team_owner_for_encryption_change().await?;
  store_e2e_password(&password)
}

#[tauri::command]
pub fn check_has_e2e_password() -> bool {
  has_e2e_password()
}

#[tauri::command]
pub fn verify_e2e_password(password: String) -> Result<bool, String> {
  match load_e2e_password()? {
    Some(stored) => Ok(stored == password),
    None => Err(serde_json::json!({ "code": "NO_E2E_PASSWORD_SET" }).to_string()),
  }
}

#[tauri::command]
pub async fn delete_e2e_password() -> Result<(), String> {
  enforce_team_owner_for_encryption_change().await?;
  remove_e2e_password()
}

/// On Team plans, only the team owner is allowed to flip the E2E password
/// state — otherwise members could lock each other out by changing the key.
async fn enforce_team_owner_for_encryption_change() -> Result<(), String> {
  use crate::cloud_auth::CLOUD_AUTH;
  if let Some(state) = CLOUD_AUTH.get_user().await {
    if state.user.plan == "team" && state.user.team_role.as_deref() != Some("owner") {
      return Err("TEAM_OWNER_ONLY".to_string());
    }
  }
  Ok(())
}

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

  #[test]
  fn test_encrypt_decrypt_roundtrip() {
    let key = [42u8; 32];
    let plaintext = b"Hello, World!";
    let encrypted = encrypt_bytes(&key, plaintext).unwrap();
    let decrypted = decrypt_bytes(&key, &encrypted).unwrap();
    assert_eq!(decrypted, plaintext);
  }

  #[test]
  fn test_encrypt_decrypt_empty_data() {
    let key = [1u8; 32];
    let plaintext = b"";
    let encrypted = encrypt_bytes(&key, plaintext).unwrap();
    let decrypted = decrypt_bytes(&key, &encrypted).unwrap();
    assert_eq!(decrypted, plaintext.to_vec());
  }

  #[test]
  fn test_encrypt_decrypt_large_data() {
    let key = [7u8; 32];
    let plaintext = vec![0xABu8; 1_048_576]; // 1MB
    let encrypted = encrypt_bytes(&key, &plaintext).unwrap();
    let decrypted = decrypt_bytes(&key, &encrypted).unwrap();
    assert_eq!(decrypted, plaintext);
  }

  #[test]
  fn test_different_keys_different_ciphertext() {
    let key1 = [1u8; 32];
    let key2 = [2u8; 32];
    let plaintext = b"same data";
    let encrypted1 = encrypt_bytes(&key1, plaintext).unwrap();
    let encrypted2 = encrypt_bytes(&key2, plaintext).unwrap();
    // Nonces are random so ciphertexts will differ regardless,
    // but decrypting with wrong key should fail
    assert!(decrypt_bytes(&key2, &encrypted1).is_err());
    assert!(decrypt_bytes(&key1, &encrypted2).is_err());
  }

  #[test]
  fn test_nonce_uniqueness() {
    let key = [5u8; 32];
    let plaintext = b"same data encrypted twice";
    let encrypted1 = encrypt_bytes(&key, plaintext).unwrap();
    let encrypted2 = encrypt_bytes(&key, plaintext).unwrap();
    // Different nonces should produce different ciphertext
    assert_ne!(encrypted1, encrypted2);
    // But both should decrypt to the same plaintext
    assert_eq!(
      decrypt_bytes(&key, &encrypted1).unwrap(),
      decrypt_bytes(&key, &encrypted2).unwrap()
    );
  }

  #[test]
  fn test_wrong_key_fails() {
    let key = [10u8; 32];
    let wrong_key = [20u8; 32];
    let plaintext = b"secret data";
    let encrypted = encrypt_bytes(&key, plaintext).unwrap();
    assert!(decrypt_bytes(&wrong_key, &encrypted).is_err());
  }

  #[test]
  fn test_key_derivation_deterministic() {
    let salt = generate_salt();
    let key1 = derive_profile_key("my_password", &salt).unwrap();
    let key2 = derive_profile_key("my_password", &salt).unwrap();
    assert_eq!(key1, key2);
  }

  #[test]
  fn test_key_derivation_different_salts() {
    let salt1 = generate_salt();
    let salt2 = generate_salt();
    let key1 = derive_profile_key("my_password", &salt1).unwrap();
    let key2 = derive_profile_key("my_password", &salt2).unwrap();
    assert_ne!(key1, key2);
  }

  #[test]
  fn test_salt_generation_unique() {
    let salt1 = generate_salt();
    let salt2 = generate_salt();
    assert_ne!(salt1, salt2);
  }

  #[test]
  fn test_password_storage_roundtrip() {
    let password = "test_password_12345";
    store_e2e_password(password).unwrap();
    assert!(has_e2e_password());
    let loaded = load_e2e_password().unwrap();
    assert_eq!(loaded, Some(password.to_string()));
    remove_e2e_password().unwrap();
    assert!(!has_e2e_password());
  }

  #[test]
  fn test_decrypt_too_short_data() {
    let key = [1u8; 32];
    assert!(decrypt_bytes(&key, &[0u8; 5]).is_err());
  }
}
