Skip to content
use actix_web::{cookie::Key, http::header::HeaderValue};

pub const DEFAULT_CACHE_CONTROL: &str = "public, max-age=31536000, immutable";

#[derive(Clone)]
pub struct Config {
    pub port: u16,
    pub cache_control: Option<HeaderValue>,
    pub db_path: String,
    pub reset_db: bool,
    pub invite_secret: Option<String>,
    pub session_key: Key,
}

impl Config {
    pub fn new(
        port: u16,
        cache_control: impl Into<Option<HeaderValue>>,
        db_path: String,
        reset_db: bool,
    ) -> Self {
        Self {
            port,
            cache_control: cache_control.into(),
            db_path,
            reset_db,
            invite_secret: None,
            session_key: Key::generate(),
        }
    }

    pub fn invites_enabled(&self) -> bool {
        self.invite_secret
            .as_deref()
            .is_some_and(|secret| !secret.is_empty())
    }

    pub fn invite_secret_matches(&self, provided: &str) -> bool {
        self.invites_enabled() && self.invite_secret.as_deref() == Some(provided)
    }

    /// Deletes the database file if `RESET_DB` is set to true.
    pub fn maybe_reset_database(&self) {
        if !self.reset_db {
            return;
        }

        log::warn!(
            "RESET_DB is set to true, deleting database file: {}",
            self.db_path
        );

        if std::path::Path::new(&self.db_path).exists() {
            if let Err(e) = std::fs::remove_file(&self.db_path) {
                log::error!("Failed to delete database file: {e}");
            } else {
                log::info!("Database file deleted successfully");
            }
        }
    }
}

pub fn parse_cache_control(val: Option<&str>) -> Option<HeaderValue> {
    val.and_then(|v| {
        let trimmed = v.trim();
        if trimmed.is_empty() {
            None
        } else if trimmed.eq_ignore_ascii_case("true") || trimmed == "1" {
            Some(HeaderValue::from_static("no-cache"))
        } else {
            HeaderValue::from_str(trimmed).ok()
        }
    })
}

pub fn from_env() -> Config {
    let port = std::env::var("PORT")
        .unwrap_or_else(|_| "8080".to_string())
        .parse()
        .unwrap_or(8080);

    let cache_control = parse_cache_control(std::env::var("CACHE_CONTROL").ok().as_deref());

    let db_path = std::env::var("DB_PATH").unwrap_or_else(|_| "pear.db".to_string());
    let reset_db = std::env::var("RESET_DB").unwrap_or_default() == "true";

    let mut config = Config::new(port, cache_control, db_path, reset_db);
    config.invite_secret = std::env::var("INVITE_SECRET")
        .ok()
        .filter(|secret| !secret.is_empty());
    config.session_key = if let Ok(secret) = std::env::var("SESSION_SECRET") {
        Key::try_from(secret.as_bytes())
            .unwrap_or_else(|_| panic!("SESSION_SECRET must be at least 64 bytes long"))
    } else {
        log::warn!("SESSION_SECRET is unset; sessions will not survive process restarts");
        Key::generate()
    };
    config
}

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

    #[test]
    fn test_parse_cache_control() {
        assert_eq!(
            parse_cache_control(Some("no-cache")),
            Some(HeaderValue::from_static("no-cache"))
        );
        assert_eq!(
            parse_cache_control(Some("true")),
            Some(HeaderValue::from_static("no-cache"))
        );
        assert_eq!(parse_cache_control(Some("")), None);
        assert_eq!(parse_cache_control(None), None);
    }

    #[test]
    fn test_maybe_reset_database_with_flag() {
        let path = format!(
            "/tmp/test_pear_reset_{}.db",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        );
        std::fs::write(&path, b"test").unwrap();

        let config = Config::new(
            8080,
            HeaderValue::from_static(DEFAULT_CACHE_CONTROL),
            path.clone(),
            true,
        );
        config.maybe_reset_database();

        assert!(!std::path::Path::new(&path).exists());
    }
}