mod infrastructure;

use std::io;
use std::sync::Arc;
use std::sync::atomic::AtomicU8;

use clap::Parser;

use stynx_code_config::load_config;
use stynx_code_engine::{QueryEngine, run_session_start_hooks};
use stynx_code_memory::FileSessionRepository;
use stynx_code_permission::ConfigAwarePermissionChecker;
use stynx_code_provider::{AnthropicProvider, OpenAiProvider};
use stynx_code_tools::{
    AskUserTool, BashTool, ExitPlanModeTool, FileEditTool, FileWriteTool,
    GlobTool, GrepTool, ReadTool, TodoReadTool, TodoWriteTool, ToolRegistry, WebFetchTool,
    WebSearchTool,
};
use stynx_code_types::{Conversation, PermissionMode};

use infrastructure::agent_tool::{AgentTool, AllInternsTool, ExploreAgentTool, InternTool};
use infrastructure::app_display::print_session_history;
use infrastructure::app_loop::run_loop;
use infrastructure::cli::Cli;
use infrastructure::conductor::AgentManager;
use infrastructure::conductor_tools::{ListAgentsTool, SpawnAgentTool, WaitAgentTool};
use infrastructure::event_renderer::render_error_box;
use infrastructure::oneshot::run_oneshot;
use infrastructure::pipe::run_pipe;
use infrastructure::skills::load_skills;
use infrastructure::terminal::{DIM, RESET, build_env_info, make_system_prompt, print_banner, prompt_resume};

