use crate::context::RequestContext;
use crate::error::WebError;
use crate::hermes_tools::ToolCallInput;
use axum::http::StatusCode;
use serde_json::{json, Value};
use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};

pub async fn mindmap_fetch(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    let target = ResourceToolTarget::from_input(context, input, "mindmap", "mindmapId")?;
    ensure_resource_scope_allowed(context, input, &target)?;
    let path = target.resolve_read_path(context, input)?;
    let content = fs::read_to_string(&path).map_err(|error| {
        WebError::bad_request_code(
            "mnote_resource_read_failed",
            format!("无法读取 mindmap resource: {error}"),
        )
        .with_context(context)
    })?;
    let data =
        serde_json::from_str::<Value>(&content).unwrap_or_else(|_| json!({ "raw": content }));
    let scope = input
        .arg_string("scope")
        .unwrap_or_else(|| "tree".into())
        .to_ascii_lowercase();
    let root = mindmap_root_value(&data).clone();
    let nodes = collect_mindmap_nodes(&root);
    let envelope = if scope == "full_envelope" {
        data
    } else {
        Value::Null
    };
    Ok(json!({
        "objectIdentity": target.object_identity,
        "resourceKind": "mindmap",
        "documentId": target.document_id,
        "mindmapId": target.resource_id,
        "resourcePath": target.resource_path,
        "scope": scope,
        "root": root,
        "envelope": envelope,
        "nodes": nodes,
        "edges": [],
        "markdownSummary": mindmap_markdown_summary(&nodes),
        "revision": file_revision(&path),
        "source": "local_folder"
    }))
}

pub async fn mindmap_apply_ops(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    ensure_resource_write_contract(context, input)?;
    let target = ResourceToolTarget::from_input(context, input, "mindmap", "mindmapId")?;
    ensure_resource_scope_allowed(context, input, &target)?;
    let path = target.resolve_write_path(context, input)?;
    let ops = input.arg_value("ops").ok_or_else(|| {
        WebError::bad_request_code("mnote_resource_ops_required", "mindmap 写工具缺少 ops")
            .with_context(context)
    })?;
    if input.dry_run.unwrap_or(false) {
        return Ok(json!({
            "dryRun": true,
            "commandName": "mnote.mindmap.apply_ops",
            "objectIdentity": target.object_identity,
            "resourceKind": "mindmap",
            "documentId": target.document_id,
            "mindmapId": target.resource_id,
            "resourcePath": target.resource_path,
            "diff": [{"op": "mindmap.apply_ops", "ops": ops}]
        }));
    }
    ensure_mindmap_revision_precondition(context, input, &path)?;
    let content = fs::read_to_string(&path).map_err(|error| {
        WebError::bad_request_code(
            "mnote_resource_read_failed",
            format!("无法读取 mindmap resource: {error}"),
        )
        .with_context(context)
    })?;
    let mut envelope = serde_json::from_str::<Value>(&content).map_err(|error| {
        WebError::bad_request_code(
            "mnote_resource_json_invalid",
            format!("mindmap resource 不是有效 JSON: {error}"),
        )
        .with_context(context)
    })?;
    apply_mindmap_ops(context, &mut envelope, &ops)?;
    let serialized = serde_json::to_string_pretty(&envelope).map_err(|error| {
        WebError::bad_request_code(
            "mnote_resource_serialize_failed",
            format!("无法序列化 mindmap JSON: {error}"),
        )
        .with_context(context)
    })?;
    fs::write(&path, serialized).map_err(|error| {
        WebError::bad_request_code(
            "mnote_resource_write_failed",
            format!("无法写入 mindmap resource: {error}"),
        )
        .with_context(context)
    })?;
    let root = mindmap_root_value(&envelope).clone();
    let nodes = collect_mindmap_nodes(&root);
    Ok(json!({
        "dryRun": false,
        "commandName": "mnote.mindmap.apply_ops",
        "objectIdentity": target.object_identity,
        "resourceKind": "mindmap",
        "documentId": target.document_id,
        "mindmapId": target.resource_id,
        "resourcePath": target.resource_path,
        "root": root,
        "nodes": nodes,
        "edges": [],
        "markdownSummary": mindmap_markdown_summary(&nodes),
        "revision": file_revision(&path),
        "changedFiles": [target.relative_path()],
        "source": "local_folder"
    }))
}

