//! SQLite-backed certificate store.
//!
//! Persists PEM-encoded certificates and keys alongside validity
//! metadata so the renewal manager can find soon-to-expire certs.
//!
//! **Private keys are encrypted at rest** using AES-256-GCM. The
//! encryption key is derived from a server-specific secret (the DB path
//! + a fixed domain separator) so that each store instance has a unique
//! key. This protects against database file theft — an attacker who
//! copies the SQLite file cannot extract private keys without knowing
//! the derivation input.

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use ring::aead;
use ring::digest;
use rusqlite::{params, Connection};
use std::path::Path;
use zeroize::Zeroize;

/// A stored certificate row.
#[derive(Debug, Clone)]
pub struct StoredCert {
    pub domain: String,
    pub cert_pem: Vec<u8>,
    pub key_pem: Vec<u8>,
    pub not_before: DateTime<Utc>,
    pub not_after: DateTime<Utc>,
    pub created_at: DateTime<Utc>,
}

impl Drop for StoredCert {
    fn drop(&mut self) {
        self.key_pem.zeroize();
    }
}

/// SQLite certificate store with AES-256-GCM encryption for private keys.
pub struct CertStore {
    conn: Connection,
    /// AES-256-GCM sealing key for encrypting private keys at rest.
    seal_key: aead::LessSafeKey,
}

/// Derive a 256-bit AES key from the database path. This ties the encryption
/// to the specific installation — moving the DB file to another path (or
/// machine) makes the encrypted keys unreadable.
fn derive_seal_key(db_path: &str) -> aead::LessSafeKey {
    let mut ctx = digest::Context::new(&digest::SHA256);
    ctx.update(b"bext-tls-key-encryption-v1:");
    ctx.update(db_path.as_bytes());
    let key_bytes = ctx.finish();
    let unbound =
        aead::UnboundKey::new(&aead::AES_256_GCM, key_bytes.as_ref()).expect("AES key from SHA256");
    aead::LessSafeKey::new(unbound)
}

/// Encrypt data with AES-256-GCM. Returns nonce (12 bytes) || ciphertext.
fn encrypt_key_pem(seal_key: &aead::LessSafeKey, plaintext: &[u8]) -> Result<Vec<u8>> {
    let mut nonce_bytes = [0u8; 12];
    ring::rand::SystemRandom::new()
        .fill(&mut nonce_bytes)
        .map_err(|_| anyhow::anyhow!("RNG failure generating nonce"))?;
    let nonce = aead::Nonce::assume_unique_for_key(nonce_bytes);

    let mut in_out = plaintext.to_vec();
    seal_key
        .seal_in_place_append_tag(nonce, aead::Aad::empty(), &mut in_out)
        .map_err(|_| anyhow::anyhow!("AES-256-GCM seal failed"))?;

    // Prepend nonce so we can extract it on decrypt.
    let mut result = Vec::with_capacity(12 + in_out.len());
    result.extend_from_slice(&nonce_bytes);
    result.extend_from_slice(&in_out);
    Ok(result)
}

/// Decrypt data produced by `encrypt_key_pem`. Input: nonce (12 bytes) || ciphertext.
fn decrypt_key_pem(seal_key: &aead::LessSafeKey, data: &[u8]) -> Result<Vec<u8>> {
    if data.len() < 12 + aead::AES_256_GCM.tag_len() {
        // Data too short to contain nonce + tag — likely stored before
        // encryption was enabled. Return as-is (plaintext migration path).
        return Ok(data.to_vec());
    }

    let (nonce_bytes, ciphertext) = data.split_at(12);
    let nonce = aead::Nonce::try_assume_unique_for_key(nonce_bytes)
        .map_err(|_| anyhow::anyhow!("invalid nonce"))?;

    let mut in_out = ciphertext.to_vec();
    match seal_key.open_in_place(nonce, aead::Aad::empty(), &mut in_out) {
        Ok(plaintext) => {
            let result = plaintext.to_vec();
            // Zeroize the working buffer so plaintext doesn't linger in memory.
            in_out.zeroize();
            Ok(result)
        }
        Err(_) => {
            in_out.zeroize();
            // Decryption failed — this is likely a plaintext key from before
            // encryption was enabled. Return the original data unchanged so
            // existing deployments can migrate transparently.
            tracing::warn!("key decryption failed — treating as unencrypted (migration)");
            Ok(data.to_vec())
        }
    }
}