#[tokio::main]
async fn main() {
    // Load .env from cwd (or walk up). Silent if missing — env vars set in the
    // shell still take precedence because dotenvy only fills *unset* keys.
    let _ = dotenvy::dotenv();

    let cli = Cli::parse();

    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "warn".into()))
        .with_writer(io::stderr).init();

    let config = load_config();
    let credential = match stynx_code_auth::resolve_credential() {
        Ok(cred) => cred,
        Err(e) => { render_error_box(&e.to_string()); std::process::exit(1); }
    };

    let mode_flag = Arc::new(AtomicU8::new(PermissionMode::Normal as u8));
    let provider = Arc::new(AnthropicProvider::new(credential, mode_flag.clone()));

    if let Some(ref model) = cli.model { provider.set_model(model); }
    else if let Some(ref model) = config.model { provider.set_model(model); }
    if let Ok(model) = std::env::var("MODEL") { provider.set_model(&model); }
    if let Some(mt) = config.max_tokens { provider.set_max_tokens(mt); }
    if let Some(total) = cli.thinking_budget {
        provider.set_max_tokens(total);
        let budget = total.saturating_sub(16384);
        provider.set_thinking_budget(budget);
    }

    // Effort: env > CLI > settings
    let effort_value = std::env::var("CLAUDE_CODE_EFFORT_LEVEL").ok()
        .or_else(|| cli.effort.clone())
        .or_else(|| config.effort.clone());
    if let Some(ref effort) = effort_value {
        let level = effort.to_lowercase();
        if ["low", "medium", "high", "max"].contains(&level.as_str()) {
            provider.set_effort(&level);
            if level == "max" && cli.thinking_budget.is_none() {
                provider.set_max_tokens(64000);
            }
        }
    }

    let pause_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));

    let mut registry = ToolRegistry::new();
    registry.register(Arc::new(BashTool::new()));
    registry.register(Arc::new(ReadTool));
    registry.register(Arc::new(FileWriteTool));
    registry.register(Arc::new(FileEditTool));
    registry.register(Arc::new(GlobTool));
    registry.register(Arc::new(GrepTool));
    let ask_user_tool = Arc::new(AskUserTool::new(pause_flag.clone()));
    let ask_user_bridge_handle = ask_user_tool.bridge_handle();
    registry.register(ask_user_tool.clone());
    registry.register(Arc::new(WebFetchTool));
    registry.register(Arc::new(WebSearchTool));
    registry.register(Arc::new(ExitPlanModeTool::new(pause_flag.clone())));
    registry.register(Arc::new(TodoWriteTool));
    registry.register(Arc::new(TodoReadTool));

    let cwd = std::env::current_dir().map(|p| p.display().to_string()).unwrap_or_else(|_| ".".into());

    for tool in stynx_code_tools::load_mcp_tools(&cwd).await { registry.register(tool); }

    let permission = Arc::new(ConfigAwarePermissionChecker::new_with_pause(
        config.permissions.clone(), mode_flag.clone(), pause_flag.clone(),
    ));

    let sub_registry = Arc::new(registry.clone_excluding(&["agent", "explore"]));
    let explore_registry = Arc::new(sub_registry.clone_excluding(&[
        "bash", "file_write", "file_edit", "ask_user_question", "web_fetch", "web_search", "todo_write", "todo_read",
    ]));

    let agent_manager = AgentManager::new();
    let mut conductor_reg = sub_registry.clone_excluding(&[]);

    registry.register(Arc::new(AgentTool::new(
        provider.clone(), sub_registry.clone(), permission.clone(), mode_flag.clone(), config.hooks.clone(),
    )));
    registry.register(Arc::new(ExploreAgentTool::new(
        provider.clone(), explore_registry, permission.clone(), mode_flag.clone(), config.hooks.clone(),
    )));

    let intern_tools: Vec<Arc<InternTool>> = build_intern_tools(
        &config, &sub_registry, &permission, &mode_flag, &config.hooks,
    );
    for t in &intern_tools {
        registry.register(t.clone());
    }
    // Always register the "all interns" fan-out, even with zero interns —
    // its description tells the senior the list is empty so the model
    // doesn't waste a call.
    if intern_tools.len() >= 2 {
        registry.register(Arc::new(AllInternsTool::new(intern_tools.clone())));
    }
    conductor_reg.register(Arc::new(SpawnAgentTool::new(
        provider.clone(), sub_registry, permission.clone(), mode_flag.clone(), config.hooks.clone(), agent_manager.clone(),
    )));
    conductor_reg.register(Arc::new(WaitAgentTool::new(agent_manager.clone())));
    conductor_reg.register(Arc::new(ListAgentsTool::new(agent_manager)));
    let conductor_engine = Arc::new(QueryEngine::new(
        provider.clone(), Arc::new(conductor_reg), permission.clone(), mode_flag.clone(), config.hooks.clone(),
    ));

    let reflect_engine = Arc::new(QueryEngine::new(
        provider.clone(), Arc::new(ToolRegistry::new()), permission.clone(), mode_flag.clone(), config.hooks.clone(),
    ));

    let tool_names = registry.tool_names();
    let registry = Arc::new(registry);
    let engine = {
        let mut e = QueryEngine::new(provider.clone(), registry, permission.clone(), mode_flag.clone(), config.hooks.clone());
        if let Some(mt) = cli.max_turns { e = e.with_max_turns(mt); }
        else if let Some(mt) = config.max_turns { e = e.with_max_turns(mt); }
        Arc::new(e)
    };

    let env = build_env_info(cwd.clone(), provider.model_name());
    let loaded_skills = load_skills(&cwd);
    let skill_pairs: Vec<(String, String, Option<String>)> = loaded_skills.iter()
        .filter(|s| s.user_invocable)
        .map(|s| (s.name.clone(), s.description.clone(), s.when_to_use.clone()))
        .collect();

    let system_prompt = cli.system.clone().unwrap_or_else(|| {
        make_system_prompt(&env, &tool_names, &skill_pairs, config.commit_attribution)
    });

    if Cli::is_piped() {
        if let Err(e) = run_pipe(&engine, system_prompt, cli.prompt.as_deref(), cli.json).await {
            render_error_box(&e);
            std::process::exit(1);
        }
        return;
    }

    if let Some(ref prompt) = cli.prompt {
        if let Err(e) = run_oneshot(&engine, system_prompt, prompt, cli.json).await {
            render_error_box(&e);
            std::process::exit(1);
        }
        return;
    }

    let session_repo: Arc<dyn stynx_code_memory::SessionRepository> =
        match FileSessionRepository::new(&cwd) {
            Ok(repo) => Arc::new(repo),
            Err(e) => {
                tracing::warn!("failed to init session repository: {e}");
                Arc::new(FileSessionRepository::with_dir(std::path::PathBuf::from(".stynx-code/projects/fallback")))
            }
        };

    let session_start_out = run_session_start_hooks(&config.hooks).await;
    let model_id = provider.model_name();
    print_banner(&cwd, &model_id);
    if !session_start_out.is_empty() { println!("  {DIM}{session_start_out}{RESET}\n"); }

    tracing::debug!("system prompt: {} chars", system_prompt.len());

    let conversation = match stynx_code_memory::load_session(&session_repo).await {
        Ok(Some(prev)) if !prev.messages.is_empty() => {
            if prompt_resume() {
                let mut c = prev;
                if c.system.is_none() { c.system = Some(system_prompt.clone()); }
                println!("  {DIM}↻ Session resumed ({} messages){RESET}\n", c.messages.len());
                print_session_history(&c.messages);
                c
            } else {
                Conversation { system: Some(system_prompt.clone()), ..Default::default() }
            }
        }
        _ => Conversation { system: Some(system_prompt.clone()), ..Default::default() },
    };

    run_loop(engine, conductor_engine, reflect_engine, session_repo, provider, config, mode_flag, cwd, system_prompt, conversation, loaded_skills, pause_flag, permission, intern_tools, ask_user_bridge_handle).await;
}