pub async fn mindmap_create_from_outline(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    ensure_resource_write_contract(context, input)?;
    let target = ResourceToolTarget::from_input(context, input, "mindmap", "mindmapId")?;
    ensure_resource_scope_allowed(context, input, &target)?;
    let path = target.resolve_create_path(context, input)?;
    let title = input
        .arg_string("title")
        .unwrap_or_else(|| "KMIND".to_string());
    let outline = input.arg_value("outline").ok_or_else(|| {
        WebError::bad_request_code("mnote_mindmap_outline_required", "创建思维导图缺少 outline")
            .with_context(context)
    })?;
    let source_refs = input.arg_value("sourceRefs").unwrap_or_else(|| json!([]));
    let envelope = mindmap_envelope_from_outline(&title, &outline, source_refs, context)?;
    let root = mindmap_root_value(&envelope).clone();
    let nodes = collect_mindmap_nodes(&root);
    let resource_relative_path = target.relative_path();
    let mut changed_files = if input.dry_run.unwrap_or(false) {
        Vec::new()
    } else {
        vec![resource_relative_path.clone()]
    };
    let mut embed_result = Value::Null;

    if !input.dry_run.unwrap_or(false) {
        let parent = path.parent().ok_or_else(|| {
            WebError::bad_request_code("mnote_resource_bad_path", "无法解析 mindmap 父目录")
                .with_context(context)
        })?;
        fs::create_dir_all(parent).map_err(|error| {
            WebError::bad_request_code(
                "mnote_resource_create_dir_failed",
                format!("无法创建 mindmap 目录: {error}"),
            )
            .with_context(context)
        })?;
        let content = serde_json::to_string_pretty(&envelope).map_err(|error| {
            WebError::bad_request_code(
                "mnote_resource_serialize_failed",
                format!("无法序列化 mindmap JSON: {error}"),
            )
            .with_context(context)
        })?;
        fs::write(&path, content).map_err(|error| {
            WebError::bad_request_code(
                "mnote_resource_write_failed",
                format!("无法写入 mindmap resource: {error}"),
            )
            .with_context(context)
        })?;
        if input
            .arg_value("embedIntoPage")
            .and_then(|value| value.as_bool())
            .unwrap_or(true)
        {
            embed_result = embed_mindmap_into_local_markdown_page(
                context,
                input,
                &target.document_id,
                &resource_relative_path,
                &title,
            )?;
            if let Some(changed_file) = embed_result
                .get("changedFile")
                .and_then(Value::as_str)
                .map(str::trim)
                .filter(|value| !value.is_empty())
            {
                changed_files.push(changed_file.to_string());
            }
        }
    }

    Ok(json!({
        "dryRun": input.dry_run.unwrap_or(false),
        "commandName": "mnote.mindmap.create_from_outline",
        "objectIdentity": target.object_identity,
        "resourceKind": "mindmap",
        "documentId": target.document_id,
        "mindmapId": target.resource_id,
        "resourcePath": target.resource_path,
        "envelope": envelope,
        "root": root,
        "nodes": nodes,
        "edges": [],
        "markdownSummary": mindmap_markdown_summary(&nodes),
        "revision": if input.dry_run.unwrap_or(false) { Value::Null } else { file_revision(&path) },
        "changedFiles": changed_files,
        "embedResult": embed_result,
        "source": "local_folder"
    }))
}

pub async fn office_fetch_summary(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    let target = ResourceToolTarget::from_input(context, input, "onlyoffice", "assetId")?;
    ensure_resource_scope_allowed(context, input, &target)?;
    let path = target.resolve_read_path(context, input)?;
    let metadata = fs::metadata(&path).map_err(|error| {
        WebError::bad_request_code(
            "mnote_resource_metadata_failed",
            format!("无法读取 Office resource 元数据: {error}"),
        )
        .with_context(context)
    })?;
    let file_name = path
        .file_name()
        .and_then(|value| value.to_str())
        .unwrap_or(target.resource_id.as_str());
    Ok(json!({
        "objectIdentity": target.object_identity,
        "resourceKind": "only_office",
        "documentId": target.document_id,
        "assetId": target.resource_id,
        "resourcePath": target.resource_path,
        "fileName": file_name,
        "mimeType": office_mime_type(&path),
        "textSummary": office_text_preview(&path),
        "outline": [],
        "revision": file_revision(&path),
        "size": metadata.len(),
        "source": "local_folder"
    }))
}

