//! Internal API for managing sdme configuration.
//!
//! Handles loading, saving, and resolving the configuration file
//! (default: `~/.config/sdme/sdmerc`, TOML format). Provides the
//! [`Config`] struct and functions for reading/writing it to disk.

use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
    #[serde(default = "default_interactive")]
    pub interactive: bool,

    #[serde(default = "default_datadir")]
    pub datadir: PathBuf,

    #[serde(default = "default_boot_timeout")]
    pub boot_timeout: u64,

    #[serde(default = "default_join_as_sudo_user")]
    pub join_as_sudo_user: bool,

    #[serde(default = "default_host_rootfs_opaque_dirs")]
    pub host_rootfs_opaque_dirs: String,

    /// Comma-separated capabilities dropped by `--hardened`.
    #[serde(default = "default_hardened_drop_caps")]
    pub hardened_drop_caps: String,

    /// Default base rootfs for OCI application images.
    #[serde(default)]
    pub default_base_fs: String,

    /// Docker Hub username for authenticated pulls.
    #[serde(default)]
    pub docker_user: String,

    /// Docker Hub personal access token for authenticated pulls.
    #[serde(default)]
    pub docker_token: String,

    /// OCI blob cache directory (empty = {datadir}/cache/oci).
    #[serde(default)]
    pub oci_cache_dir: String,

    /// Maximum OCI blob cache size (e.g. "10G"). "0" disables the cache.
    #[serde(default = "default_oci_cache_max_size")]
    pub oci_cache_max_size: String,
}

fn default_interactive() -> bool {
    true
}

fn default_datadir() -> PathBuf {
    PathBuf::from("/var/lib/sdme")
}

fn default_boot_timeout() -> u64 {
    60
}

fn default_join_as_sudo_user() -> bool {
    true
}

fn default_host_rootfs_opaque_dirs() -> String {
    "/etc/systemd/system,/var/log".to_string()
}

fn default_hardened_drop_caps() -> String {
    crate::security::HARDENED_DROP_CAPS.join(",")
}

fn default_oci_cache_max_size() -> String {
    "10G".to_string()
}

impl Default for Config {
    fn default() -> Self {
        Self {
            interactive: true,
            datadir: default_datadir(),
            boot_timeout: default_boot_timeout(),
            join_as_sudo_user: default_join_as_sudo_user(),
            host_rootfs_opaque_dirs: default_host_rootfs_opaque_dirs(),
            hardened_drop_caps: default_hardened_drop_caps(),
            default_base_fs: String::new(),
            docker_user: String::new(),
            docker_token: String::new(),
            oci_cache_dir: String::new(),
            oci_cache_max_size: default_oci_cache_max_size(),
        }
    }
}

impl Config {
    pub fn display(&self) {
        let interactive = if self.interactive { "yes" } else { "no" };
        let join_as_sudo_user = if self.join_as_sudo_user { "yes" } else { "no" };
        println!("interactive = {interactive}");
        println!("datadir = {}", self.datadir.display());
        println!("boot_timeout = {}", self.boot_timeout);
        println!("join_as_sudo_user = {join_as_sudo_user}");
        println!("host_rootfs_opaque_dirs = {}", self.host_rootfs_opaque_dirs);
        println!("hardened_drop_caps = {}", self.hardened_drop_caps);
        println!("default_base_fs = {}", self.default_base_fs);
        println!("docker_user = {}", self.docker_user);
        let docker_token_display = if self.docker_token.is_empty() {
            String::new()
        } else {
            let len = self.docker_token.len();
            if len <= 8 {
                "*".repeat(len)
            } else {
                format!(
                    "{}…{}",
                    &self.docker_token[..4],
                    &self.docker_token[len - 4..]
                )
            }
        };
        println!("docker_token = {docker_token_display}");
        println!("oci_cache_dir = {}", self.oci_cache_dir);
        println!("oci_cache_max_size = {}", self.oci_cache_max_size);
    }
}

fn sudo_user_config_path() -> Option<PathBuf> {
    let su = crate::sudo_user()?;
    Some(su.home.join(".config").join("sdme").join("sdmerc"))
}

pub fn config_path() -> Result<PathBuf> {
    // When running under sudo, prefer the invoking user's config if it exists.
    if let Some(path) = sudo_user_config_path() {
        if path.exists() {
            return Ok(path);
        }
    }
    let base = if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
        PathBuf::from(xdg)
    } else {
        let home = std::env::var("HOME").context("HOME not set")?;
        PathBuf::from(home).join(".config")
    };
    Ok(base.join("sdme").join("sdmerc"))
}

pub fn resolve_path(path: Option<&Path>) -> Result<PathBuf> {
    match path {
        Some(p) => Ok(p.to_path_buf()),
        None => config_path(),
    }
}

pub fn load(path: Option<&Path>) -> Result<Config> {
    let path = resolve_path(path)?;
    if !path.exists() {
        return Ok(Config::default());
    }
    let contents = std::fs::read_to_string(&path)
        .with_context(|| format!("failed to read {}", path.display()))?;
    let config: Config =
        toml::from_str(&contents).with_context(|| format!("failed to parse {}", path.display()))?;
    Ok(config)
}

