//! `socratic-council` CLI entry point.

use clap::{Parser, Subcommand};
use socratic_council::catalog::{catalog_models, DiscoveredModel, ModelSource};
use socratic_council::config::Config;
use socratic_council::engine::{default_agents, DebateEvent, Engine};
use socratic_council::http_client;
use socratic_council::providers::scan::scan_models;
use socratic_council::tui::{self, AppContext};
use socratic_council::types::{Agent, Provider, ReasoningTier};
use std::collections::HashMap;
use std::io::Write;

#[derive(Parser)]
#[command(
    name = "socratic-council",
    version,
    about = "A terminal multi-agent debate workstation — eight AI agents argue any topic."
)]
struct Cli {
    #[command(subcommand)]
    command: Option<Command>,
}

#[derive(Subcommand)]
enum Command {
    /// Start a debate on a topic (default action).
    Run {
        /// The debate topic (omit to open the Home view, or to be prompted with --no-tui).
        topic: Vec<String>,
        /// Restrict to these providers (comma-separated slugs).
        #[arg(long)]
        providers: Option<String>,
        /// Reasoning tier: low | medium | high.
        #[arg(long)]
        tier: Option<ReasoningTier>,
        /// Turn cap (0 = until you quit).
        #[arg(long)]
        max_turns: Option<u32>,
        /// Plain streaming output instead of the TUI.
        #[arg(long)]
        no_tui: bool,
        /// Scan each provider's live models before starting.
        #[arg(long)]
        scan: bool,
    },
    /// List catalog models, or scan a provider's live models.
    Models {
        /// Limit to one provider slug.
        #[arg(long)]
        provider: Option<String>,
        /// Query the provider's live /models endpoint.
        #[arg(long)]
        scan: bool,
    },
    /// Show which providers have an API key configured.
    Providers,
    /// Manage configuration and keys.
    Config {
        #[command(subcommand)]
        action: ConfigAction,
    },
}

#[derive(Subcommand)]
enum ConfigAction {
    /// Print the config file path.
    Path,
    /// Store an API key for a provider (read from stdin).
    SetKey {
        /// Provider slug (openai, anthropic, …).
        provider: String,
    },
}

#[tokio::main]
async fn main() {
    let cli = Cli::parse();
    let result = match cli.command {
        None => cmd_run(RunArgs::default()).await,
        Some(Command::Run { topic, providers, tier, max_turns, no_tui, scan }) => {
            cmd_run(RunArgs {
                topic: topic.join(" "),
                providers,
                tier,
                max_turns,
                no_tui,
                scan,
            })
            .await
        }
        Some(Command::Models { provider, scan }) => cmd_models(provider, scan).await,
        Some(Command::Providers) => cmd_providers(),
        Some(Command::Config { action }) => cmd_config(action),
    };
    if let Err(e) = result {
        eprintln!("error: {e:#}");
        std::process::exit(1);
    }
}

#[derive(Default)]
struct RunArgs {
    topic: String,
    providers: Option<String>,
    tier: Option<ReasoningTier>,
    max_turns: Option<u32>,
    no_tui: bool,
    scan: bool,
}

fn parse_provider_filter(spec: &Option<String>) -> Option<Vec<Provider>> {
    spec.as_ref().map(|s| {
        s.split(',')
            .filter_map(|slug| Provider::from_slug(slug.trim()))
            .collect::<Vec<_>>()
    })
}

async fn cmd_run(args: RunArgs) -> anyhow::Result<()> {
    let mut config = Config::load()?;
    // CLI flags override the inherited / default config.
    if let Some(tier) = args.tier {
        config.council_tier = tier;
    }
    if let Some(n) = args.max_turns {
        config.max_turns = n;
    }

    let configured = config.configured_providers();
    if configured.is_empty() {
        anyhow::bail!(
            "no API keys configured. Set one with `socratic-council config set-key <provider>`, \
             a <PROVIDER>_API_KEY env var, or open the desktop app once."
        );
    }

    // Providers to prepare models for: configured ∩ --providers filter.
    let filter = parse_provider_filter(&args.providers);
    let providers: Vec<Provider> = configured
        .into_iter()
        .filter(|p| filter.as_ref().map(|f| f.contains(p)).unwrap_or(true))
        .collect();
    if providers.is_empty() {
        anyhow::bail!("none of the requested providers have a key configured");
    }

    let http = http_client(config.proxy.as_deref());

    // Build the available-models map (scan or catalog) for those providers.
    // A `--scan` pass resolves each key anyway, so capture them to avoid
    // re-prompting the keychain when the debate launches.
    let mut available: HashMap<Provider, Vec<DiscoveredModel>> = HashMap::new();
    let mut prefetched_keys: HashMap<Provider, String> = HashMap::new();
    for provider in &providers {
        let models = if args.scan {
            let key = config.resolve_api_key(*provider).unwrap_or_default();
            if !key.is_empty() {
                prefetched_keys.insert(*provider, key.clone());
            }
            match scan_models(&http, *provider, &config.base_url(*provider), &key).await {
                Ok(m) => m,
                Err(_) => catalog_models(*provider),
            }
        } else {
            catalog_models(*provider)
        };
        available.insert(*provider, models);
    }

    if args.no_tui {
        return run_plain_debate(config, http, available, prefetched_keys, &args, &providers).await;
    }

    let initial_topic =
        if args.topic.trim().is_empty() { None } else { Some(args.topic.clone()) };
    let ctx = AppContext { http, config, available, providers, prefetched_keys };
    tui::run(ctx, initial_topic).await
}