pub async fn office_propose_changes(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<Value, WebError> {
    let target = ResourceToolTarget::from_input(context, input, "onlyoffice", "assetId")?;
    ensure_resource_scope_allowed(context, input, &target)?;
    let path = target.resolve_write_path(context, input)?;
    let instructions = input.arg_string("instructions").ok_or_else(|| {
        WebError::bad_request_code(
            "mnote_resource_instructions_required",
            "office 建议工具缺少 instructions",
        )
        .with_context(context)
    })?;
    Ok(json!({
        "dryRun": input.dry_run.unwrap_or(false),
        "commandName": "mnote.office.propose_changes",
        "objectIdentity": target.object_identity,
        "resourceKind": "only_office",
        "documentId": target.document_id,
        "assetId": target.resource_id,
        "resourcePath": target.resource_path,
        "basisRevision": input.arg_value("basisRevision"),
        "revision": file_revision(&path),
        "changeSummary": instructions,
        "suggestedEdits": [{
            "kind": "instruction",
            "text": instructions
        }],
        "requiresOnlyOffice": true,
        "requiresOfficeCli": true,
        "writesBinary": false
    }))
}

struct ResourceToolTarget {
    document_id: String,
    resource_id: String,
    object_identity: String,
    resource_path: Option<String>,
}

impl ResourceToolTarget {
    fn from_input(
        context: &RequestContext,
        input: &ToolCallInput,
        resource_kind: &str,
        resource_id_arg: &str,
    ) -> Result<Self, WebError> {
        let document_id = input.effective_document_id().ok_or_else(|| {
            WebError::bad_request_code(
                "mnote_resource_document_required",
                "资源工具缺少 documentId",
            )
            .with_context(context)
        })?;
        let resource_id = input.arg_string(resource_id_arg).ok_or_else(|| {
            WebError::bad_request_code(
                "mnote_resource_id_required",
                format!("资源工具缺少 {resource_id_arg}"),
            )
            .with_context(context)
        })?;
        let object_identity = format!("resource:{resource_kind}:{document_id}:{resource_id}");
        Ok(Self {
            document_id,
            resource_id,
            object_identity,
            resource_path: input.arg_string("resourcePath"),
        })
    }

    fn resolve_read_path(
        &self,
        context: &RequestContext,
        input: &ToolCallInput,
    ) -> Result<PathBuf, WebError> {
        let root_uri = local_root_uri_for_resource(input).ok_or_else(|| {
            WebError::new(
                StatusCode::FORBIDDEN,
                "mnote_resource_root_uri_required",
                "资源工具需要授权 rootUri",
            )
            .with_context(context)
        })?;
        let relative = self.relative_path();
        crate::routes::ensure_local_path_read_access(context, &root_uri, &relative)
            .map_err(|error| error.with_context(context))
    }

    fn resolve_write_path(
        &self,
        context: &RequestContext,
        input: &ToolCallInput,
    ) -> Result<PathBuf, WebError> {
        let root_uri = local_root_uri_for_resource(input).ok_or_else(|| {
            WebError::new(
                StatusCode::FORBIDDEN,
                "mnote_resource_root_uri_required",
                "资源工具需要授权 rootUri",
            )
            .with_context(context)
        })?;
        let root = crate::routes::ensure_local_workspace_access(context, &root_uri)
            .map_err(|error| error.with_context(context))?;
        let relative = self.relative_path();
        let path = root.join(relative);
        let canonical = path.canonicalize().map_err(|error| {
            WebError::bad_request_code(
                "mnote_resource_unavailable",
                format!("无法访问资源文件: {error}"),
            )
            .with_context(context)
        })?;
        if !canonical.starts_with(root) {
            return Err(WebError::bad_request_code(
                "mnote_resource_root_escape",
                "资源工具不能越过授权目录",
            )
            .with_context(context));
        }
        Ok(canonical)
    }

    fn resolve_create_path(
        &self,
        context: &RequestContext,
        input: &ToolCallInput,
    ) -> Result<PathBuf, WebError> {
        let root_uri = local_root_uri_for_resource(input).ok_or_else(|| {
            WebError::new(
                StatusCode::FORBIDDEN,
                "mnote_resource_root_uri_required",
                "资源工具需要授权 rootUri",
            )
            .with_context(context)
        })?;
        let root = crate::routes::ensure_local_workspace_access(context, &root_uri)
            .map_err(|error| error.with_context(context))?;
        let relative = self.relative_path();
        let relative_path = Path::new(&relative);
        if relative_path.is_absolute()
            || relative_path
                .components()
                .any(|component| matches!(component, std::path::Component::ParentDir))
        {
            return Err(WebError::bad_request_code(
                "mnote_resource_root_escape",
                "资源工具不能越过授权目录",
            )
            .with_context(context));
        }
        let path = root.join(relative_path);
        if !path.starts_with(&root) {
            return Err(WebError::bad_request_code(
                "mnote_resource_root_escape",
                "资源工具不能越过授权目录",
            )
            .with_context(context));
        }
        Ok(path)
    }

    fn relative_path(&self) -> String {
        self.resource_path
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
            .unwrap_or_else(|| self.resource_id.clone())
    }
}

fn ensure_resource_scope_allowed(
    context: &RequestContext,
    input: &ToolCallInput,
    target: &ResourceToolTarget,
) -> Result<(), WebError> {
    let Some(scope) = input.arg_value("aiAccessScope") else {
        return Ok(());
    };
    let allowed = scope
        .get("allowedResourceIds")
        .or_else(|| scope.get("allowed_resource_ids"))
        .and_then(Value::as_array)
        .map(|values| {
            values
                .iter()
                .filter_map(Value::as_str)
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(ToOwned::to_owned)
                .collect::<HashSet<_>>()
        })
        .unwrap_or_default();
    if allowed.is_empty()
        || allowed.contains(&target.resource_id)
        || allowed.contains(&target.object_identity)
    {
        return Ok(());
    }
    Err(WebError::new(
        StatusCode::FORBIDDEN,
        "mnote_resource_ai_scope_forbidden",
        "当前 AI scope 不允许访问该资源",
    )
    .with_context(context))
}

fn ensure_resource_write_contract(
    context: &RequestContext,
    input: &ToolCallInput,
) -> Result<(), WebError> {
    crate::hermes_tools::ensure_write_authorized(context, input)
}

fn local_root_uri_for_resource(input: &ToolCallInput) -> Option<String> {
    input.effective_root_uri().or_else(|| {
        input
            .arg_value("aiAccessScope")
            .and_then(|scope| {
                scope
                    .get("allowedRoots")
                    .or_else(|| scope.get("allowed_roots"))
                    .cloned()
            })
            .and_then(|allowed_roots| {
                allowed_roots.as_array().and_then(|roots| {
                    roots
                        .iter()
                        .filter_map(|root| {
                            root.get("rootUri")
                                .or_else(|| root.get("root_uri"))
                                .and_then(Value::as_str)
                        })
                        .map(str::trim)
                        .find(|root_uri| !root_uri.is_empty())
                        .map(ToOwned::to_owned)
                })
            })
    })
}

fn embed_mindmap_into_local_markdown_page(
    context: &RequestContext,
    input: &ToolCallInput,
    document_id: &str,
    resource_relative_path: &str,
    title: &str,
) -> Result<Value, WebError> {
    let root_uri = local_root_uri_for_resource(input).ok_or_else(|| {
        WebError::new(
            StatusCode::FORBIDDEN,
            "mnote_resource_root_uri_required",
            "资源工具需要授权 rootUri",
        )
        .with_context(context)
    })?;
    let root = crate::routes::ensure_local_workspace_access(context, &root_uri)
        .map_err(|error| error.with_context(context))?;
    let page_relative_path = local_markdown_relative_path_from_document_id(context, document_id)?;
    let page_path = root.join(&page_relative_path);
    if !page_path.starts_with(&root) {
        return Err(WebError::bad_request_code(
            "mnote_resource_root_escape",
            "资源工具不能越过授权目录",
        )
        .with_context(context));
    }
    if !page_path.is_file() {
        return Err(WebError::bad_request_code(
            "mnote_resource_page_not_found",
            "找不到要绑定 mindmap 的本地 Markdown 页面",
        )
        .with_context(context));
    }
    let href = markdown_href_from_page(&root, &page_path, resource_relative_path);
    let mut markdown = fs::read_to_string(&page_path).map_err(|error| {
        WebError::bad_request_code(
            "mnote_resource_page_read_failed",
            format!("无法读取本地 Markdown 页面: {error}"),
        )
        .with_context(context)
    })?;
    let link = format!("[{}]({href})", markdown_link_label(title));
    if markdown.contains(&link) || markdown.contains(&format!("]({href})")) {
        return Ok(json!({
            "status": "already_present",
            "documentId": document_id,
            "changedFile": page_relative_path,
            "href": href
        }));
    }
    if !markdown.ends_with('\n') {
        markdown.push('\n');
    }
    if !markdown.ends_with("\n\n") {
        markdown.push('\n');
    }
    markdown.push_str(&link);
    markdown.push('\n');
    fs::write(&page_path, markdown).map_err(|error| {
        WebError::bad_request_code(
            "mnote_resource_page_write_failed",
            format!("无法写入本地 Markdown 页面: {error}"),
        )
        .with_context(context)
    })?;
    Ok(json!({
        "status": "embedded",
        "documentId": document_id,
        "changedFile": page_relative_path,
        "href": href
    }))
}

fn local_markdown_relative_path_from_document_id(
    context: &RequestContext,
    document_id: &str,
) -> Result<String, WebError> {
    let encoded = document_id
        .trim()
        .strip_prefix("local-md:")
        .ok_or_else(|| {
            WebError::bad_request_code(
                "mnote_resource_local_markdown_required",
                "绑定 mindmap 需要 local-md 页面",
            )
            .with_context(context)
        })?;
    let relative = crate::routes::decode_local_id_segment(encoded)
        .map_err(|error| error.with_context(context))?;
    let path = Path::new(&relative);
    if path.is_absolute()
        || path
            .components()
            .any(|component| matches!(component, std::path::Component::ParentDir))
    {
        return Err(WebError::bad_request_code(
            "mnote_resource_root_escape",
            "资源工具不能越过授权目录",
        )
        .with_context(context));
    }
    Ok(relative)
}

fn markdown_href_from_page(root: &Path, page_path: &Path, resource_relative_path: &str) -> String {
    let page_dir = page_path.parent().unwrap_or(root);
    let target = root.join(resource_relative_path);
    if let Ok(relative) = target.strip_prefix(page_dir) {
        return path_to_markdown_href(relative);
    }
    let page_dir_relative = page_dir.strip_prefix(root).unwrap_or(Path::new(""));
    let depth = page_dir_relative
        .components()
        .filter(|component| matches!(component, std::path::Component::Normal(_)))
        .count();
    let mut href = String::new();
    for _ in 0..depth {
        href.push_str("../");
    }
    href.push_str(&resource_relative_path.replace('\\', "/"));
    href
}

fn path_to_markdown_href(path: &Path) -> String {
    path.to_string_lossy().replace('\\', "/")
}

fn markdown_link_label(value: &str) -> String {
    value
        .trim()
        .replace('\\', "\\\\")
        .replace('[', "\\[")
        .replace(']', "\\]")
        .replace('\n', " ")
}

fn default_mindmap_view() -> Value {
    json!({
        "state": {
            "scale": 1,
            "sx": 0,
            "sy": 0,
            "x": -44.99991989135742_f64,
            "y": -15.500006675720217_f64
        },
        "transform": {
            "a": 1,
            "b": 0,
            "c": 0,
            "d": 1,
            "e": -44.99991989135742_f64,
            "f": -15.500006675720217_f64,
            "originX": 0,
            "originY": 0,
            "rotate": 0,
            "scaleX": 1,
            "scaleY": 1,
            "shear": 0,
            "translateX": -44.99991989135742_f64,
            "translateY": -15.500006675720217_f64
        }
    })
}

fn mindmap_envelope_from_outline(
    title: &str,
    outline: &Value,
    source_refs: Value,
    context: &RequestContext,
) -> Result<Value, WebError> {
    let outline_items = outline.as_array().ok_or_else(|| {
        WebError::bad_request_code(
            "mnote_mindmap_outline_invalid",
            "mindmap outline 必须是数组",
        )
        .with_context(context)
    })?;
    let title = title.trim();
    let root_title = if title.is_empty() { "KMIND" } else { title };
    Ok(json!({
        "data": {
            "children": mindmap_outline_items_to_children(outline_items, "node"),
            "data": {
                "expand": true,
                "isActive": false,
                "text": root_title,
                "uid": "root"
            }
        },
        "view": default_mindmap_view(),
        "metadata": {
            "sourceRefs": source_refs
        }
    }))
}

fn mindmap_outline_items_to_children(items: &[Value], prefix: &str) -> Vec<Value> {
    items
        .iter()
        .enumerate()
        .map(|(index, item)| {
            let ordinal = index + 1;
            let uid = format!("{prefix}_{ordinal}");
            let text = item
                .get("text")
                .or_else(|| item.get("title"))
                .and_then(Value::as_str)
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .unwrap_or("未命名节点");
            let children = item
                .get("children")
                .and_then(Value::as_array)
                .map(|children| mindmap_outline_items_to_children(children, &uid))
                .unwrap_or_default();
            json!({
                "data": {
                    "expand": true,
                    "isActive": false,
                    "text": text,
                    "uid": uid
                },
                "children": children
            })
        })
        .collect()
}

fn mindmap_root_value(value: &Value) -> &Value {
    value
        .get("data")
        .filter(|data| data.get("data").is_some() || data.get("children").is_some())
        .unwrap_or(value)
}

fn mindmap_root_value_mut(value: &mut Value) -> Option<&mut Value> {
    let has_envelope_root = value
        .get("data")
        .map(|data| data.get("data").is_some() || data.get("children").is_some())
        .unwrap_or(false);
    if has_envelope_root {
        return value.get_mut("data");
    }
    Some(value)
}

fn apply_mindmap_ops(
    context: &RequestContext,
    envelope: &mut Value,
    ops: &Value,
) -> Result<(), WebError> {
    let ops = ops.as_array().ok_or_else(|| {
        WebError::bad_request_code("mnote_resource_ops_invalid", "mindmap ops 必须是数组")
            .with_context(context)
    })?;
    let root = mindmap_root_value_mut(envelope).ok_or_else(|| {
        WebError::bad_request_code("mnote_resource_json_invalid", "mindmap 缺少 root")
            .with_context(context)
    })?;
    for op in ops {
        apply_mindmap_op(context, root, op)?;
    }
    Ok(())
}

fn apply_mindmap_op(
    context: &RequestContext,
    root: &mut Value,
    op: &Value,
) -> Result<(), WebError> {
    let op_name = op
        .get("op")
        .or_else(|| op.get("type"))
        .or_else(|| op.get("action"))
        .and_then(Value::as_str)
        .map(normalize_mindmap_op_name)
        .ok_or_else(|| {
            WebError::bad_request_code("mnote_resource_op_required", "mindmap op 缺少 op")
                .with_context(context)
        })?;
    match op_name.as_str() {
        "updatetext" | "updatenode" => {
            let node_id = mindmap_op_string(op, &["nodeId", "node_id", "id"]).ok_or_else(|| {
                WebError::bad_request_code("mnote_resource_node_required", "更新节点缺少 nodeId")
                    .with_context(context)
            })?;
            let text = mindmap_op_string(op, &["text", "title"]).ok_or_else(|| {
                WebError::bad_request_code("mnote_resource_text_required", "更新节点缺少 text")
                    .with_context(context)
            })?;
            let node = find_mindmap_node_mut(root, &node_id).ok_or_else(|| {
                WebError::bad_request_code("mnote_resource_node_not_found", "找不到要更新的节点")
                    .with_context(context)
            })?;
            set_mindmap_node_text(node, &text);
        }
        "insertchild" | "addchild" => {
            let parent_id = mindmap_op_string(op, &["parentId", "parent_id", "nodeId"])
                .ok_or_else(|| {
                    WebError::bad_request_code(
                        "mnote_resource_parent_required",
                        "插入子节点缺少 parentId",
                    )
                    .with_context(context)
                })?;
            let parent = find_mindmap_node_mut(root, &parent_id).ok_or_else(|| {
                WebError::bad_request_code("mnote_resource_node_not_found", "找不到父节点")
                    .with_context(context)
            })?;
            let child_input = op.get("node").unwrap_or(op);
            let child = mindmap_node_from_input(context, child_input)?;
            ensure_mindmap_children_array(context, parent)?.push(child);
        }
        "deletenode" => {
            let node_id = mindmap_op_string(op, &["nodeId", "node_id", "id"]).ok_or_else(|| {
                WebError::bad_request_code("mnote_resource_node_required", "删除节点缺少 nodeId")
                    .with_context(context)
            })?;
            if mindmap_node_matches(root, &node_id) {
                return Err(WebError::bad_request_code(
                    "mnote_resource_root_delete_forbidden",
                    "不能删除 mindmap 根节点",
                )
                .with_context(context));
            }
            remove_mindmap_node(root, &node_id).ok_or_else(|| {
                WebError::bad_request_code("mnote_resource_node_not_found", "找不到要删除的节点")
                    .with_context(context)
            })?;
        }
        _ => {
            return Err(WebError::bad_request_code(
                "mnote_resource_op_unsupported",
                format!("暂不支持 mindmap op: {op_name}"),
            )
            .with_context(context));
        }
    }
    Ok(())
}

fn normalize_mindmap_op_name(value: &str) -> String {
    value
        .chars()
        .filter(|ch| *ch != '_' && *ch != '-' && !ch.is_whitespace())
        .flat_map(char::to_lowercase)
        .collect()
}

fn mindmap_op_string(op: &Value, keys: &[&str]) -> Option<String> {
    keys.iter().find_map(|key| {
        op.get(*key)
            .and_then(Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(ToOwned::to_owned)
    })
}

fn find_mindmap_node_mut<'a>(node: &'a mut Value, node_id: &str) -> Option<&'a mut Value> {
    if mindmap_node_matches(node, node_id) {
        return Some(node);
    }
    let children = node.get_mut("children").and_then(Value::as_array_mut)?;
    for child in children {
        if let Some(found) = find_mindmap_node_mut(child, node_id) {
            return Some(found);
        }
    }
    None
}

