// vapora-agents: Agent configuration module
// Load and parse agent definitions from TOML

use std::path::Path;

use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("Failed to read config file: {0}")]
    ReadError(#[from] std::io::Error),

    #[error("Failed to parse config: {0}")]
    ParseJson(#[from] serde_json::Error),

    #[error("Invalid configuration: {0}")]
    ValidationError(String),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
    pub registry: RegistryConfig,
    pub agents: Vec<AgentDefinition>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryConfig {
    #[serde(default = "default_max_agents")]
    pub max_agents_per_role: u32,
    #[serde(default = "default_health_check_interval")]
    pub health_check_interval: u64,
    #[serde(default = "default_agent_timeout")]
    pub agent_timeout: u64,
}

fn default_max_agents() -> u32 {
    5
}

fn default_health_check_interval() -> u64 {
    30
}

fn default_agent_timeout() -> u64 {
    300
}

/// Re-exported from `vapora-shared` so callers using
/// `vapora_agents::config::AgentDefinition` continue to compile without
/// changes.
pub use vapora_shared::AgentDefinition;

impl AgentConfig {
    /// Load configuration from a TOML or NCL file. When the path has a `.ncl`
    /// extension, `nickel export --format json` is invoked and the resulting
    /// JSON is parsed. Otherwise the file is read and parsed as TOML.
    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
        let path = path.as_ref();

        let (raw, is_json) = if path.extension().and_then(|e| e.to_str()) == Some("ncl") {
            let out = std::process::Command::new("nickel")
                .args(["export", "--format", "json"])
                .arg(path)
                .output()
                .map_err(|e| {
                    ConfigError::ReadError(std::io::Error::other(format!(
                        "Failed to invoke nickel for {:?}: {}",
                        path, e
                    )))
                })?;
            if !out.status.success() {
                let stderr = String::from_utf8_lossy(&out.stderr);
                return Err(ConfigError::ReadError(std::io::Error::other(format!(
                    "nickel export failed for {:?}: {}",
                    path, stderr
                ))));
            }
            let json = String::from_utf8(out.stdout).map_err(|e| {
                ConfigError::ReadError(std::io::Error::other(format!(
                    "nickel output is not valid UTF-8: {}",
                    e
                )))
            })?;
            (json, true)
        } else {
            let content = std::fs::read_to_string(path)?;
            (content, false)
        };

        let interpolated = interpolate_env_vars(&raw);

        let config: Self = if is_json {
            serde_json::from_str(&interpolated)?
        } else {
            toml::from_str(&interpolated).map_err(|e| {
                ConfigError::ReadError(std::io::Error::other(format!(
                    "Failed to parse TOML: {}",
                    e
                )))
            })?
        };

        config.validate()?;
        Ok(config)
    }

    /// Load configuration from environment or default file
    pub fn from_env() -> Result<Self, ConfigError> {
        let config_path = std::env::var("VAPORA_AGENT_CONFIG")
            .unwrap_or_else(|_| "config/config.ncl".to_string());

        if Path::new(&config_path).exists() {
            Self::load(&config_path)
        } else {
            Ok(Self::default())
        }
    }

    /// Validate configuration
    fn validate(&self) -> Result<(), ConfigError> {
        // Check that all agent roles are unique
        let mut roles = std::collections::HashSet::new();
        for agent in &self.agents {
            if !roles.insert(&agent.role) {
                return Err(ConfigError::ValidationError(format!(
                    "Duplicate agent role: {}",
                    agent.role
                )));
            }
        }

        // Check that we have at least one agent
        if self.agents.is_empty() {
            return Err(ConfigError::ValidationError(
                "No agents defined in configuration".to_string(),
            ));
        }

        Ok(())
    }

    /// Get agent definition by role
    pub fn get_by_role(&self, role: &str) -> Option<&AgentDefinition> {
        self.agents.iter().find(|a| a.role == role)
    }

    /// List all agent roles
    pub fn list_roles(&self) -> Vec<String> {
        self.agents.iter().map(|a| a.role.clone()).collect()
    }
}

impl Default for AgentConfig {
    fn default() -> Self {
        Self {
            registry: RegistryConfig {
                max_agents_per_role: default_max_agents(),
                health_check_interval: default_health_check_interval(),
                agent_timeout: default_agent_timeout(),
            },
            agents: vec![AgentDefinition {
                role: "developer".to_string(),
                description: "Code developer".to_string(),
                llm_provider: "claude".to_string(),
                llm_model: "claude-sonnet-4".to_string(),
                parallelizable: true,
                priority: 80,
                capabilities: vec!["coding".to_string()],
                system_prompt: None,
            }],
        }
    }
}

/// Expand every `${VAR}` / `${VAR:-default}` reference in `content`.
/// Unresolved vars without a default are replaced with an empty string.
fn interpolate_env_vars(content: &str) -> String {
    let mut result = String::with_capacity(content.len());
    let mut remaining = content;
    while let Some(start) = remaining.find("${") {
        result.push_str(&remaining[..start]);
        let after_open = &remaining[start + 2..];
        if let Some(close) = after_open.find('}') {
            let var_expr = &after_open[..close];
            let value = if let Some(sep) = var_expr.find(":-") {
                let var_name = &var_expr[..sep];
                let default_val = &var_expr[sep + 2..];
                std::env::var(var_name).unwrap_or_else(|_| default_val.to_string())
            } else {
                std::env::var(var_expr).unwrap_or_default()
            };
            result.push_str(&value);
            remaining = &after_open[close + 1..];
        } else {
            result.push_str("${");
            remaining = after_open;
        }
    }
    result.push_str(remaining);
    result
}

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

    #[test]
    fn test_default_values() {
        let config = AgentConfig {
            registry: RegistryConfig {
                max_agents_per_role: 5,
                health_check_interval: 30,
                agent_timeout: 300,
            },
            agents: vec![AgentDefinition {
                role: "developer".to_string(),
                description: "Code developer".to_string(),
                llm_provider: "claude".to_string(),
                llm_model: "claude-sonnet-4".to_string(),
                parallelizable: true,
                priority: 80,
                capabilities: vec!["coding".to_string()],
                system_prompt: None,
            }],
        };

        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_duplicate_roles() {
        let config = AgentConfig {
            registry: RegistryConfig {
                max_agents_per_role: 5,
                health_check_interval: 30,
                agent_timeout: 300,
            },
            agents: vec![
                AgentDefinition {
                    role: "developer".to_string(),
                    description: "Code developer 1".to_string(),
                    llm_provider: "claude".to_string(),
                    llm_model: "claude-sonnet-4".to_string(),
                    parallelizable: true,
                    priority: 80,
                    capabilities: vec![],
                    system_prompt: None,
                },
                AgentDefinition {
                    role: "developer".to_string(),
                    description: "Code developer 2".to_string(),
                    llm_provider: "claude".to_string(),
                    llm_model: "claude-sonnet-4".to_string(),
                    parallelizable: true,
                    priority: 80,
                    capabilities: vec![],
                    system_prompt: None,
                },
            ],
        };

        assert!(config.validate().is_err());
    }

    #[test]
    fn test_get_by_role() {
        let config = AgentConfig {
            registry: RegistryConfig {
                max_agents_per_role: 5,
                health_check_interval: 30,
                agent_timeout: 300,
            },
            agents: vec![AgentDefinition {
                role: "architect".to_string(),
                description: "System architect".to_string(),
                llm_provider: "claude".to_string(),
                llm_model: "claude-opus-4".to_string(),
                parallelizable: false,
                priority: 100,
                capabilities: vec!["architecture".to_string()],
                system_prompt: None,
            }],
        };

        let agent = config.get_by_role("architect");
        assert!(agent.is_some());
        assert_eq!(agent.unwrap().description, "System architect");

        assert!(config.get_by_role("nonexistent").is_none());
    }
}