// Intern gets a small, well-known toolset. Strict OpenAI-compat endpoints
// (DeepSeek in particular) reject tool schemas with JSON-Schema features
// they don't recognize, so we keep this list lean.
const INTERN_TOOLS: &[&str] = &[
    "bash", "read", "file_write", "file_edit", "glob", "grep",
];

struct ResolvedIntern {
    name: String,
    provider_label: String,
    description: String,
    base_url: String,
    api_key: String,
    model: String,
}

fn build_intern_tools(
    config: &stynx_code_config::Settings,
    sub_registry: &Arc<ToolRegistry>,
    permission: &Arc<ConfigAwarePermissionChecker>,
    mode_flag: &Arc<AtomicU8>,
    hooks: &stynx_code_config::HooksConfig,
) -> Vec<Arc<InternTool>> {
    let resolved = resolve_interns(config);
    let intern_registry = Arc::new(sub_registry.keep_only(INTERN_TOOLS));

    let mut out: Vec<Arc<InternTool>> = Vec::new();
    let mut seen_tool_names: std::collections::HashSet<String> = std::collections::HashSet::new();

    for r in resolved {
        let tool_name = sanitize_tool_name(&format!("delegate_to_{}", r.name));
        if !seen_tool_names.insert(tool_name.clone()) {
            tracing::warn!(name = %r.name, "duplicate intern name, skipping");
            continue;
        }

        let intern_provider: Arc<dyn stynx_code_types::Provider> = Arc::new(
            OpenAiProvider::new(&r.provider_label, r.base_url.clone(), r.api_key.clone(), &r.model),
        );

        eprintln!(
            "  \x1b[2m· intern ready: {name} ({provider} / {model})\x1b[0m",
            name = r.name, provider = r.provider_label, model = r.model,
        );

        out.push(Arc::new(InternTool::new(
            intern_provider,
            intern_registry.clone(),
            permission.clone(),
            mode_flag.clone(),
            hooks.clone(),
            r.name.clone(),
            tool_name,
            r.description.clone(),
        )));
    }
    out
}