fn mindmap_node_matches(node: &Value, node_id: &str) -> bool {
    mindmap_node_id(node)
        .as_deref()
        .map(|value| value == node_id)
        .unwrap_or(false)
}

fn mindmap_node_id(node: &Value) -> Option<String> {
    node.pointer("/data/uid")
        .or_else(|| node.pointer("/data/id"))
        .or_else(|| node.get("uid"))
        .or_else(|| node.get("id"))
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
}

fn set_mindmap_node_text(node: &mut Value, text: &str) {
    if let Some(data) = node.get_mut("data").and_then(Value::as_object_mut) {
        data.insert("text".into(), json!(text));
        return;
    }
    if let Some(object) = node.as_object_mut() {
        object.insert("text".into(), json!(text));
    }
}

fn ensure_mindmap_children_array<'a>(
    context: &RequestContext,
    node: &'a mut Value,
) -> Result<&'a mut Vec<Value>, WebError> {
    let object = node.as_object_mut().ok_or_else(|| {
        WebError::bad_request_code("mnote_resource_node_invalid", "mindmap 节点必须是对象")
            .with_context(context)
    })?;
    let children = object
        .entry("children")
        .or_insert_with(|| Value::Array(Vec::new()));
    if !children.is_array() {
        *children = Value::Array(Vec::new());
    }
    Ok(children.as_array_mut().expect("children 已归一为数组"))
}