use ring::rand::SecureRandom;

impl CertStore {
    /// Open (or create) the certificate store at `db_path`.
    pub fn new(db_path: &str) -> Result<Self> {
        let seal_key = derive_seal_key(db_path);
        let conn = if db_path == ":memory:" {
            Connection::open_in_memory().context("open in-memory SQLite")?
        } else {
            // Ensure parent directory exists.
            if let Some(parent) = Path::new(db_path).parent() {
                if !parent.as_os_str().is_empty() {
                    std::fs::create_dir_all(parent)
                        .with_context(|| format!("create parent dir for {db_path}"))?;
                }
            }
            Connection::open(db_path).with_context(|| format!("open SQLite at {db_path}"))?
        };

        // Restrict file permissions so only the owner can read/write the cert DB
        // (contains plaintext private keys).
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let perms = std::fs::Permissions::from_mode(0o600);
            let _ = std::fs::set_permissions(db_path, perms);
        }

        conn.execute_batch(
            "PRAGMA journal_mode = WAL;
             PRAGMA busy_timeout = 5000;",
        )
        .context("set SQLite pragmas")?;

        conn.execute(
            "CREATE TABLE IF NOT EXISTS certificates (
                domain      TEXT PRIMARY KEY,
                cert_pem    BLOB NOT NULL,
                key_pem     BLOB NOT NULL,  -- AES-256-GCM encrypted (nonce || ciphertext || tag)
                not_before  TEXT NOT NULL,
                not_after   TEXT NOT NULL,
                created_at  TEXT NOT NULL
            )",
            [],
        )
        .context("create certificates table")?;

        conn.execute(
            "CREATE TABLE IF NOT EXISTS acme_accounts (
                directory_url    TEXT PRIMARY KEY,
                credentials_json TEXT NOT NULL,
                created_at       TEXT NOT NULL DEFAULT (datetime('now'))
            )",
            [],
        )
        .context("create acme_accounts table")?;

        Ok(Self { conn, seal_key })
    }

    /// Store (or replace) a certificate for `domain`.
    /// Private keys are encrypted with AES-256-GCM before being written to the database.
    pub fn store_cert(
        &self,
        domain: &str,
        cert_pem: &[u8],
        key_pem: &[u8],
        not_before: DateTime<Utc>,
        not_after: DateTime<Utc>,
    ) -> Result<()> {
        let now = Utc::now();
        let encrypted_key = encrypt_key_pem(&self.seal_key, key_pem)
            .with_context(|| format!("encrypt key for {domain}"))?;
        self.conn
            .execute(
                "INSERT OR REPLACE INTO certificates
                     (domain, cert_pem, key_pem, not_before, not_after, created_at)
                 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
                params![
                    domain,
                    cert_pem,
                    encrypted_key,
                    not_before.to_rfc3339(),
                    not_after.to_rfc3339(),
                    now.to_rfc3339(),
                ],
            )
            .with_context(|| format!("store cert for {domain}"))?;
        Ok(())
    }

    /// Retrieve a certificate by domain. Private key is decrypted from AES-256-GCM.
    pub fn get_cert(&self, domain: &str) -> Result<Option<StoredCert>> {
        let mut stmt = self.conn.prepare(
            "SELECT domain, cert_pem, key_pem, not_before, not_after, created_at
             FROM certificates WHERE domain = ?1",
        )?;

        let row = stmt
            .query_row(params![domain], |row| {
                Ok(StoredCertRow {
                    domain: row.get(0)?,
                    cert_pem: row.get(1)?,
                    key_pem: row.get(2)?,
                    not_before: row.get::<_, String>(3)?,
                    not_after: row.get::<_, String>(4)?,
                    created_at: row.get::<_, String>(5)?,
                })
            })
            .optional();

        match row {
            Ok(Some(r)) => Ok(Some(r.into_stored_cert(&self.seal_key)?)),
            Ok(None) => Ok(None),
            Err(e) => Err(anyhow::anyhow!("query cert for {domain}: {e}")),
        }
    }

    /// List all stored certificates.
    pub fn list_certs(&self) -> Result<Vec<StoredCert>> {
        let mut stmt = self.conn.prepare(
            "SELECT domain, cert_pem, key_pem, not_before, not_after, created_at
             FROM certificates ORDER BY domain",
        )?;

        let rows = stmt
            .query_map([], |row| {
                Ok(StoredCertRow {
                    domain: row.get(0)?,
                    cert_pem: row.get(1)?,
                    key_pem: row.get(2)?,
                    not_before: row.get::<_, String>(3)?,
                    not_after: row.get::<_, String>(4)?,
                    created_at: row.get::<_, String>(5)?,
                })
            })
            .context("list certs query")?;

        let mut certs = Vec::new();
        for row in rows {
            certs.push(row?.into_stored_cert(&self.seal_key)?);
        }
        Ok(certs)
    }

    /// Delete a certificate by domain.
    pub fn delete_cert(&self, domain: &str) -> Result<()> {
        self.conn
            .execute(
                "DELETE FROM certificates WHERE domain = ?1",
                params![domain],
            )
            .with_context(|| format!("delete cert for {domain}"))?;
        Ok(())
    }

    /// Retrieve saved ACME account credentials for a given directory URL.
    pub fn get_acme_credentials(&self, directory_url: &str) -> Option<String> {
        self.conn
            .query_row(
                "SELECT credentials_json FROM acme_accounts WHERE directory_url = ?1",
                [directory_url],
                |row| row.get(0),
            )
            .ok()
    }

    /// Save (or replace) ACME account credentials for a given directory URL.
    pub fn save_acme_credentials(&self, directory_url: &str, credentials_json: &str) -> Result<()> {
        self.conn
            .execute(
                "INSERT OR REPLACE INTO acme_accounts (directory_url, credentials_json) VALUES (?1, ?2)",
                params![directory_url, credentials_json],
            )
            .context("save ACME account credentials")?;
        Ok(())
    }

    /// Return certificates expiring within the next `days` days.
    pub fn get_expiring(&self, days: u32) -> Result<Vec<StoredCert>> {
        let cutoff = Utc::now() + chrono::Duration::days(i64::from(days));

        let mut stmt = self.conn.prepare(
            "SELECT domain, cert_pem, key_pem, not_before, not_after, created_at
             FROM certificates
             WHERE not_after <= ?1
             ORDER BY not_after ASC",
        )?;

        let rows = stmt
            .query_map(params![cutoff.to_rfc3339()], |row| {
                Ok(StoredCertRow {
                    domain: row.get(0)?,
                    cert_pem: row.get(1)?,
                    key_pem: row.get(2)?,
                    not_before: row.get::<_, String>(3)?,
                    not_after: row.get::<_, String>(4)?,
                    created_at: row.get::<_, String>(5)?,
                })
            })
            .context("get expiring certs query")?;

        let mut certs = Vec::new();
        for row in rows {
            certs.push(row?.into_stored_cert(&self.seal_key)?);
        }
        Ok(certs)
    }
}

