//! 移动端 App 远程映射网关：局域网 WebSocket，实时镜像终端 + 会话遥控。
//!
//! 传输选型：同步 tungstenite + 每连接一线程，贴合项目"无 tokio、纯 std 线程 +
//! generation 代际取消"的既有范式（同 wechat / hooks）。
//!
//! 分工（模型 B）：
//! - 数据面（实时 PTY 字节流 in/out）直连 PtyManager，低延迟；
//! - 控制面（new/resume/close/answer）经 emit("mobile-inbound") 交前端复用编排；
//! - 静态列表（项目 / 历史会话）网关在 Rust 侧直接读；
//! - 活动会话快照由前端 mobile_publish_sessions 发布。

pub mod store;

use std::collections::HashMap;
use std::io::{ErrorKind, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use base64::Engine;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tauri::{AppHandle, Emitter, Manager};
use tungstenite::stream::MaybeTlsStream;
use tungstenite::{Message, WebSocket};

use crate::AppState;

/// 连接 id 分配器
static CONN_SEQ: AtomicU64 = AtomicU64::new(1);

/// 内嵌的移动端页面（编译时嵌入项目根 mobile-test.html），普通 HTTP GET 时返回
const MOBILE_HTML: &str = include_str!("../../../mobile-test.html");

/// 移动端网关托管态（由 lib.rs 以 .manage() 注入）
#[derive(Default)]
pub struct MobileState {
    /// 局域网网关代际：翻转开关 / 解绑时 +1，令旧监听与连接线程优雅退出
    generation: Arc<AtomicU64>,
    /// 公网中继拨出代际：与 LAN 独立，翻转中继开关 / 改地址时 +1，不影响 LAN 网关
    relay_generation: Arc<AtomicU64>,
    /// 中继是否已拨通（register 成功后置 true，断开置 false；供设置页显示状态）
    relay_online: Arc<AtomicBool>,
    /// 活动会话快照（前端 mobile_publish_sessions 发布）
    sessions: Arc<Mutex<Vec<MobileSessionMeta>>>,
    /// 已连接客户端出站信箱：connId -> Sender<Message>（供广播 sessions/prompt/status）
    conns: Arc<Mutex<HashMap<u64, Sender<Message>>>>,
}

impl MobileState {
    pub fn new() -> Self {
        Self::default()
    }
    /// 递增局域网网关代际：让当前运行的 LAN 网关（监听线程与所有连接线程）退出
    pub fn bump_generation(&self) {
        self.generation.fetch_add(1, Ordering::Relaxed);
    }
    /// 递增中继拨出代际：让当前中继链路线程退出（不影响 LAN 网关）
    pub fn bump_relay_generation(&self) {
        self.relay_generation.fetch_add(1, Ordering::Relaxed);
    }
}

/// 活动会话元数据（前端 React 态的快照，供移动端发现在跑的会话）
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MobileSessionMeta {
    pub tab_id: String,
    pub session_id: String,
    pub project_path: String,
    pub title: String,
    pub status: String,
    #[serde(default)]
    pub model: String,
}

/// 客户端 → 桌面 的控制帧
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
enum ClientMsg {
    Hello { token: String },
    List,
    ListSessions { project_path: String },
    ListModels,
    ListQuota,
    ListDir { path: String },
    ListRoots, // 目录浏览起点：盘符（Windows）/ 根目录 + 主目录
    SearchFiles { root: String, query: String }, // 项目内按文件名受限递归搜索
    AddProject { path: String }, // 把目录登记为项目（复用桌面 add_project 同一套写配置逻辑）
    // 移动端文件管理（写操作受 mobile_fs_enabled 总开关约束；删除一律进回收站）
    FsCreate { path: String, is_dir: bool },
    FsRename { path: String, new_name: String },
    FsDelete { path: String },
    FsCopy { src: String, dest_dir: String },
    FsMove { src: String, dest_dir: String },
    FsZip { paths: Vec<String>, dest: String },
    FsUnzip { path: String, dest_dir: String },
    FsReadText { path: String }, // 编辑前读全文（带 mtime 供冲突校验）
    FsWriteText {
        path: String,
        content: String,
        #[serde(default)]
        expected_mtime_ms: u64,
    },
    FsWatch { path: String }, // 订阅目录变更（非递归，去抖推 fsChanged）；断连/换目录即失效
    FsUnwatch,
    History { project_path: String, session_id: String },
    ReadFile { path: String }, // 只读预览文件内容
    Attach { tab_id: String },
    Detach { tab_id: String },
    Input { tab_id: String, data: String }, // base64 原始字节
    Image {
        tab_id: String,
        #[serde(default)]
        name: String,
        data: String, // base64 图片字节（客户端已压缩）
    },
    Resize { tab_id: String, cols: u16, rows: u16 },
    New {
        project_path: String,
        #[serde(default)]
        model: String,
        #[serde(default)]
        permission_mode: String,
        #[serde(default)]
        extra_args: String,
    },
    Resume {
        project_path: String,
        session_id: String,
        #[serde(default)]
        permission_mode: String,
    },
    Close { tab_id: String },
    Answer {
        tab_id: String,
        #[serde(default)]
        prompt_id: String,
        choice: String,
    },
    // 移动端隧道管理（复用桌面隧道模块逻辑；不含设置注册密钥）
    TunnelList,
    TunnelAdd {
        subdomain: String,
        local_port: u16,
    },
    TunnelUpdate {
        id: String,
        subdomain: String,
        local_port: u16,
    },
    TunnelRemove {
        id: String,
    },
    TunnelToggle {
        id: String,
        enabled: bool,
    },
    TunnelSetMaster {
        enabled: bool,
    },
    Ping,
}

// ---------- 网关启动与连接处理 ----------

/// 启动网关监听线程；按 mobile_enabled 由 lib.rs 决定是否调用。
pub fn start_gateway(app: AppHandle) {
    let mstate = app.state::<MobileState>();
    let my_gen = mstate.generation.load(Ordering::Relaxed);
    let generation = mstate.generation.clone();
    let port = app
        .state::<AppState>()
        .config
        .lock()
        .unwrap()
        .settings
        .mobile_port;

    let _ = std::thread::Builder::new()
        .name("mobile-gateway".into())
        .spawn(move || {
            let listener = match TcpListener::bind(("0.0.0.0", port)) {
                Ok(l) => l,
                Err(e) => {
                    eprintln!("移动端网关启动失败（端口 {port}）: {e}");
                    return;
                }
            };
            // 非阻塞 accept：配合代际检查实现优雅停机
            let _ = listener.set_nonblocking(true);
            for stream in listener.incoming() {
                if generation.load(Ordering::Relaxed) != my_gen {
                    break;
                }
                match stream {
                    Ok(s) => {
                        let app = app.clone();
                        let generation = generation.clone();
                        std::thread::spawn(move || {
                            let _ = handle_conn(app, s, generation, my_gen);
                        });
                    }
                    Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
                        std::thread::sleep(Duration::from_millis(150));
                    }
                    Err(_) => break,
                }
            }
        });
}

