use serde::Serialize;
use serde_json::{json, Value};
use std::fs;
use std::path::PathBuf;
use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_notification::NotificationExt;

/// 通过 hooks 上报的会话状态事件，原样转发给前端做状态标记
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HookEventPayload {
    pub event: String,
    pub session_id: String,
    pub cwd: String,
    pub message: String,
    /// Stop 事件时为最后一条 assistant 回复的摘要
    pub summary: String,
    /// Stop 事件时为会话当前实际使用的模型 ID
    pub model: String,
    /// Stop 事件时为本回合是否使用了扩展思考
    pub thinking: bool,
    /// Stop 事件时为从会话记录解析出的标题（自动名，未应用 GUI 覆盖名）；其他事件为空
    pub title: String,
}

const HOOK_PATH: &str = "/ccgui-hook";
const HOOK_EVENTS: [&str; 3] = ["UserPromptSubmit", "Stop", "Notification"];

pub(crate) fn settings_path() -> PathBuf {
    dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join(".claude")
        .join("settings.json")
}

fn hook_command(port: u16) -> String {
    // curl.exe 是 Win10 1803+ 自带的；GUI 未运行时请求失败不影响 Claude Code 继续工作
    format!("curl.exe -s -m 3 --data-binary @- http://127.0.0.1:{port}{HOOK_PATH}")
}

fn is_ours(entry: &Value) -> bool {
    entry
        .get("hooks")
        .and_then(Value::as_array)
        .map(|hooks| {
            hooks.iter().any(|h| {
                h.get("command")
                    .and_then(Value::as_str)
                    .map(|c| c.contains(HOOK_PATH))
                    .unwrap_or(false)
            })
        })
        .unwrap_or(false)
}

/// 在 ~/.claude/settings.json 中登记状态上报 hooks（保留用户已有配置，幂等）
pub fn install(port: u16) -> Result<(), String> {
    let path = settings_path();
    let mut root: Value = match fs::read_to_string(&path) {
        Ok(raw) => serde_json::from_str(&raw).map_err(|e| format!("settings.json 解析失败: {e}"))?,
        Err(_) => json!({}),
    };
    if !root.is_object() {
        return Err("settings.json 顶层不是对象".into());
    }
    let hooks = root
        .as_object_mut()
        .unwrap()
        .entry("hooks")
        .or_insert_with(|| json!({}));
    if !hooks.is_object() {
        return Err("settings.json 中 hooks 字段不是对象".into());
    }
    for event in HOOK_EVENTS {
        let arr = hooks
            .as_object_mut()
            .unwrap()
            .entry(event)
            .or_insert_with(|| json!([]));
        let Some(list) = arr.as_array_mut() else {
            continue;
        };
        list.retain(|e| !is_ours(e));
        list.push(json!({
            "hooks": [{ "type": "command", "command": hook_command(port) }]
        }));
    }
    let pretty = serde_json::to_string_pretty(&root).map_err(|e| e.to_string())?;
    crate::fsx::write_atomic(&path, pretty.as_bytes())
}

pub fn uninstall() -> Result<(), String> {
    let path = settings_path();
    let raw = match fs::read_to_string(&path) {
        Ok(r) => r,
        Err(_) => return Ok(()),
    };
    let mut root: Value =
        serde_json::from_str(&raw).map_err(|e| format!("settings.json 解析失败: {e}"))?;
    let Some(hooks) = root.get_mut("hooks").and_then(Value::as_object_mut) else {
        return Ok(());
    };
    for event in HOOK_EVENTS {
        if let Some(list) = hooks.get_mut(event).and_then(Value::as_array_mut) {
            list.retain(|e| !is_ours(e));
        }
    }
    // 清掉变成空数组的事件键，避免留垃圾
    hooks.retain(|_, v| v.as_array().map(|a| !a.is_empty()).unwrap_or(true));
    let pretty = serde_json::to_string_pretty(&root).map_err(|e| e.to_string())?;
    crate::fsx::write_atomic(&path, pretty.as_bytes())
}

pub fn installed() -> bool {
    fs::read_to_string(settings_path())
        .map(|raw| raw.contains(HOOK_PATH))
        .unwrap_or(false)
}

