Skip to content
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Server {
    pub name: String,
    pub url: String,
    pub pin: String,
}

impl Server {
    pub fn new(name: impl Into<String>, url: impl Into<String>, pin: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            url: url.into().trim().trim_end_matches('/').to_string(),
            pin: pin.into().trim().to_string(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Profile {
    pub name: String,
    pub servers: Vec<Server>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ConfigState {
    pub config_dir: String,
    pub main_config_path: String,
    pub profiles: Vec<Profile>,
    pub active_profile_name: String,
    pub active_server: Server,
}

pub fn get_default_server() -> Server {
    let url = std::env::var("NOTIFY_URL")
        .or_else(|_| std::env::var("SERVER_URL"))
        .unwrap_or_else(|_| "http://127.0.0.1:8080".to_string());
    let pin = std::env::var("NOTIFY_PIN")
        .or_else(|_| std::env::var("PIN"))
        .or_else(|_| std::env::var("NOTIFY_API_KEY"))
        .or_else(|_| std::env::var("API_KEY"))
        .unwrap_or_else(|_| "123456".to_string());
    Server::new("", url, pin)
}

pub fn get_config_dir() -> PathBuf {
    if let Ok(dir) = std::env::var("NOTIFY_CONFIG_DIR") {
        let p = PathBuf::from(dir.trim());
        if !p.as_os_str().is_empty() {
            return p;
        }
    }

    dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join(".config")
        .join("notify")
}

pub fn normalize_server(val: &serde_json::Value) -> Option<Server> {
    let obj = val.as_object()?;

    let url = obj
        .get("url")
        .or_else(|| obj.get("server"))
        .or_else(|| obj.get("host"))
        .or_else(|| obj.get("endpoint"))
        .or_else(|| obj.get("notify_url"))
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .trim()
        .trim_end_matches('/')
        .to_string();

    if url.is_empty() {
        return None;
    }

    let pin = obj
        .get("pin")
        .or_else(|| obj.get("token"))
        .or_else(|| obj.get("apiKey"))
        .or_else(|| obj.get("api_key"))
        .or_else(|| obj.get("password"))
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .trim()
        .to_string();

    let name = obj
        .get("name")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .trim()
        .to_string();

    Some(Server { name, url, pin })
}

pub fn parse_profile(filename_or_name: &str, content: &str) -> Profile {
    let clean_name = filename_or_name
        .trim_end_matches(".json")
        .trim()
        .to_string();

    let parsed: serde_json::Value = match serde_json::from_str(content) {
        Ok(v) => v,
        Err(_) => {
            return Profile {
                name: clean_name,
                servers: Vec::new(),
            };
        }
    };

    let mut profile_name = clean_name;
    let mut servers = Vec::new();

    if let Some(arr) = parsed.as_array() {
        for item in arr {
            if let Some(srv) = normalize_server(item) {
                servers.push(srv);
            }
        }
    } else if let Some(obj) = parsed.as_object() {
        if let Some(n) = obj.get("name").and_then(|v| v.as_str()) {
            let t = n.trim();
            if !t.is_empty() {
                profile_name = t.to_string();
            }
        }

        if let Some(servers_arr) = obj.get("servers").and_then(|v| v.as_array()) {
            for item in servers_arr {
                if let Some(srv) = normalize_server(item) {
                    servers.push(srv);
                }
            }
        } else if let Some(srv) = normalize_server(&parsed) {
            servers.push(srv);
        }
    }

    Profile {
        name: profile_name,
        servers,
    }
}

pub fn load_config_from_dir(dir: &Path) -> ConfigState {
    let main_path = dir.join("notify.json");
    let mut profiles = Vec::new();
    let mut active_name = String::new();

    // 1. Read notify.json first
    if main_path.exists()
        && let Ok(content) = std::fs::read_to_string(&main_path)
    {
        if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content)
            && let Some(act) = val.get("active").and_then(|v| v.as_str())
        {
            active_name = act.trim().to_string();
        }

        let main_profile = parse_profile("notify", &content);
        if !main_profile.servers.is_empty() {
            profiles.push(main_profile);
        }
    }

    // 2. Scan other *.json profile files in directory
    if let Ok(entries) = std::fs::read_dir(dir) {
        let mut extra_files: Vec<PathBuf> = entries
            .filter_map(|e| e.ok())
            .map(|e| e.path())
            .filter(|p| {
                p.is_file()
                    && p.extension().and_then(|s| s.to_str()) == Some("json")
                    && p.file_name() != Some(std::ffi::OsStr::new("notify.json"))
            })
            .collect();
        extra_files.sort();

        for path in extra_files {
            if let Some(file_name) = path.file_name().and_then(|s| s.to_str())
                && let Ok(content) = std::fs::read_to_string(&path)
            {
                let prof = parse_profile(file_name, &content);
                if !prof.servers.is_empty() && !profiles.iter().any(|p| p.name == prof.name) {
                    profiles.push(prof);
                }
            }
        }
    }

    // 3. Determine active profile and active server
    if active_name.is_empty() && !profiles.is_empty() {
        active_name = profiles[0].name.clone();
    }

    let active_profile = profiles.iter().find(|p| p.name == active_name);

    let active_server = active_profile
        .and_then(|p| p.servers.first())
        .cloned()
        .unwrap_or_else(get_default_server);

    ConfigState {
        config_dir: dir.to_string_lossy().to_string(),
        main_config_path: main_path.to_string_lossy().to_string(),
        profiles,
        active_profile_name: active_name,
        active_server,
    }
}

pub fn load_config() -> ConfigState {
    load_config_from_dir(&get_config_dir())
}

pub fn set_active_profile_in_dir(dir: &Path, name: &str) -> Result<ConfigState, String> {
    let _ = std::fs::create_dir_all(dir);
    let main_path = dir.join("notify.json");

    let mut current_json = if main_path.exists() {
        let content = std::fs::read_to_string(&main_path).map_err(|e| e.to_string())?;
        serde_json::from_str::<serde_json::Value>(&content).unwrap_or(serde_json::json!({}))
    } else {
        serde_json::json!({
            "name": "notify",
            "url": get_default_server().url,
            "pin": get_default_server().pin,
        })
    };

    if let Some(obj) = current_json.as_object_mut() {
        obj.insert(
            "active".to_string(),
            serde_json::Value::String(name.to_string()),
        );
    }

    let serialized = serde_json::to_string_pretty(&current_json).map_err(|e| e.to_string())?;
    std::fs::write(&main_path, serialized + "\n").map_err(|e| e.to_string())?;

    Ok(load_config_from_dir(dir))
}

pub fn set_active_profile(name: &str) -> Result<ConfigState, String> {
    set_active_profile_in_dir(&get_config_dir(), name)
}

pub fn save_profile_in_dir(
    dir: &Path,
    name: &str,
    url: &str,
    pin: &str,
    make_active: bool,
) -> Result<ConfigState, String> {
    std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;

    let trimmed_name = name.trim();
    let profile_name = if trimmed_name.is_empty() {
        "notify"
    } else {
        trimmed_name
    };

    let target_file = if profile_name == "notify" {
        dir.join("notify.json")
    } else {
        dir.join(format!("{profile_name}.json"))
    };

    let normalized_url = url.trim().trim_end_matches('/').to_string();
    let normalized_pin = pin.trim().to_string();

    let json_val = serde_json::json!({
        "name": profile_name,
        "url": normalized_url,
        "pin": normalized_pin,
        "api_key": normalized_pin,
        "active": if make_active || profile_name == "notify" { profile_name } else { "" }
    });

    let content = serde_json::to_string_pretty(&json_val).map_err(|e| e.to_string())? + "\n";
    std::fs::write(&target_file, content).map_err(|e| e.to_string())?;

    if make_active && profile_name != "notify" {
        let _ = set_active_profile_in_dir(dir, profile_name);
    }

    Ok(load_config_from_dir(dir))
}

pub fn save_profile(
    name: &str,
    url: &str,
    pin: &str,
    make_active: bool,
) -> Result<ConfigState, String> {
    save_profile_in_dir(&get_config_dir(), name, url, pin, make_active)
}

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

    #[test]
    fn test_normalize_server_aliases() {
        let json1 = serde_json::json!({
            "name": "home",
            "server": "https://notify.example.com/",
            "apiKey": "my_key"
        });
        let s1 = normalize_server(&json1).unwrap();
        assert_eq!(s1.name, "home");
        assert_eq!(s1.url, "https://notify.example.com");
        assert_eq!(s1.pin, "my_key");

        let json2 = serde_json::json!({
            "endpoint": "http://192.168.1.10:8080",
            "token": "tok123"
        });
        let s2 = normalize_server(&json2).unwrap();
        assert_eq!(s2.url, "http://192.168.1.10:8080");
        assert_eq!(s2.pin, "tok123");
    }

    #[test]
    fn test_parse_profile_single_object() {
        let raw = r#"{
            "name": "work",
            "url": "http://work.internal:8080",
            "pin": "secret"
        }"#;
        let prof = parse_profile("work", raw);
        assert_eq!(prof.name, "work");
        assert_eq!(prof.servers.len(), 1);
        assert_eq!(prof.servers[0].url, "http://work.internal:8080");
        assert_eq!(prof.servers[0].pin, "secret");
    }

    #[test]
    fn test_parse_profile_servers_array() {
        let raw = r#"{
            "name": "cluster",
            "servers": [
                { "name": "node1", "url": "http://node1:8080", "pin": "p1" },
                { "name": "node2", "url": "http://node2:8080", "pin": "p2" }
            ]
        }"#;
        let prof = parse_profile("cluster", raw);
        assert_eq!(prof.name, "cluster");
        assert_eq!(prof.servers.len(), 2);
        assert_eq!(prof.servers[0].name, "node1");
        assert_eq!(prof.servers[1].name, "node2");
    }