fn handle_conn(
    app: AppHandle,
    stream: TcpStream,
    generation: Arc<AtomicU64>,
    my_gen: u64,
) -> Result<(), Box<dyn std::error::Error>> {
    // 握手用阻塞 + 超时保护
    stream.set_nonblocking(false)?;
    stream.set_read_timeout(Some(Duration::from_secs(10)))?;
    // 窥探请求头：普通浏览器 GET 返回内嵌页面，只有 WebSocket 升级才走网关
    let mut peek_buf = [0u8; 1024];
    let n = stream.peek(&mut peek_buf).unwrap_or(0);
    if n == 0 {
        return Ok(());
    }
    let head = String::from_utf8_lossy(&peek_buf[..n]).to_ascii_lowercase();
    if !head.contains("upgrade: websocket") {
        serve_http(stream);
        return Ok(());
    }
    let mut ws = match tungstenite::accept(stream) {
        Ok(ws) => ws,
        Err(_) => return Ok(()),
    };
    // 事件循环用短读超时轮询：读客户端消息 + 推 PTY 字节
    ws.get_ref()
        .set_read_timeout(Some(Duration::from_millis(20)))?;
    run_session(&app, &mut ws, &generation, my_gen);
    Ok(())
}

/// 通用会话事件循环：对「LAN 直连（WebSocket<TcpStream>）」与「中继拨出
/// （WebSocket<MaybeTlsStream<TcpStream>>）」复用同一套协议处理。
/// 调用方须在进入前把底层 socket 的读超时设为 ~20ms（用于非阻塞轮询）。
fn run_session<S: Read + Write>(
    app: &AppHandle,
    ws: &mut WebSocket<S>,
    generation: &Arc<AtomicU64>,
    my_gen: u64,
) {
    let conn_id = CONN_SEQ.fetch_add(1, Ordering::Relaxed);
    let (out_tx, out_rx): (Sender<Message>, Receiver<Message>) = channel();
    let expected = store::load_or_init_token();
    let mut authed = false;

    let ptys = app.state::<AppState>().ptys.clone();
    // 本连接 attach 的 PTY 字节流：tabId -> (订阅 id, 接收端)
    let mut attached: HashMap<String, (u64, Receiver<Vec<u8>>)> = HashMap::new();
    // 文件管理：当前订阅的目录监听（换目录即替换，断连随循环结束 drop 即停）
    let mut fs_watch: Option<(String, notify::RecommendedWatcher, Receiver<()>)> = None;
    let mut fs_watch_dirty = false;
    let mut fs_watch_last = std::time::Instant::now();

    loop {
        if generation.load(Ordering::Relaxed) != my_gen {
            break;
        }

        // 1) 读客户端消息（20ms 超时）
        match ws.read() {
            Ok(Message::Text(txt)) => {
                if let Ok(msg) = serde_json::from_str::<ClientMsg>(&txt) {
                    match msg {
                        ClientMsg::Hello { token } => {
                            if token == expected {
                                authed = true;
                                app.state::<MobileState>()
                                    .conns
                                    .lock()
                                    .unwrap()
                                    .insert(conn_id, out_tx.clone());
                                let _ = out_tx.send(json_msg(json!({"type":"ok","what":"hello"})));
                                let _ = out_tx.send(build_sessions_msg(app));
                            } else {
                                let _ = ws.send(json_msg(json!({"type":"error","message":"鉴权失败"})));
                                break;
                            }
                        }
                        _ if !authed => {
                            let _ = ws.send(json_msg(json!({"type":"error","message":"未鉴权"})));
                            break;
                        }
                        ClientMsg::List => {
                            let _ = out_tx.send(build_sessions_msg(app));
                        }
                        ClientMsg::ListSessions { project_path } => {
                            let list = crate::sessions::list_sessions(&project_path);
                            let _ = out_tx.send(json_msg(json!({
                                "type": "sessionList",
                                "projectPath": project_path,
                                "sessions": list,
                            })));
                        }
                        ClientMsg::History {
                            project_path,
                            session_id,
                        } => {
                            // 完整历史直接读 transcript 文件，不经终端缓冲
                            let msgs = crate::sessions::read_history(&project_path, &session_id);
                            let _ = out_tx
                                .send(json_msg(json!({"type":"history","messages":msgs})));
                        }
                        ClientMsg::ReadFile { path } => {
                            // 只读预览：复用桌面 read_file_preview（文本/Markdown/图片/二进制/过大）；
                            // 大文件读取+base64 放后台线程，避免阻塞本连接的终端流
                            let tx = out_tx.clone();
                            std::thread::spawn(move || {
                                let msg = match crate::fsx::read_file_preview(&path) {
                                    Ok(p) => {
                                        let mut v = serde_json::to_value(&p).unwrap_or_else(|_| json!({}));
                                        if let Some(o) = v.as_object_mut() {
                                            o.insert("type".to_string(), json!("fileContent"));
                                            o.insert("path".to_string(), json!(path));
                                        }
                                        json_msg(v)
                                    }
                                    Err(e) => json_msg(json!({"type":"fileError","path":path,"message":e})),
                                };
                                let _ = tx.send(msg);
                            });
                        }
                        ClientMsg::TunnelList => {
                            let _ = out_tx.send(tunnel_status_msg(app));
                        }
                        ClientMsg::TunnelAdd {
                            subdomain,
                            local_port,
                        } => {
                            let _ = out_tx
                                .send(tunnel_result_msg(crate::tunnel::add(app, &subdomain, local_port)));
                        }
                        ClientMsg::TunnelUpdate {
                            id,
                            subdomain,
                            local_port,
                        } => {
                            let _ = out_tx.send(tunnel_result_msg(crate::tunnel::update(
                                app, &id, &subdomain, local_port,
                            )));
                        }
                        ClientMsg::TunnelRemove { id } => {
                            let _ = out_tx.send(tunnel_result_msg(crate::tunnel::remove(app, &id)));
                        }
                        ClientMsg::TunnelToggle { id, enabled } => {
                            let _ =
                                out_tx.send(tunnel_result_msg(crate::tunnel::toggle(app, &id, enabled)));
                        }
                        ClientMsg::TunnelSetMaster { enabled } => {
                            let _ =
                                out_tx.send(tunnel_result_msg(crate::tunnel::set_master(app, enabled)));
                        }
                        ClientMsg::ListModels => {
                            // 模型清单需联网（最长 10s），放后台线程拉取，避免阻塞本连接的终端流
                            // 三态：官方=官方实时清单；直连/路由=当前接口/路由聚合清单（与桌面端菜单同源）
                            let tx = out_tx.clone();
                            std::thread::spawn(move || {
                                let (mode, result) = match crate::api_inject::status() {
                                    Ok(s) if s.mode != "official" => {
                                        (s.mode, crate::api_inject::active_models())
                                    }
                                    Ok(s) => (s.mode, crate::quota::list_models()),
                                    Err(_) => ("official".to_string(), crate::quota::list_models()),
                                };
                                let msg = match result {
                                    Ok(models) => json!({"type":"models","mode":mode,"models":models}),
                                    Err(e) => json!({"type":"models","mode":mode,"models":[],"error":e}),
                                };
                                let _ = tx.send(json_msg(msg));
                            });
                        }
                        ClientMsg::ListQuota => {
                            // 账号额度查询需联网（最长 ~20s），放后台线程，避免阻塞本连接的终端流
                            let tx = out_tx.clone();
                            std::thread::spawn(move || {
                                let q = crate::quota::query_account_quota();
                                let _ = tx.send(json_msg(json!({ "type": "quota", "quota": q })));
                            });
                        }
                        ClientMsg::ListDir { path } => {
                            // 文件树按需逐层加载：返回该目录直接子项（目录在前）
                            match crate::fsx::list_dir(&path) {
                                Ok(entries) => {
                                    let _ = out_tx.send(json_msg(json!({
                                        "type": "dirList",
                                        "path": path,
                                        "entries": entries,
                                    })));
                                }
                                Err(e) => {
                                    // 定向回该目录节点（前端在树内联展示），不走全局 error 弹窗
                                    let _ = out_tx.send(json_msg(json!({
                                        "type": "dirError",
                                        "path": path,
                                        "message": format!("读取目录失败: {e}"),
                                    })));
                                }
                            }
                        }
                        ClientMsg::ListRoots => {
                            let _ = out_tx.send(json_msg(json!({
                                "type": "roots",
                                "entries": crate::fsx::list_roots(),
                            })));
                        }
                        ClientMsg::SearchFiles { root, query } => {
                            // 受限递归可能扫大目录，放后台线程，避免阻塞本连接的终端流
                            let tx = out_tx.clone();
                            std::thread::spawn(move || {
                                let msg = match crate::fsx::search_files(&root, &query) {
                                    Ok(entries) => json_msg(json!({
                                        "type": "searchResults",
                                        "root": root,
                                        "query": query,
                                        "entries": entries,
                                    })),
                                    Err(e) => json_msg(json!({
                                        "type": "searchResults",
                                        "root": root,
                                        "query": query,
                                        "entries": [],
                                        "error": e,
                                    })),
                                };
                                let _ = tx.send(msg);
                            });
                        }
                        ClientMsg::AddProject { path } => {
                            match crate::add_project_core(app, &path) {
                                Ok((added, norm, name)) => {
                                    let _ = out_tx.send(json_msg(json!({
                                        "type": "projectAdded",
                                        "path": norm,
                                        "name": name,
                                        "added": added,
                                    })));
                                    if added {
                                        // 项目集已变：广播最新 sessions 帧给所有移动端，
                                        // 并通知桌面前端刷新配置（侧栏项目列表）
                                        broadcast(app, build_sessions_msg(app));
                                        let _ = app.emit(
                                            "mobile-inbound",
                                            json!({"kind":"projectAdded","path":norm}),
                                        );
                                    }
                                }
                                Err(e) => {
                                    let _ = out_tx.send(
                                        json_msg(json!({"type":"error","message":e})),
                                    );
                                }
                            }
                        }
                        // ---------- 文件管理（写操作过总开关；重 IO 一律后台线程）----------
                        ClientMsg::FsCreate { path, is_dir } => {
                            let _ = out_tx.send(if fs_enabled(app) {
                                fs_result_msg("create", crate::fsx::create_entry(&path, is_dir).map(|_| path))
                            } else {
                                fs_denied_msg("create")
                            });
                        }
                        ClientMsg::FsRename { path, new_name } => {
                            let _ = out_tx.send(if fs_enabled(app) {
                                fs_result_msg("rename", crate::fsx::rename_entry(&path, &new_name))
                            } else {
                                fs_denied_msg("rename")
                            });
                        }
                        ClientMsg::FsDelete { path } => {
                            if fs_enabled(app) {
                                // 大目录进回收站可能耗时，放后台线程
                                let tx = out_tx.clone();
                                std::thread::spawn(move || {
                                    let _ = tx.send(fs_result_msg(
                                        "delete",
                                        crate::fsx::delete_entry(&path).map(|_| path),
                                    ));
                                });
                            } else {
                                let _ = out_tx.send(fs_denied_msg("delete"));
                            }
                        }
                        ClientMsg::FsCopy { src, dest_dir } => {
                            if fs_enabled(app) {
                                let tx = out_tx.clone();
                                std::thread::spawn(move || {
                                    let _ = tx.send(fs_result_msg(
                                        "copy",
                                        crate::fsx::copy_entry(&src, &dest_dir),
                                    ));
                                });
                            } else {
                                let _ = out_tx.send(fs_denied_msg("copy"));
                            }
                        }
                        ClientMsg::FsMove { src, dest_dir } => {
                            if fs_enabled(app) {
                                let tx = out_tx.clone();
                                std::thread::spawn(move || {
                                    let _ = tx.send(fs_result_msg(
                                        "move",
                                        crate::fsx::move_entry(&src, &dest_dir),
                                    ));
                                });
                            } else {
                                let _ = out_tx.send(fs_denied_msg("move"));
                            }
                        }
                        ClientMsg::FsZip { paths, dest } => {
                            if fs_enabled(app) {
                                let tx = out_tx.clone();
                                std::thread::spawn(move || {
                                    let _ = tx.send(fs_result_msg(
                                        "zip",
                                        crate::fsx::zip_entries(&paths, &dest),
                                    ));
                                });
                            } else {
                                let _ = out_tx.send(fs_denied_msg("zip"));
                            }
                        }
                        ClientMsg::FsUnzip { path, dest_dir } => {
                            if fs_enabled(app) {
                                let tx = out_tx.clone();
                                std::thread::spawn(move || {
                                    let _ = tx.send(fs_result_msg(
                                        "unzip",
                                        crate::fsx::unzip_to(&path, &dest_dir),
                                    ));
                                });
                            } else {
                                let _ = out_tx.send(fs_denied_msg("unzip"));
                            }
                        }
                        ClientMsg::FsReadText { path } => {
                            let _ = out_tx.send(if !fs_enabled(app) {
                                fs_denied_msg("readText")
                            } else {
                                match crate::fsx::read_file_text(&path) {
                                    Ok((content, mtime)) => json_msg(json!({
                                        "type":"fsText","path":path,"content":content,"mtimeMs":mtime
                                    })),
                                    Err(e) => json_msg(json!({"type":"fsError","op":"readText","message":e})),
                                }
                            });
                        }
                        ClientMsg::FsWriteText {
                            path,
                            content,
                            expected_mtime_ms,
                        } => {
                            if !fs_enabled(app) {
                                let _ = out_tx.send(fs_denied_msg("write"));
                            } else {
                                match crate::fsx::write_file_text(&path, &content, expected_mtime_ms) {
                                    Ok(m) => {
                                        let _ = out_tx.send(json_msg(json!({"type":"fsSaved","path":path,"mtimeMs":m})));
                                    }
                                    Err(e) if e == "conflict" => {
                                        // 读取后被外部改过：前端确认后以 expected_mtime_ms=0 强制覆盖重发
                                        let _ = out_tx.send(json_msg(json!({"type":"fsConflict","path":path})));
                                    }
                                    Err(e) => {
                                        let _ = out_tx.send(json_msg(json!({"type":"fsError","op":"write","message":e})));
                                    }
                                }
                            }
                        }
                        ClientMsg::FsWatch { path } => {
                            fs_watch = None; // 先丢旧的（drop 即停止监听）
                            if fs_enabled(app) {
                                use notify::Watcher as _;
                                let (tx, rx) = channel::<()>();
                                match notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
                                    if res.is_ok() {
                                        let _ = tx.send(());
                                    }
                                }) {
                                    Ok(mut w) => {
                                        match w.watch(
                                            std::path::Path::new(&path),
                                            notify::RecursiveMode::NonRecursive,
                                        ) {
                                            Ok(()) => {
                                                fs_watch = Some((path, w, rx));
                                                fs_watch_dirty = false;
                                            }
                                            Err(e) => {
                                                let _ = out_tx.send(json_msg(json!({"type":"fsError","op":"watch","message":e.to_string()})));
                                            }
                                        }
                                    }
                                    Err(e) => {
                                        let _ = out_tx.send(json_msg(json!({"type":"fsError","op":"watch","message":e.to_string()})));
                                    }
                                }
                            } else {
                                let _ = out_tx.send(fs_denied_msg("watch"));
                            }
                        }
                        ClientMsg::FsUnwatch => {
                            fs_watch = None;
                        }
                        ClientMsg::Attach { tab_id } => {
                            // 原始环形快照兜底（快、覆盖后台未显示的会话），随后请求桌面序列化完整回看历史替换
                            let snap = ptys.snapshot(&tab_id);
                            if !snap.is_empty() {
                                let _ = out_tx.send(data_msg(&tab_id, &snap));
                            }
                            let (sub_id, rx) = ptys.subscribe(&tab_id);
                            if let Some((old_id, _)) = attached.insert(tab_id.clone(), (sub_id, rx)) {
                                ptys.unsubscribe(&tab_id, old_id);
                            }
                            // 请求桌面前端序列化该会话完整缓冲，定向回传给本连接（conn_id）
                            let _ = app.emit(
                                "mobile-inbound",
                                json!({"kind":"serialize","connId":conn_id,"tabId":tab_id}),
                            );
                        }
                        ClientMsg::Detach { tab_id } => {
                            if let Some((sub_id, _)) = attached.remove(&tab_id) {
                                ptys.unsubscribe(&tab_id, sub_id);
                            }
                        }
                        ClientMsg::Input { tab_id, data } => {
                            if let Ok(bytes) =
                                base64::engine::general_purpose::STANDARD.decode(&data)
                            {
                                ptys.write_bytes(&tab_id, &bytes);
                            }
                        }
                        ClientMsg::Image { tab_id, name, data } => {
                            // 与桌面拖拽图片一致：落盘临时文件，把带引号的路径写进该会话输入行
                            // （不自动回车，等用户补一句说明再提交）。Claude Code 收到路径后自行读图。
                            if let Ok(bytes) =
                                base64::engine::general_purpose::STANDARD.decode(&data)
                            {
                                match save_mobile_image(&bytes, &name) {
                                    Ok(path) => {
                                        ptys.write_bytes(
                                            &tab_id,
                                            format!("\"{}\" ", path).as_bytes(),
                                        );
                                        let _ = out_tx.send(json_msg(
                                            json!({"type":"imageOk","tabId":tab_id,"path":path}),
                                        ));
                                    }
                                    Err(e) => {
                                        let _ = out_tx.send(json_msg(
                                            json!({"type":"error","message":format!("图片保存失败: {e}")}),
                                        ));
                                    }
                                }
                            }
                        }
                        ClientMsg::Resize { tab_id, cols, rows } => {
                            ptys.resize(&tab_id, cols, rows);
                        }
                        ClientMsg::New {
                            project_path,
                            model,
                            permission_mode,
                            extra_args,
                        } => {
                            let _ = app.emit(
                                "mobile-inbound",
                                json!({
                                    "kind": "new",
                                    "projectPath": project_path,
                                    "model": model,
                                    "permissionMode": permission_mode,
                                    "extraArgs": extra_args,
                                }),
                            );
                        }
                        ClientMsg::Resume {
                            project_path,
                            session_id,
                            permission_mode,
                        } => {
                            let _ = app.emit(
                                "mobile-inbound",
                                json!({
                                    "kind": "resume",
                                    "projectPath": project_path,
                                    "sessionId": session_id,
                                    "permissionMode": permission_mode,
                                }),
                            );
                        }
                        ClientMsg::Close { tab_id } => {
                            let _ = app
                                .emit("mobile-inbound", json!({"kind":"close","tabId":tab_id}));
                        }
                        ClientMsg::Answer {
                            tab_id,
                            prompt_id,
                            choice,
                        } => {
                            let _ = app.emit(
                                "mobile-inbound",
                                json!({
                                    "kind": "answer",
                                    "tabId": tab_id,
                                    "promptId": prompt_id,
                                    "choice": choice,
                                }),
                            );
                        }
                        ClientMsg::Ping => {
                            let _ = out_tx.send(json_msg(json!({"type":"pong"})));
                        }
                    }
                } else if let Some(ctrl) = relay_ctrl(&txt) {
                    // 中继链路的信令帧（LAN 直连不会出现）
                    match ctrl {
                        RelayCtrl::Reset => {
                            // 手机在中继侧上线/下线：重置鉴权与订阅，等待其（重新）hello
                            authed = false;
                            app.state::<MobileState>()
                                .conns
                                .lock()
                                .unwrap()
                                .remove(&conn_id);
                            for (t, (sid, _)) in std::mem::take(&mut attached) {
                                ptys.unsubscribe(&t, sid);
                            }
                            fs_watch = None; // 对端下线：目录监听一并失效（重连后前端会重新订阅）
                        }
                        RelayCtrl::Fatal => break, // 令牌不匹配等：断链，交由外层重连
                        RelayCtrl::Ignore => {}
                    }
                }
            }
            Ok(Message::Close(_)) => break,
            Ok(_) => {} // Ping/Pong/Binary/Frame：tungstenite 自动处理 ping
            Err(tungstenite::Error::Io(ref e))
                if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut =>
            {
                // 读超时，正常，继续轮询出站
            }
            Err(_) => break,
        }

        // 2) 把 attach 的 PTY 字节写给客户端
        let mut dead: Vec<String> = Vec::new();
        for (tab_id, (_sub, rx)) in attached.iter() {
            loop {
                match rx.try_recv() {
                    Ok(bytes) => {
                        if ws.send(data_msg(tab_id, &bytes)).is_err() {
                            cleanup(app, conn_id, &ptys, &attached);
                            return;
                        }
                    }
                    Err(TryRecvError::Empty) => break,
                    Err(TryRecvError::Disconnected) => {
                        dead.push(tab_id.clone());
                        break;
                    }
                }
            }
        }
        for t in dead {
            attached.remove(&t);
        }

        // 2.5) 文件管理：目录监听事件去抖 500ms 后推 fsChanged（吸收批量变更，避免刷屏）
        if let Some((watched, _w, rx)) = &fs_watch {
            while rx.try_recv().is_ok() {
                fs_watch_dirty = true;
                fs_watch_last = std::time::Instant::now();
            }
            if fs_watch_dirty && fs_watch_last.elapsed() >= Duration::from_millis(500) {
                fs_watch_dirty = false;
                if ws
                    .send(json_msg(json!({"type":"fsChanged","path":watched})))
                    .is_err()
                {
                    cleanup(app, conn_id, &ptys, &attached);
                    return;
                }
            }
        }

        // 3) 出站信箱：广播来的 sessions/prompt/status、hello ok、pong
        while let Ok(m) = out_rx.try_recv() {
            if ws.send(m).is_err() {
                cleanup(app, conn_id, &ptys, &attached);
                return;
            }
        }
        let _ = ws.flush();
    }

    cleanup(app, conn_id, &ptys, &attached);
}