pub fn save(config: &Config, path: Option<&Path>) -> Result<()> {
    let path = resolve_path(path)?;
    if let Some(parent) = path.parent() {
        std::fs::DirBuilder::new()
            .recursive(true)
            .mode(0o700)
            .create(parent)
            .with_context(|| format!("failed to create {}", parent.display()))?;
    }
    let contents = toml::to_string(config).context("failed to serialize config")?;
    crate::atomic_write(&path, contents.as_bytes())
        .with_context(|| format!("failed to write config {}", path.display()))?;
    // Ensure restrictive permissions (atomic_write creates with default umask).
    std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
        .with_context(|| format!("failed to set permissions on {}", path.display()))?;
    Ok(())
}

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

    use std::sync::Mutex;

    // Tests must run serially because they modify XDG_CONFIG_HOME.
    static ENV_LOCK: Mutex<()> = Mutex::new(());

    struct TempConfig {
        dir: PathBuf,
        _guard: std::sync::MutexGuard<'static, ()>,
    }

    impl TempConfig {
        fn new() -> Self {
            let guard = ENV_LOCK.lock().unwrap();
            let dir = std::env::temp_dir().join(format!(
                "sdme-test-{}-{:?}",
                std::process::id(),
                std::thread::current().id()
            ));
            let _ = fs::remove_dir_all(&dir);
            fs::create_dir_all(&dir).unwrap();
            std::env::set_var("XDG_CONFIG_HOME", &dir);
            Self { dir, _guard: guard }
        }
    }

    impl Drop for TempConfig {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.dir);
            std::env::remove_var("XDG_CONFIG_HOME");
        }
    }

    #[test]
    fn test_default_config() {
        let config = Config::default();
        assert!(config.interactive);
        assert_eq!(config.datadir, PathBuf::from("/var/lib/sdme"));
    }

    #[test]
    fn test_load_missing_file() {
        let _tmp = TempConfig::new();
        let config = load(None).unwrap();
        assert!(config.interactive);
    }

    #[test]
    fn test_save_and_load() {
        let _tmp = TempConfig::new();
        let config = Config {
            interactive: false,
            ..Config::default()
        };
        save(&config, None).unwrap();
        let loaded = load(None).unwrap();
        assert!(!loaded.interactive);
    }

    #[test]
    fn test_load_partial_config() {
        let _tmp = TempConfig::new();
        let path = config_path().unwrap();
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        // Write an empty TOML file; missing keys should get defaults.
        fs::write(&path, "").unwrap();
        let config = load(None).unwrap();
        assert!(config.interactive);
    }

    #[test]
    fn test_explicit_path() {
        let dir = std::env::temp_dir().join(format!("sdme-test-explicit-{}", std::process::id()));
        let _ = fs::remove_dir_all(&dir);
        fs::create_dir_all(&dir).unwrap();
        let path = dir.join("custom-sdmerc");

        // Load from non-existent explicit path returns default.
        let config = load(Some(&path)).unwrap();
        assert!(config.interactive);

        // Save to explicit path then reload.
        let config = Config {
            interactive: false,
            ..Config::default()
        };
        save(&config, Some(&path)).unwrap();
        let loaded = load(Some(&path)).unwrap();
        assert!(!loaded.interactive);

        let _ = fs::remove_dir_all(&dir);
    }

    #[test]
    fn test_save_and_load_custom_datadir() {
        let _tmp = TempConfig::new();
        let config = Config {
            interactive: true,
            datadir: PathBuf::from("/tmp/custom-data"),
            ..Config::default()
        };
        save(&config, None).unwrap();
        let loaded = load(None).unwrap();
        assert_eq!(loaded.datadir, PathBuf::from("/tmp/custom-data"));
    }

    #[test]
    fn test_config_path_xdg() {
        let _tmp = TempConfig::new();
        let path = config_path().unwrap();
        assert!(path.ends_with("sdme/sdmerc"));
    }

    #[test]
    fn test_sudo_user_config_path_unset() {
        let _guard = ENV_LOCK.lock().unwrap();
        std::env::remove_var("SUDO_USER");
        assert!(sudo_user_config_path().is_none());
    }

    #[test]
    fn test_default_host_rootfs_opaque_dirs() {
        let config = Config::default();
        assert_eq!(
            config.host_rootfs_opaque_dirs,
            "/etc/systemd/system,/var/log"
        );
    }

    #[test]
    fn test_save_and_load_host_rootfs_opaque_dirs() {
        let _tmp = TempConfig::new();
        let config = Config {
            host_rootfs_opaque_dirs: "/var/log".to_string(),
            ..Config::default()
        };
        save(&config, None).unwrap();
        let loaded = load(None).unwrap();
        assert_eq!(loaded.host_rootfs_opaque_dirs, "/var/log");
    }

    #[test]
    fn test_save_and_load_host_rootfs_opaque_dirs_empty() {
        let _tmp = TempConfig::new();
        let config = Config {
            host_rootfs_opaque_dirs: String::new(),
            ..Config::default()
        };
        save(&config, None).unwrap();
        let loaded = load(None).unwrap();
        assert_eq!(loaded.host_rootfs_opaque_dirs, "");
    }

    #[test]
    fn test_default_base_fs_empty_by_default() {
        let config = Config::default();
        assert_eq!(config.default_base_fs, "");
    }

    #[test]
    fn test_save_and_load_default_base_fs() {
        let _tmp = TempConfig::new();
        let config = Config {
            default_base_fs: "ubuntu".to_string(),
            ..Config::default()
        };
        save(&config, None).unwrap();
        let loaded = load(None).unwrap();
        assert_eq!(loaded.default_base_fs, "ubuntu");
    }

    #[test]
    fn test_sudo_user_config_path_valid() {
        let _guard = ENV_LOCK.lock().unwrap();
        let user = std::env::var("USER").unwrap();
        std::env::set_var("SUDO_USER", &user);
        let path = sudo_user_config_path();
        std::env::remove_var("SUDO_USER");
        let path = path.expect("should resolve for current user");
        assert!(path.ends_with(".config/sdme/sdmerc"));
    }
}