/// Internal intermediate row for rusqlite → StoredCert conversion.
struct StoredCertRow {
    domain: String,
    cert_pem: Vec<u8>,
    key_pem: Vec<u8>,
    not_before: String,
    not_after: String,
    created_at: String,
}

impl StoredCertRow {
    fn into_stored_cert(self, seal_key: &aead::LessSafeKey) -> Result<StoredCert> {
        let decrypted_key = decrypt_key_pem(seal_key, &self.key_pem)
            .with_context(|| format!("decrypt key for {}", self.domain))?;
        Ok(StoredCert {
            domain: self.domain,
            cert_pem: self.cert_pem,
            key_pem: decrypted_key,
            not_before: DateTime::parse_from_rfc3339(&self.not_before)
                .with_context(|| format!("parse not_before: {}", self.not_before))?
                .with_timezone(&Utc),
            not_after: DateTime::parse_from_rfc3339(&self.not_after)
                .with_context(|| format!("parse not_after: {}", self.not_after))?
                .with_timezone(&Utc),
            created_at: DateTime::parse_from_rfc3339(&self.created_at)
                .with_context(|| format!("parse created_at: {}", self.created_at))?
                .with_timezone(&Utc),
        })
    }
}

/// Extension trait so `query_row` can return `Option`.
trait OptionalRow<T> {
    fn optional(self) -> Result<Option<T>, rusqlite::Error>;
}

