use std::fs;
use std::io::{Read, Write};
use std::net::ToSocketAddrs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;

use crate::core::agent::{Agent, ClaudeCodeAdapter, CodexAdapter};
use crate::core::store::Store;
use crate::core::sync::{self, SyncOutcome};
use crate::core::{ClosureError, Result};

use super::args::{
    AgentCommands, AuthCommands, BackupCommands, Cli, Commands, ProviderAddEditArgs,
    ProviderCommands, SessionCommands, VaultCommands,
};

/// Runs the CLI command dispatch based on parsed arguments.
pub fn run(cli: Cli) -> Result<()> {
    let closure_dir = dirs::home_dir()
        .ok_or_else(|| {
            ClosureError::Io(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "home directory not found",
            ))
        })?
        .join(".kozbox")
        .join("closure");

    let workdata_dir = closure_dir.join("workdata");
    let sync_dir = closure_dir.join("sync");
    std::fs::create_dir_all(&workdata_dir)?;

    let store = Store::new(&workdata_dir);

    match cli.command {
        Some(Commands::Auth { command }) => match command {
            Some(AuthCommands::Logout) => {
                store.logout()?;
                println!("已锁定。需要的时候再来——我一般在采购部和工程部之间来回跑。");
                Ok(())
            }
            None => cmd_auth(&store),
        },
        Some(Commands::Init) => {
            if store.has_encrypted() {
                println!("vault 已经存在，无需重复初始化。");
                return Ok(());
            }
            let password = rpassword::prompt_password("设置 vault 密码: ")?;
            let confirm = rpassword::prompt_password("确认密码: ")?;
            if password != confirm {
                eprintln!("两次输入的密码不一致。");
                return Err(ClosureError::Crypto("password mismatch".to_string()));
            }
            store.init(&password)?;
            println!("vault 初始化完成。");
            Ok(())
        }
        Some(Commands::Sync) => cmd_sync(&workdata_dir, &sync_dir),
        Some(Commands::Backup { command }) => cmd_backup(&store, command),
        Some(Commands::Doctor) => cmd_doctor(&store, &sync_dir),
        Some(Commands::Vault { command }) => cmd_vault(&store, command),
        Some(Commands::SetUpstream { url }) => cmd_set_upstream(&sync_dir, &url),
        Some(Commands::Session { command }) => cmd_session(command),
        Some(Commands::ClaudeCode { command }) => {
            handle_agent_command("claude-code", &store, command)
        }
        Some(Commands::Codex { command }) => handle_agent_command("codex", &store, command),
        None => Err(ClosureError::Crypto("TUI not yet implemented".to_string())),
    }
}

fn cmd_auth(store: &Store) -> Result<()> {
    if !store.has_encrypted() {
        eprintln!("vault 尚未初始化。请先运行 `closure init` 创建。");
        return Err(ClosureError::Crypto("vault not initialized".to_string()));
    }
    if store.is_authenticated() {
        println!("已经解锁了，博士。");
        return Ok(());
    }
    let password = rpassword::prompt_password("Password: ")?;
    store.authenticate(&password)?;
    println!("已解锁。欢迎回来，博士。");
    Ok(())
}