/// Plain (non-TUI) streaming debate for piping / scripting.
async fn run_plain_debate(
    config: Config,
    http: reqwest::Client,
    available: HashMap<Provider, Vec<DiscoveredModel>>,
    mut keys: HashMap<Provider, String>,
    args: &RunArgs,
    providers: &[Provider],
) -> anyhow::Result<()> {
    let mut topic = args.topic.clone();
    if topic.trim().is_empty() {
        print!("Debate topic> ");
        std::io::stdout().flush()?;
        let mut line = String::new();
        std::io::stdin().read_line(&mut line)?;
        topic = line.trim().to_string();
    }
    if topic.is_empty() {
        anyhow::bail!("no topic given");
    }

    let mut agents: Vec<Agent> = default_agents(config.council_tier)
        .into_iter()
        .filter(|a| providers.contains(&a.provider))
        .collect();
    agents.sort_by(|a, b| a.name.cmp(&b.name));

    // Resolve any key not already prefetched (reads the keychain at most once
    // per provider), then drop agents whose key couldn't be resolved.
    for agent in &agents {
        if keys.contains_key(&agent.provider) {
            continue;
        }
        if let Some(key) = config.resolve_api_key(agent.provider) {
            keys.insert(agent.provider, key);
        }
    }
    agents.retain(|a| keys.contains_key(&a.provider));
    if agents.is_empty() {
        anyhow::bail!("could not read an API key for any selected provider");
    }

    let max_turns = match config.max_turns {
        0 => 1000,
        n => n,
    };
    let engine = Engine::new(http, config, topic, agents, available, keys, max_turns);
    run_plain(engine).await;
    Ok(())
}

async fn run_plain(engine: Engine) {
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;
    use tokio::sync::mpsc::unbounded_channel;

    let (tx, mut rx) = unbounded_channel();
    let cancel = Arc::new(AtomicBool::new(false));
    let engine_cancel = cancel.clone();
    let handle = tokio::spawn(async move { engine.run(tx, engine_cancel).await });

    let mut current = String::new();
    loop {
        tokio::select! {
            _ = tokio::signal::ctrl_c() => {
                cancel.store(true, Ordering::Relaxed);
                eprintln!("\n(stopping…)");
                break;
            }
            maybe = rx.recv() => {
                let Some(ev) = maybe else { break };
                match ev {
                    DebateEvent::Moderator(text) => println!("\n— {text}\n"),
                    DebateEvent::TurnStarted { name, model, .. } => {
                        print!("\n{name} ({model}):\n  ");
                        let _ = std::io::stdout().flush();
                        current.clear();
                    }
                    DebateEvent::Token(t) => {
                        current.push_str(&t);
                        print!("{t}");
                        let _ = std::io::stdout().flush();
                    }
                    DebateEvent::TurnEnded { .. } => println!(),
                    DebateEvent::Error(e) => eprintln!("\n[error] {e}"),
                    DebateEvent::Done => break,
                    _ => {}
                }
            }
        }
    }
    handle.abort();
    let _ = handle.await;
}

async fn cmd_models(provider: Option<String>, scan: bool) -> anyhow::Result<()> {
    let config = Config::load()?;
    let http = http_client(config.proxy.as_deref());
    let providers: Vec<Provider> = match provider {
        Some(slug) => vec![Provider::from_slug(&slug)
            .ok_or_else(|| anyhow::anyhow!("unknown provider: {slug}"))?],
        None => Provider::ALL.to_vec(),
    };

    for provider in providers {
        println!("\n{} ({})", provider.display_name(), provider.slug());
        let models = if scan {
            // resolve_api_key may read the keychain — fine, the user asked to scan.
            match config.resolve_api_key(provider) {
                Some(key) => match scan_models(&http, provider, &config.base_url(provider), &key).await
                {
                    Ok(m) => m,
                    Err(e) => {
                        println!("  scan failed ({e}); showing catalog");
                        catalog_models(provider)
                    }
                },
                None => {
                    println!("  no API key; showing catalog");
                    catalog_models(provider)
                }
            }
        } else {
            catalog_models(provider)
        };
        for m in models {
            let tag = if m.source == ModelSource::Scanned { "live" } else { "cat" };
            println!("  [{tag}] {}", m.id);
        }
    }
    Ok(())
}

fn cmd_providers() -> anyhow::Result<()> {
    let config = Config::load()?;
    println!("Providers:");
    for provider in Provider::ALL {
        let configured = config.is_configured(provider);
        let mark = if configured { "✓" } else { " " };
        println!(
            "  [{mark}] {:<10} {}",
            provider.slug(),
            if configured { "configured" } else { "no key" }
        );
    }
    Ok(())
}

fn cmd_config(action: ConfigAction) -> anyhow::Result<()> {
    match action {
        ConfigAction::Path => {
            println!("{}", Config::config_path()?.display());
            Ok(())
        }
        ConfigAction::SetKey { provider } => {
            let provider = Provider::from_slug(&provider)
                .ok_or_else(|| anyhow::anyhow!("unknown provider: {provider}"))?;
            print!("Paste {} API key: ", provider.display_name());
            std::io::stdout().flush()?;
            let mut key = String::new();
            std::io::stdin().read_line(&mut key)?;
            let key = key.trim().to_string();
            if key.is_empty() {
                anyhow::bail!("no key entered");
            }
            let mut config = Config::load()?;
            config.set_key(provider, key);
            config.save_keys()?;
            config.save()?;
            println!("Saved {} key.", provider.display_name());
            Ok(())
        }
    }
}
