Skip to content
use actix_web::http::header::HeaderValue;
use std::env;
use std::path::Path;

pub const DEFAULT_CACHE_CONTROL: &str = "max-age=3600";
pub const DEFAULT_PORT: u16 = 8090;
pub const DEFAULT_ADMIN_PIN: &str = "123456";
pub const DEFAULT_DUCKDB_PATH: &str = "pokedex.duckdb";
pub const DEFAULT_SQLITE_PATH: &str = "battles.db";

#[derive(Clone, Debug)]
pub struct Config {
    pub port: u16,
    pub cache_control: Option<HeaderValue>,
    pub duckdb_path: String,
    pub sqlite_path: String,
    pub admin_pin: String,
}

impl Config {
    #[must_use]
    pub fn new(
        port: u16,
        cache_control: Option<HeaderValue>,
        duckdb_path: String,
        sqlite_path: String,
        admin_pin: String,
    ) -> Self {
        Self {
            port,
            cache_control,
            duckdb_path,
            sqlite_path,
            admin_pin,
        }
    }

    #[must_use]
    pub fn is_valid_pin(&self, candidate: &str) -> bool {
        let trimmed = candidate.trim();
        trimmed.len() == 6
            && trimmed.chars().all(|c| c.is_ascii_digit())
            && trimmed == self.admin_pin
    }

    pub fn maybe_reset_databases(&self) {
        let should_reset = env::var("RESET_DB")
            .is_ok_and(|val| matches!(val.to_ascii_lowercase().as_str(), "true" | "1" | "yes"));

        if should_reset {
            log::info!("RESET_DB is set; resetting database files...");
            for path in [&self.duckdb_path, &self.sqlite_path] {
                if Path::new(path).exists() {
                    let _ = std::fs::remove_file(path);
                    let _ = std::fs::remove_file(format!("{path}.wal"));
                    let _ = std::fs::remove_file(format!("{path}-shm"));
                    let _ = std::fs::remove_file(format!("{path}-wal"));
                    log::info!("Removed {path}");
                }
            }
        }
    }
}

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("no-cache")
            || trimmed.eq_ignore_ascii_case("true")
            || trimmed == "1"
        {
            Some(HeaderValue::from_static("no-cache"))
        } else {
            HeaderValue::from_str(trimmed).ok()
        }
    })
}

#[must_use]
pub fn from_env() -> Config {
    let port = env::var("PORT")
        .ok()
        .and_then(|p| p.parse::<u16>().ok())
        .unwrap_or(DEFAULT_PORT);

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

    let duckdb_path = env::var("DUCKDB_PATH").unwrap_or_else(|_| DEFAULT_DUCKDB_PATH.to_string());
    let sqlite_path = env::var("SQLITE_PATH").unwrap_or_else(|_| DEFAULT_SQLITE_PATH.to_string());
    let admin_pin = env::var("ADMIN_PIN")
        .or_else(|_| env::var("PIN"))
        .unwrap_or_else(|_| DEFAULT_ADMIN_PIN.to_string());

    Config::new(port, cache_control, duckdb_path, sqlite_path, admin_pin)
}

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

    #[test]
    fn test_config_pin_validation() {
        let cfg = Config::new(
            8090,
            None,
            "test.duckdb".into(),
            "test.db".into(),
            "654321".into(),
        );
        assert!(cfg.is_valid_pin("654321"));
        assert!(cfg.is_valid_pin(" 654321 \n"));
        assert!(!cfg.is_valid_pin("1234"));
        assert!(!cfg.is_valid_pin("12345"));
        assert!(!cfg.is_valid_pin("1234567"));
        assert!(!cfg.is_valid_pin("abcdef"));
    }

    #[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("public, max-age=3600")),
            Some(HeaderValue::from_static("public, max-age=3600"))
        );
        assert_eq!(parse_cache_control(Some("")), None);
        assert_eq!(parse_cache_control(None), None);
    }
}