fn cmd_sync(workdata_dir: &Path, sync_dir: &Path) -> Result<()> {
    let git_dir = sync_dir.join(".git");
    if !git_dir.exists() {
        return Err(ClosureError::Crypto(
            "sync 目录不是 git 仓库。请先用 `closure set-upstream <url>` 设置同步上游。"
                .to_string(),
        ));
    }

    std::fs::create_dir_all(sync_dir)?;

    let pull = Command::new("git")
        .args(["-C", sync_dir.to_str().unwrap_or("."), "pull", "--ff-only"])
        .output()
        .map_err(|e| ClosureError::SyncConflict(format!("git pull failed: {e}")))?;
    if !pull.status.success() {
        let stderr = String::from_utf8_lossy(&pull.stderr);
        return Err(ClosureError::SyncConflict(format!(
            "git pull failed: {stderr}"
        )));
    }

    let outcome = sync::sync_dirs(workdata_dir, sync_dir)?;

    let add = Command::new("git")
        .args(["-C", sync_dir.to_str().unwrap_or("."), "add", "-A"])
        .output()
        .map_err(|e| ClosureError::SyncConflict(format!("git add failed: {e}")))?;
    if !add.status.success() {
        return Err(ClosureError::SyncConflict("git add failed".to_string()));
    }

    let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
    let commit = Command::new("git")
        .args([
            "-C",
            sync_dir.to_str().unwrap_or("."),
            "commit",
            "-m",
            &format!("sync: {timestamp}"),
            "--allow-empty",
        ])
        .output()
        .map_err(|e| ClosureError::SyncConflict(format!("git commit failed: {e}")))?;
    if !commit.status.success() {
        let stderr = String::from_utf8_lossy(&commit.stderr);
        if !stderr.contains("nothing to commit") {
            return Err(ClosureError::SyncConflict(format!(
                "git commit failed: {stderr}"
            )));
        }
    }

    let push = Command::new("git")
        .args(["-C", sync_dir.to_str().unwrap_or("."), "push"])
        .output()
        .map_err(|e| ClosureError::SyncConflict(format!("git push failed: {e}")))?;
    if !push.status.success() {
        let stderr = String::from_utf8_lossy(&push.stderr);
        return Err(ClosureError::SyncConflict(format!(
            "git push failed: {stderr}"
        )));
    }

    match outcome {
        SyncOutcome::NoChange => println!("同步完成：两端一致，无需变更。"),
        SyncOutcome::UpdatedFromSource => {
            println!("同步完成：已将本地 workdata 更新到 sync 仓库。")
        }
        SyncOutcome::UpdatedFromDestination => {
            println!("同步完成：已将 sync 仓库更新回本地 workdata。")
        }
    }
    Ok(())
}

fn cmd_backup(store: &Store, command: BackupCommands) -> Result<()> {
    match command {
        BackupCommands::List => {
            let entries = store.list_backups()?;
            if entries.is_empty() {
                println!("暂无备份。");
                return Ok(());
            }
            for entry in entries {
                println!(
                    "{}  {}  {}",
                    entry.id,
                    entry.created_at,
                    entry.files.join(", ")
                );
            }
            Ok(())
        }
        BackupCommands::Inspect { id } => {
            let entry = store.inspect_backup(&id)?;
            println!("id: {}", entry.id);
            println!("created_at: {}", entry.created_at);
            println!("files: {}", entry.files.join(", "));
            Ok(())
        }
        BackupCommands::Restore { id } => {
            let entry = store.restore_backup(&id)?;
            println!("已恢复备份 {}。", entry.id);
            Ok(())
        }
    }
}

fn cmd_doctor(store: &Store, sync_dir: &Path) -> Result<()> {
    let health = store.verify()?;
    println!(
        "vault: {}",
        if health.is_healthy() {
            "ok"
        } else {
            "degraded"
        }
    );
    println!(
        "files: encrypted={} decrypted={} state={} manifest={}",
        health.encrypted_exists,
        health.decrypted_exists,
        health.state_exists,
        health.manifest_exists
    );
    for issue in &health.issues {
        println!("issue: {issue}");
    }

    let git_ok = sync_dir.join(".git").exists();
    println!("sync_repo: {}", if git_ok { "ok" } else { "missing" });

    if let Ok(backups) = store.list_backups() {
        println!("backups: {}", backups.len());
    }

    Ok(())
}

fn cmd_vault(store: &Store, command: VaultCommands) -> Result<()> {
    match command {
        VaultCommands::Verify => {
            let health = store.verify()?;
            if health.is_healthy() {
                println!("vault verify: ok");
                return Ok(());
            }
            println!("vault verify: degraded");
            for issue in health.issues {
                println!("issue: {issue}");
            }
            Err(ClosureError::Validation(
                "vault verification failed".to_string(),
            ))
        }
    }
}