fn cleanup(
    app: &AppHandle,
    conn_id: u64,
    ptys: &crate::pty::PtyManager,
    attached: &HashMap<String, (u64, Receiver<Vec<u8>>)>,
) {
    app.state::<MobileState>()
        .conns
        .lock()
        .unwrap()
        .remove(&conn_id);
    for (tab_id, (sub_id, _)) in attached.iter() {
        ptys.unsubscribe(tab_id, *sub_id);
    }
}

// ---------- 中继信令帧 ----------

/// 中继链路上桌面端可能收到的信令帧（register 之后由中继下发，非 ClientMsg）
enum RelayCtrl {
    /// 对端手机上线/下线：重置本会话鉴权与订阅
    Reset,
    /// 致命错误（如配对令牌不匹配）：应断链
    Fatal,
    /// 其它信令（registered 等）：忽略
    Ignore,
}

/// 解析中继信令帧；不是已知信令则返回 None（交回 ClientMsg 处理链）
fn relay_ctrl(txt: &str) -> Option<RelayCtrl> {
    let v: serde_json::Value = serde_json::from_str(txt).ok()?;
    match v.get("type").and_then(|t| t.as_str())? {
        "peer-online" | "peer-offline" => Some(RelayCtrl::Reset),
        "error" => Some(RelayCtrl::Fatal),
        "registered" => Some(RelayCtrl::Ignore),
        _ => None,
    }
}

