mod agents;
mod api_inject;
mod claude_path;
mod fsx;
mod hooks;
mod index;
mod mcp;
mod mobile;
mod pty;
mod quota;
mod registry;
mod router_proxy;
mod rules;
mod sessions;
pub(crate) mod store;
mod tunnel;
mod watch;
mod wechat;

use serde::Serialize;
use std::path::Path;
use std::sync::Mutex;
use tauri::menu::{Menu, MenuItem};
use tauri::tray::TrayIconBuilder;
use tauri::{AppHandle, Emitter, Manager, State, WindowEvent};

use store::{Category, CommandItem, Config, ProjectEntry, RuleItem, Settings};

pub struct AppState {
    pub config: Mutex<Config>,
    pub ptys: pty::PtyManager,
    /// 启动时由 --open 传入、等待前端就绪后消费的项目路径
    pub pending_open: Mutex<Option<OpenProjectPayload>>,
}

#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenProjectPayload {
    pub path: String,
    pub is_new: bool,
}

/// 统一路径形式：绝对化 + 反斜杠，避免同一目录因写法不同被视为两个项目
fn normalize_path(path: &str) -> String {
    let p = Path::new(path.trim().trim_matches('"'));
    let abs = std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
    let s = abs.to_string_lossy().into_owned();
    // 去掉 canonicalize 产生的 \\?\ 前缀
    s.strip_prefix(r"\\?\").map(str::to_string).unwrap_or(s)
}

fn project_name(path: &str) -> String {
    Path::new(path)
        .file_name()
        .map(|s| s.to_string_lossy().into_owned())
        .unwrap_or_else(|| path.to_string())
}

/// 处理"在 CCGUI 中打开"：新项目入列并通知前端自动开会话，已有项目仅切换
fn handle_open(app: &AppHandle, raw_path: &str) {
    let path = normalize_path(raw_path);
    if path.is_empty() || !Path::new(&path).is_dir() {
        return;
    }
    let state = app.state::<AppState>();
    let is_new = {
        let mut config = state.config.lock().unwrap();
        let exists = config.projects.iter().any(|p| p.path == path);
        if !exists {
            config.projects.push(ProjectEntry {
                name: project_name(&path),
                path: path.clone(),
            });
            let _ = store::save(&config);
        }
        !exists
    };
    // 新项目且开启索引：注册 MCP 服务器并触发全量扫描
    if is_new && state.config.lock().unwrap().settings.index_enabled {
        let _ = index::register_server(&path);
        app.state::<index::IndexState>().scan(&path);
    }
    let payload = OpenProjectPayload { path, is_new };
    // 前端可能尚未挂载（冷启动场景），同时存一份等它来取
    *state.pending_open.lock().unwrap() = Some(payload.clone());
    let _ = app.emit("open-project", payload);
}

fn parse_open_arg(argv: &[String]) -> Option<String> {
    let pos = argv.iter().position(|a| a == "--open")?;
    argv.get(pos + 1).cloned()
}

fn show_main_window(app: &AppHandle) {
    if let Some(win) = app.get_webview_window("main") {
        let _ = win.show();
        let _ = win.unminimize();
        let _ = win.set_focus();
    }
}

// ---------- 配置与项目 ----------

#[tauri::command]
fn get_config(state: State<AppState>) -> Config {
    state.config.lock().unwrap().clone()
}

#[tauri::command]
fn take_pending_open(state: State<AppState>) -> Option<OpenProjectPayload> {
    state.pending_open.lock().unwrap().take()
}

#[tauri::command]
fn update_settings(
    app: AppHandle,
    state: State<AppState>,
    index: State<index::IndexState>,
    settings: Settings,
) -> Result<Config, String> {
    let (
        was_enabled,
        now_enabled,
        was_wechat,
        now_wechat,
        was_mobile,
        now_mobile,
        was_relay,
        now_relay,
        was_relay_url,
        now_relay_url,
        roots,
        snapshot,
    ) = {
        let mut config = state.config.lock().unwrap();
        let was_enabled = config.settings.index_enabled;
        let was_wechat = config.settings.wechat_enabled;
        let was_mobile = config.settings.mobile_enabled;
        let was_relay = config.settings.mobile_relay_enabled;
        let was_relay_url = config.settings.mobile_relay_url.clone();
        // 隧道字段只由专用命令（tunnel_*）修改，设置页整份保存不得覆盖，故先存后还原
        let keep_tunnel_enabled = config.settings.tunnel_enabled;
        let keep_tunnel_secret = config.settings.tunnel_secret.clone();
        let keep_tunnel_mappings = config.settings.tunnel_mappings.clone();
        config.settings = settings;
        config.settings.tunnel_enabled = keep_tunnel_enabled;
        config.settings.tunnel_secret = keep_tunnel_secret;
        config.settings.tunnel_mappings = keep_tunnel_mappings;
        store::save(&config)?;
        let now_enabled = config.settings.index_enabled;
        let now_wechat = config.settings.wechat_enabled;
        let now_mobile = config.settings.mobile_enabled;
        let now_relay = config.settings.mobile_relay_enabled;
        let now_relay_url = config.settings.mobile_relay_url.clone();
        let roots: Vec<String> = config.projects.iter().map(|p| p.path.clone()).collect();
        (
            was_enabled,
            now_enabled,
            was_wechat,
            now_wechat,
            was_mobile,
            now_mobile,
            was_relay,
            now_relay,
            was_relay_url,
            now_relay_url,
            roots,
            config.clone(),
        )
    };
    // 索引开关翻转：开 → 为所有项目注册并扫描；关 → 注销并删除索引库
    if !was_enabled && now_enabled {
        for root in &roots {
            let _ = index::register_server(root);
            index.scan(root);
        }
    } else if was_enabled && !now_enabled {
        for root in &roots {
            let _ = index::unregister_server(root);
            index.drop_project(root);
        }
    }
    // 微信开关翻转：开 → 有凭据则启动长轮询；关 → 停止后台线程
    if !was_wechat && now_wechat {
        wechat::WechatState::start_session(&app);
    } else if was_wechat && !now_wechat {
        wechat::WechatState::stop_session(&app);
    }
    // 移动端网关开关翻转：开 → 启动监听；关 → 递增代际停机
    if !was_mobile && now_mobile {
        mobile::start_gateway(app.clone());
    } else if was_mobile && !now_mobile {
        app.state::<mobile::MobileState>().bump_generation();
    }
    // 公网中继拨出：开关或地址变化都重启（独立代际，不影响 LAN 网关）
    let relay_changed = was_relay != now_relay || (now_relay && was_relay_url != now_relay_url);
    if relay_changed {
        app.state::<mobile::MobileState>().bump_relay_generation();
        if now_relay {
            mobile::start_relay_link(app.clone());
        }
        // 隧道依赖中继：中继开关/地址变化时一并重启隧道链路（关中继即停隧道）
        let tunnel_on = { state.config.lock().unwrap().settings.tunnel_enabled };
        app.state::<tunnel::TunnelState>().bump_generation();
        if now_relay && tunnel_on {
            tunnel::start_tunnel_link(app.clone());
        }
    }
    Ok(snapshot)
}

/// 添加项目到配置（桌面 add_project 命令与移动端 AddProject 消息共用）：
/// 规范化路径、按路径去重、落盘；新增且开启索引时注册 MCP 并触发扫描。
/// 返回 (是否新增, 规范化后的路径, 项目名)。
pub fn add_project_core(app: &AppHandle, path: &str) -> Result<(bool, String, String), String> {
    let path = normalize_path(path);
    if !Path::new(&path).is_dir() {
        return Err(format!("目录不存在: {path}"));
    }
    let name = project_name(&path);
    let (added, enabled) = {
        let state = app.state::<AppState>();
        let mut config = state.config.lock().unwrap();
        let mut added = false;
        if !config.projects.iter().any(|p| p.path == path) {
            config.projects.push(ProjectEntry {
                name: name.clone(),
                path: path.clone(),
            });
            store::save(&config)?;
            added = true;
        }
        (added, config.settings.index_enabled)
    };
    if added && enabled {
        let _ = index::register_server(&path);
        app.state::<index::IndexState>().scan(&path);
    }
    Ok((added, path, name))
}

#[tauri::command]
fn add_project(app: AppHandle, path: String) -> Result<Config, String> {
    add_project_core(&app, &path)?;
    let state = app.state::<AppState>();
    let snapshot = state.config.lock().unwrap().clone();
    Ok(snapshot)
}

#[tauri::command]
fn remove_project(
    state: State<AppState>,
    index: State<index::IndexState>,
    path: String,
) -> Result<Config, String> {
    let snapshot = {
        let mut config = state.config.lock().unwrap();
        config.projects.retain(|p| p.path != path);
        store::save(&config)?;
        config.clone()
    };
    // 移除项目时无条件清理：注销 MCP 服务器并删除索引库
    let _ = index::unregister_server(&path);
    index.drop_project(&path);
    Ok(snapshot)
}

// ---------- 会话 ----------

#[tauri::command]
fn list_sessions(project_path: String) -> Vec<sessions::SessionInfo> {
    sessions::list_sessions(&project_path)
}

#[tauri::command]
fn preview_session(
    project_path: String,
    session_id: String,
) -> Result<Vec<sessions::PreviewMessage>, String> {
    sessions::preview_session(&project_path, &session_id)
}

#[tauri::command]
fn delete_session(project_path: String, session_id: String) -> Result<(), String> {
    sessions::delete_session(&project_path, &session_id)
}

#[tauri::command]
fn rename_session(
    state: State<AppState>,
    session_id: String,
    title: String,
) -> Result<Config, String> {
    let mut config = state.config.lock().unwrap();
    if title.trim().is_empty() {
        config.title_overrides.remove(&session_id);
    } else {
        config.title_overrides.insert(session_id, title.trim().to_string());
    }
    store::save(&config)?;
    Ok(config.clone())
}

#[tauri::command]
fn search_sessions(
    state: State<AppState>,
    project_path: Option<String>,
    query: String,
) -> Vec<sessions::SearchHit> {
    let projects: Vec<String> = match project_path {
        Some(p) => vec![p],
        None => state
            .config
            .lock()
            .unwrap()
            .projects
            .iter()
            .map(|p| p.path.clone())
            .collect(),
    };
    sessions::search_sessions(&projects, &query, 100)
}

// ---------- 终端 ----------

#[tauri::command]
fn pty_spawn(
    app: AppHandle,
    state: State<AppState>,
    tab_id: String,
    cwd: String,
    args: Vec<String>,
    cols: u16,
    rows: u16,
) -> Result<(), String> {
    let claude_path = state.config.lock().unwrap().settings.claude_path.clone();
    state
        .ptys
        .spawn(app, tab_id, cwd, args, cols, rows, &claude_path)
}

#[tauri::command]
fn pty_write(state: State<AppState>, tab_id: String, data: String) {
    state.ptys.write(&tab_id, &data);
}

#[tauri::command]
fn pty_resize(state: State<AppState>, tab_id: String, cols: u16, rows: u16) {
    state.ptys.resize(&tab_id, cols, rows);
}

#[tauri::command]
fn pty_kill(state: State<AppState>, tab_id: String) {
    state.ptys.kill(&tab_id);
}

// ---------- 指令库 ----------

#[tauri::command]
fn save_command(state: State<AppState>, mut item: CommandItem) -> Result<Config, String> {
    let mut config = state.config.lock().unwrap();
    item.updated_at = store::now_ms();
    if let Some(existing) = config.commands.iter_mut().find(|c| c.id == item.id) {
        *existing = item;
    } else {
        config.commands.push(item);
    }
    store::save(&config)?;
    Ok(config.clone())
}

#[tauri::command]
fn delete_command(state: State<AppState>, id: String) -> Result<Config, String> {
    let mut config = state.config.lock().unwrap();
    config.commands.retain(|c| c.id != id);
    store::save(&config)?;
    Ok(config.clone())
}

#[tauri::command]
fn save_command_category(state: State<AppState>, category: Category) -> Result<Config, String> {
    let mut config = state.config.lock().unwrap();
    if let Some(existing) = config
        .command_categories
        .iter_mut()
        .find(|c| c.id == category.id)
    {
        *existing = category;
    } else {
        config.command_categories.push(category);
    }
    config.command_categories.sort_by_key(|c| c.order);
    store::save(&config)?;
    Ok(config.clone())
}

#[tauri::command]
fn delete_command_category(state: State<AppState>, id: String) -> Result<Config, String> {
    let mut config = state.config.lock().unwrap();
    config.command_categories.retain(|c| c.id != id);
    // 该分类下的指令归入"未分类"
    for cmd in config.commands.iter_mut() {
        if cmd.category_id.as_deref() == Some(&id) {
            cmd.category_id = None;
        }
    }
    store::save(&config)?;
    Ok(config.clone())
}

// ---------- 规则库 ----------

#[tauri::command]
fn save_rule(state: State<AppState>, mut item: RuleItem) -> Result<Config, String> {
    let mut config = state.config.lock().unwrap();
    item.updated_at = store::now_ms();
    if let Some(existing) = config.rules.iter_mut().find(|r| r.id == item.id) {
        *existing = item;
    } else {
        config.rules.push(item);
    }
    store::save(&config)?;
    Ok(config.clone())
}

#[tauri::command]
fn delete_rule(state: State<AppState>, id: String) -> Result<Config, String> {
    let mut config = state.config.lock().unwrap();
    config.rules.retain(|r| r.id != id);
    store::save(&config)?;
    Ok(config.clone())
}

#[tauri::command]
fn save_rule_category(state: State<AppState>, category: Category) -> Result<Config, String> {
    let mut config = state.config.lock().unwrap();
    if let Some(existing) = config
        .rule_categories
        .iter_mut()
        .find(|c| c.id == category.id)
    {
        *existing = category;
    } else {
        config.rule_categories.push(category);
    }
    config.rule_categories.sort_by_key(|c| c.order);
    store::save(&config)?;
    Ok(config.clone())
}

#[tauri::command]
fn delete_rule_category(state: State<AppState>, id: String) -> Result<Config, String> {
    let mut config = state.config.lock().unwrap();
    config.rule_categories.retain(|c| c.id != id);
    for rule in config.rules.iter_mut() {
        if rule.category_id.as_deref() == Some(&id) {
            rule.category_id = None;
        }
    }
    store::save(&config)?;
    Ok(config.clone())
}

// ---------- 项目规则注入 ----------

#[tauri::command]
fn list_injected_rules(project_path: String) -> Vec<rules::InjectedRule> {
    rules::list_injected(&project_path)
}

#[tauri::command]
fn inject_rule(state: State<AppState>, project_path: String, rule_id: String) -> Result<String, String> {
    let (name, content) = {
        let config = state.config.lock().unwrap();
        let rule = config
            .rules
            .iter()
            .find(|r| r.id == rule_id)
            .ok_or("规则不存在")?;
        (rule.name.clone(), rule.content.clone())
    };
    rules::inject(&project_path, &name, &content)
}

#[tauri::command]
fn set_rule_enabled(project_path: String, slug: String, enabled: bool) -> Result<(), String> {
    rules::set_enabled(&project_path, &slug, enabled)
}

#[tauri::command]
fn remove_injected_rule(project_path: String, slug: String) -> Result<(), String> {
    rules::remove(&project_path, &slug)
}

#[tauri::command]
fn read_injected_rule(project_path: String, slug: String) -> String {
    rules::read_injected(&project_path, &slug)
}

#[tauri::command]
fn read_claudemd(project_path: String) -> String {
    rules::read_claudemd(&project_path)
}

#[tauri::command]
fn write_claudemd(project_path: String, content: String) -> Result<(), String> {
    rules::write_claudemd(&project_path, &content)
}

#[tauri::command]
fn rule_slug(name: String) -> String {
    rules::slugify(&name)
}

// ---------- 系统集成 ----------

#[tauri::command]
fn install_context_menu() -> Result<(), String> {
    registry::install_context_menu()
}

#[tauri::command]
fn uninstall_context_menu() -> Result<(), String> {
    registry::uninstall_context_menu()
}

#[tauri::command]
fn context_menu_status() -> bool {
    registry::context_menu_installed()
}

#[tauri::command]
fn install_hooks(state: State<AppState>) -> Result<(), String> {
    let port = state.config.lock().unwrap().settings.hook_port;
    hooks::install(port)
}

#[tauri::command]
fn uninstall_hooks() -> Result<(), String> {
    hooks::uninstall()
}

#[tauri::command]
fn hooks_status() -> bool {
    hooks::installed()
}

#[tauri::command]
fn claude_path_info(state: State<AppState>) -> claude_path::ClaudePathInfo {
    let custom = state.config.lock().unwrap().settings.claude_path.clone();
    claude_path::describe(&custom)
}

/// 用当前配置跑一次 `claude --version`，返回版本号或失败原因
#[tauri::command]
fn claude_path_test(state: State<AppState>) -> Result<String, String> {
    let custom = state.config.lock().unwrap().settings.claude_path.clone();
    claude_path::test(&custom)
}

/// 当前 CCGUI 拉起的运行中会话数（更新前闸门用）
#[tauri::command]
fn active_session_count(state: State<AppState>) -> usize {
    state.ptys.active_count()
}

/// 手动更新 Claude Code：要求当前 CCGUI 没有会话在运行，否则 claude.exe 被锁无法替换。
/// 注意：GUI 外的会话（VS Code 等）占用时仍可能失败，错误原样透传给前端展示。
// 标注 (async)：运行 claude update 子进程可能耗时数十秒，移出主线程避免 UI 冻结
#[tauri::command(async)]
fn claude_update(state: State<AppState>) -> Result<String, String> {
    let count = state.ptys.active_count();
    if count > 0 {
        return Err(format!(
            "仍有 {count} 个会话在运行，请先关闭所有终端标签再更新（claude.exe 被占用时无法替换）"
        ));
    }
    let custom = state.config.lock().unwrap().settings.claude_path.clone();
    claude_path::update(&custom)
}

// ---------- 文件树与剪贴板 ----------

// 标注 (async)：目录列举属磁盘 IO，移出主线程
#[tauri::command(async)]
fn fs_list_dir(path: String) -> Result<Vec<fsx::FsEntry>, String> {
    fsx::list_dir(&path)
}

// 标注 (async)：写剪贴板带重试退避(最坏数百 ms),移出主线程避免 UI 冻结
#[tauri::command(async)]
fn clipboard_set_text(text: String) -> Result<(), String> {
    fsx::set_clipboard_text(&text)
}

#[tauri::command]
fn clipboard_get_text() -> Option<String> {
    fsx::get_clipboard_text()
}

#[tauri::command]
fn clipboard_image_to_temp() -> Result<Option<String>, String> {
    fsx::clipboard_image_to_temp()
}

#[tauri::command]
fn relative_path(base: String, path: String) -> String {
    fsx::relative_to(&base, &path)
}

#[tauri::command]
fn reveal_in_explorer(path: String) -> Result<(), String> {
    fsx::reveal_in_explorer(&path)
}

// 标注 (async)：扫描全部 JSONL 聚合 token 属 IO 密集，移出主线程避免 UI 冻结
#[tauri::command(async)]
fn usage_stats(
    day_start_ms: u64,
    week_start_ms: u64,
    month_start_ms: u64,
) -> sessions::UsageStats {
    sessions::usage_stats(day_start_ms, week_start_ms, month_start_ms)
}

// 标注 (async)：内部 ureq 同步阻塞请求两个接口（最长约 20s），必须移出主线程
#[tauri::command(async)]
fn account_quota() -> quota::AccountQuota {
    quota::query_account_quota()
}

// 标注 (async)：网络请求 /v1/models，移出主线程
#[tauri::command(async)]
fn list_models() -> Result<Vec<quota::ModelInfo>, String> {
    quota::list_models()
}

// 标注 (async)：读取 transcript 解析模型/thinking 属文件 IO，移出主线程
#[tauri::command(async)]
fn session_meta(project_path: String, session_id: String) -> sessions::SessionMeta {
    sessions::session_meta(&project_path, &session_id)
}

// 标注 (async)：读 transcript 尾部取最近一条回复文本，属文件 IO，移出主线程
#[tauri::command(async)]
fn last_assistant(project_path: String, session_id: String, max_chars: usize) -> String {
    sessions::last_assistant_text(&project_path, &session_id, max_chars)
}

// 标注 (async)：读 transcript 尾部取最近一个 tool_use，属文件 IO，移出主线程
#[tauri::command(async)]
fn last_tool_use(project_path: String, session_id: String) -> Option<sessions::LastToolUse> {
    sessions::last_tool_use(&project_path, &session_id)
}

// 标注 (async)：逐日扫描 30 天 JSONL，IO 密集，移出主线程
#[tauri::command(async)]
fn usage_trend(today_start_ms: u64, days: u32) -> Vec<sessions::UsageBucket> {
    sessions::usage_trend(today_start_ms, days)
}

// 标注 (async)：读会话全文写 Markdown，IO 量可能较大，移出主线程
#[tauri::command(async)]
fn export_session(
    project_path: String,
    session_id: String,
    title: String,
    dest: String,
) -> Result<(), String> {
    sessions::export_session_markdown(&project_path, &session_id, &title, &dest)
}

// ---------- 文件操作与监听 ----------

// 标注 (async)：读最大 2MB 文件 / 大图 base64 编码，移出主线程
#[tauri::command(async)]
fn fs_read_preview(path: String) -> Result<fsx::FilePreview, String> {
    fsx::read_file_preview(&path)
}

// 标注 (async)：写文件落盘属磁盘 IO，移出主线程
#[tauri::command(async)]
fn fs_write_text(path: String, content: String, expected_mtime_ms: u64) -> Result<u64, String> {
    fsx::write_file_text(&path, &content, expected_mtime_ms)
}

#[tauri::command]
fn fs_create(path: String, is_dir: bool) -> Result<(), String> {
    fsx::create_entry(&path, is_dir)
}

#[tauri::command]
fn fs_rename(path: String, new_name: String) -> Result<String, String> {
    fsx::rename_entry(&path, &new_name)
}

#[tauri::command]
fn fs_delete(path: String) -> Result<(), String> {
    fsx::delete_entry(&path)
}

#[tauri::command]
fn fs_watch(
    app: AppHandle,
    state: State<watch::WatchState>,
    project_path: String,
) -> Result<(), String> {
    watch::watch_project(app, &state, project_path)
}

// ---------- MCP 服务器 ----------

#[tauri::command]
fn mcp_list(project_path: Option<String>) -> mcp::McpServers {
    mcp::list(project_path.as_deref())
}

#[tauri::command]
fn mcp_save(
    scope: String,
    project_path: Option<String>,
    name: String,
    server_json: String,
) -> Result<(), String> {
    mcp::save(&scope, project_path.as_deref(), &name, &server_json)
}

#[tauri::command]
fn mcp_delete(scope: String, project_path: Option<String>, name: String) -> Result<(), String> {
    mcp::delete(&scope, project_path.as_deref(), &name)
}

// ---------- 代码索引 ----------

#[tauri::command]
fn index_status(index: State<index::IndexState>, project_path: String) -> index::ProjectStatus {
    index.status_of(&project_path)
}

// 标注 (async)：register_server 写 .mcp.json 属磁盘 IO，移出主线程
#[tauri::command(async)]
fn index_rebuild(
    state: State<AppState>,
    index: State<index::IndexState>,
    project_path: String,
) -> Result<(), String> {
    if !state.config.lock().unwrap().settings.index_enabled {
        return Err("代码索引未开启".into());
    }
    let _ = index::register_server(&project_path);
    index.rebuild(&project_path);
    Ok(())
}

/// --mcp-index 模式入口：作为 Claude Code 的 stdio MCP 服务器运行（不启动 GUI）。
/// 项目根从 --project 参数取，缺省回退到当前工作目录。
pub fn run_mcp_index(args: &[String]) {
    let root = args
        .iter()
        .position(|a| a == "--project")
        .and_then(|i| args.get(i + 1))
        .cloned()
        .unwrap_or_else(|| {
            std::env::current_dir()
                .map(|p| p.to_string_lossy().into_owned())
                .unwrap_or_default()
        });
    index::server::run_stdio(root);
}

/// --index-scan 模式入口：对 --project 指定的项目做一次同步全量索引并打印结果。
fn arg_value<'a>(args: &'a [String], flag: &str) -> Option<&'a String> {
    args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1))
}