fn cmd_set_upstream(sync_dir: &Path, url: &str) -> Result<()> {
    let git_dir = sync_dir.join(".git");
    if git_dir.exists() {
        let set = Command::new("git")
            .args([
                "-C",
                sync_dir.to_str().unwrap_or("."),
                "remote",
                "set-url",
                "origin",
                url,
            ])
            .output()
            .map_err(|e| ClosureError::SyncConflict(format!("git remote set-url failed: {e}")))?;
        if !set.status.success() {
            let stderr = String::from_utf8_lossy(&set.stderr);
            return Err(ClosureError::SyncConflict(format!(
                "git remote set-url failed: {stderr}"
            )));
        }
        println!("sync 上游已更新为 {}", url);
    } else {
        let is_empty = sync_dir
            .read_dir()
            .map(|mut d| d.next().is_none())
            .unwrap_or(true);
        if !is_empty {
            return Err(ClosureError::Crypto(
                "sync 目录非空且不是 git 仓库，无法设置上游。请清空该目录后重试。".to_string(),
            ));
        }
        let parent = sync_dir.parent().unwrap_or_else(|| Path::new("."));
        std::fs::create_dir_all(parent)?;
        let _ = std::fs::remove_dir(sync_dir);
        let clone = Command::new("git")
            .args(["clone", url, sync_dir.to_str().unwrap_or(".")])
            .output()
            .map_err(|e| ClosureError::SyncConflict(format!("git clone failed: {e}")))?;
        if !clone.status.success() {
            let stderr = String::from_utf8_lossy(&clone.stderr);
            return Err(ClosureError::SyncConflict(format!(
                "git clone failed: {stderr}"
            )));
        }
        println!("已克隆 sync 仓库 {} 到本地。", url);
    }
    Ok(())
}

fn handle_agent_command(agent_name: &str, store: &Store, command: AgentCommands) -> Result<()> {
    let AgentCommands::Provider { command } = command;

    match command {
        ProviderCommands::Select { name } => {
            let store_data = store.load()?;
            let provider = store_data
                .providers
                .iter()
                .find(|p| p.name() == name && p.target() == agent_name)
                .ok_or_else(|| ClosureError::ProviderNotFound(name.clone()))?;

            apply_provider(agent_name, provider)?;

            let mut state = store.load_state()?;
            state.insert(agent_name.to_string(), Some(name.clone()));
            store.save_state(&state)?;

            println!("已切换到 {} | {} 端点信号良好。", name, agent_name);
            Ok(())
        }
        ProviderCommands::List => {
            let store_data = store.load()?;
            let state = store.load_state()?;
            let active = state.get(agent_name).cloned().flatten();

            let providers: Vec<_> = store_data
                .providers
                .iter()
                .filter(|p| p.target() == agent_name)
                .collect();
            if providers.is_empty() {
                println!("{} 还没有配置接入点。", agent_name);
                println!("没有就自己搭一个呗？");
                return Ok(());
            }
            println!("{} 可用接入点:", agent_name);
            for provider in &providers {
                let marker = active
                    .as_ref()
                    .filter(|a| *a == provider.name())
                    .map(|_| " (*)")
                    .unwrap_or("");
                println!("  {}{}", provider.name(), marker);
            }
            Ok(())
        }
        ProviderCommands::Add { name, args } => cmd_provider_add(agent_name, store, &name, &args),
        ProviderCommands::Edit { name, args } => cmd_provider_edit(agent_name, store, &name, &args),
        ProviderCommands::Remove { name } => {
            store.ensure_password()?;
            let mut store_data = store.load()?;
            let idx = store_data
                .providers
                .iter()
                .position(|p| p.name() == name && p.target() == agent_name)
                .ok_or_else(|| ClosureError::ProviderNotFound(name.clone()))?;
            store_data.providers.remove(idx);
            store.save(&store_data)?;
            println!("Removed provider '{}'.", name);
            Ok(())
        }
        ProviderCommands::Show { name } => cmd_provider_show(agent_name, store, &name),
        ProviderCommands::Check { name } => cmd_provider_check(agent_name, store, &name),
        ProviderCommands::Preset { template, name, args } => {
            cmd_provider_preset(agent_name, store, &template, &name, &args)
        }
        ProviderCommands::ListPresets => {
            cmd_list_presets();
            Ok(())
        }
    }
}