// ---------- 公网中继拨出 ----------

/// 从 MaybeTlsStream 取出底层 TcpStream（用于设置读超时）
fn tcp_of(s: &MaybeTlsStream<TcpStream>) -> Option<&TcpStream> {
    match s {
        MaybeTlsStream::Plain(t) => Some(t),
        MaybeTlsStream::Rustls(t) => Some(&t.sock),
        _ => None,
    }
}

/// 启动中继拨出线程：主动连到公网中继（wss），注册为 host，跑通用会话循环；
/// 断线自动重连，受 relay_generation 代际守护（翻转开关 / 改地址即优雅退出）。
pub fn start_relay_link(app: AppHandle) {
    let mstate = app.state::<MobileState>();
    let my_gen = mstate.relay_generation.load(Ordering::Relaxed);
    let generation = mstate.relay_generation.clone();
    let online = mstate.relay_online.clone();
    let url = {
        let state = app.state::<AppState>();
        let cfg = state.config.lock().unwrap();
        cfg.settings.mobile_relay_url.trim().to_string()
    };
    if url.is_empty() {
        return;
    }
    let _ = std::thread::Builder::new()
        .name("mobile-relay".into())
        .spawn(move || {
            while generation.load(Ordering::Relaxed) == my_gen {
                if let Err(e) = relay_once(&app, &url, &generation, my_gen) {
                    eprintln!("移动端中继连接结束：{e}");
                }
                online.store(false, Ordering::Relaxed);
                // 断线重连：约 3s 退避，其间响应代际停机
                for _ in 0..30 {
                    if generation.load(Ordering::Relaxed) != my_gen {
                        break;
                    }
                    std::thread::sleep(Duration::from_millis(100));
                }
            }
        });
}

