use anyhow::{Context, Result, bail};
use std::path::PathBuf;
use tonic::transport::{Certificate, Identity, ServerTlsConfig};
use tracing::{info, warn};

const DEFAULT_PKI_DIR: &str = "/etc/lesavka/pki";

pub fn server_tls_config() -> Result<Option<ServerTlsConfig>> {
    let cert_path = env_path("LESAVKA_TLS_CERT").unwrap_or_else(|| default_path("server.crt"));
    let key_path = env_path("LESAVKA_TLS_KEY").unwrap_or_else(|| default_path("server.key"));
    let require_tls = bool_env("LESAVKA_REQUIRE_TLS");

    if !cert_path.exists() || !key_path.exists() {
        if require_tls {
            bail!(
                "LESAVKA_REQUIRE_TLS=1 but TLS identity files are missing: cert={} key={}",
                cert_path.display(),
                key_path.display()
            );
        }
        warn!(
            cert = %cert_path.display(),
            key = %key_path.display(),
            "🔓 TLS identity not present; serving plaintext gRPC for local/dev use"
        );
        return Ok(None);
    }

    let cert = std::fs::read(&cert_path)
        .with_context(|| format!("reading TLS server certificate {}", cert_path.display()))?;
    let key = std::fs::read(&key_path)
        .with_context(|| format!("reading TLS server key {}", key_path.display()))?;
    let mut tls = ServerTlsConfig::new().identity(Identity::from_pem(cert, key));

    let client_ca_path =
        env_path("LESAVKA_TLS_CLIENT_CA").unwrap_or_else(|| default_path("ca.crt"));
    if client_ca_path.exists() {
        let client_ca = std::fs::read(&client_ca_path)
            .with_context(|| format!("reading TLS client CA {}", client_ca_path.display()))?;
        tls = tls.client_ca_root(Certificate::from_pem(client_ca));
        if bool_env("LESAVKA_TLS_CLIENT_AUTH_OPTIONAL") {
            tls = tls.client_auth_optional(true);
            warn!("🔐 TLS enabled with optional client certificates");
        } else {
            info!("🔐 TLS enabled with required client certificate authentication");
        }
    } else {
        warn!(
            ca = %client_ca_path.display(),
            "🔐 TLS enabled without client certificate authentication; install a client CA for production"
        );
    }

    Ok(Some(tls))
}

fn env_path(name: &str) -> Option<PathBuf> {
    std::env::var_os(name)
        .filter(|value| !value.is_empty())
        .map(PathBuf::from)
}

fn default_path(file_name: &str) -> PathBuf {
    PathBuf::from(DEFAULT_PKI_DIR).join(file_name)
}

fn bool_env(name: &str) -> bool {
    std::env::var(name)
        .map(|value| {
            let value = value.trim().to_ascii_lowercase();
            !value.is_empty() && value != "0" && value != "false" && value != "no"
        })
        .unwrap_or(false)
}

#[cfg(test)]
mod tests {
    use super::{bool_env, default_path, env_path, server_tls_config};
    use temp_env::with_vars;
    use tempfile::tempdir;

    #[test]
    fn missing_identity_is_allowed_unless_tls_is_required() {
        with_vars(
            [
                ("LESAVKA_REQUIRE_TLS", None::<&str>),
                ("LESAVKA_TLS_CERT", Some("/tmp/lesavka-missing-cert")),
                ("LESAVKA_TLS_KEY", Some("/tmp/lesavka-missing-key")),
            ],
            || {
                assert!(
                    server_tls_config()
                        .expect("optional tls should not error")
                        .is_none()
                )
            },
        );
    }

    #[test]
    fn require_tls_errors_when_identity_is_missing() {
        with_vars(
            [
                ("LESAVKA_REQUIRE_TLS", Some("1")),
                ("LESAVKA_TLS_CERT", Some("/tmp/lesavka-missing-cert")),
                ("LESAVKA_TLS_KEY", Some("/tmp/lesavka-missing-key")),
            ],
            || {
                let err = server_tls_config().expect_err("required tls should fail");
                assert!(err.to_string().contains("LESAVKA_REQUIRE_TLS=1"));
            },
        );
    }

    #[test]
    fn helpers_parse_paths_and_boolean_overrides() {
        with_vars(
            [
                ("LESAVKA_TLS_CERT", Some("/tmp/server.crt")),
                ("LESAVKA_REQUIRE_TLS", Some("yes")),
            ],
            || {
                assert_eq!(
                    env_path("LESAVKA_TLS_CERT").unwrap(),
                    std::path::PathBuf::from("/tmp/server.crt")
                );
                assert!(bool_env("LESAVKA_REQUIRE_TLS"));
            },
        );
        with_vars(
            [
                ("LESAVKA_TLS_CERT", Some("")),
                ("LESAVKA_REQUIRE_TLS", Some("false")),
            ],
            || {
                assert!(env_path("LESAVKA_TLS_CERT").is_none());
                assert!(!bool_env("LESAVKA_REQUIRE_TLS"));
                assert_eq!(
                    default_path("server.crt"),
                    std::path::PathBuf::from("/etc/lesavka/pki/server.crt")
                );
            },
        );
    }

    #[test]
    fn tls_identity_can_load_with_and_without_client_ca() {
        let dir = tempdir().expect("tls dir");
        let cert = dir.path().join("server.crt");
        let key = dir.path().join("server.key");
        let ca = dir.path().join("ca.crt");
        std::fs::write(&cert, b"not a real cert").expect("cert");
        std::fs::write(&key, b"not a real key").expect("key");
        std::fs::write(&ca, b"not a real ca").expect("ca");

        with_vars(
            [
                ("LESAVKA_REQUIRE_TLS", Some("1")),
                ("LESAVKA_TLS_CERT", Some(cert.to_string_lossy().as_ref())),
                ("LESAVKA_TLS_KEY", Some(key.to_string_lossy().as_ref())),
                ("LESAVKA_TLS_CLIENT_CA", Some(ca.to_string_lossy().as_ref())),
                ("LESAVKA_TLS_CLIENT_AUTH_OPTIONAL", Some("1")),
            ],
            || {
                assert!(server_tls_config().expect("tls config").is_some());
            },
        );

        with_vars(
            [
                ("LESAVKA_REQUIRE_TLS", Some("1")),
                ("LESAVKA_TLS_CERT", Some(cert.to_string_lossy().as_ref())),
                ("LESAVKA_TLS_KEY", Some(key.to_string_lossy().as_ref())),
                (
                    "LESAVKA_TLS_CLIENT_CA",
                    Some(dir.path().join("missing-ca.crt").to_string_lossy().as_ref()),
                ),
                ("LESAVKA_TLS_CLIENT_AUTH_OPTIONAL", Some("0")),
            ],
            || {
                assert!(
                    server_tls_config()
                        .expect("tls config without ca")
                        .is_some()
                );
            },
        );
    }

    #[test]
    fn existing_but_unreadable_identity_reports_context() {
        let dir = tempdir().expect("tls dir");
        let key = dir.path().join("server.key");
        std::fs::write(&key, b"not a real key").expect("key");

        with_vars(
            [
                ("LESAVKA_REQUIRE_TLS", Some("1")),
                (
                    "LESAVKA_TLS_CERT",
                    Some(dir.path().to_string_lossy().as_ref()),
                ),
                ("LESAVKA_TLS_KEY", Some(key.to_string_lossy().as_ref())),
            ],
            || {
                let err = server_tls_config().expect_err("directory cert should fail");
                assert!(err.to_string().contains("reading TLS server certificate"));
            },
        );
    }
}