fn cmd_provider_add(
    agent_name: &str,
    store: &Store,
    name: &str,
    args: &ProviderAddEditArgs,
) -> Result<()> {
    validate_agent_args(agent_name, args)?;

    let mut store_data = store.load()?;
    let exists = store_data
        .providers
        .iter()
        .any(|p| p.name() == name && p.target() == agent_name);
    if exists {
        return Err(ClosureError::Crypto(format!(
            "provider '{}' already exists for {}",
            name, agent_name
        )));
    }

    let provider = build_provider(name, agent_name, args)?;
    store_data.providers.push(provider);
    store.ensure_password()?;
    store.save(&store_data)?;

    println!("已添加 provider '{}' ({})。", name, agent_name);
    Ok(())
}

fn cmd_provider_edit(
    agent_name: &str,
    store: &Store,
    name: &str,
    args: &ProviderAddEditArgs,
) -> Result<()> {
    validate_agent_args(agent_name, args)?;

    let mut store_data = store.load()?;
    let idx = store_data
        .providers
        .iter()
        .position(|p| p.name() == name && p.target() == agent_name)
        .ok_or_else(|| ClosureError::ProviderNotFound(name.to_string()))?;

    let model = args.model.as_deref();
    let key = args.key.as_deref();
    let url = args.url.as_deref();
    let model_provider = args.model_provider.as_deref();
    let review_model = args.review_model.as_deref();
    store_data.providers[idx].update(model, key, url, model_provider, review_model, &args.env);
    store.ensure_password()?;
    store.save(&store_data)?;

    let state = store.load_state()?;
    if state.get(agent_name).cloned().flatten().as_deref() == Some(name) {
        apply_provider(agent_name, &store_data.providers[idx])?;
    }

    println!("已更新 provider '{}' ({})。", name, agent_name);
    Ok(())
}

fn validate_agent_args(agent_name: &str, args: &ProviderAddEditArgs) -> Result<()> {
    let has_codex_flags = args.key.is_some()
        || args.url.is_some()
        || args.model_provider.is_some()
        || args.review_model.is_some();
    if agent_name == "claude-code" && has_codex_flags {
        return Err(ClosureError::Validation(
            "--key / --url / --provider / --review-model 仅适用于 codex。".to_string(),
        ));
    }
    if agent_name == "codex" && !args.env.is_empty() {
        return Err(ClosureError::Validation(
            "--env 仅适用于 claude-code。".to_string(),
        ));
    }
    Ok(())
}

fn build_provider(
    name: &str,
    agent_name: &str,
    args: &ProviderAddEditArgs,
) -> Result<crate::core::provider::Provider> {
    use crate::core::provider::Provider;
    match agent_name {
        "claude-code" => {
            let env: std::collections::HashMap<String, String> = args.env.iter().cloned().collect();
            Ok(Provider::ClaudeCode {
                name: name.to_string(),
                model: args.model.clone(),
                env,
            })
        }
        "codex" => Ok(Provider::Codex {
            name: name.to_string(),
            api_key: args.key.clone(),
            base_url: args.url.clone(),
            model: args.model.clone(),
            model_provider: args.model_provider.clone(),
            review_model: args.review_model.clone(),
        }),
        _ => Err(ClosureError::AgentConfigNotFound(agent_name.to_string())),
    }
}

fn apply_provider(agent_name: &str, provider: &crate::core::provider::Provider) -> Result<()> {
    match agent_name {
        "claude-code" => {
            let config_path = dirs::home_dir()
                .ok_or_else(|| {
                    ClosureError::Io(std::io::Error::new(
                        std::io::ErrorKind::NotFound,
                        "home directory not found",
                    ))
                })?
                .join(".claude")
                .join("settings.json");
            let adapter = ClaudeCodeAdapter::new(&config_path);
            adapter.apply(provider)
        }
        "codex" => {
            let home = dirs::home_dir().ok_or_else(|| {
                ClosureError::Io(std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    "home directory not found",
                ))
            })?;
            let config_path = home.join(".codex").join("config.toml");
            let auth_path = home.join(".codex").join("auth.json");
            let adapter = CodexAdapter::new(&config_path, &auth_path);
            adapter.apply(provider)
        }
        _ => Err(ClosureError::AgentConfigNotFound(agent_name.to_string())),
    }
}