pub fn run_index_scan(args: &[String]) {
    // 用非 panic 的写法：release 是 windows 子系统，从无重定向的控制台运行时 stdout
    // 句柄可能无效，println! 会 panic。重定向/管道（CI 用法）下句柄有效，输出正常。
    use std::io::Write;
    let Some(root) = arg_value(args, "--project") else {
        let _ = writeln!(std::io::stderr(), "用法: ccgui --index-scan --project <项目路径>");
        std::process::exit(2);
    };
    match index::scan_once(root) {
        Ok((files, symbols)) => {
            let _ = writeln!(
                std::io::stdout(),
                "索引完成: {files} 文件 / {symbols} 符号定义 -> {root}"
            );
        }
        Err(e) => {
            let _ = writeln!(std::io::stderr(), "索引失败: {e}");
            std::process::exit(1);
        }
    }
}

pub fn run() {
    let mut config = store::load();
    if store::seed_builtin_commands(&mut config) {
        let _ = store::save(&config);
    }
    let hook_port = config.settings.hook_port;

    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_notification::init())
        .plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| {
            // 第二个实例启动：把参数转发给本实例并把窗口提到前台
            show_main_window(app);
            if let Some(path) = parse_open_arg(&argv) {
                handle_open(app, &path);
            }
        }))
        .manage(AppState {
            config: Mutex::new(config),
            ptys: pty::PtyManager::default(),
            pending_open: Mutex::new(None),
        })
        .manage(watch::WatchState::default())
        .manage(index::IndexState::start())
        .manage(wechat::WechatState::new())
        .manage(mobile::MobileState::new())
        .manage(tunnel::TunnelState::new())
        .setup(move |app| {
            // 托盘：显示主窗口 / 退出
            let show_item = MenuItem::with_id(app, "show", "显示主窗口", true, None::<&str>)?;
            let quit_item = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
            let menu = Menu::with_items(app, &[&show_item, &quit_item])?;
            let mut tray = TrayIconBuilder::new()
                .menu(&menu)
                .show_menu_on_left_click(false)
                .tooltip("CCGUI")
                .on_menu_event(|app, event| match event.id().as_ref() {
                    "show" => show_main_window(app),
                    "quit" => {
                        app.state::<AppState>().ptys.kill_all();
                        app.exit(0);
                    }
                    _ => {}
                })
                .on_tray_icon_event(|tray, event| {
                    if let tauri::tray::TrayIconEvent::Click {
                        button: tauri::tray::MouseButton::Left,
                        button_state: tauri::tray::MouseButtonState::Up,
                        ..
                    } = event
                    {
                        show_main_window(tray.app_handle());
                    }
                });
            if let Some(icon) = app.default_window_icon() {
                tray = tray.icon(icon.clone());
            }
            tray.build(app)?;

            // hooks 状态服务器：失败（端口占用）不阻塞启动，仅丢失状态感知
            if let Err(e) = hooks::start_server(app.handle().clone(), hook_port) {
                eprintln!("{e}");
            }

            // 多模型路由代理：settings 处于路由注入态时自动拉起（否则已注入的 claude 会话全部断流）
            api_inject::ensure_router_on_boot();

            // 代码索引：清理孤儿库；开启时为所有项目注册 MCP 并触发全量扫描（后台线程执行）
            {
                let state = app.state::<AppState>();
                let cfg = state.config.lock().unwrap();
                let roots: Vec<String> = cfg.projects.iter().map(|p| p.path.clone()).collect();
                let enabled = cfg.settings.index_enabled;
                drop(cfg);
                index::gc_orphans(&roots);
                if enabled {
                    let index = app.state::<index::IndexState>();
                    for root in &roots {
                        let _ = index::register_server(root);
                        index.scan(root);
                    }
                }
            }

            // 微信对接：开启且已有凭据时，启动后台长轮询
            {
                let state = app.state::<AppState>();
                let wechat_on = state.config.lock().unwrap().settings.wechat_enabled;
                if wechat_on {
                    wechat::WechatState::start_session(app.handle());
                }
            }

            // 移动端网关：开启时启动局域网 WebSocket 网关（失败不阻塞启动）
            {
                let state = app.state::<AppState>();
                let (mobile_on, relay_on, tunnel_on) = {
                    let cfg = state.config.lock().unwrap();
                    (
                        cfg.settings.mobile_enabled,
                        cfg.settings.mobile_relay_enabled,
                        cfg.settings.tunnel_enabled,
                    )
                };
                if mobile_on {
                    mobile::start_gateway(app.handle().clone());
                }
                // 公网中继拨出：开启时主动连到 VPS 中继（失败自动重连，不阻塞启动）
                if relay_on {
                    mobile::start_relay_link(app.handle().clone());
                }
                // 公网隧道拨出：需同时开启中继与隧道（独立代际，不影响镜像链路）
                if relay_on && tunnel_on {
                    tunnel::start_tunnel_link(app.handle().clone());
                }
            }

            // 冷启动时的 --open 参数
            let argv: Vec<String> = std::env::args().collect();
            if let Some(path) = parse_open_arg(&argv) {
                handle_open(app.handle(), &path);
            }
            Ok(())
        })
        .on_window_event(|window, event| {
            if let WindowEvent::CloseRequested { api, .. } = event {
                let state = window.app_handle().state::<AppState>();
                let close_to_tray = state.config.lock().unwrap().settings.close_to_tray;
                if close_to_tray {
                    api.prevent_close();
                    let _ = window.hide();
                } else {
                    state.ptys.kill_all();
                }
            }
            // 窗口重新聚焦：兜底对账，修复监听器在 git 批量操作下丢事件造成的索引不一致
            if let WindowEvent::Focused(true) = event {
                index::reconcile_on_focus(window.app_handle());
            }
        })
        .invoke_handler(tauri::generate_handler![
            get_config,
            take_pending_open,
            update_settings,
            add_project,
            remove_project,
            list_sessions,
            preview_session,
            delete_session,
            rename_session,
            search_sessions,
            pty_spawn,
            pty_write,
            pty_resize,
            pty_kill,
            save_command,
            delete_command,
            save_command_category,
            delete_command_category,
            save_rule,
            delete_rule,
            save_rule_category,
            delete_rule_category,
            list_injected_rules,
            inject_rule,
            set_rule_enabled,
            remove_injected_rule,
            read_injected_rule,
            read_claudemd,
            write_claudemd,
            rule_slug,
            install_context_menu,
            uninstall_context_menu,
            context_menu_status,
            install_hooks,
            uninstall_hooks,
            hooks_status,
            claude_path_info,
            claude_path_test,
            active_session_count,
            claude_update,
            fs_list_dir,
            clipboard_set_text,
            clipboard_get_text,
            clipboard_image_to_temp,
            relative_path,
            reveal_in_explorer,
            usage_stats,
            account_quota,
            list_models,
            session_meta,
            last_assistant,
            last_tool_use,
            usage_trend,
            export_session,
            fs_read_preview,
            fs_write_text,
            fs_create,
            fs_rename,
            fs_delete,
            fs_watch,
            mcp_list,
            mcp_save,
            mcp_delete,
            index_status,
            index_rebuild,
            wechat::wechat_status,
            wechat::wechat_start_login,
            wechat::wechat_unbind,
            wechat::wechat_send_notice,
            mobile::mobile_status,
            mobile::mobile_pairing_url,
            mobile::mobile_relay_pairing_url,
            mobile::mobile_unbind,
            mobile::mobile_publish_sessions,
            mobile::mobile_push,
            mobile::mobile_snapshot,
            tunnel::tunnel_status,
            tunnel::tunnel_set_master,
            tunnel::tunnel_set_secret,
            tunnel::tunnel_add,
            tunnel::tunnel_update,
            tunnel::tunnel_remove,
            tunnel::tunnel_toggle,
            api_inject::api_inject_status,
            api_inject::api_inject_list,
            api_inject::api_inject_save,
            api_inject::api_inject_delete,
            api_inject::api_inject_fetch_models,
            api_inject::api_inject_apply,
            api_inject::api_inject_reset,
            api_inject::api_inject_active_models,
            api_inject::api_inject_router_get,
            api_inject::api_inject_router_save,
            api_inject::router_status,
            api_inject::router_logs,
            api_inject::restart_app,
            agents::agents_list,
            agents::agents_set_model,
            agents::agents_create,
            agents::agents_builtin_list,
            agents::agents_set_builtin_model,
            agents::agents_delete
        ])
        .run(tauri::generate_context!())
        .expect("CCGUI 启动失败");
}
