use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::Value as YamlValue;
use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct SddConfig {
    pub providers: ProviderConfig,
    pub runtime: RuntimeConfig,
    pub memory: MemoryConfig,
    pub intelligence: IntelligenceConfig,
    pub optimization: OptimizationConfig,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct ProviderConfig {
    pub default: Option<String>,
    pub model: Option<String>,
    pub effort: Option<String>,
    pub offline: Option<bool>,
    pub registry: BTreeMap<String, ProviderDefinition>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct ProviderDefinition {
    pub kind: String,
    pub base_url: Option<String>,
    pub auth_env: Option<String>,
    pub auth_methods: Vec<ProviderAuthDefinition>,
    pub model: Option<String>,
    pub models: Vec<ProviderModelDefinition>,
    pub effort: Option<String>,
    pub efforts: Vec<String>,
    pub stage_models: BTreeMap<String, String>,
    pub stage_efforts: BTreeMap<String, String>,
    pub capabilities: Vec<String>,
    pub timeouts: ProviderTimeouts,
    pub retries: ProviderRetries,
    pub token_budget: Option<u32>,
    pub usage_limits: Vec<ProviderUsageLimit>,
    #[serde(default = "default_true")]
    pub enabled: bool,
}

impl Default for ProviderDefinition {
    fn default() -> Self {
        Self {
            kind: "custom".to_string(),
            base_url: None,
            auth_env: None,
            auth_methods: Vec::new(),
            model: None,
            models: Vec::new(),
            effort: None,
            efforts: Vec::new(),
            stage_models: BTreeMap::new(),
            stage_efforts: BTreeMap::new(),
            capabilities: Vec::new(),
            timeouts: ProviderTimeouts::default(),
            retries: ProviderRetries::default(),
            token_budget: None,
            usage_limits: Vec::new(),
            enabled: true,
        }
    }
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct ProviderModelDefinition {
    pub id: String,
    pub label: String,
    pub context_window_tokens: Option<u32>,
    pub pricing: ProviderModelPricing,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
#[serde(default)]
pub struct ProviderModelPricing {
    pub currency: Option<String>,
    pub input_per_million: Option<f64>,
    pub cached_input_per_million: Option<f64>,
    pub output_per_million: Option<f64>,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct ProviderAuthDefinition {
    pub id: String,
    pub label: String,
    pub kind: String,
    pub env: Option<String>,
    pub command: Option<String>,
    pub check_command: Option<String>,
    pub check_success_text: Option<String>,
    pub check_success_regex: Option<String>,
    pub login_command: Option<String>,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct ProviderUsageLimit {
    pub window: String,
    pub limit: Option<f64>,
    pub remaining: Option<f64>,
    pub unit: String,
    pub resets_at: Option<String>,
    pub source: Option<String>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct ProviderTimeouts {
    pub request_ms: u64,
}

impl Default for ProviderTimeouts {
    fn default() -> Self {
        Self { request_ms: 30_000 }
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct ProviderRetries {
    pub max_attempts: u8,
}

impl Default for ProviderRetries {
    fn default() -> Self {
        Self { max_attempts: 1 }
    }
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct RuntimeConfig {
    pub node_effect_bridge: NodeEffectBridgeConfig,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct NodeEffectBridgeConfig {
    pub enabled: bool,
    pub command: Option<String>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct MemoryConfig {
    #[serde(default = "default_true")]
    pub learning_enabled: bool,
    pub index_path: String,
    #[serde(default = "default_true")]
    pub redact_secrets: bool,
}

impl Default for MemoryConfig {
    fn default() -> Self {
        Self {
            learning_enabled: true,
            index_path: ".sdd/memory/learnings.jsonl".to_string(),
            redact_secrets: true,
        }
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct IntelligenceConfig {
    #[serde(default = "default_true")]
    pub enabled: bool,
    pub index_dir: String,
    pub context_pack_dir: String,
    pub max_context_chars: usize,
    #[serde(default = "default_true")]
    pub redact_secrets: bool,
    pub code_intelligence: CodeIntelligenceConfig,
}

impl Default for IntelligenceConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            index_dir: ".sdd/intelligence".to_string(),
            context_pack_dir: ".sdd/intelligence/context-packs".to_string(),
            max_context_chars: 24_000,
            redact_secrets: true,
            code_intelligence: CodeIntelligenceConfig::default(),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct CodeIntelligenceConfig {
    #[serde(default = "default_true")]
    pub enabled: bool,
    pub preferred: String,
    pub providers: BTreeMap<String, CodeIntelligenceProviderConfig>,
}

impl Default for CodeIntelligenceConfig {
    fn default() -> Self {
        let mut providers = BTreeMap::new();
        providers.insert(
            "codegraph".to_string(),
            CodeIntelligenceProviderConfig {
                command: "codegraph".to_string(),
                index_marker: ".codegraph/".to_string(),
            },
        );
        providers.insert(
            "lexa".to_string(),
            CodeIntelligenceProviderConfig {
                command: "lexa".to_string(),
                index_marker: ".lexa/graph.lexa".to_string(),
            },
        );
        Self {
            enabled: true,
            preferred: "auto".to_string(),
            providers,
        }
    }
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct CodeIntelligenceProviderConfig {
    pub command: String,
    pub index_marker: String,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct OptimizationConfig {
    #[serde(default = "default_true")]
    pub enabled: bool,
    pub codegraph: OptimizationCodeGraphConfig,
    pub rtk: OptimizationToolConfig,
    pub caveman: OptimizationCavemanConfig,
    pub ignored_paths: Vec<String>,
}

impl Default for OptimizationConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            codegraph: OptimizationCodeGraphConfig::default(),
            rtk: OptimizationToolConfig {
                enabled: "auto".to_string(),
                command: "rtk".to_string(),
            },
            caveman: OptimizationCavemanConfig::default(),
            ignored_paths: Vec::new(),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct OptimizationCodeGraphConfig {
    #[serde(default = "default_codegraph_mode")]
    pub mode: String,
    #[serde(default = "default_codegraph_command")]
    pub command: String,
    #[serde(default = "default_codegraph_index_marker")]
    pub index_marker: String,
    pub auto_index: bool,
}

impl Default for OptimizationCodeGraphConfig {
    fn default() -> Self {
        Self {
            mode: "adaptive".to_string(),
            command: "codegraph".to_string(),
            index_marker: ".codegraph/".to_string(),
            auto_index: true,
        }
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct OptimizationToolConfig {
    #[serde(default = "default_auto")]
    pub enabled: String,
    #[serde(default = "default_rtk_command")]
    pub command: String,
}

impl Default for OptimizationToolConfig {
    fn default() -> Self {
        Self {
            enabled: "auto".to_string(),
            command: "rtk".to_string(),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct OptimizationCavemanConfig {
    #[serde(default = "default_auto")]
    pub enabled: String,
    #[serde(default = "default_caveman_command")]
    pub command: String,
    #[serde(default = "default_caveman_apply_to")]
    pub apply_to: Vec<String>,
}

impl Default for OptimizationCavemanConfig {
    fn default() -> Self {
        Self {
            enabled: "auto".to_string(),
            command: "caveman".to_string(),
            apply_to: vec![
                "trace".to_string(),
                "handoff".to_string(),
                "memory-derived".to_string(),
            ],
        }
    }
}

fn default_auto() -> String {
    "auto".to_string()
}

fn default_codegraph_mode() -> String {
    "adaptive".to_string()
}

fn default_codegraph_command() -> String {
    "codegraph".to_string()
}

fn default_codegraph_index_marker() -> String {
    ".codegraph/".to_string()
}

fn default_rtk_command() -> String {
    "rtk".to_string()
}

fn default_caveman_command() -> String {
    "caveman".to_string()
}

fn default_caveman_apply_to() -> Vec<String> {
    vec![
        "trace".to_string(),
        "handoff".to_string(),
        "memory-derived".to_string(),
    ]
}

#[derive(Clone, Debug, Serialize)]
pub struct ProviderSelection {
    pub id: String,
    pub kind: String,
    pub model: String,
    pub effort: String,
    pub offline: bool,
    pub enabled: bool,
    pub auth_env: Option<String>,
    pub auth_present: Option<bool>,
    pub auth_methods: Vec<ProviderAuthStatus>,
    pub models: Vec<ProviderModelDefinition>,
    pub efforts: Vec<String>,
    pub stage_models: BTreeMap<String, String>,
    pub stage_efforts: BTreeMap<String, String>,
    pub capabilities: Vec<String>,
    pub token_budget: Option<u32>,
    pub usage_limits: Vec<ProviderUsageLimit>,
}

#[derive(Clone, Debug, Default)]
pub struct ProviderResolveOptions {
    pub provider: Option<String>,
    pub model: Option<String>,
    pub effort: Option<String>,
    pub offline: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProviderAuthCheckMode {
    Fast,
    Full,
}

#[derive(Clone, Debug, Serialize)]
pub struct ProviderCatalogEntry {
    pub id: String,
    pub kind: String,
    pub model: Option<String>,
    pub effort: Option<String>,
    pub enabled: bool,
    pub auth_env: Option<String>,
    pub auth_present: Option<bool>,
    pub auth_methods: Vec<ProviderAuthStatus>,
    pub models: Vec<ProviderModelDefinition>,
    pub efforts: Vec<String>,
    pub stage_models: BTreeMap<String, String>,
    pub stage_efforts: BTreeMap<String, String>,
    pub capabilities: Vec<String>,
    pub token_budget: Option<u32>,
    pub usage_limits: Vec<ProviderUsageLimit>,
}

#[derive(Clone, Debug, Serialize)]
pub struct ProviderAuthStatus {
    pub id: String,
    pub label: String,
    pub kind: String,
    pub source: String,
    pub present: bool,
    pub check_command: Option<String>,
    pub login_command: Option<String>,
    pub detail: Option<String>,
}

pub fn builtin_provider_registry() -> BTreeMap<String, ProviderDefinition> {
    let mut registry = BTreeMap::new();
    registry.insert(
        "claude".to_string(),
        provider(
            "anthropic",
            Some("ANTHROPIC_API_KEY"),
            Some("claude-sonnet-4-6"),
            Some("xhigh"),
            &[
                model_choice("claude-haiku-4-5", "Claude Haiku 4.5"),
                model_choice("claude-sonnet-4-6", "Claude Sonnet 4.6"),
                model_choice("claude-opus-4-8", "Claude Opus 4.8"),
            ],
            &["medium", "high", "xhigh"],
            &[
                ("idea", "claude-sonnet-4-6"),
                ("prd", "claude-sonnet-4-6"),
                ("techspec", "claude-opus-4-8"),
                ("tasks", "claude-sonnet-4-6"),
                ("refinement", "claude-sonnet-4-6"),
                ("execution", "claude-sonnet-4-6"),
                ("review", "claude-opus-4-8"),
                ("memory", "claude-sonnet-4-6"),
            ],
            &[
                ("idea", "medium"),
                ("prd", "high"),
                ("techspec", "xhigh"),
                ("tasks", "high"),
                ("refinement", "high"),
                ("execution", "high"),
                ("review", "xhigh"),
                ("memory", "medium"),
            ],
            &["chat", "tools"],
            &[
                cli_auth_checked(
                    "claude-code",
                    "Claude Code",
                    "claude",
                    "claude auth status",
                    Some("\"loggedIn\": true"),
                    None,
                    "claude auth login",
                ),
                env_auth("anthropic-api", "Anthropic API", "ANTHROPIC_API_KEY"),
            ],
        ),
    );
    registry.insert(
        "codex".to_string(),
        provider(
            "openai-compatible",
            Some("OPENAI_API_KEY"),
            Some("gpt-5.5"),
            Some("xhigh"),
            &[
                model_choice("gpt-5.4", "Codex / GPT-5.4"),
                model_choice("gpt-5.5", "Codex / GPT-5.5"),
            ],
            &["medium", "high", "xhigh"],
            &[
                ("idea", "gpt-5.4"),
                ("prd", "gpt-5.4"),
                ("techspec", "gpt-5.5"),
                ("tasks", "gpt-5.4"),
                ("refinement", "gpt-5.4"),
                ("execution", "gpt-5.5"),
                ("review", "gpt-5.5"),
                ("memory", "gpt-5.4"),
            ],
            &[
                ("idea", "medium"),
                ("prd", "high"),
                ("techspec", "xhigh"),
                ("tasks", "high"),
                ("refinement", "high"),
                ("execution", "high"),
                ("review", "xhigh"),
                ("memory", "medium"),
            ],
            &["chat", "tools", "code"],
            &[
                cli_auth_checked(
                    "codex-cli",
                    "Codex CLI",
                    "codex",
                    "codex login status",
                    Some("Logged in"),
                    None,
                    "codex login",
                ),
                env_auth("openai-api", "OpenAI API", "OPENAI_API_KEY"),
            ],
        ),
    );
    registry.insert(
        "antigravity".to_string(),
        provider(
            "local-cli",
            Some("GEMINI_API_KEY"),
            Some("gemini-3.1-pro"),
            Some("high"),
            &[
                model_choice("gemini-3.5-flash", "Gemini 3.5 Flash"),
                model_choice("gemini-3.1-pro", "Gemini 3.1 Pro"),
                model_choice("claude-opus-4.6", "Claude Opus 4.6 via Antigravity"),
            ],
            &["medium", "high", "xhigh"],
            &[
                ("idea", "gemini-3.5-flash"),
                ("prd", "gemini-3.1-pro"),
                ("techspec", "gemini-3.1-pro"),
                ("tasks", "gemini-3.1-pro"),
                ("refinement", "gemini-3.1-pro"),
                ("execution", "gemini-3.1-pro"),
                ("review", "gemini-3.1-pro"),
                ("memory", "gemini-3.5-flash"),
            ],
            &[
                ("idea", "medium"),
                ("prd", "high"),
                ("techspec", "xhigh"),
                ("tasks", "high"),
                ("refinement", "high"),
                ("execution", "high"),
                ("review", "xhigh"),
                ("memory", "medium"),
            ],
            &["chat"],
            &[
                cli_auth_checked(
                    "antigravity-cli",
                    "Antigravity CLI",
                    "agy",
                    "agy models",
                    None,
                    None,
                    "agy",
                ),
                env_auth("google-ai-api", "Google AI API", "GEMINI_API_KEY"),
            ],
        ),
    );
    registry.insert(
        "opencode".to_string(),
        provider(
            "local-cli",
            None,
            Some("opencode-go/deepseek-v4-pro"),
            Some("xhigh"),
            &[
                model_choice("opencode/deepseek-v4-flash", "Zen / DeepSeek v4 Flash"),
                model_choice("opencode/kimi-k2.6", "Zen / Kimi K2.6"),
                model_choice("opencode/minimax-m3-free", "Zen / MiniMax M3"),
                model_choice("opencode/mimo-v2.5-free", "Zen / MiMo v2.5"),
                model_choice("opencode/glm-5.1", "Zen / GLM 5.1"),
                model_choice("opencode-go/deepseek-v4-flash", "Go / DeepSeek v4 Flash"),
                model_choice("opencode-go/deepseek-v4-pro", "Go / DeepSeek v4 Pro"),
                model_choice("opencode-go/kimi-k2.6", "Go / Kimi K2.6"),
                model_choice("opencode-go/minimax-m3", "Go / MiniMax M3"),
                model_choice("opencode-go/mimo-v2.5", "Go / MiMo v2.5"),
                model_choice("opencode-go/glm-5.1", "Go / GLM 5.1"),
                model_choice("opencode-go/qwen3.7-plus", "Go / Qwen 3.7 Plus"),
                model_choice("opencode-go/qwen3.7-max", "Go / Qwen 3.7 Max"),
            ],
            &["medium", "high", "xhigh"],
            &[
                ("idea", "opencode-go/deepseek-v4-flash"),
                ("prd", "opencode-go/deepseek-v4-pro"),
                ("techspec", "opencode-go/kimi-k2.6"),
                ("tasks", "opencode-go/qwen3.7-plus"),
                ("refinement", "opencode-go/qwen3.7-plus"),
                ("execution", "opencode-go/kimi-k2.6"),
                ("review", "opencode-go/glm-5.1"),
                ("memory", "opencode-go/minimax-m3"),
            ],
            &[
                ("idea", "medium"),
                ("prd", "high"),
                ("techspec", "xhigh"),
                ("tasks", "high"),
                ("refinement", "high"),
                ("execution", "high"),
                ("review", "xhigh"),
                ("memory", "medium"),
            ],
            &["chat", "code"],
            &[
                cli_auth_checked(
                    "opencode-cli",
                    "opencode CLI",
                    "opencode",
                    "opencode providers list",
                    None,
                    Some(r"\b[1-9][0-9]* credentials?\b"),
                    "opencode providers login",
                ),
                cli_auth_checked(
                    "opencode-provider",
                    "opencode Go/Zen credential",
                    "opencode",
                    "opencode providers list",
                    None,
                    Some(r"\b[1-9][0-9]* credentials?\b"),
                    "opencode providers login",
                ),
            ],
        ),
    );
    registry.insert(
        "cursor".to_string(),
        provider(
            "local-cli",
            None,
            Some("composer-2.5"),
            Some("high"),
            &[
                model_choice("composer-2.5-fast", "Composer 2.5 Fast"),
                model_choice("composer-2.5", "Composer 2.5"),
                model_choice("sonnet-4.6", "Sonnet 4.6"),
                model_choice("haiku", "Haiku"),
            ],
            &["medium", "high", "xhigh"],
            &[
                ("idea", "composer-2.5-fast"),
                ("prd", "composer-2.5"),
                ("techspec", "sonnet-4.6"),
                ("tasks", "composer-2.5"),
                ("refinement", "composer-2.5"),
                ("execution", "composer-2.5"),
                ("review", "sonnet-4.6"),
                ("memory", "haiku"),
            ],
            &[
                ("idea", "medium"),
                ("prd", "high"),
                ("techspec", "xhigh"),
                ("tasks", "high"),
                ("refinement", "high"),
                ("execution", "high"),
                ("review", "xhigh"),
                ("memory", "medium"),
            ],
            &["chat", "tools", "code"],
            &[cli_auth_checked(
                "cursor-cli",
                "Cursor CLI",
                "cursor",
                "cursor agent status",
                Some("Logged in"),
                None,
                "cursor agent login",
            )],
        ),
    );
    registry
}

#[allow(clippy::too_many_arguments)]
fn provider(
    kind: &str,
    auth_env: Option<&str>,
    model: Option<&str>,
    effort: Option<&str>,
    models: &[ProviderModelDefinition],
    efforts: &[&str],
    stage_models: &[(&str, &str)],
    stage_efforts: &[(&str, &str)],
    capabilities: &[&str],
    auth_methods: &[ProviderAuthDefinition],
) -> ProviderDefinition {
    ProviderDefinition {
        kind: kind.to_string(),
        auth_env: auth_env.map(str::to_string),
        auth_methods: auth_methods.to_vec(),
        model: model.map(str::to_string),
        models: models.to_vec(),
        effort: effort.map(str::to_string),
        efforts: efforts.iter().map(|item| (*item).to_string()).collect(),
        stage_models: stage_models
            .iter()
            .map(|(stage, model)| ((*stage).to_string(), (*model).to_string()))
            .collect(),
        stage_efforts: stage_efforts
            .iter()
            .map(|(stage, effort)| ((*stage).to_string(), (*effort).to_string()))
            .collect(),
        capabilities: capabilities
            .iter()
            .map(|item| (*item).to_string())
            .collect(),
        ..ProviderDefinition::default()
    }
}

fn model_choice(id: &str, label: &str) -> ProviderModelDefinition {
    ProviderModelDefinition {
        id: id.to_string(),
        label: label.to_string(),
        context_window_tokens: None,
        pricing: ProviderModelPricing::default(),
    }
}

fn env_auth(id: &str, label: &str, env: &str) -> ProviderAuthDefinition {
    ProviderAuthDefinition {
        id: id.to_string(),
        label: label.to_string(),
        kind: "env".to_string(),
        env: Some(env.to_string()),
        command: None,
        check_command: None,
        check_success_text: None,
        check_success_regex: None,
        login_command: None,
    }
}

fn cli_auth(id: &str, label: &str, command: &str) -> ProviderAuthDefinition {
    ProviderAuthDefinition {
        id: id.to_string(),
        label: label.to_string(),
        kind: "cli".to_string(),
        env: None,
        command: Some(command.to_string()),
        check_command: None,
        check_success_text: None,
        check_success_regex: None,
        login_command: None,
    }
}

fn cli_auth_checked(
    id: &str,
    label: &str,
    command: &str,
    check_command: &str,
    check_success_text: Option<&str>,
    check_success_regex: Option<&str>,
    login_command: &str,
) -> ProviderAuthDefinition {
    ProviderAuthDefinition {
        check_command: Some(check_command.to_string()),
        check_success_text: check_success_text.map(str::to_string),
        check_success_regex: check_success_regex.map(str::to_string),
        login_command: Some(login_command.to_string()),
        ..cli_auth(id, label, command)
    }
}

fn default_true() -> bool {
    true
}

pub fn load_sdd_config(root: &Path) -> Result<SddConfig> {
    let path = root.join("sdd.config.yaml");
    if !path.exists() {
        return Ok(SddConfig::default());
    }
    let text = std::fs::read_to_string(&path)
        .with_context(|| format!("failed to read {}", path.display()))?;
    serde_yaml::from_str(&text).with_context(|| format!("invalid YAML in {}", path.display()))
}

pub fn provider_registry(root: &Path) -> Result<BTreeMap<String, ProviderDefinition>> {
    let config = load_sdd_config(root)?;
    let mut registry = builtin_provider_registry();
    for (id, provider) in config.providers.registry {
        let id = normalize_provider_id(&id);
        let provider = merge_provider_definition(registry.get(&id), provider);
        registry.insert(id, provider);
    }
    Ok(registry)
}

fn merge_provider_definition(
    builtin: Option<&ProviderDefinition>,
    mut provider: ProviderDefinition,
) -> ProviderDefinition {
    if let Some(builtin) = builtin {
        if provider.auth_env.is_none() {
            provider.auth_env = builtin.auth_env.clone();
        }
        if provider.auth_methods.is_empty() {
            provider.auth_methods = builtin.auth_methods.clone();
        }
        if provider.model.as_deref().is_none_or(is_placeholder_model) {
            provider.model = builtin.model.clone();
        }
        if provider.models.is_empty() {
            provider.models = builtin.models.clone();
        }
        if provider.effort.is_none() {
            provider.effort = builtin.effort.clone();
        }
        if provider.efforts.is_empty() {
            provider.efforts = builtin.efforts.clone();
        }
        if provider.stage_models.is_empty() {
            provider.stage_models = builtin.stage_models.clone();
        }
        if provider.stage_efforts.is_empty() {
            provider.stage_efforts = builtin.stage_efforts.clone();
        }
    }
    if provider.auth_methods.is_empty() {
        if let Some(auth_env) = provider.auth_env.clone() {
            provider.auth_methods = vec![env_auth("api-key", "API key", &auth_env)];
        }
    }
    if provider.models.is_empty() {
        if let Some(model) = provider
            .model
            .clone()
            .filter(|model| !is_placeholder_model(model))
        {
            provider.models = vec![model_choice(&model, &model)];
        }
    }
    if provider.efforts.is_empty() {
        if let Some(effort) = provider.effort.clone() {
            provider.efforts = vec![effort];
        }
    }
    provider
}

pub fn provider_catalog(root: &Path) -> Result<Vec<ProviderCatalogEntry>> {
    provider_catalog_with_auth(root, ProviderAuthCheckMode::Full)
}

pub fn provider_catalog_fast(root: &Path) -> Result<Vec<ProviderCatalogEntry>> {
    provider_catalog_with_auth(root, ProviderAuthCheckMode::Fast)
}

fn provider_catalog_with_auth(
    root: &Path,
    auth_mode: ProviderAuthCheckMode,
) -> Result<Vec<ProviderCatalogEntry>> {
    let registry = provider_registry(root)?;
    Ok(registry
        .into_iter()
        .map(|(id, mut provider)| {
            apply_agent_overrides(root, &id, &mut provider);
            let auth_methods = match auth_mode {
                ProviderAuthCheckMode::Fast => {
                    auth_statuses_with_mode(&provider, ProviderAuthCheckMode::Fast)
                }
                ProviderAuthCheckMode::Full => auth_statuses(&provider),
            };
            let auth_present = auth_present_from_statuses(&auth_methods);
            ProviderCatalogEntry {
                id,
                kind: provider.kind,
                model: provider.model,
                effort: provider.effort,
                enabled: provider.enabled,
                auth_present,
                auth_methods,
                auth_env: provider.auth_env,
                models: provider.models,
                efforts: provider.efforts,
                stage_models: provider.stage_models,
                stage_efforts: provider.stage_efforts,
                capabilities: provider.capabilities,
                token_budget: provider.token_budget,
                usage_limits: provider.usage_limits,
            }
        })
        .collect())
}

pub fn resolve_provider(
    root: &Path,
    options: &ProviderResolveOptions,
) -> Result<ProviderSelection> {
    resolve_provider_with_auth(root, options, ProviderAuthCheckMode::Full)
}

pub fn resolve_provider_fast(
    root: &Path,
    options: &ProviderResolveOptions,
) -> Result<ProviderSelection> {
    resolve_provider_with_auth(root, options, ProviderAuthCheckMode::Fast)
}

fn resolve_provider_with_auth(
    root: &Path,
    options: &ProviderResolveOptions,
    auth_mode: ProviderAuthCheckMode,
) -> Result<ProviderSelection> {
    let config = load_sdd_config(root)?;
    let mut registry = builtin_provider_registry();
    for (id, provider) in config.providers.registry.clone() {
        let id = normalize_provider_id(&id);
        let provider = merge_provider_definition(registry.get(&id), provider);
        registry.insert(id, provider);
    }
    let provider_id = options
        .provider
        .clone()
        .and_then(non_empty_owned)
        .or_else(|| env_string("SDD_PROVIDER"))
        .or_else(|| config.providers.default.clone().and_then(non_empty_owned))
        .map(|value| normalize_provider_id(&value))
        .unwrap_or_else(|| "codex".to_string());

    let mut provider = registry
        .get(&provider_id)
        .cloned()
        .unwrap_or_else(|| ProviderDefinition {
            kind: "custom".to_string(),
            enabled: true,
            ..ProviderDefinition::default()
        });
    apply_agent_overrides(root, &provider_id, &mut provider);

    let model = options
        .model
        .clone()
        .and_then(non_empty_owned)
        .or_else(|| env_string("SDD_MODEL"))
        .or_else(|| provider.model.clone().and_then(non_empty_owned))
        .or_else(|| config.providers.model.clone().and_then(non_empty_owned))
        .unwrap_or_else(|| "default".to_string());
    let effort = options
        .effort
        .clone()
        .and_then(non_empty_owned)
        .or_else(|| env_string("SDD_EFFORT"))
        .or_else(|| env_string("SDD_REASONING_EFFORT"))
        .or_else(|| provider.effort.clone().and_then(non_empty_owned))
        .or_else(|| config.providers.effort.clone().and_then(non_empty_owned))
        .unwrap_or_else(|| "medium".to_string());
    let offline =
        options.offline || env_truthy("SDD_OFFLINE") || config.providers.offline.unwrap_or(true);
    let auth_methods = match auth_mode {
        ProviderAuthCheckMode::Fast => {
            auth_statuses_with_mode(&provider, ProviderAuthCheckMode::Fast)
        }
        ProviderAuthCheckMode::Full => auth_statuses(&provider),
    };
    let auth_present = auth_present_from_statuses(&auth_methods);

    Ok(ProviderSelection {
        id: provider_id,
        kind: provider.kind,
        model,
        effort,
        offline,
        enabled: provider.enabled,
        auth_env: provider.auth_env,
        auth_present,
        auth_methods,
        models: provider.models,
        efforts: provider.efforts,
        stage_models: provider.stage_models,
        stage_efforts: provider.stage_efforts,
        capabilities: provider.capabilities,
        token_budget: provider.token_budget,
        usage_limits: provider.usage_limits,
    })
}

#[derive(Default)]
struct AgentOverrides {
    stage_models: BTreeMap<String, String>,
    stage_efforts: BTreeMap<String, String>,
}

fn apply_agent_overrides(root: &Path, provider_id: &str, provider: &mut ProviderDefinition) {
    let overrides = agent_overrides(root, provider_id);
    if overrides.stage_models.is_empty() && overrides.stage_efforts.is_empty() {
        return;
    }
    let valid_models = provider
        .models
        .iter()
        .map(|model| model.id.as_str())
        .collect::<BTreeSet<_>>();
    for (stage, model) in overrides.stage_models {
        if valid_models.is_empty() || valid_models.contains(model.as_str()) {
            provider.stage_models.insert(stage, model);
        }
    }
    let valid_efforts = provider
        .efforts
        .iter()
        .map(|effort| effort.as_str())
        .collect::<BTreeSet<_>>();
    for (stage, effort) in overrides.stage_efforts {
        if valid_efforts.is_empty() || valid_efforts.contains(effort.as_str()) {
            provider.stage_efforts.insert(stage, effort);
        }
    }
}

fn agent_overrides(root: &Path, provider_id: &str) -> AgentOverrides {
    match provider_id {
        "codex" => codex_agent_overrides(root),
        "claude" => claude_agent_overrides(root),
        "cursor" => routing_file_overrides(root.join(".cursor/model-routing.yaml")),
        "opencode" => routing_file_overrides(root.join(".opencode/model-routing.yaml")),
        _ => AgentOverrides::default(),
    }
}

fn codex_agent_overrides(root: &Path) -> AgentOverrides {
    let mut overrides = AgentOverrides::default();
    for stage in SDD_STAGES {
        let path = root.join(format!(".codex/agents/{stage}.toml"));
        if let Some(text) = read_to_string(&path) {
            if let Some(model) =
                collect_line_value(&text, "model").filter(|model| !is_placeholder_model(model))
            {
                overrides.stage_models.insert(stage.to_string(), model);
            }
            if let Some(effort) = collect_line_value(&text, "model_reasoning_effort")
                .or_else(|| collect_line_value(&text, "effort"))
            {
                overrides.stage_efforts.insert(stage.to_string(), effort);
            }
        }
    }
    overrides
}

fn claude_agent_overrides(root: &Path) -> AgentOverrides {
    let mut overrides = AgentOverrides::default();
    for stage in SDD_STAGES {
        let path = root.join(format!(".claude/agents/{stage}.md"));
        if let Some(text) = read_to_string(&path) {
            if let Some(model) = collect_yaml_frontmatter_value(&text, "model")
                .filter(|model| !is_placeholder_model(model))
            {
                overrides.stage_models.insert(stage.to_string(), model);
            }
            if let Some(effort) = collect_yaml_frontmatter_value(&text, "effort") {
                overrides.stage_efforts.insert(stage.to_string(), effort);
            }
        }
    }
    overrides
}

fn routing_file_overrides(path: PathBuf) -> AgentOverrides {
    let Some(text) = read_to_string(&path) else {
        return AgentOverrides::default();
    };
    let Ok(doc) = serde_yaml::from_str::<YamlValue>(&text) else {
        return AgentOverrides::default();
    };
    let mut overrides = AgentOverrides::default();
    let Some(stages) = doc.get("stages").and_then(YamlValue::as_mapping) else {
        return overrides;
    };
    for (key, value) in stages {
        let Some(stage) = key.as_str().filter(|stage| SDD_STAGES.contains(stage)) else {
            continue;
        };
        if let Some(model) = value.get("model").and_then(YamlValue::as_str) {
            overrides
                .stage_models
                .insert(stage.to_string(), model.to_string());
        }
        if let Some(effort) = value.get("effort").and_then(YamlValue::as_str) {
            overrides
                .stage_efforts
                .insert(stage.to_string(), effort.to_string());
        }
    }
    overrides
}

fn collect_line_value(text: &str, key: &str) -> Option<String> {
    let prefix = format!("{key} =");
    text.lines()
        .map(str::trim)
        .find_map(|line| line.strip_prefix(&prefix))
        .map(clean_value)
        .filter(|value| !value.is_empty())
}

fn collect_yaml_frontmatter_value(text: &str, key: &str) -> Option<String> {
    let mut lines = text.lines();
    if lines.next().map(str::trim) != Some("---") {
        return None;
    }
    let prefix = format!("{key}:");
    for line in lines {
        let line = line.trim();
        if line == "---" {
            break;
        }
        if let Some(value) = line.strip_prefix(&prefix) {
            let value = clean_value(value);
            if !value.is_empty() {
                return Some(value);
            }
        }
    }
    None
}

fn clean_value(value: &str) -> String {
    value
        .trim()
        .trim_matches('"')
        .trim_matches('\'')
        .to_string()
}

fn read_to_string(path: &Path) -> Option<String> {
    fs::read_to_string(path).ok()
}

const SDD_STAGES: &[&str] = &[
    "idea",
    "prd",
    "techspec",
    "tasks",
    "refinement",
    "execution",
    "review",
    "memory",
];

pub fn auth_statuses(provider: &ProviderDefinition) -> Vec<ProviderAuthStatus> {
    auth_statuses_with_mode(provider, ProviderAuthCheckMode::Full)
}

fn auth_statuses_with_mode(
    provider: &ProviderDefinition,
    auth_mode: ProviderAuthCheckMode,
) -> Vec<ProviderAuthStatus> {
    let methods = if provider.auth_methods.is_empty() {
        provider
            .auth_env
            .as_ref()
            .map(|auth_env| vec![env_auth("api-key", "API key", auth_env)])
            .unwrap_or_default()
    } else {
        provider.auth_methods.clone()
    };
    methods
        .into_iter()
        .map(|method| {
            let source = match method.kind.as_str() {
                "env" => method.env.clone().unwrap_or_default(),
                "cli" => method.command.clone().unwrap_or_default(),
                _ => method
                    .env
                    .clone()
                    .or(method.command.clone())
                    .unwrap_or_default(),
            };
            let present = match method.kind.as_str() {
                "env" => method
                    .env
                    .as_ref()
                    .map(|name| env::var_os(name).is_some())
                    .unwrap_or(false),
                "cli" => cli_auth_present(&method, auth_mode),
                _ => false,
            };
            let detail = auth_detail(&method, present, auth_mode);
            ProviderAuthStatus {
                id: method.id,
                label: method.label,
                kind: method.kind,
                source,
                present,
                check_command: method.check_command,
                login_command: method.login_command,
                detail,
            }
        })
        .collect()
}

fn cli_auth_present(method: &ProviderAuthDefinition, auth_mode: ProviderAuthCheckMode) -> bool {
    let Some(command) = method.command.as_deref() else {
        return false;
    };
    if !command_exists(command) {
        return false;
    }
    if auth_mode == ProviderAuthCheckMode::Fast {
        return true;
    }
    let Some(check_command) = method.check_command.as_deref() else {
        return true;
    };
    let Some(output) = run_check_command(check_command) else {
        return false;
    };
    if !output.status.success() {
        return false;
    }
    let combined = format!(
        "{}\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    if let Some(expected) = method.check_success_text.as_deref() {
        return combined.contains(expected);
    }
    if let Some(pattern) = method.check_success_regex.as_deref() {
        return regex::Regex::new(pattern)
            .map(|regex| regex.is_match(&combined))
            .unwrap_or(false);
    }
    true
}

fn auth_detail(
    method: &ProviderAuthDefinition,
    present: bool,
    auth_mode: ProviderAuthCheckMode,
) -> Option<String> {
    match (method.kind.as_str(), auth_mode, present) {
        ("cli", ProviderAuthCheckMode::Fast, true) if method.check_command.is_some() => {
            Some("sessão não verificada no modo rápido; rode `sdd providers doctor`".to_string())
        }
        ("cli", _, false) if method.command.is_some() => {
            Some("comando ausente ou sessão inválida".to_string())
        }
        _ => None,
    }
}

fn run_check_command(command: &str) -> Option<std::process::Output> {
    let mut parts = command.split_whitespace();
    let program = parts.next()?;
    Command::new(program).args(parts).output().ok()
}

fn auth_present_from_statuses(statuses: &[ProviderAuthStatus]) -> Option<bool> {
    if statuses.is_empty() {
        None
    } else {
        Some(statuses.iter().any(|status| status.present))
    }
}

fn command_exists(command: &str) -> bool {
    crate::runtime::platform::executable_exists(command)
}

pub fn normalize_provider_id(value: &str) -> String {
    match value.trim().to_ascii_lowercase().as_str() {
        "agy" | "gemini" | "google-antigravity" => "antigravity".to_string(),
        value => value.to_string(),
    }
}

fn non_empty_owned(value: String) -> Option<String> {
    if value.trim().is_empty() {
        None
    } else {
        Some(value)
    }
}

fn is_placeholder_model(value: &str) -> bool {
    let normalized = value.trim();
    normalized.is_empty() || normalized == "default" || normalized.ends_with("-default")
}

fn env_string(name: &str) -> Option<String> {
    env::var(name).ok().and_then(non_empty_owned)
}

fn env_truthy(name: &str) -> bool {
    env::var(name)
        .ok()
        .map(|value| {
            matches!(
                value.trim().to_ascii_lowercase().as_str(),
                "1" | "true" | "yes" | "on"
            )
        })
        .unwrap_or(false)
}

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

    #[test]
    fn provider_catalog_carries_usage_limits_from_config() {
        let dir = tempfile::tempdir().unwrap();
        fs::write(
            dir.path().join("sdd.config.yaml"),
            r#"
providers:
  registry:
    codex:
      kind: openai-compatible
      usage_limits:
        - window: weekly
          limit: 40
          remaining: 12.5
          unit: hours
          resets_at: segunda 00:00
          source: manual
"#,
        )
        .unwrap();

        let catalog = provider_catalog(dir.path()).unwrap();
        let codex = catalog
            .iter()
            .find(|provider| provider.id == "codex")
            .unwrap();
        assert_eq!(codex.usage_limits.len(), 1);
        assert_eq!(codex.usage_limits[0].window, "weekly");
        assert_eq!(codex.usage_limits[0].remaining, Some(12.5));
        assert_eq!(codex.usage_limits[0].unit, "hours");
    }

    #[test]
    fn auth_statuses_keep_login_command_when_cli_is_missing() {
        let provider = ProviderDefinition {
            auth_methods: vec![ProviderAuthDefinition {
                id: "missing-cli".to_string(),
                label: "Missing CLI".to_string(),
                kind: "cli".to_string(),
                env: None,
                command: Some("sdd-definitely-missing-cli".to_string()),
                check_command: Some("sdd-definitely-missing-cli status".to_string()),
                check_success_text: Some("ok".to_string()),
                check_success_regex: None,
                login_command: Some("sdd-definitely-missing-cli login".to_string()),
            }],
            ..ProviderDefinition::default()
        };

        let statuses = auth_statuses(&provider);
        assert_eq!(statuses.len(), 1);
        assert!(!statuses[0].present);
        assert_eq!(
            statuses[0].login_command.as_deref(),
            Some("sdd-definitely-missing-cli login")
        );
        assert_eq!(
            statuses[0].check_command.as_deref(),
            Some("sdd-definitely-missing-cli status")
        );
    }

    #[test]
    fn fast_auth_status_skips_session_check_command() {
        // Comando garantidamente presente no SO atual: `cmd` no Windows, `sh` no Unix.
        let present_command = if cfg!(windows) { "cmd" } else { "sh" };
        let provider = ProviderDefinition {
            auth_methods: vec![ProviderAuthDefinition {
                id: "local-cli".to_string(),
                label: "Local CLI".to_string(),
                kind: "cli".to_string(),
                env: None,
                command: Some(present_command.to_string()),
                check_command: Some("sdd-definitely-missing-cli status".to_string()),
                check_success_text: Some("ok".to_string()),
                check_success_regex: None,
                login_command: Some("sh login".to_string()),
            }],
            ..ProviderDefinition::default()
        };

        let fast = auth_statuses_with_mode(&provider, ProviderAuthCheckMode::Fast);
        assert!(fast[0].present);
        assert!(fast[0]
            .detail
            .as_deref()
            .is_some_and(|detail| detail.contains("modo rápido")));

        let full = auth_statuses_with_mode(&provider, ProviderAuthCheckMode::Full);
        assert!(!full[0].present);
    }

    #[test]
    fn normalize_provider_id_keeps_antigravity_aliases_compatible() {
        assert_eq!(normalize_provider_id("antigravity"), "antigravity");
        assert_eq!(normalize_provider_id("agy"), "antigravity");
        assert_eq!(normalize_provider_id("gemini"), "antigravity");
        assert_eq!(normalize_provider_id("google-antigravity"), "antigravity");
    }
}