    #[test]
    fn test_load_and_switch_profile_in_dir() {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let temp_dir = std::env::temp_dir().join(format!("notify_cfg_test_{now}"));
        let _ = std::fs::create_dir_all(&temp_dir);

        // Initially empty
        let cfg0 = load_config_from_dir(&temp_dir);
        assert!(cfg0.profiles.is_empty());
        assert_eq!(cfg0.active_server.pin, get_default_server().pin);

        // Save "notify" profile
        let cfg1 =
            save_profile_in_dir(&temp_dir, "notify", "http://10.0.0.1:8080", "1111", true).unwrap();
        assert_eq!(cfg1.profiles.len(), 1);
        assert_eq!(cfg1.active_profile_name, "notify");
        assert_eq!(cfg1.active_server.url, "http://10.0.0.1:8080");
        assert_eq!(cfg1.active_server.pin, "1111");

        // Save another profile "office"
        let cfg2 = save_profile_in_dir(
            &temp_dir,
            "office",
            "https://office.example.com",
            "2222",
            false,
        )
        .unwrap();
        assert_eq!(cfg2.profiles.len(), 2);
        assert_eq!(cfg2.active_profile_name, "notify");

        // Switch to "office"
        let cfg3 = set_active_profile_in_dir(&temp_dir, "office").unwrap();
        assert_eq!(cfg3.active_profile_name, "office");
        assert_eq!(cfg3.active_server.url, "https://office.example.com");
        assert_eq!(cfg3.active_server.pin, "2222");

        let _ = std::fs::remove_dir_all(&temp_dir);
    }
}