fn mindmap_node_from_input(context: &RequestContext, input: &Value) -> Result<Value, WebError> {
    if input.get("data").is_some() || input.get("children").is_some() {
        let mut node = input.clone();
        ensure_mindmap_children_array(context, &mut node)?;
        return Ok(node);
    }
    let text = input
        .get("text")
        .or_else(|| input.get("title"))
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .unwrap_or("未命名节点");
    let uid = input
        .get("uid")
        .or_else(|| input.get("id"))
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .unwrap_or(text);
    let mut node = json!({
        "data": {
            "expand": true,
            "isActive": false,
            "text": text,
            "uid": uid
        },
        "children": []
    });
    if let Some(object) = node.as_object_mut() {
        for key in ["metadata", "sourceRefs", "refs", "note", "hyperlink"] {
            if let Some(value) = input.get(key) {
                object.insert(key.into(), value.clone());
            }
        }
    }
    Ok(node)
}

fn remove_mindmap_node(parent: &mut Value, node_id: &str) -> Option<Value> {
    let children = parent.get_mut("children").and_then(Value::as_array_mut)?;
    if let Some(index) = children
        .iter()
        .position(|child| mindmap_node_matches(child, node_id))
    {
        return Some(children.remove(index));
    }
    for child in children {
        if let Some(removed) = remove_mindmap_node(child, node_id) {
            return Some(removed);
        }
    }
    None
}