fn cmd_provider_show(agent_name: &str, store: &Store, name: &str) -> Result<()> {
    use crate::core::provider::Provider;
    let store_data = store.load()?;
    let provider = store_data
        .providers
        .iter()
        .find(|p| p.name() == name && p.target() == agent_name)
        .ok_or_else(|| ClosureError::ProviderNotFound(name.to_string()))?;

    match provider {
        Provider::ClaudeCode { name, model, env } => {
            println!("name: {name}");
            println!("target: claude-code");
            println!("model: {}", model.as_deref().unwrap_or("(not set)"));
            println!("env:");
            if env.is_empty() {
                println!("  (none)");
            }
            for (k, v) in env {
                let masked = if k.contains("TOKEN") || k.contains("KEY") {
                    mask_secret(v)
                } else {
                    v.clone()
                };
                println!("  {k}={masked}");
            }
        }
        Provider::Codex {
            name,
            api_key,
            base_url,
            model,
            model_provider,
            review_model,
        } => {
            println!("name: {name}");
            println!("target: codex");
            println!(
                "api_key: {}",
                api_key.as_deref().map(|k| mask_secret(k)).as_deref().unwrap_or("(not set)")
            );
            println!("base_url: {}", base_url.as_deref().unwrap_or("(not set)"));
            println!("model: {}", model.as_deref().unwrap_or("(not set)"));
            println!(
                "model_provider: {}",
                model_provider.as_deref().unwrap_or("(not set)")
            );
            println!(
                "review_model: {}",
                review_model.as_deref().unwrap_or("(not set)")
            );
        }
    }
    Ok(())
}

fn mask_secret(s: &str) -> String {
    if s.len() <= 8 {
        return "****".to_string();
    }
    format!("{}****{}", &s[..4], &s[s.len() - 4..])
}

fn cmd_provider_preset(
    agent_name: &str,
    store: &Store,
    template: &str,
    name: &str,
    args: &ProviderAddEditArgs,
) -> Result<()> {
    if agent_name != "claude-code" {
        return Err(ClosureError::Validation(
            "预设目前仅适用于 claude-code。".to_string(),
        ));
    }

    let provider = crate::core::preset::resolve_preset(
        template,
        name,
        args.model.as_deref(),
        args.key.as_deref(),
        args.url.as_deref(),
        &args.env,
    )
    .map_err(|e| ClosureError::Validation(e))?;

    let mut store_data = store.load()?;
    let exists = store_data
        .providers
        .iter()
        .any(|p| p.name() == name && p.target() == agent_name);
    if exists {
        return Err(ClosureError::Crypto(format!(
            "provider '{}' already exists for {}",
            name, agent_name
        )));
    }

    store_data.providers.push(provider);
    store.ensure_password()?;
    store.save(&store_data)?;
    println!("已通过预设 '{}' 添加 provider '{}' ({})。", template, name, agent_name);
    Ok(())
}

fn cmd_list_presets() {
    println!("可用的 provider 预设模板：\n");
    for p in crate::core::preset::PRESETS {
        println!("  {:<24} {}", p.name, p.description);
    }
    println!("\n用法: closure cc p preset <模板名> <provider名> --key <key> [--url <url>] [--model <model>]");
}