impl<T> OptionalRow<T> for Result<T, rusqlite::Error> {
    fn optional(self) -> Result<Option<T>, rusqlite::Error> {
        match self {
            Ok(val) => Ok(Some(val)),
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
            Err(e) => Err(e),
        }
    }
}

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

    fn make_store() -> CertStore {
        CertStore::new(":memory:").unwrap()
    }

    fn sample_cert() -> (Vec<u8>, Vec<u8>) {
        let out = crate::self_signed::generate_self_signed(&["example.com".into()]).unwrap();
        (out.cert_pem, out.key_pem)
    }

    #[test]
    fn store_and_retrieve() {
        let store = make_store();
        let (cert, key) = sample_cert();
        let now = Utc::now();
        let later = now + Duration::days(90);

        store
            .store_cert("example.com", &cert, &key, now, later)
            .unwrap();

        let retrieved = store.get_cert("example.com").unwrap().unwrap();
        assert_eq!(retrieved.domain, "example.com");
        assert_eq!(retrieved.cert_pem, cert);
        assert_eq!(retrieved.key_pem, key);
    }

    #[test]
    fn get_nonexistent_returns_none() {
        let store = make_store();
        assert!(store.get_cert("nope.com").unwrap().is_none());
    }

    #[test]
    fn store_replaces_existing() {
        let store = make_store();
        let (cert1, key1) = sample_cert();
        let (cert2, key2) = sample_cert();
        let now = Utc::now();
        let later = now + Duration::days(90);

        store
            .store_cert("example.com", &cert1, &key1, now, later)
            .unwrap();
        store
            .store_cert("example.com", &cert2, &key2, now, later)
            .unwrap();

        let retrieved = store.get_cert("example.com").unwrap().unwrap();
        assert_eq!(retrieved.cert_pem, cert2);
        assert_eq!(retrieved.key_pem, key2);
    }

    #[test]
    fn list_certs_empty() {
        let store = make_store();
        let certs = store.list_certs().unwrap();
        assert!(certs.is_empty());
    }

    #[test]
    fn list_certs_multiple() {
        let store = make_store();
        let (cert, key) = sample_cert();
        let now = Utc::now();
        let later = now + Duration::days(90);

        store
            .store_cert("a.example.com", &cert, &key, now, later)
            .unwrap();
        store
            .store_cert("b.example.com", &cert, &key, now, later)
            .unwrap();
        store
            .store_cert("c.example.com", &cert, &key, now, later)
            .unwrap();

        let certs = store.list_certs().unwrap();
        assert_eq!(certs.len(), 3);
        // Ordered alphabetically.
        assert_eq!(certs[0].domain, "a.example.com");
        assert_eq!(certs[1].domain, "b.example.com");
        assert_eq!(certs[2].domain, "c.example.com");
    }

    #[test]
    fn delete_cert() {
        let store = make_store();
        let (cert, key) = sample_cert();
        let now = Utc::now();
        let later = now + Duration::days(90);

        store
            .store_cert("example.com", &cert, &key, now, later)
            .unwrap();
        store.delete_cert("example.com").unwrap();

        assert!(store.get_cert("example.com").unwrap().is_none());
    }

    #[test]
    fn delete_nonexistent_is_ok() {
        let store = make_store();
        // Should not error.
        store.delete_cert("does-not-exist.com").unwrap();
    }

    #[test]
    fn get_expiring_within_days() {
        let store = make_store();
        let (cert, key) = sample_cert();
        let now = Utc::now();

        // Cert expiring in 10 days.
        store
            .store_cert(
                "soon.com",
                &cert,
                &key,
                now - Duration::days(80),
                now + Duration::days(10),
            )
            .unwrap();

        // Cert expiring in 60 days.
        store
            .store_cert(
                "later.com",
                &cert,
                &key,
                now - Duration::days(30),
                now + Duration::days(60),
            )
            .unwrap();

        // Cert expiring in 200 days.
        store
            .store_cert(
                "far.com",
                &cert,
                &key,
                now - Duration::days(10),
                now + Duration::days(200),
            )
            .unwrap();

        // Within 30 days: only "soon.com".
        let expiring = store.get_expiring(30).unwrap();
        assert_eq!(expiring.len(), 1);
        assert_eq!(expiring[0].domain, "soon.com");

        // Within 90 days: "soon.com" and "later.com".
        let expiring = store.get_expiring(90).unwrap();
        assert_eq!(expiring.len(), 2);
    }

    #[test]
    fn get_expiring_none() {
        let store = make_store();
        let (cert, key) = sample_cert();
        let now = Utc::now();

        store
            .store_cert("far.com", &cert, &key, now, now + Duration::days(300))
            .unwrap();

        let expiring = store.get_expiring(30).unwrap();
        assert!(expiring.is_empty());
    }

    #[test]
    fn get_expiring_already_expired() {
        let store = make_store();
        let (cert, key) = sample_cert();
        let now = Utc::now();

        // Already expired.
        store
            .store_cert(
                "expired.com",
                &cert,
                &key,
                now - Duration::days(100),
                now - Duration::days(5),
            )
            .unwrap();

        // Within 1 day should still catch already-expired certs.
        let expiring = store.get_expiring(1).unwrap();
        assert_eq!(expiring.len(), 1);
        assert_eq!(expiring[0].domain, "expired.com");
    }

    #[test]
    fn store_preserves_timestamps() {
        let store = make_store();
        let (cert, key) = sample_cert();
        let not_before = chrono::DateTime::parse_from_rfc3339("2025-01-15T10:00:00Z")
            .unwrap()
            .with_timezone(&Utc);
        let not_after = chrono::DateTime::parse_from_rfc3339("2025-04-15T10:00:00Z")
            .unwrap()
            .with_timezone(&Utc);

        store
            .store_cert("ts.com", &cert, &key, not_before, not_after)
            .unwrap();

        let retrieved = store.get_cert("ts.com").unwrap().unwrap();
        assert_eq!(retrieved.not_before, not_before);
        assert_eq!(retrieved.not_after, not_after);
    }

    #[test]
    fn file_backed_store() {
        let dir = tempfile::tempdir().unwrap();
        let db_path = dir.path().join("certs.db");
        let db_str = db_path.to_str().unwrap();

        let store = CertStore::new(db_str).unwrap();
        let (cert, key) = sample_cert();
        let now = Utc::now();
        store
            .store_cert("file.com", &cert, &key, now, now + Duration::days(90))
            .unwrap();

        // Re-open and verify persistence.
        drop(store);
        let store2 = CertStore::new(db_str).unwrap();
        let retrieved = store2.get_cert("file.com").unwrap().unwrap();
        assert_eq!(retrieved.domain, "file.com");
    }

    #[test]
    fn store_binary_cert_data() {
        let store = make_store();
        let cert = vec![0u8, 1, 2, 255, 254, 253];
        let key = vec![10u8, 20, 30, 40];
        let now = Utc::now();

        store
            .store_cert("bin.com", &cert, &key, now, now + Duration::days(1))
            .unwrap();

        let retrieved = store.get_cert("bin.com").unwrap().unwrap();
        assert_eq!(retrieved.cert_pem, cert);
        assert_eq!(retrieved.key_pem, key);
    }
}