/// 本地 HTTP 服务器：接收 hooks 上报，转发给前端并按需发系统通知。
/// 端口被占用时返回 Err，由调用方提示用户改端口。
pub fn start_server(app: AppHandle, port: u16) -> Result<(), String> {
    let server = tiny_http::Server::http(("127.0.0.1", port))
        .map_err(|e| format!("hooks 状态服务器启动失败（端口 {port}）: {e}"))?;
    std::thread::spawn(move || {
        for mut request in server.incoming_requests() {
            if request.url() != HOOK_PATH {
                let _ = request.respond(tiny_http::Response::empty(404));
                continue;
            }
            let mut body = String::new();
            let _ = request.as_reader().read_to_string(&mut body);
            let Ok(payload) = serde_json::from_str::<Value>(&body) else {
                let _ = request.respond(tiny_http::Response::from_string("ok"));
                continue;
            };
            let event = payload
                .get("hook_event_name")
                .and_then(Value::as_str)
                .unwrap_or("")
                .to_string();
            // UserPromptSubmit 的 hook stdout 会被 Claude Code 注入为本轮上下文。开启代码索引时，
            // 借这条每轮触发的通道重申“优先走 ccgui-index 符号工具而非逐文件 grep”——比仅在 MCP
            // initialize 注入一次的 instructions 约束更强。响应体只经 curl stdout 给 Claude Code，
            // 不参与下面的前端事件转发，故不影响状态徽标与系统通知。
            const INDEX_GUIDE: &str = "[ccgui-index] 本项目已启用 ccgui-index 符号索引（MCP）。\
涉及代码定位、引用与调用关系、类型层级、导入依赖时，优先用其工具（search_symbols / \
goto_definition / get_symbol_source / find_references / call_hierarchy / type_hierarchy / \
who_imports / module_graph），不要逐文件 grep 或通读；这些工具为 deferred，\
调用前先用 ToolSearch 加载其 schema。纯文本、字面量或注释检索可直接用 Grep；\
结果为空或疑有盲区时，用 index_status 查覆盖再回退。";
            let reply: &str = if event == "UserPromptSubmit"
                && app
                    .state::<crate::AppState>()
                    .config
                    .lock()
                    .unwrap()
                    .settings
                    .index_enabled
            {
                INDEX_GUIDE
            } else {
                "ok"
            };
            let _ = request.respond(tiny_http::Response::from_string(reply));
            let session_id = payload
                .get("session_id")
                .and_then(Value::as_str)
                .unwrap_or("")
                .to_string();
            let cwd = payload
                .get("cwd")
                .and_then(Value::as_str)
                .unwrap_or("")
                .to_string();
            let message = payload
                .get("message")
                .and_then(Value::as_str)
                .unwrap_or("")
                .to_string();
            if event.is_empty() {
                continue;
            }
            // 任意带 transcript_path 的事件都读一次：提取当前模型（让栏头徽标尽量实时，
            // 不必等回合结束），Stop 时其中的文本另作完成摘要使用。
            let last = payload
                .get("transcript_path")
                .and_then(Value::as_str)
                .and_then(crate::sessions::last_assistant_info)
                .unwrap_or_default();
            let summary = last.text;
            // 仅 Stop 时解析会话标题：回合结束后会话记录已含首条用户消息/摘要，
            // 借此让前端把标签卡的"新会话"占位热更新为真实标题（与历史列表口径一致）。
            // 非 Stop 事件无需多读文件头，留空。
            let title = if event == "Stop" {
                payload
                    .get("transcript_path")
                    .and_then(Value::as_str)
                    .and_then(|p| crate::sessions::parse_title(&PathBuf::from(p)))
                    .unwrap_or_default()
            } else {
                String::new()
            };
            let _ = app.emit(
                "session-status",
                HookEventPayload {
                    event: event.clone(),
                    session_id,
                    cwd: cwd.clone(),
                    message: message.clone(),
                    summary: summary.clone(),
                    model: last.model,
                    thinking: last.thinking,
                    title,
                },
            );
            maybe_notify(&app, &event, &cwd, &message, &summary);
        }
    });
    Ok(())
}

/// 窗口失焦时，按设置发送系统通知
fn maybe_notify(app: &AppHandle, event: &str, cwd: &str, message: &str, summary: &str) {
    let focused = app
        .get_webview_window("main")
        .map(|w| w.is_focused().unwrap_or(false))
        .unwrap_or(false);
    if focused {
        return;
    }
    let state = app.state::<crate::AppState>();
    let settings = state.config.lock().unwrap().settings.clone();
    let project = std::path::Path::new(cwd)
        .file_name()
        .map(|s| s.to_string_lossy().into_owned())
        .unwrap_or_else(|| cwd.to_string());
    let (title, body) = match event {
        "Notification" if settings.notify_on_attention => (
            format!("{project} - 等待处理"),
            if message.is_empty() {
                "Claude 正在等待你的输入".to_string()
            } else {
                message.to_string()
            },
        ),
        "Stop" if settings.notify_on_done => (
            format!("{project} - 回合结束"),
            if summary.is_empty() {
                "Claude 已完成本轮任务".to_string()
            } else {
                summary.to_string()
            },
        ),
        _ => return,
    };
    let _ = app.notification().builder().title(title).body(body).show();
}