/// 一次中继会话：连接 → 注册 host → 跑会话循环，返回即代表本次链路结束。
fn relay_once(
    app: &AppHandle,
    url: &str,
    generation: &Arc<AtomicU64>,
    my_gen: u64,
) -> Result<(), Box<dyn std::error::Error>> {
    let token = store::load_or_init_token();
    let room = store::load_or_init_room();
    let (mut ws, _resp) = tungstenite::connect(url)?;
    // 握手完成后把底层 socket 设为短读超时，供事件循环轮询
    if let Some(tcp) = tcp_of(ws.get_ref()) {
        let _ = tcp.set_read_timeout(Some(Duration::from_millis(20)));
    }
    // 注册为桌面端（host）：房间号 + 配对令牌
    ws.send(Message::Text(
        json!({"type":"register","role":"host","room":room,"token":token}).to_string(),
    ))?;
    ws.flush()?;
    app.state::<MobileState>()
        .relay_online
        .store(true, Ordering::Relaxed);
    run_session(app, &mut ws, generation, my_gen);
    Ok(())
}

/// 普通 HTTP GET（浏览器直接打开 http://ip:port/）返回内嵌的移动端页面
fn serve_http(mut stream: TcpStream) {
    let mut buf = [0u8; 4096];
    let _ = stream.read(&mut buf);
    // 优先用 %APPDATA%/ccgui/mobile.html（可热替换，改页面不必重编译），否则用内嵌版
    let custom = dirs::config_dir()
        .map(|d| d.join("ccgui").join("mobile.html"))
        .and_then(|p| std::fs::read(p).ok());
    let body: &[u8] = custom.as_deref().unwrap_or(MOBILE_HTML.as_bytes());
    let header = format!(
        "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
        body.len()
    );
    let _ = stream.write_all(header.as_bytes());
    let _ = stream.write_all(body);
    let _ = stream.flush();
}