fn cmd_provider_check(agent_name: &str, store: &Store, name: &str) -> Result<()> {
    let store_data = store.load()?;
    let provider = store_data
        .providers
        .iter()
        .find(|p| p.name() == name && p.target() == agent_name)
        .ok_or_else(|| ClosureError::ProviderNotFound(name.to_string()))?;

    let base_url = provider_base_url(provider, agent_name);

    let endpoint = format!("{}/v1/messages", base_url.trim_end_matches('/'));

    // Parse host:port from URL
    let (host, port) = parse_host_port(&base_url)?;

    println!("checking: {agent_name} / {name}");
    println!("endpoint: {endpoint}");
    println!("host: {host}");
    println!("port: {port}");

    // 1. TCP connect test (resolve hostname then connect)
    let tcp_start = Instant::now();
    let addr = format!("{host}:{port}");
    let socket_addr = addr.to_socket_addrs().ok().and_then(|mut addrs| addrs.next());
    let tcp = match socket_addr {
        Some(sa) => std::net::TcpStream::connect_timeout(
            &sa,
            std::time::Duration::from_secs(10),
        ),
        None => Err(std::io::Error::new(
            std::io::ErrorKind::AddrNotAvailable,
            format!("failed to resolve: {host}"),
        )),
    };
    let tcp_elapsed = tcp_start.elapsed();

    match tcp {
        Ok(_) => {
            println!(
                "connect: {}.{:03}s",
                tcp_elapsed.as_secs(),
                tcp_elapsed.subsec_millis()
            );
        }
        Err(e) => {
            println!(
                "connect: FAILED ({}) after {}.{:03}s",
                e,
                tcp_elapsed.as_secs(),
                tcp_elapsed.subsec_millis()
            );
            println!("result: unreachable");
            return Ok(());
        }
    }

    // 2. HTTP POST with streaming to measure TTFT
    println!("sending streaming request...");

    let http_start = Instant::now();
    let body = serde_json::json!({
        "model": "claude-sonnet-4-20250514",
        "max_tokens": 1,
        "messages": [{"role": "user", "content": "ping"}],
        "stream": true
    });

    let resp = ureq::AgentBuilder::new()
        .timeout_connect(std::time::Duration::from_secs(10))
        .timeout_read(std::time::Duration::from_secs(15))
        .build()
        .post(&endpoint)
        .set("Content-Type", "application/json")
        .set("x-api-key", "closure-check")
        .set("anthropic-version", "2023-06-01")
        .send_json(&body);

    match resp {
        Ok(response) => {
            let status = response.status();
            println!("status: {status}");

            // Read first chunk to measure TTFT
            let mut reader = response.into_reader();
            let mut buf = [0u8; 4096];
            let ttft = http_start.elapsed();
            match reader.read(&mut buf) {
                Ok(n) if n > 0 => {
                    let preview = String::from_utf8_lossy(&buf[..n.min(200)]);
                    println!(
                        "ttft: {}.{:03}s",
                        ttft.as_secs(),
                        ttft.subsec_millis()
                    );
                    println!("first_bytes: {}", preview.lines().next().unwrap_or("(empty)"));
                    println!("result: reachable");
                }
                Ok(_) => {
                    println!(
                        "ttft: {}.{:03}s (empty body)",
                        ttft.as_secs(),
                        ttft.subsec_millis()
                    );
                    println!("result: reachable (empty response)");
                }
                Err(e) => {
                    println!(
                        "ttft: {}.{:03}s",
                        ttft.as_secs(),
                        ttft.subsec_millis()
                    );
                    println!("read error: {e}");
                    println!("result: reachable (stream error)");
                }
            }
        }
        Err(ureq::Error::Status(status, response)) => {
            let ttft = http_start.elapsed();
            println!("status: {status}");
            println!(
                "ttft: {}.{:03}s",
                ttft.as_secs(),
                ttft.subsec_millis()
            );

            // Try to read error body for diagnostics
            let mut reader = response.into_reader();
            let mut err_body = String::new();
            let _ = reader.read_to_string(&mut err_body);
            if !err_body.is_empty() {
                println!("body: {}", &err_body[..err_body.len().min(300)]);
            }
            println!("result: reachable (credentials required)");
        }
        Err(e) => {
            let elapsed = http_start.elapsed();
            println!(
                "http: FAILED after {}.{:03}s - {e}",
                elapsed.as_secs(),
                elapsed.subsec_millis()
            );
            println!("result: unreachable");
        }
    }

    Ok(())
}

