//! Gateway configuration types — pure parsing and validation.
//!
//! Contains `ProjectEntry`, `GatewayConfig`, and validation logic.
//! All filesystem I/O (loading from disk) lives in `io.rs`.

use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// A single project entry in `projects.toml`.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ProjectEntry {
    /// Base URL of the project's huskies container (e.g. `http://localhost:3001`).
    pub url: String,
}

/// Top-level `projects.toml` config.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GatewayConfig {
    /// Map of project name → container URL.
    #[serde(default)]
    pub projects: BTreeMap<String, ProjectEntry>,
}

/// Validate that a gateway config has at least one project.
///
/// Returns the name of the first project (alphabetically) on success,
/// or an error message if the config is empty.
pub fn validate_config(config: &GatewayConfig) -> Result<String, String> {
    if config.projects.is_empty() {
        return Err("projects.toml must define at least one project".to_string());
    }
    Ok(config.projects.keys().next().unwrap().clone())
}

/// Validate that a project name exists in the given project map.
///
/// Returns the project's URL on success.
pub fn validate_project_exists(
    projects: &BTreeMap<String, ProjectEntry>,
    name: &str,
) -> Result<String, String> {
    projects.get(name).map(|p| p.url.clone()).ok_or_else(|| {
        let available: Vec<&str> = projects.keys().map(|s| s.as_str()).collect();
        format!(
            "unknown project '{name}'. Available: {}",
            available.join(", ")
        )
    })
}

/// Escape a string as a TOML quoted string.
pub fn toml_string(s: &str) -> String {
    format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}

/// Serialize a `bot.toml` content string from the given fields.
pub fn serialize_bot_config(
    transport: &str,
    homeserver: Option<&str>,
    username: Option<&str>,
    password: Option<&str>,
    slack_bot_token: Option<&str>,
    slack_signing_secret: Option<&str>,
) -> String {
    match transport {
        "slack" => {
            format!(
                "enabled = true\ntransport = \"slack\"\n\nslack_bot_token = {}\nslack_signing_secret = {}\nslack_channel_ids = []\n",
                toml_string(slack_bot_token.unwrap_or("")),
                toml_string(slack_signing_secret.unwrap_or("")),
            )
        }
        _ => {
            format!(
                "enabled = true\ntransport = \"matrix\"\n\nhomeserver = {}\nusername = {}\npassword = {}\nroom_ids = []\nallowed_users = []\n",
                toml_string(homeserver.unwrap_or("")),
                toml_string(username.unwrap_or("")),
                toml_string(password.unwrap_or("")),
            )
        }
    }
}

// ── Tests ────────────────────────────────────────────────────────────────────

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

    #[test]
    fn parse_valid_projects_toml() {
        let toml_str = r#"
[projects.huskies]
url = "http://localhost:3001"

[projects.robot-studio]
url = "http://localhost:3002"
"#;
        let config: GatewayConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(config.projects.len(), 2);
        assert_eq!(config.projects["huskies"].url, "http://localhost:3001");
        assert_eq!(config.projects["robot-studio"].url, "http://localhost:3002");
    }

    #[test]
    fn parse_empty_projects_toml() {
        let toml_str = "[projects]\n";
        let config: GatewayConfig = toml::from_str(toml_str).unwrap();
        assert!(config.projects.is_empty());
    }

    #[test]
    fn validate_config_rejects_empty() {
        let config = GatewayConfig {
            projects: BTreeMap::new(),
        };
        assert!(validate_config(&config).is_err());
    }

    #[test]
    fn validate_config_returns_first_project_name() {
        let mut projects = BTreeMap::new();
        projects.insert(
            "beta".into(),
            ProjectEntry {
                url: "http://b".into(),
            },
        );
        projects.insert(
            "alpha".into(),
            ProjectEntry {
                url: "http://a".into(),
            },
        );
        let config = GatewayConfig { projects };
        assert_eq!(validate_config(&config).unwrap(), "alpha");
    }

    #[test]
    fn validate_project_exists_succeeds() {
        let mut projects = BTreeMap::new();
        projects.insert(
            "p1".into(),
            ProjectEntry {
                url: "http://p1".into(),
            },
        );
        assert_eq!(
            validate_project_exists(&projects, "p1").unwrap(),
            "http://p1"
        );
    }

    #[test]
    fn validate_project_exists_fails() {
        let projects = BTreeMap::new();
        assert!(validate_project_exists(&projects, "missing").is_err());
    }

    #[test]
    fn toml_string_escapes_quotes() {
        assert_eq!(toml_string(r#"a"b"#), r#""a\"b""#);
    }

    #[test]
    fn toml_string_escapes_backslashes() {
        assert_eq!(toml_string(r"a\b"), r#""a\\b""#);
    }

    #[test]
    fn serialize_bot_config_matrix() {
        let content = serialize_bot_config(
            "matrix",
            Some("https://mx.io"),
            Some("@bot:mx.io"),
            Some("pass"),
            None,
            None,
        );
        assert!(content.contains("transport = \"matrix\""));
        assert!(content.contains("homeserver = \"https://mx.io\""));
    }

    #[test]
    fn serialize_bot_config_slack() {
        let content =
            serialize_bot_config("slack", None, None, None, Some("xoxb-123"), Some("secret"));
        assert!(content.contains("transport = \"slack\""));
        assert!(content.contains("slack_bot_token = \"xoxb-123\""));
    }
}