// ---------- 帧构造 ----------

fn json_msg(v: serde_json::Value) -> Message {
    Message::Text(v.to_string())
}

/// 隧道状态帧：把 TunnelStatus 展平并附 type=tunnelStatus
fn tunnel_status_msg(app: &AppHandle) -> Message {
    tunnel_result_msg(Ok(crate::tunnel::status(app)))
}
/// 隧道操作结果 → 帧：成功回状态、失败回 tunnelError
fn tunnel_result_msg(r: Result<crate::tunnel::TunnelStatus, String>) -> Message {
    match r {
        Ok(s) => {
            let mut v = serde_json::to_value(&s).unwrap_or_else(|_| json!({}));
            if let Some(o) = v.as_object_mut() {
                o.insert("type".to_string(), json!("tunnelStatus"));
            }
            Message::Text(v.to_string())
        }
        Err(e) => json_msg(json!({ "type": "tunnelError", "message": e })),
    }
}

/// 移动端文件管理总开关（设置项 mobile_fs_enabled）：关闭时写操作统一拒绝
fn fs_enabled(app: &AppHandle) -> bool {
    app.state::<AppState>()
        .config
        .lock()
        .unwrap()
        .settings
        .mobile_fs_enabled
}

/// 文件操作统一回复：成功 fsOk（path=最终落点路径）、失败 fsError
fn fs_result_msg(op: &str, r: Result<String, String>) -> Message {
    match r {
        Ok(p) => json_msg(json!({"type":"fsOk","op":op,"path":p})),
        Err(e) => json_msg(json!({"type":"fsError","op":op,"message":e})),
    }
}

