// vapora-llm-router: Configuration module
// Load and parse LLM router configuration from TOML

use std::collections::HashMap;
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 LLMRouterConfig {
    pub routing: RoutingConfig,
    pub providers: HashMap<String, ProviderConfig>,
    #[serde(default)]
    pub routing_rules: Vec<RoutingRule>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingConfig {
    pub default_provider: String,
    #[serde(default = "default_true")]
    pub cost_tracking_enabled: bool,
    #[serde(default = "default_true")]
    pub fallback_enabled: bool,
}

fn default_true() -> bool {
    true
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
    #[serde(default = "default_true")]
    pub enabled: bool,
    pub api_key: Option<String>,
    pub url: Option<String>,
    pub model: String,
    #[serde(default = "default_max_tokens")]
    pub max_tokens: usize,
    #[serde(default = "default_temperature")]
    pub temperature: f32,
    #[serde(default)]
    pub cost_per_1m_input: f64,
    #[serde(default)]
    pub cost_per_1m_output: f64,
}

fn default_max_tokens() -> usize {
    4096
}

fn default_temperature() -> f32 {
    0.7
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingRule {
    pub name: String,
    pub condition: HashMap<String, String>,
    pub provider: String,
    pub model_override: Option<String>,
}

impl LLMRouterConfig {
    /// 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 with full `${VAR}` interpolation applied pre-parse.
    /// Otherwise the file is read and parsed as TOML (legacy compatibility).
    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 {
            let mut c: Self = toml::from_str(&interpolated).map_err(|e| {
                ConfigError::ReadError(std::io::Error::other(format!(
                    "Failed to parse TOML: {}",
                    e
                )))
            })?;
            // Legacy TOML path: expand env vars in specific fields
            c.expand_env_vars();
            c
        };

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

    /// Expand environment variables in API key and URL fields (TOML path only).
    fn expand_env_vars(&mut self) {
        for provider in self.providers.values_mut() {
            if let Some(ref api_key) = provider.api_key {
                provider.api_key = Some(expand_env_var(api_key));
            }
            if let Some(ref url) = provider.url {
                provider.url = Some(expand_env_var(url));
            }
        }
    }

    /// Validate configuration
    fn validate(&self) -> Result<(), ConfigError> {
        // Check that default provider exists
        if !self.providers.contains_key(&self.routing.default_provider) {
            return Err(ConfigError::ValidationError(format!(
                "Default provider '{}' not found in providers",
                self.routing.default_provider
            )));
        }

        // Check that all routing rules reference valid providers
        for rule in &self.routing_rules {
            if !self.providers.contains_key(&rule.provider) {
                return Err(ConfigError::ValidationError(format!(
                    "Routing rule '{}' references unknown provider '{}'",
                    rule.name, rule.provider
                )));
            }
        }

        Ok(())
    }

    /// Get provider configuration by name
    pub fn get_provider(&self, name: &str) -> Option<&ProviderConfig> {
        self.providers.get(name)
    }

    /// Find routing rule matching conditions
    pub fn find_rule(&self, conditions: &HashMap<String, String>) -> Option<&RoutingRule> {
        self.routing_rules.iter().find(|rule| {
            rule.condition
                .iter()
                .all(|(key, value)| conditions.get(key).map(|v| v == value).unwrap_or(false))
        })
    }
}

/// Expand every `${VAR}` / `${VAR:-default}` reference in `content`.
/// Unresolved vars without a default are replaced with an empty string.
pub(crate) 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
}

/// Expand environment variables in format ${VAR} or ${VAR:-default} (single
/// token).
fn expand_env_var(input: &str) -> String {
    if !input.starts_with("${") || !input.ends_with('}') {
        return input.to_string();
    }

    let var_part = &input[2..input.len() - 1];

    // Handle ${VAR:-default} format
    if let Some(pos) = var_part.find(":-") {
        let var_name = &var_part[..pos];
        let default_value = &var_part[pos + 2..];
        std::env::var(var_name).unwrap_or_else(|_| default_value.to_string())
    } else {
        // Handle ${VAR} format
        std::env::var(var_part).unwrap_or_default()
    }
}

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

    #[test]
    fn test_expand_env_var() {
        std::env::set_var("TEST_VAR", "test_value");
        assert_eq!(expand_env_var("${TEST_VAR}"), "test_value");
        assert_eq!(expand_env_var("plain_text"), "plain_text");
        assert_eq!(expand_env_var("${NONEXISTENT:-default}"), "default");
    }

    #[test]
    fn test_config_validation() {
        let config = LLMRouterConfig {
            routing: RoutingConfig {
                default_provider: "claude".to_string(),
                cost_tracking_enabled: true,
                fallback_enabled: true,
            },
            providers: {
                let mut map = HashMap::new();
                map.insert(
                    "claude".to_string(),
                    ProviderConfig {
                        enabled: true,
                        api_key: Some("test".to_string()),
                        url: None,
                        model: "claude-sonnet-4".to_string(),
                        max_tokens: 4096,
                        temperature: 0.7,
                        cost_per_1m_input: 3.0,
                        cost_per_1m_output: 15.0,
                    },
                );
                map
            },
            routing_rules: vec![],
        };

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

    #[test]
    fn test_invalid_default_provider() {
        let config = LLMRouterConfig {
            routing: RoutingConfig {
                default_provider: "nonexistent".to_string(),
                cost_tracking_enabled: true,
                fallback_enabled: true,
            },
            providers: HashMap::new(),
            routing_rules: vec![],
        };

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