use aes_gcm::{
    Aes256Gcm, KeyInit, Nonce,
    aead::{Aead, OsRng, rand_core::RngCore},
};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};

use crate::RuntimeError;

#[derive(Clone)]
pub struct SecretCrypto {
    cipher: Aes256Gcm,
    key_version: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct CipherEnvelope {
    nonce_b64: String,
    ciphertext_b64: String,
}

impl SecretCrypto {
    pub fn new(master_key: &str) -> Result<Self, RuntimeError> {
        let trimmed = master_key.trim();
        if trimmed.is_empty() {
            return Err(RuntimeError::SecretCrypto {
                details: "CRANK_MASTER_KEY must not be empty".to_owned(),
            });
        }

        let digest = Sha256::digest(trimmed.as_bytes());
        let cipher = Aes256Gcm::new_from_slice(digest.as_slice()).map_err(|error| {
            RuntimeError::SecretCrypto {
                details: format!("failed to initialize secret crypto: {error}"),
            }
        })?;

        Ok(Self {
            cipher,
            key_version: "v1".to_owned(),
        })
    }

    pub fn key_version(&self) -> &str {
        &self.key_version
    }

    pub fn encrypt(&self, value: &Value) -> Result<String, RuntimeError> {
        let plaintext = serde_json::to_vec(value).map_err(|error| RuntimeError::SecretCrypto {
            details: format!("failed to serialize secret value: {error}"),
        })?;
        let mut nonce_bytes = [0_u8; 12];
        OsRng.fill_bytes(&mut nonce_bytes);
        let nonce = Nonce::from_slice(&nonce_bytes);
        let ciphertext = self
            .cipher
            .encrypt(nonce, plaintext.as_ref())
            .map_err(|error| RuntimeError::SecretCrypto {
                details: format!("failed to encrypt secret value: {error}"),
            })?;
        let envelope = CipherEnvelope {
            nonce_b64: STANDARD.encode(nonce_bytes),
            ciphertext_b64: STANDARD.encode(ciphertext),
        };

        serde_json::to_string(&envelope).map_err(|error| RuntimeError::SecretCrypto {
            details: format!("failed to encode secret ciphertext: {error}"),
        })
    }

    pub fn decrypt(&self, ciphertext: &str) -> Result<Value, RuntimeError> {
        let envelope: CipherEnvelope =
            serde_json::from_str(ciphertext).map_err(|error| RuntimeError::SecretCrypto {
                details: format!("failed to decode secret envelope: {error}"),
            })?;
        let nonce_bytes =
            STANDARD
                .decode(envelope.nonce_b64)
                .map_err(|error| RuntimeError::SecretCrypto {
                    details: format!("failed to decode secret nonce: {error}"),
                })?;
        let ciphertext_bytes = STANDARD.decode(envelope.ciphertext_b64).map_err(|error| {
            RuntimeError::SecretCrypto {
                details: format!("failed to decode secret payload: {error}"),
            }
        })?;
        let plaintext = self
            .cipher
            .decrypt(Nonce::from_slice(&nonce_bytes), ciphertext_bytes.as_ref())
            .map_err(|error| RuntimeError::SecretCrypto {
                details: format!("failed to decrypt secret value: {error}"),
            })?;

        serde_json::from_slice(&plaintext).map_err(|error| RuntimeError::SecretCrypto {
            details: format!("failed to deserialize secret value: {error}"),
        })
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::SecretCrypto;

    #[test]
    fn roundtrips_secret_payload() {
        let crypto = SecretCrypto::new("test-master-key").unwrap();
        let plaintext = json!({
            "token": "top-secret",
            "username": "demo"
        });

        let ciphertext = crypto.encrypt(&plaintext).unwrap();
        let decrypted = crypto.decrypt(&ciphertext).unwrap();

        assert_eq!(decrypted, plaintext);
    }
}