fn fs_denied_msg(op: &str) -> Message {
    json_msg(json!({"type":"fsError","op":op,"message":"移动端文件管理未开启，请在电脑端「设置 → 移动端」中打开"}))
}

fn data_msg(tab_id: &str, bytes: &[u8]) -> Message {
    let b64 = base64::engine::general_purpose::STANDARD.encode(bytes);
    Message::Text(json!({"type":"data","tabId":tab_id,"data":b64}).to_string())
}

/// 移动端发来的图片：按魔数判后缀，落盘到 %TEMP%/ccgui，返回本地路径（同微信图片落盘约定）
fn save_mobile_image(bytes: &[u8], _name: &str) -> Result<String, String> {
    let ext = if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
        "png"
    } else if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
        "jpg"
    } else if bytes.starts_with(b"GIF8") {
        "gif"
    } else if bytes.starts_with(b"RIFF") {
        "webp"
    } else if bytes.starts_with(b"BM") {
        "bmp"
    } else {
        "png"
    };
    let dir = std::env::temp_dir().join("ccgui");
    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
    let path = dir.join(format!("mobile-{}.{ext}", crate::store::now_ms()));
    std::fs::write(&path, bytes).map_err(|e| e.to_string())?;
    Ok(path.to_string_lossy().into_owned())
}

fn build_sessions_msg(app: &AppHandle) -> Message {
    let active = app.state::<MobileState>().sessions.lock().unwrap().clone();
    let (projects, fs_on): (Vec<serde_json::Value>, bool) = {
        let state = app.state::<AppState>();
        let cfg = state.config.lock().unwrap();
        (
            cfg.projects
                .iter()
                .map(|p| json!({"path": p.path, "name": p.name}))
                .collect(),
            cfg.settings.mobile_fs_enabled,
        )
    };
    json_msg(json!({
        "type": "sessions",
        "projects": projects,
        "tabs": active,
        "fsEnabled": fs_on, // 文件管理总开关：移动端据此显隐「文件」入口
    }))
}

fn broadcast(app: &AppHandle, msg: Message) {
    let mstate = app.state::<MobileState>();
    let conns = mstate.conns.lock().unwrap();
    for tx in conns.values() {
        let _ = tx.send(msg.clone());
    }
}

