nautalid 0.1.0

Scratch container substrate — TLS 1.3 HTTP/2+3 kernel, LID/AetherDB, optional filter bus (GPL-3.0-or-later).
//! Optional server surfaces — each enabled explicitly via **`NAUTALID_*`** env.

use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;

/// TCP/UDP listen endpoints for HTTP/2 and HTTP/3.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ListenEndpoints {
    pub h2: SocketAddr,
    pub h3: SocketAddr,
    pub h2_enabled: bool,
    pub h3_enabled: bool,
}

/// Static file hosting (`NAUTALID_STATIC=1`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StaticSurface {
    pub root: PathBuf,
    pub prefix: String,
}

/// Reverse HTTP proxy (`NAUTALID_PROXY=1`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProxySurface {
    pub upstream: String,
    pub prefix: String,
}

/// WebSocket upgrade endpoint (`NAUTALID_WEBSOCKET=1`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebSocketSurface {
    pub path: String,
}

/// gRPC server (`NAUTALID_GRPC=1`) — dedicated TLS listener.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrpcSurface {
    pub listen: SocketAddr,
}

impl GrpcSurface {
    pub fn from_listen(raw: &str) -> Result<Self, String> {
        raw.parse::<SocketAddr>()
            .map(|listen| Self { listen })
            .map_err(|e| format!("invalid grpc listen address: {e}"))
    }
}

/// Runtime toggles for optional server surfaces.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SurfaceConfig {
    pub listen: ListenEndpoints,
    pub static_files: Option<StaticSurface>,
    pub proxy: Option<ProxySurface>,
    pub websocket: Option<WebSocketSurface>,
    pub grpc: Option<GrpcSurface>,
    pub tls_reload: bool,
    pub probe_h3: bool,
}

impl SurfaceConfig {
    /// Load surface toggles from the environment (all optional surfaces default **off**).
    #[must_use]
    pub fn from_env() -> Self {
        let bind = bind_addr_from_env();
        let h2_port = port_from_env("NAUTALID_H2_PORT").unwrap_or_else(default_port);
        let h3_port = port_from_env("NAUTALID_H3_PORT").unwrap_or_else(default_port);
        Self {
            listen: ListenEndpoints {
                h2: SocketAddr::new(bind, h2_port),
                h3: SocketAddr::new(bind, h3_port),
                h2_enabled: !env_disabled("NAUTALID_H2"),
                h3_enabled: !env_disabled("NAUTALID_H3"),
            },
            static_files: static_from_env(),
            proxy: proxy_from_env(),
            websocket: websocket_from_env(),
            grpc: grpc_from_env(),
            tls_reload: env_enabled("NAUTALID_TLS_RELOAD"),
            probe_h3: env_enabled("NAUTALID_PROBE_H3"),
        }
    }
}

#[must_use]
pub fn bind_addr_from_env() -> IpAddr {
    std::env::var("NAUTALID_BIND_ADDR")
        .ok()
        .and_then(|raw| raw.trim().parse().ok())
        .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED))
}

#[must_use]
pub fn default_port() -> u16 {
    port_from_env("PORT").unwrap_or(8080)
}

fn port_from_env(name: &str) -> Option<u16> {
    std::env::var(name)
        .ok()
        .and_then(|raw| raw.trim().parse().ok())
}

fn static_from_env() -> Option<StaticSurface> {
    if !env_enabled("NAUTALID_STATIC") {
        return None;
    }
    let root = std::env::var("NAUTALID_STATIC_ROOT")
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .map(PathBuf::from)?;
    let prefix = std::env::var("NAUTALID_STATIC_PREFIX")
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|p| p.starts_with('/'))
        .unwrap_or_else(|| "/static".to_string());
    Some(StaticSurface { root, prefix })
}

fn proxy_from_env() -> Option<ProxySurface> {
    if !env_enabled("NAUTALID_PROXY") {
        return None;
    }
    let upstream = std::env::var("NAUTALID_PROXY_UPSTREAM")
        .ok()
        .map(|s| s.trim().trim_end_matches('/').to_string())
        .filter(|s| !s.is_empty())?;
    let prefix = std::env::var("NAUTALID_PROXY_PREFIX")
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|p| p.starts_with('/'))
        .unwrap_or_else(|| "/proxy".to_string());
    Some(ProxySurface { upstream, prefix })
}

fn websocket_from_env() -> Option<WebSocketSurface> {
    if !env_enabled("NAUTALID_WEBSOCKET") {
        return None;
    }
    let path = std::env::var("NAUTALID_WEBSOCKET_PATH")
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|p| p.starts_with('/'))
        .unwrap_or_else(|| "/ws".to_string());
    Some(WebSocketSurface { path })
}

fn grpc_from_env() -> Option<GrpcSurface> {
    if !env_enabled("NAUTALID_GRPC") {
        return None;
    }
    let listen = std::env::var("NAUTALID_GRPC_LISTEN")
        .ok()
        .filter(|s| !s.trim().is_empty())?;
    GrpcSurface::from_listen(listen.trim()).ok()
}

#[must_use]
pub fn env_enabled(name: &str) -> bool {
    matches!(
        std::env::var(name).ok().as_deref(),
        Some("1") | Some("true") | Some("yes")
    )
}

#[must_use]
pub fn env_disabled(name: &str) -> bool {
    matches!(
        std::env::var(name).ok().as_deref(),
        Some("0") | Some("false") | Some("no")
    )
}

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

    #[test]
    fn surfaces_default_off() {
        unsafe {
            std::env::remove_var("NAUTALID_STATIC");
            std::env::remove_var("NAUTALID_PROXY");
            std::env::remove_var("NAUTALID_WEBSOCKET");
            std::env::remove_var("NAUTALID_GRPC");
            std::env::remove_var("NAUTALID_SURFACES_KDL_PATH");
        }
        let cfg = SurfaceConfig::from_env();
        assert!(cfg.static_files.is_none());
        assert!(cfg.proxy.is_none());
        assert!(cfg.websocket.is_none());
        assert!(cfg.grpc.is_none());
    }

    #[test]
    fn static_enabled_requires_root() {
        unsafe {
            std::env::set_var("NAUTALID_STATIC", "1");
            std::env::set_var("NAUTALID_STATIC_ROOT", "/var/www");
            std::env::set_var("NAUTALID_STATIC_PREFIX", "/assets");
        }
        let cfg = SurfaceConfig::from_env();
        let surface = cfg.static_files.expect("static");
        assert_eq!(surface.root, PathBuf::from("/var/www"));
        assert_eq!(surface.prefix, "/assets");
        unsafe {
            std::env::remove_var("NAUTALID_STATIC");
            std::env::remove_var("NAUTALID_STATIC_ROOT");
            std::env::remove_var("NAUTALID_STATIC_PREFIX");
        }
    }
}