fn ensure_mindmap_revision_precondition(
    context: &RequestContext,
    input: &ToolCallInput,
    path: &Path,
) -> Result<(), WebError> {
    let Some(expected) = input.arg_value("expectedRevision") else {
        return Ok(());
    };
    let current = file_revision(path);
    if revision_label(&expected).as_deref() != revision_label(&current).as_deref() {
        return Err(WebError::bad_request_code(
            "mnote_resource_revision_conflict",
            "mindmap resource revision 已变化，请重新读取后再写入",
        )
        .with_context(context));
    }
    Ok(())
}

fn revision_label(value: &Value) -> Option<String> {
    match value {
        Value::String(value) => Some(value.trim().to_string()).filter(|value| !value.is_empty()),
        Value::Number(value) => Some(value.to_string()),
        _ => None,
    }
}

fn collect_mindmap_nodes(value: &Value) -> Vec<Value> {
    let mut nodes = Vec::new();
    collect_mindmap_nodes_inner(mindmap_root_value(value), &mut nodes);
    nodes
}

fn collect_mindmap_nodes_inner(value: &Value, nodes: &mut Vec<Value>) {
    let text = value
        .pointer("/data/text")
        .or_else(|| value.get("text"))
        .and_then(Value::as_str)
        .unwrap_or("");
    let id = value
        .pointer("/data/uid")
        .or_else(|| value.pointer("/data/id"))
        .or_else(|| value.get("uid"))
        .or_else(|| value.get("id"))
        .and_then(Value::as_str)
        .unwrap_or("root");
    if !text.is_empty() {
        nodes.push(json!({ "id": id, "text": text }));
    }
    if let Some(children) = value.get("children").and_then(Value::as_array) {
        for child in children {
            collect_mindmap_nodes_inner(child, nodes);
        }
    }
}

fn mindmap_markdown_summary(nodes: &[Value]) -> String {
    nodes
        .iter()
        .filter_map(|node| node.get("text").and_then(Value::as_str))
        .take(20)
        .map(|text| format!("- {text}"))
        .collect::<Vec<_>>()
        .join("\n")
}

fn file_revision(path: &Path) -> Value {
    let Ok(metadata) = fs::metadata(path) else {
        return Value::Null;
    };
    let modified_ms = metadata
        .modified()
        .ok()
        .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|duration| duration.as_millis())
        .unwrap_or_default();
    json!(format!("{}:{}", metadata.len(), modified_ms))
}

fn office_mime_type(path: &Path) -> &'static str {
    match path
        .extension()
        .and_then(|value| value.to_str())
        .unwrap_or("")
    {
        "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
        _ => "application/octet-stream",
    }
}

fn office_text_preview(path: &Path) -> Value {
    match fs::read_to_string(path) {
        Ok(text) => json!(text.chars().take(4000).collect::<String>()),
        Err(_) => Value::Null,
    }
}