/// 枚举本机可用于局域网访问的 IPv4 候选（排除回环 / link-local / 已知虚拟网段），
/// 私有 LAN 段靠前。用列表而非单探，避免 TUN/代理虚拟网卡（如 Clash 的 198.18.x）误导配对地址。
fn local_ips() -> Vec<String> {
    let mut ips: Vec<String> = if_addrs::get_if_addrs()
        .unwrap_or_default()
        .into_iter()
        .filter_map(|iface| match iface.ip() {
            std::net::IpAddr::V4(v4) => Some(v4),
            _ => None,
        })
        .filter(|v4| {
            let o = v4.octets();
            !v4.is_loopback()
                && !v4.is_link_local()
                && !(o[0] == 198 && (o[1] == 18 || o[1] == 19)) // RFC2544 / Clash fake-ip
                && !(o[0] == 100 && (64..=127).contains(&o[1])) // CGNAT / tailscale
        })
        .map(|v4| v4.to_string())
        .collect();
    ips.sort_by_key(|ip| if is_private_lan(ip) { 0 } else { 1 });
    ips.dedup();
    if ips.is_empty() {
        ips.push("127.0.0.1".into());
    }
    ips
}

/// 是否常见家用/办公私有 LAN 段（用于把真实 LAN IP 排到候选前面）
fn is_private_lan(ip: &str) -> bool {
    let o: Vec<u8> = ip.split('.').filter_map(|s| s.parse().ok()).collect();
    o.len() == 4
        && (o[0] == 192 && o[1] == 168 || o[0] == 10 || (o[0] == 172 && (16..=31).contains(&o[1])))
}

// ---------- Tauri 命令 ----------

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MobileStatus {
    pub enabled: bool,
    pub port: u16,
    pub token: String,
    pub local_ips: Vec<String>,
    pub conn_count: usize,
    /// 公网中继拨出是否开启
    pub relay_enabled: bool,
    /// 中继地址（wss://...）
    pub relay_url: String,
    /// 本机房间号（配对码的一部分）
    pub relay_room: String,
    /// 中继当前是否已拨通
    pub relay_online: bool,
}

#[tauri::command]
pub fn mobile_status(app: AppHandle) -> MobileStatus {
    let (enabled, port, relay_enabled, relay_url) = {
        let state = app.state::<AppState>();
        let cfg = state.config.lock().unwrap();
        (
            cfg.settings.mobile_enabled,
            cfg.settings.mobile_port,
            cfg.settings.mobile_relay_enabled,
            cfg.settings.mobile_relay_url.clone(),
        )
    };
    let mstate = app.state::<MobileState>();
    let conn_count = mstate.conns.lock().unwrap().len();
    let relay_online = mstate.relay_online.load(Ordering::Relaxed);
    MobileStatus {
        enabled,
        port,
        token: store::load_or_init_token(),
        local_ips: local_ips(),
        conn_count,
        relay_enabled,
        relay_url,
        relay_room: store::load_or_init_room(),
        relay_online,
    }
}

/// 公网中继配对链接：把 wss 地址转成手机可直接打开的 https 页面，并带上房间号 + 令牌，
/// 形如 https://relay.你的域名/?room=xxxx&token=yyyy（手机打开即自动连回本机）
#[tauri::command]
pub fn mobile_relay_pairing_url(app: AppHandle) -> String {
    let url = {
        let state = app.state::<AppState>();
        let cfg = state.config.lock().unwrap();
        cfg.settings.mobile_relay_url.trim().to_string()
    };
    if url.is_empty() {
        return String::new();
    }
    let base = url
        .trim_end_matches('/')
        .replacen("wss://", "https://", 1)
        .replacen("ws://", "http://", 1);
    format!(
        "{}/?room={}&token={}",
        base,
        store::load_or_init_room(),
        store::load_or_init_token()
    )
}

/// 配对二维码内容：ws://IP:port/?token=...
#[tauri::command]
pub fn mobile_pairing_url(app: AppHandle) -> String {
    let port = app
        .state::<AppState>()
        .config
        .lock()
        .unwrap()
        .settings
        .mobile_port;
    let ip = local_ips().into_iter().next().unwrap_or_else(|| "127.0.0.1".into());
    format!("ws://{}:{}/?token={}", ip, port, store::load_or_init_token())
}

/// 重置令牌：删除令牌文件使旧配对失效（现有已鉴权连接保留，关闭再开启开关才断开所有连接）
#[tauri::command]
pub fn mobile_unbind() {
    store::unbind();
}

/// 前端发布活动会话快照，并主动推给已连接客户端
#[tauri::command]
pub fn mobile_publish_sessions(app: AppHandle, sessions: Vec<MobileSessionMeta>) {
    *app.state::<MobileState>().sessions.lock().unwrap() = sessions;
    let msg = build_sessions_msg(&app);
    broadcast(&app, msg);
}

/// 前端把结构化选项 / 状态等推给移动端（广播原样 JSON）
#[tauri::command]
pub fn mobile_push(app: AppHandle, payload: serde_json::Value) {
    broadcast(&app, json_msg(payload));
}

/// 桌面把序列化好的完整回看缓冲定向发给发起 attach 的手机连接（snapshot 帧，纯文本含 ANSI）
#[tauri::command]
pub fn mobile_snapshot(app: AppHandle, conn_id: u64, tab_id: String, data: String) {
    let mstate = app.state::<MobileState>();
    let conns = mstate.conns.lock().unwrap();
    if let Some(tx) = conns.get(&conn_id) {
        let _ = tx.send(json_msg(json!({"type":"snapshot","tabId":tab_id,"data":data})));
    }
}