fn provider_base_url(provider: &crate::core::provider::Provider, _agent_name: &str) -> String {
    use crate::core::provider::Provider;
    match provider {
        Provider::ClaudeCode { env, .. } => env
            .get("ANTHROPIC_BASE_URL")
            .cloned()
            .unwrap_or_else(|| "https://api.anthropic.com".to_string()),
        Provider::Codex { base_url, .. } => base_url
            .clone()
            .unwrap_or_else(|| "https://api.openai.com".to_string()),
    }
}

fn parse_host_port(url: &str) -> Result<(String, u16)> {
    // Simple URL parsing without the `url` crate
    let without_scheme = url
        .strip_prefix("https://")
        .or_else(|| url.strip_prefix("http://"))
        .unwrap_or(url);

    let (host, port) = if let Some((h, p)) = without_scheme.split_once(':') {
        let port: u16 = p.split('/').next().unwrap_or(p).parse().map_err(|_| {
            ClosureError::Validation(format!("invalid port in URL: {url}"))
        })?;
        (h.to_string(), port)
    } else {
        let host = without_scheme.split('/').next().unwrap_or(without_scheme);
        let port = if url.starts_with("https://") { 443 } else { 80 };
        (host.to_string(), port)
    };

    Ok((host, port))
}

fn cmd_session(command: SessionCommands) -> Result<()> {
    match command {
        SessionCommands::Export { output } => export_sessions(&output),
    }
}

fn export_sessions(output: &Path) -> Result<()> {
    let home = dirs::home_dir().ok_or_else(|| {
        ClosureError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            "home directory not found",
        ))
    })?;
    let projects_dir = home.join(".claude").join("projects");

    if !projects_dir.exists() {
        return Err(ClosureError::Validation(
            "未找到 Claude Code sessions 目录 (~/.claude/projects)。".to_string(),
        ));
    }

    // Collect all .jsonl files
    let mut session_files: Vec<(std::path::PathBuf, String)> = Vec::new();
    walk_sessions(&projects_dir, &projects_dir, &mut session_files)?;

    if session_files.is_empty() {
        return Err(ClosureError::Validation(
            "未找到可导出的 session 文件。".to_string(),
        ));
    }

    // Create output directory if needed
    if let Some(parent) = output.parent() {
        fs::create_dir_all(parent)?;
    }

    let file = fs::File::create(output)?;
    let mut zip = zip::ZipWriter::new(file);
    let options = zip::write::SimpleFileOptions::default()
        .compression_method(zip::CompressionMethod::Deflated)
        .unix_permissions(0o644);

    // Write manifest
    let manifest = serde_json::json!({
        "exported_at": chrono::Utc::now().to_rfc3339(),
        "session_count": session_files.len(),
        "files": session_files.iter().map(|(_, rel)| rel).collect::<Vec<_>>(),
        "source": "closure session export",
    });
    let manifest_bytes = serde_json::to_vec_pretty(&manifest)?;
    zip.start_file("manifest.json", options)?;
    zip.write_all(&manifest_bytes)?;

    // Include project-config.json if it exists
    let project_config = home.join(".claude").join("project-config.json");
    if project_config.exists() {
        if let Ok(bytes) = fs::read(&project_config) {
            zip.start_file("project-config.json", options)?;
            zip.write_all(&bytes)?;
        }
    }

    // Write each session file
    for (abs_path, rel_path) in &session_files {
        let bytes = fs::read(abs_path)?;
        zip.start_file(rel_path, options)?;
        zip.write_all(&bytes)?;
    }

    zip.finish()?;
    println!(
        "已导出 {} 个 session 文件到 {}",
        session_files.len(),
        output.display()
    );
    Ok(())
}

fn walk_sessions(
    base: &Path,
    dir: &Path,
    acc: &mut Vec<(std::path::PathBuf, String)>,
) -> Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            walk_sessions(base, &path, acc)?;
        } else if path.extension().map_or(false, |e| e == "jsonl") {
            let rel = path
                .strip_prefix(base)
                .unwrap_or(&path)
                .to_string_lossy()
                .to_string();
            acc.push((path, rel));
        }
    }
    Ok(())
}