fn resolve_interns(config: &stynx_code_config::Settings) -> Vec<ResolvedIntern> {
    let mut out: Vec<ResolvedIntern> = Vec::new();
    let mut names: std::collections::HashSet<String> = std::collections::HashSet::new();

    for cfg in &config.interns {
        if let Some(r) = resolve_one(cfg) {
            if names.insert(r.name.clone()) {
                out.push(r);
            }
        }
    }

    // Env fallbacks: only auto-register a default intern when the user hasn't
    // already configured one with the same name in settings.json.
    if !names.contains("deepseek") {
        if let Some(r) = legacy_deepseek_intern() {
            names.insert(r.name.clone());
            out.push(r);
        }
    }

    if !names.contains("qwen") {
        if let Some(r) = legacy_qwen_intern() {
            names.insert(r.name.clone());
            out.push(r);
        }
    }

    for r in openrouter_env_interns() {
        if names.insert(r.name.clone()) {
            out.push(r);
        }
    }

    for r in qwen_env_interns() {
        if names.insert(r.name.clone()) {
            out.push(r);
        }
    }

    out
}

fn resolve_one(cfg: &stynx_code_config::InternConfig) -> Option<ResolvedIntern> {
    let name = cfg.name.trim();
    if name.is_empty() { return None; }
    let provider = cfg.provider.trim().to_lowercase();

    let (default_base, default_key_env, provider_label) = match provider.as_str() {
        "deepseek" => ("https://api.deepseek.com/v1", "DEEPSEEK_API_KEY", "deepseek"),
        "openrouter" => ("https://openrouter.ai/api/v1", "OPENROUTER_API_KEY", "openrouter"),
        "openai" => ("https://api.openai.com/v1", "OPENAI_API_KEY", "openai"),
        "qwen" => (
            "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
            "QWEN_API_KEY",
            "qwen",
        ),
        "custom" => ("", "", "custom"),
        _ => {
            tracing::warn!(name = %name, provider = %provider, "unknown intern provider, skipping");
            return None;
        }
    };

    let base_url = cfg.base_url.clone()
        .filter(|s| !s.trim().is_empty())
        .unwrap_or_else(|| default_base.to_string());
    if base_url.is_empty() {
        tracing::warn!(name = %name, "custom intern needs base_url, skipping");
        return None;
    }

    let key_env = cfg.api_key_env.clone()
        .filter(|s| !s.trim().is_empty())
        .unwrap_or_else(|| default_key_env.to_string());
    let api_key = std::env::var(&key_env).ok().filter(|s| !s.trim().is_empty());
    let api_key = match api_key {
        Some(k) => k,
        None => {
            tracing::warn!(name = %name, env = %key_env, "intern missing api key in env, skipping");
            return None;
        }
    };

    let description = cfg.description.clone().unwrap_or_else(|| default_description(name, &cfg.model));

    Some(ResolvedIntern {
        name: name.to_string(),
        provider_label: provider_label.to_string(),
        description,
        base_url,
        api_key,
        model: cfg.model.clone(),
    })
}

fn legacy_deepseek_intern() -> Option<ResolvedIntern> {
    let api_key = std::env::var("DEEPSEEK_API_KEY").ok().filter(|s| !s.trim().is_empty())?;
    let base_url = std::env::var("DEEPSEEK_BASE_URL")
        .ok().filter(|s| !s.trim().is_empty())
        .unwrap_or_else(|| "https://api.deepseek.com/v1".to_string());
    let model = std::env::var("INTERN_MODEL")
        .or_else(|_| std::env::var("DEEPSEEK_MODEL"))
        .ok().filter(|s| !s.trim().is_empty())
        .unwrap_or_else(|| "deepseek-chat".to_string());
    Some(ResolvedIntern {
        name: "deepseek".to_string(),
        provider_label: "deepseek".to_string(),
        description: default_description("deepseek", &model),
        base_url,
        api_key,
        model,
    })
}

fn legacy_qwen_intern() -> Option<ResolvedIntern> {
    let api_key = std::env::var("QWEN_API_KEY").ok().filter(|s| !s.trim().is_empty())?;
    let base_url = std::env::var("QWEN_BASE_URL")
        .ok().filter(|s| !s.trim().is_empty())
        .unwrap_or_else(|| "https://dashscope-intl.aliyuncs.com/compatible-mode/v1".to_string());
    let model = std::env::var("QWEN_MODEL")
        .ok().filter(|s| !s.trim().is_empty())
        .unwrap_or_else(|| "qwen-plus".to_string());
    Some(ResolvedIntern {
        name: "qwen".to_string(),
        provider_label: "qwen".to_string(),
        description: default_description("qwen", &model),
        base_url,
        api_key,
        model,
    })
}

