Skip to content
use actix_web::http::header::HeaderValue;

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

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

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

    pub fn is_valid_pin(&self, input: &str) -> bool {
        let trimmed = input.trim();
        trimmed.len() == 6 && trimmed.chars().all(|c| c.is_ascii_digit()) && trimmed == self.pin
    }

    /// Deletes the database file if `RESET_DB` is set to true.
    /// Logs warnings and results appropriately.
    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(|_| "notify.db".to_string());
    let reset_db = std::env::var("RESET_DB").unwrap_or_default() == "true";
    let pin = std::env::var("PIN").unwrap_or_else(|_| "123456".to_string());

    Config::new(port, cache_control, db_path, reset_db, pin)
}

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

    #[test]
    fn test_config_new() {
        let config = Config::new(
            8080,
            HeaderValue::from_static(DEFAULT_CACHE_CONTROL),
            "notify.db".to_string(),
            false,
            "123456".to_string(),
        );
        assert_eq!(config.port, 8080);
        assert!(config.is_valid_pin("123456"));
        assert!(config.is_valid_pin(" 123456 "));
        assert!(!config.is_valid_pin("000000"));
        assert!(!config.is_valid_pin("12345"));
        assert!(!config.is_valid_pin("1234567"));
        assert!(!config.is_valid_pin(""));
        assert_eq!(
            config.cache_control,
            Some(HeaderValue::from_static(DEFAULT_CACHE_CONTROL))
        );
    }

    #[test]
    fn test_config_new_with_none() {
        let config = Config::new(
            8080,
            None,
            "notify.db".to_string(),
            false,
            "123456".to_string(),
        );
        assert_eq!(config.cache_control, None);
    }

    #[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("no-store")),
            Some(HeaderValue::from_static("no-store"))
        );
        assert_eq!(
            parse_cache_control(Some("true")),
            Some(HeaderValue::from_static("no-cache"))
        );
        assert_eq!(
            parse_cache_control(Some("1")),
            Some(HeaderValue::from_static("no-cache"))
        );
        assert_eq!(parse_cache_control(Some("")), None);
        assert_eq!(parse_cache_control(Some("   ")), None);
        assert_eq!(parse_cache_control(None), None);
    }

    #[test]
    fn test_maybe_reset_database_no_flag() {
        let path = format!(
            "/tmp/test_notify_no_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(),
            false,
            "123456".to_string(),
        );
        config.maybe_reset_database();

        assert!(std::path::Path::new(&path).exists());
        let _ = std::fs::remove_file(&path);
    }

    #[test]
    fn test_maybe_reset_database_with_flag() {
        let path = format!(
            "/tmp/test_notify_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,
            "123456".to_string(),
        );
        config.maybe_reset_database();

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