// Parses QWEN_INTERNS="name1:model1,name2:model2" when QWEN_API_KEY is set.
fn qwen_env_interns() -> Vec<ResolvedIntern> {
    let mut out: Vec<ResolvedIntern> = Vec::new();
    let api_key = match std::env::var("QWEN_API_KEY").ok().filter(|s| !s.trim().is_empty()) {
        Some(k) => k,
        None => return out,
    };
    let base_url = std::env::var("QWEN_BASE_URL")
        .ok().filter(|s| !s.trim().is_empty())
        .unwrap_or_else(|| "https://dashscope-intl.aliyuncs.com/compatible-mode/v1".to_string());
    let spec = match std::env::var("QWEN_INTERNS").ok().filter(|s| !s.trim().is_empty()) {
        Some(s) => s,
        None => return out,
    };
    for entry in spec.split(',') {
        let entry = entry.trim();
        if entry.is_empty() { continue; }
        let (name, model) = match entry.split_once(':') {
            Some((n, m)) => (n.trim(), m.trim()),
            None => {
                tracing::warn!(entry = %entry, "QWEN_INTERNS entry must be name:model, skipping");
                continue;
            }
        };
        if name.is_empty() || model.is_empty() { continue; }
        out.push(ResolvedIntern {
            name: name.to_string(),
            provider_label: "qwen".to_string(),
            description: default_description(name, model),
            base_url: base_url.clone(),
            api_key: api_key.clone(),
            model: model.to_string(),
        });
    }
    out
}

// Parses OPENROUTER_INTERNS="name1:model1,name2:model2" when
// OPENROUTER_API_KEY is set.
fn openrouter_env_interns() -> Vec<ResolvedIntern> {
    let mut out: Vec<ResolvedIntern> = Vec::new();
    let api_key = match std::env::var("OPENROUTER_API_KEY").ok().filter(|s| !s.trim().is_empty()) {
        Some(k) => k,
        None => return out,
    };
    let base_url = std::env::var("OPENROUTER_BASE_URL")
        .ok().filter(|s| !s.trim().is_empty())
        .unwrap_or_else(|| "https://openrouter.ai/api/v1".to_string());
    let spec = match std::env::var("OPENROUTER_INTERNS").ok().filter(|s| !s.trim().is_empty()) {
        Some(s) => s,
        None => return out,
    };
    for entry in spec.split(',') {
        let entry = entry.trim();
        if entry.is_empty() { continue; }
        let (name, model) = match entry.split_once(':') {
            Some((n, m)) => (n.trim(), m.trim()),
            None => {
                tracing::warn!(entry = %entry, "OPENROUTER_INTERNS entry must be name:model, skipping");
                continue;
            }
        };
        if name.is_empty() || model.is_empty() { continue; }
        out.push(ResolvedIntern {
            name: name.to_string(),
            provider_label: "openrouter".to_string(),
            description: default_description(name, model),
            base_url: base_url.clone(),
            api_key: api_key.clone(),
            model: model.to_string(),
        });
    }
    out
}

fn default_description(name: &str, model: &str) -> String {
    format!(
        "Hand off a focused, well-scoped subtask to the '{name}' intern (model: {model}). \
The intern has bash/read/file_write/file_edit/glob/grep available but cannot spawn further sub-agents. \
Use this for grunt work — boilerplate, mechanical refactors, gathering data, drafting code that you'll review. \
Provide explicit acceptance criteria. Returns the intern's summary + output."
    )
}

fn sanitize_tool_name(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
            out.push(c);
        } else {
            out.push('_');
        }
    }
    if out.len() > 64 { out.truncate(64); }
    out
}
