use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::page_aggregate::{
    PageAggregate, PageAggregateSource, PageBody, PageHead, PageIdentity, PageLayout, PageOptions,
    PagePermissions, PageStats, PageTree,
};
use crate::routes::local_markdown_parser::{
    file_stem_title, parse_markdown_page, split_frontmatter,
};
use crate::routes::local_search_index;
use crate::routes::snapshot_support::ProjectionSnapshot;
use axum::extract::{Extension, Multipart, Path as AxumPath, Query, State};
use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
use axum::Json;
use bridge_runtime::project_legacy_content_to_block_document;
use control_plane::AppendAuditInput;
use control_plane::{
    CreateShareLinkInput, DirectoryGrantInput, DirectoryGrantLookup, DirectoryGrantRecord,
    OutboxEventInput, ShareLinkRecord,
};
use core_protocol::{
    KernelObjectIdentity, KernelObjectKind, ObjectWorkspacePath, WorkspaceSourceKind,
};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use std::cmp::Ordering;
use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
use time::OffsetDateTime;

#[cfg(test)]
static LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS: std::sync::atomic::AtomicU64 =
    std::sync::atomic::AtomicU64::new(0);
#[cfg(test)]
static LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS: std::sync::atomic::AtomicU64 =
    std::sync::atomic::AtomicU64::new(0);

#[derive(Debug, Clone)]
struct LocalPageTreeSnapshotCacheEntry {
    watch_revision: LocalFolderWatchRevision,
    snapshot: ProjectionSnapshot,
}

static LOCAL_PAGE_TREE_SNAPSHOT_CACHE: OnceLock<
    Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>>,
> = OnceLock::new();

fn local_page_tree_snapshot_cache(
) -> &'static Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>> {
    LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
}

#[cfg(test)]
pub(crate) fn reset_local_page_tree_snapshot_test_loads() {
    LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.store(0, std::sync::atomic::Ordering::SeqCst);
    LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.store(0, std::sync::atomic::Ordering::SeqCst);
    if let Some(cache) = LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get() {
        if let Ok(mut cache) = cache.lock() {
            cache.clear();
        }
    }
}

#[cfg(test)]
pub(crate) fn local_page_tree_snapshot_test_loads() -> u64 {
    LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.load(std::sync::atomic::Ordering::SeqCst)
}

#[cfg(test)]
fn local_page_tree_snapshot_scan_test_loads() -> u64 {
    LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.load(std::sync::atomic::Ordering::SeqCst)
}

#[derive(Debug, Clone)]
struct LocalFolderEntry {
    path: PathBuf,
    relative_path: String,
    file_name: String,
    is_dir: bool,
    is_symlink: bool,
    is_readonly: bool,
}

#[derive(Debug, Clone, Default)]
struct LocalFolderMetadata {
    page_options: BTreeMap<String, Value>,
    trash_entries: BTreeMap<String, LocalTrashEntry>,
    uploaded_assets: BTreeMap<String, LocalUploadedAssetEntry>,
    file_order: BTreeMap<String, Vec<String>>,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct AffectedParent {
    relative_path: String,
    reason: String,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct RevealTarget {
    relative_path: String,
    row_id: String,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalFileOperationResource {
    document_id: Option<String>,
    relative_path: String,
    object_identity: KernelObjectIdentity,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalFileOperationResult {
    schema: String,
    operation_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    batch_id: Option<String>,
    action: String,
    root_uri: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    resource: Option<LocalFileOperationResource>,
    #[serde(skip_serializing_if = "Option::is_none")]
    previous_resource: Option<LocalFileOperationResource>,
    affected_parents: Vec<AffectedParent>,
    #[serde(skip_serializing_if = "Option::is_none")]
    reveal_target: Option<RevealTarget>,
    #[serde(skip_serializing_if = "Option::is_none")]
    select_target: Option<RevealTarget>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LocalWorkspaceManifest {
    #[serde(default, alias = "workspace_id")]
    workspace_id: String,
    #[serde(default, alias = "owner_id")]
    owner_id: String,
    #[serde(default, alias = "created_at")]
    created_at: String,
    #[serde(default, alias = "capabilities")]
    capabilities: Vec<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LocalAccessPolicy {
    #[serde(default)]
    admins: Vec<String>,
    #[serde(default)]
    grants: Vec<LocalAccessGrant>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LocalAccessGrant {
    #[serde(default)]
    id: String,
    #[serde(default, alias = "user_id")]
    user_id: String,
    #[serde(default, alias = "root_uri")]
    root_uri: String,
    #[serde(default, alias = "root_path")]
    root_path: String,
    #[serde(default, alias = "access")]
    permission: String,
    #[serde(default = "default_true")]
    recursive: bool,
    #[serde(default)]
    capabilities: Vec<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LocalShareGrantsStore {
    #[serde(default)]
    grants: Vec<LocalShareGrant>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LocalShareGrant {
    #[serde(default)]
    id: String,
    #[serde(default, alias = "share_id")]
    share_id: String,
    #[serde(default, alias = "owner_user_id")]
    owner_user_id: String,
    #[serde(default, alias = "target_user_id")]
    target_user_id: String,
    #[serde(default, alias = "root_uri")]
    root_uri: String,
    #[serde(default, alias = "root_path")]
    root_path: String,
    #[serde(default, alias = "documentId")]
    document_id: String,
    #[serde(default, alias = "allowedResourceIds")]
    allowed_resource_ids: Vec<String>,
    #[serde(default, alias = "access")]
    permission: String,
    #[serde(default)]
    capabilities: Vec<String>,
    #[serde(default, alias = "createdAt")]
    created_at: String,
    #[serde(default, alias = "revokedAt")]
    revoked_at: Option<String>,
    #[serde(default = "default_true")]
    active: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LocalAccessMode {
    Read,
    Write,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct LocalTrashEntry {
    #[serde(default, alias = "documentId")]
    pub(crate) document_id: String,
    #[serde(default, alias = "resourceKind")]
    pub(crate) resource_kind: String,
    #[serde(default, alias = "resourceScope")]
    pub(crate) resource_scope: String,
    #[serde(default, alias = "originalFilePath")]
    pub(crate) original_file_path: String,
    #[serde(default, alias = "trashedFilePath")]
    pub(crate) trashed_file_path: String,
    #[serde(default, alias = "trashEntryId")]
    pub(crate) trash_entry_id: String,
    #[serde(default, alias = "originalRelativePath")]
    pub(crate) original_relative_path: String,
    #[serde(default, alias = "trashRelativePath")]
    pub(crate) trash_relative_path: String,
    #[serde(default, alias = "deletedAtMs")]
    pub(crate) deleted_at_ms: u128,
    #[serde(default, alias = "archivedAt")]
    pub(crate) archived_at: u128,
    #[serde(default, alias = "purgedAt")]
    pub(crate) purged_at: Option<u128>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LocalUploadedAssetEntry {
    #[serde(default, alias = "documentId")]
    document_id: String,
    #[serde(default, alias = "relativePath")]
    relative_path: String,
    #[serde(default, alias = "fileName")]
    file_name: String,
    #[serde(default, alias = "createdAtMs")]
    created_at_ms: u128,
}

#[derive(Debug, Clone)]
struct LocalFolderRow {
    node_id: String,
    row_id: String,
    parent_node_id: Option<String>,
    title: String,
    depth: u32,
    position: u32,
    row_kind: String,
    icon_hint: String,
    relative_path: String,
    source_uri: String,
    child_count: u32,
    expandable: bool,
    expanded_by_default: bool,
    document_id: Option<String>,
    capabilities: Vec<String>,
    workspace_id: String,
    root_source_uri: String,
}

#[derive(Debug, Clone)]
struct LocalFolderScanResult {
    rows: Vec<LocalFolderRow>,
    watch_revision: LocalFolderWatchRevision,
}

#[derive(Debug, Clone)]
pub(crate) struct LocalUploadFile {
    name: String,
    content_type: String,
    bytes: Vec<u8>,
}

#[derive(Debug, Clone)]
struct LocalAssetUploadFields {
    file: LocalUploadFile,
    root_uri: String,
    document_id: String,
    target_relative_path: Option<String>,
    upload_intent: String,
    kind: String,
}

fn local_folder_ok_response(
    context: &RequestContext,
    result: Value,
) -> (StatusCode, HeaderMap, Json<Value>) {
    (
        StatusCode::OK,
        HeaderMap::new(),
        Json(json!({
            "ok": true,
            "requestId": context.trace.request_id,
            "traceId": context.trace.trace_id,
            "owner": "mnote-web",
            "result": result,
        })),
    )
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalFileOpenQuery {
    pub root_uri: String,
    pub path: String,
    #[serde(default)]
    pub download: Option<bool>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalResourceReadQuery {
    pub root_uri: String,
    pub path: String,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalResourceWriteRequest {
    #[serde(default, alias = "root_uri")]
    pub root_uri: String,
    #[serde(default)]
    pub path: String,
    #[serde(default, alias = "expected_file_version")]
    pub expected_file_version: Option<String>,
    #[serde(default, alias = "content_format")]
    pub content_format: Option<String>,
    #[serde(default)]
    pub content: Value,
    #[serde(default, alias = "tiptap_document")]
    pub tiptap_document: Option<Value>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalAccessValidateRootRequest {
    #[serde(default, alias = "root_uri")]
    pub root_uri: String,
    #[serde(default, alias = "root_path")]
    pub root_path: String,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalAccessGrantRequest {
    // 兼容旧 share/access grant payload；SQLite 目录授权创建时由控制面生成真实 grant id。
    #[allow(dead_code)]
    #[serde(default)]
    pub id: String,
    #[serde(default, alias = "user_id")]
    pub user_id: String,
    #[serde(default, alias = "root_uri")]
    pub root_uri: String,
    #[serde(default, alias = "root_path")]
    pub root_path: String,
    #[serde(default, alias = "access")]
    pub permission: String,
    #[serde(default = "default_true")]
    pub recursive: bool,
    #[serde(default)]
    pub capabilities: Vec<String>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalShareGrantRequest {
    #[serde(default)]
    pub id: String,
    #[serde(default, alias = "share_id")]
    pub share_id: String,
    #[serde(default, alias = "owner_user_id")]
    pub owner_user_id: String,
    #[serde(default, alias = "target_user_id")]
    pub target_user_id: String,
    #[serde(default, alias = "root_uri")]
    pub root_uri: String,
    #[serde(default, alias = "root_path")]
    pub root_path: String,
    #[serde(default, alias = "documentId")]
    pub document_id: String,
    #[serde(default, alias = "allowedResourceIds")]
    pub allowed_resource_ids: Vec<String>,
    #[serde(default, alias = "access")]
    pub permission: String,
    #[serde(default)]
    pub capabilities: Vec<String>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShareLinkListQuery {
    #[serde(default, alias = "workspace_id")]
    pub workspace_id: String,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalShareLinkRequest {
    #[serde(default, alias = "workspace_id")]
    pub workspace_id: String,
    #[serde(default, alias = "resource_kind")]
    pub resource_kind: String,
    #[serde(default, alias = "resource_id")]
    pub resource_id: String,
    #[serde(default)]
    pub token: Option<String>,
    #[serde(default, alias = "access")]
    pub permission: String,
    #[serde(default, alias = "expires_at")]
    pub expires_at: Option<String>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SharedCacheRecordRequest {
    pub root_uri: String,
    pub share_id: String,
    pub permission: String,
    #[serde(default)]
    pub remote_version: String,
    #[serde(default)]
    pub base_version: String,
    #[serde(default)]
    pub source_actor: String,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncPendingChangeRequest {
    pub root_uri: String,
    pub share_id: String,
    pub resource_id: String,
    #[serde(default)]
    pub local_version: String,
    #[serde(default)]
    pub base_version: String,
    #[serde(default)]
    pub remote_version: String,
    #[serde(default)]
    pub change_summary: String,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncConflictReportRequest {
    pub root_uri: String,
    pub share_id: String,
    pub resource_id: String,
    #[serde(default)]
    pub local_version: String,
    #[serde(default)]
    pub remote_version: String,
    #[serde(default)]
    pub base_version: String,
    #[serde(default)]
    pub actor_id: String,
    #[serde(default)]
    pub summary: String,
}

const DEFAULT_LOCAL_WORKSPACE_BASE_DIR: &str = "/mnt/Data1T/Mnote_data";
const DEFAULT_LOCAL_WORKSPACE_SLUG: &str = "my-space";
const ENV_LOCAL_ADMIN_USER_IDS: &str = "MNOTE_ADMIN_USER_IDS";
const ENV_LOCAL_ACCESS_POLICY_FILE: &str = "MNOTE_LOCAL_ACCESS_POLICY_FILE";
const ENV_SHARE_GRANTS_FILE: &str = "MNOTE_SHARE_GRANTS_FILE";

fn default_true() -> bool {
    true
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalFolderWatchRevision {
    pub root_uri: String,
    pub revision: String,
    pub entry_count: usize,
    pub latest_modified_ms: u128,
}

pub(crate) fn ensure_local_workspace_access_for_actor(
    actor_id: &str,
    actor_type: &str,
    root_uri: &str,
) -> Result<PathBuf, WebError> {
    ensure_local_workspace_access_for_actor_with_mode(
        actor_id,
        actor_type,
        root_uri,
        LocalAccessMode::Write,
    )
}

pub(crate) fn ensure_local_workspace_read_access_for_actor(
    actor_id: &str,
    actor_type: &str,
    root_uri: &str,
) -> Result<PathBuf, WebError> {
    ensure_local_workspace_access_for_actor_with_mode(
        actor_id,
        actor_type,
        root_uri,
        LocalAccessMode::Read,
    )
}

fn ensure_local_workspace_access_for_actor_with_mode(
    actor_id: &str,
    actor_type: &str,
    root_uri: &str,
    mode: LocalAccessMode,
) -> Result<PathBuf, WebError> {
    let actor_id = actor_id.trim();
    let actor_type = actor_type.trim();
    if actor_id.is_empty()
        || actor_id == "anonymous"
        || actor_type.is_empty()
        || actor_type == "anonymous"
    {
        return Err(WebError::new(
            StatusCode::UNAUTHORIZED,
            "local_workspace_auth_required",
            "本地工作区访问需要先登录",
        ));
    }

    let policy = load_local_access_policy().unwrap_or_default();
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    if !canonical_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_not_directory",
            "本地 rootUri 必须指向目录",
        ));
    }

    if is_local_admin(actor_id, actor_type, &policy) {
        return Ok(canonical_root);
    }
    if local_access_policy_allows(&policy, actor_id, &canonical_root, mode) {
        return Ok(canonical_root);
    }
    if let Ok(manifest) = load_local_workspace_manifest(&canonical_root) {
        if manifest.owner_id.trim() == actor_id {
            return Ok(canonical_root);
        }
    }

    Err(WebError::new(
        StatusCode::FORBIDDEN,
        "local_workspace_access_denied",
        "当前用户无权访问该本地工作区",
    ))
}

pub(crate) fn ensure_local_workspace_access_with_state(
    state: &AppState,
    context: &RequestContext,
    root_uri: &str,
    mode: LocalAccessMode,
) -> Result<PathBuf, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    if !canonical_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_not_directory",
            "本地 rootUri 必须指向目录",
        ));
    }
    let actor_id = context.auth.actor_id.trim();
    let sqlite_access = state
        .control_plane()
        .resolve_access(actor_id, &file_uri_for_path(&canonical_root))
        .map_err(|error| WebError::internal(format!("SQLite 控制面授权查询失败: {error}")))?;
    if local_access_permission_allows(&sqlite_access.permission, mode) {
        return Ok(canonical_root);
    }
    ensure_local_workspace_access_for_actor_with_mode(
        &context.auth.actor_id,
        &context.auth.actor_type,
        root_uri,
        mode,
    )
}

pub(crate) fn ensure_local_workspace_read_access_with_state(
    state: &AppState,
    context: &RequestContext,
    root_uri: &str,
) -> Result<PathBuf, WebError> {
    ensure_local_workspace_access_with_state(state, context, root_uri, LocalAccessMode::Read)
}

pub(crate) fn ensure_local_workspace_write_access_with_state(
    state: &AppState,
    context: &RequestContext,
    root_uri: &str,
) -> Result<PathBuf, WebError> {
    ensure_local_workspace_access_with_state(state, context, root_uri, LocalAccessMode::Write)
}

pub(crate) fn ensure_local_path_read_access(
    context: &RequestContext,
    root_uri: &str,
    candidate_path: &str,
) -> Result<PathBuf, WebError> {
    let canonical_root = ensure_local_workspace_read_access(context, root_uri)?;
    let candidate_path = candidate_path.trim();
    if candidate_path.is_empty() {
        return Err(WebError::bad_request_code(
            "local_file_path_required",
            "本地文件读取缺少文件路径",
        ));
    }
    let raw_path = candidate_path
        .strip_prefix("file://")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from(candidate_path));
    let target_path = if raw_path.is_absolute() {
        raw_path
    } else {
        canonical_root.join(raw_path)
    };
    let canonical_target = target_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_file_unavailable",
            format!("无法访问本地文件: {error}"),
        )
    })?;
    if !canonical_target.starts_with(&canonical_root) {
        return Err(WebError::bad_request_code(
            "local_file_root_escape",
            "本地文件读取不能越过授权目录",
        ));
    }
    if !canonical_target.is_file() {
        return Err(WebError::bad_request_code(
            "local_file_not_file",
            "本地文件读取目标必须是文件",
        ));
    }
    Ok(canonical_target)
}

pub(crate) fn ensure_local_workspace_read_access(
    context: &RequestContext,
    root_uri: &str,
) -> Result<PathBuf, WebError> {
    ensure_local_workspace_read_access_for_actor(
        &context.auth.actor_id,
        &context.auth.actor_type,
        root_uri,
    )
}

pub(crate) fn ensure_local_workspace_access(
    context: &RequestContext,
    root_uri: &str,
) -> Result<PathBuf, WebError> {
    ensure_local_workspace_access_for_actor(
        &context.auth.actor_id,
        &context.auth.actor_type,
        root_uri,
    )
}

fn local_access_policy_path() -> PathBuf {
    std::env::var(ENV_LOCAL_ACCESS_POLICY_FILE)
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .map(PathBuf::from)
        .unwrap_or_else(|| {
            default_local_workspace_base_dir()
                .join("control-plane")
                .join("access-policy.json")
        })
}

pub(crate) fn control_plane_db_path_display() -> String {
    std::env::var("MNOTE_CONTROL_PLANE_DB_PATH")
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| {
            default_local_workspace_base_dir()
                .join("control-plane")
                .join("control-plane.db")
                .display()
                .to_string()
        })
}

fn local_share_grants_path() -> PathBuf {
    std::env::var(ENV_SHARE_GRANTS_FILE)
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .map(PathBuf::from)
        .unwrap_or_else(|| {
            default_local_workspace_base_dir()
                .join("control-plane")
                .join("share-grants.json")
        })
}

fn load_local_access_policy() -> Result<LocalAccessPolicy, WebError> {
    let path = local_access_policy_path();
    if !path.exists() {
        return Ok(LocalAccessPolicy::default());
    }
    let content = fs::read_to_string(&path).map_err(|error| {
        WebError::bad_request_code(
            "local_access_policy_read_failed",
            format!("无法读取本地目录授权策略 {}: {error}", path.display()),
        )
    })?;
    serde_json::from_str::<LocalAccessPolicy>(&content).map_err(|error| {
        WebError::bad_request_code(
            "local_access_policy_invalid",
            format!("本地目录授权策略格式非法: {error}"),
        )
    })
}

fn load_local_share_grants_store() -> Result<LocalShareGrantsStore, WebError> {
    let path = local_share_grants_path();
    if !path.exists() {
        return Ok(LocalShareGrantsStore::default());
    }
    let content = fs::read_to_string(&path).map_err(|error| {
        WebError::bad_request_code(
            "local_share_grants_read_failed",
            format!("无法读取分享授权控制面 {}: {error}", path.display()),
        )
    })?;
    serde_json::from_str::<LocalShareGrantsStore>(&content).map_err(|error| {
        WebError::bad_request_code(
            "local_share_grants_invalid",
            format!("分享授权控制面格式非法: {error}"),
        )
    })
}

fn write_local_share_grants_store(store: &LocalShareGrantsStore) -> Result<(), WebError> {
    let path = local_share_grants_path();
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|error| {
            WebError::bad_request_code(
                "local_share_grants_dir_create_failed",
                format!("无法创建分享授权目录 {}: {error}", parent.display()),
            )
        })?;
    }
    let content = serde_json::to_string_pretty(store)
        .map_err(|error| WebError::internal(format!("无法序列化分享授权控制面: {error}")))?;
    fs::write(&path, format!("{content}\n")).map_err(|error| {
        WebError::bad_request_code(
            "local_share_grants_write_failed",
            format!("无法写入分享授权控制面 {}: {error}", path.display()),
        )
    })
}

fn require_local_access_policy_admin(
    context: &RequestContext,
) -> Result<LocalAccessPolicy, WebError> {
    let policy = load_local_access_policy()?;
    let actor_id = context.auth.actor_id.trim();
    let actor_type = context.auth.actor_type.trim();
    if actor_id.is_empty()
        || actor_id == "anonymous"
        || actor_type.is_empty()
        || actor_type == "anonymous"
    {
        return Err(WebError::new(
            StatusCode::UNAUTHORIZED,
            "local_access_policy_auth_required",
            "管理本地目录授权需要先登录",
        ));
    }
    if !is_local_admin(actor_id, actor_type, &policy) {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_access_policy_admin_required",
            "只有管理员可以管理本地目录授权",
        ));
    }
    Ok(policy)
}

fn require_share_grants_admin(context: &RequestContext) -> Result<(), WebError> {
    let policy = load_local_access_policy()?;
    let actor_id = context.auth.actor_id.trim();
    let actor_type = context.auth.actor_type.trim();
    if actor_id.is_empty()
        || actor_id == "anonymous"
        || actor_type.is_empty()
        || actor_type == "anonymous"
    {
        return Err(WebError::new(
            StatusCode::UNAUTHORIZED,
            "local_share_grants_auth_required",
            "管理分享授权需要先登录",
        ));
    }
    if !is_local_admin(actor_id, actor_type, &policy) {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_share_grants_admin_required",
            "只有管理员可以管理分享授权",
        ));
    }
    Ok(())
}

pub(crate) fn is_local_access_policy_admin_context(context: &RequestContext) -> bool {
    let actor_id = context.auth.actor_id.trim();
    let actor_type = context.auth.actor_type.trim();
    is_local_access_policy_admin_actor(actor_id, actor_type)
}

pub(crate) fn is_local_access_policy_admin_actor(actor_id: &str, actor_type: &str) -> bool {
    if actor_id.is_empty()
        || actor_id == "anonymous"
        || actor_type.is_empty()
        || actor_type == "anonymous"
    {
        return false;
    }
    let policy = load_local_access_policy().unwrap_or_default();
    is_local_admin(actor_id, actor_type, &policy)
}

fn env_local_admin_ids() -> Vec<String> {
    std::env::var(ENV_LOCAL_ADMIN_USER_IDS)
        .ok()
        .map(|value| {
            value
                .split([',', ';', '\n', ' '])
                .map(str::trim)
                .filter(|item| !item.is_empty())
                .map(ToOwned::to_owned)
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

fn canonical_root_from_admin_request(root_uri: &str, root_path: &str) -> Result<PathBuf, WebError> {
    let path = if !root_uri.trim().is_empty() {
        parse_file_root_uri(root_uri.trim())?
    } else if !root_path.trim().is_empty() {
        PathBuf::from(root_path.trim())
    } else {
        return Err(WebError::bad_request_code(
            "local_access_policy_root_required",
            "必须提供 rootUri 或 rootPath",
        ));
    };
    let canonical = path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_access_policy_root_unavailable",
            format!("无法访问授权目录: {error}"),
        )
    })?;
    if !canonical.is_dir() {
        return Err(WebError::bad_request_code(
            "local_access_policy_root_not_directory",
            "授权目标必须是目录",
        ));
    }
    Ok(canonical)
}

fn normalize_local_access_permission(permission: &str) -> Result<String, WebError> {
    let permission = permission.trim();
    match permission {
        "read" | "write" => Ok(permission.to_string()),
        _ => Err(WebError::bad_request_code(
            "local_access_policy_permission_invalid",
            "授权 permission 只能是 read 或 write",
        )),
    }
}

fn normalize_local_access_capabilities(capabilities: &[String]) -> Result<Vec<String>, WebError> {
    let mut normalized = Vec::new();
    for capability in capabilities {
        let capability = capability.trim();
        if capability.is_empty() {
            continue;
        }
        match capability {
            "ai" | "share" => {
                if !normalized.iter().any(|item| item == capability) {
                    normalized.push(capability.to_string());
                }
            }
            _ => {
                return Err(WebError::bad_request_code(
                    "local_access_policy_capability_invalid",
                    "授权 capability 只能是 ai 或 share",
                ));
            }
        }
    }
    normalized.sort();
    Ok(normalized)
}

fn normalize_share_grant_permission(permission: &str) -> Result<String, WebError> {
    let permission = permission.trim();
    match permission {
        "read" | "write" => Ok(permission.to_string()),
        _ => Err(WebError::bad_request_code(
            "local_share_grant_permission_invalid",
            "分享授权 permission 只能是 read 或 write",
        )),
    }
}

fn normalize_share_grant_capabilities(capabilities: &[String]) -> Result<Vec<String>, WebError> {
    let mut normalized = Vec::new();
    for capability in capabilities {
        let capability = capability.trim();
        if capability.is_empty() {
            continue;
        }
        match capability {
            "ai" | "share" => {
                if !normalized.iter().any(|item| item == capability) {
                    normalized.push(capability.to_string());
                }
            }
            _ => {
                return Err(WebError::bad_request_code(
                    "local_share_grant_capability_invalid",
                    "分享授权 capability 只能是 ai 或 share",
                ));
            }
        }
    }
    normalized.sort();
    Ok(normalized)
}

fn generate_local_share_grant_id(share_id: &str, target_user_id: &str, root: &Path) -> String {
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis();
    let mut hasher = DefaultHasher::new();
    share_id.hash(&mut hasher);
    target_user_id.hash(&mut hasher);
    root.display().to_string().hash(&mut hasher);
    format!("share_grant_{now}_{:x}", hasher.finish())
}

fn generate_local_folder_share_id(
    owner_user_id: &str,
    target_user_id: &str,
    root: &Path,
) -> String {
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis();
    let mut hasher = DefaultHasher::new();
    owner_user_id.hash(&mut hasher);
    target_user_id.hash(&mut hasher);
    root.display().to_string().hash(&mut hasher);
    format!("folder_{now}_{:x}", hasher.finish())
}

fn ensure_path_inside_owned_local_workspace(
    actor_id: &str,
    canonical_path: &Path,
) -> Result<(), WebError> {
    let mut cursor = canonical_path;
    loop {
        if let Ok(manifest) = load_local_workspace_manifest(cursor) {
            if manifest.owner_id.trim() == actor_id.trim() {
                return Ok(());
            }
            return Err(WebError::new(
                StatusCode::FORBIDDEN,
                "local_share_grant_owner_workspace_required",
                "普通用户只能授权自己空间下的文件夹",
            ));
        }
        cursor = match cursor.parent() {
            Some(parent) => parent,
            None => break,
        };
    }
    Err(WebError::new(
        StatusCode::FORBIDDEN,
        "local_share_grant_owner_workspace_required",
        "普通用户只能授权自己空间下的文件夹",
    ))
}

fn share_grant_payload(grant: &LocalShareGrant) -> Value {
    json!({
        "id": grant.id,
        "shareId": grant.share_id,
        "ownerUserId": grant.owner_user_id,
        "targetUserId": grant.target_user_id,
        "rootUri": grant.root_uri,
        "rootPath": grant.root_path,
        "documentId": grant.document_id,
        "allowedResourceIds": grant.allowed_resource_ids,
        "permission": grant.permission,
        "capabilities": grant.capabilities,
        "createdAt": grant.created_at,
        "revokedAt": grant.revoked_at,
        "active": grant.active,
    })
}

fn share_link_payload(link: &ShareLinkRecord) -> Value {
    json!({
        "id": link.id,
        "workspaceId": link.workspace_id,
        "resourceKind": link.resource_kind,
        "resourceId": link.resource_id,
        "permission": link.permission,
        "createdBy": link.created_by,
        "expiresAt": link.expires_at,
        "revokedAt": link.revoked_at,
        "createdAt": link.created_at,
        "updatedAt": link.updated_at,
        "revision": link.revision,
    })
}

fn append_control_plane_outbox_event(state: &AppState, event_type: &str, payload: Value) {
    let delta = json!({
        "kind": "control_plane_event",
        "eventType": event_type,
        "payload": payload,
    });
    let _ = state.control_plane().append_outbox(OutboxEventInput {
        topic: "control".to_string(),
        event_type: event_type.to_string(),
        payload_json: serde_json::to_string(&delta).unwrap_or_else(|_| "{}".to_string()),
    });
    let _ = state.stream_delta_tx.send(delta);
}

fn list_sqlite_share_links_for_context(
    state: &AppState,
    context: &RequestContext,
    query: ShareLinkListQuery,
) -> Result<Value, WebError> {
    let _policy = require_local_access_policy_admin(context)?;
    let workspace_id = query.workspace_id.trim();
    if workspace_id.is_empty() {
        return Err(WebError::bad_request_code(
            "share_link_workspace_required",
            "必须提供 workspaceId",
        ));
    }
    let links = state
        .control_plane()
        .list_share_links(workspace_id)
        .map_err(|error| WebError::internal(format!("SQLite 分享链接读取失败: {error}")))?;
    Ok(json!({
        "ok": true,
        "controlPlane": "sqlite",
        "links": links.iter().map(share_link_payload).collect::<Vec<_>>(),
    }))
}

fn create_sqlite_share_link_for_context(
    state: &AppState,
    context: &RequestContext,
    request: LocalShareLinkRequest,
) -> Result<Value, WebError> {
    let _policy = require_local_access_policy_admin(context)?;
    let workspace_id = request.workspace_id.trim();
    let resource_kind = request.resource_kind.trim();
    let resource_id = request.resource_id.trim();
    if workspace_id.is_empty() || resource_kind.is_empty() || resource_id.is_empty() {
        return Err(WebError::bad_request_code(
            "share_link_resource_required",
            "必须提供 workspaceId、resourceKind 和 resourceId",
        ));
    }
    let permission = normalize_share_grant_permission(&request.permission)?;
    let created = state
        .control_plane()
        .create_share_link(CreateShareLinkInput {
            id: None,
            workspace_id: workspace_id.to_string(),
            resource_kind: resource_kind.to_string(),
            resource_id: resource_id.to_string(),
            token: request.token,
            permission,
            created_by: context.auth.actor_id.trim().to_string(),
            expires_at: request.expires_at,
        })
        .map_err(|error| WebError::internal(format!("SQLite 分享链接写入失败: {error}")))?;
    let _ = state.control_plane().append_audit(AppendAuditInput {
        actor_user_id: Some(context.auth.actor_id.trim().to_string()),
        action: "control.share.created".to_string(),
        target_kind: "share_link".to_string(),
        target_id: Some(created.link.id.clone()),
        metadata_json: serde_json::to_string(&json!({
            "workspaceId": created.link.workspace_id,
            "resourceKind": created.link.resource_kind,
            "resourceId": created.link.resource_id,
            "permission": created.link.permission,
        }))
        .unwrap_or_else(|_| "{}".to_string()),
    });
    append_control_plane_outbox_event(
        state,
        "control.share.created",
        json!({
            "link": share_link_payload(&created.link),
        }),
    );
    Ok(json!({
        "ok": true,
        "controlPlane": "sqlite",
        "token": created.token,
        "link": share_link_payload(&created.link),
    }))
}

fn revoke_sqlite_share_link_for_context(
    state: &AppState,
    context: &RequestContext,
    link_id: &str,
) -> Result<Value, WebError> {
    let _policy = require_local_access_policy_admin(context)?;
    let link_id = link_id.trim();
    if link_id.is_empty() {
        return Err(WebError::bad_request_code(
            "share_link_id_required",
            "必须提供 linkId",
        ));
    }
    state
        .control_plane()
        .revoke_share_link(link_id)
        .map_err(|error| match error {
            control_plane::ControlPlaneError::NotFound(message) => {
                WebError::new(StatusCode::NOT_FOUND, "share_link_not_found", message)
            }
            other => WebError::internal(format!("SQLite 分享链接撤销失败: {other}")),
        })?;
    let _ = state.control_plane().append_audit(AppendAuditInput {
        actor_user_id: Some(context.auth.actor_id.trim().to_string()),
        action: "control.share.revoked".to_string(),
        target_kind: "share_link".to_string(),
        target_id: Some(link_id.to_string()),
        metadata_json: "{}".to_string(),
    });
    append_control_plane_outbox_event(
        state,
        "control.share.revoked",
        json!({
            "linkId": link_id,
        }),
    );
    Ok(json!({
        "ok": true,
        "controlPlane": "sqlite",
        "revokedLinkId": link_id,
    }))
}

fn list_local_share_grants_for_context(
    context: &RequestContext,
    admin_required: bool,
) -> Result<Value, WebError> {
    let is_admin = is_local_access_policy_admin_context(context);
    if admin_required {
        require_share_grants_admin(context)?;
    } else {
        let actor_id = context.auth.actor_id.trim();
        if actor_id.is_empty() || actor_id == "anonymous" {
            return Err(WebError::new(
                StatusCode::UNAUTHORIZED,
                "local_share_grants_auth_required",
                "查看分享授权需要先登录",
            ));
        }
    }
    let store = load_local_share_grants_store()?;
    let grants = store
        .grants
        .iter()
        .filter(|grant| {
            if is_admin {
                true
            } else {
                let actor_id = context.auth.actor_id.trim();
                grant.owner_user_id.trim() == actor_id || grant.target_user_id.trim() == actor_id
            }
        })
        .map(share_grant_payload)
        .collect::<Vec<_>>();
    Ok(json!({
        "ok": true,
        "admin": is_admin,
        "grantsPath": local_share_grants_path().display().to_string(),
        "grants": grants,
        "store": {
            "grants": grants,
        }
    }))
}

fn add_local_share_grant_for_context(
    context: &RequestContext,
    mut request: LocalShareGrantRequest,
) -> Result<Value, WebError> {
    require_share_grants_admin(context)?;
    if request.owner_user_id.trim().is_empty() {
        request.owner_user_id = context.auth.actor_id.trim().to_string();
    }
    add_local_share_grant_for_context_inner(context, request, true)
}

fn add_user_share_grant_for_context(
    context: &RequestContext,
    mut request: LocalShareGrantRequest,
) -> Result<Value, WebError> {
    let actor_id = context.auth.actor_id.trim();
    if actor_id.is_empty() || actor_id == "anonymous" {
        return Err(WebError::new(
            StatusCode::UNAUTHORIZED,
            "local_share_grants_auth_required",
            "创建分享授权需要先登录",
        ));
    }
    request.owner_user_id = actor_id.to_string();
    add_local_share_grant_for_context_inner(context, request, false)
}

fn add_local_share_grant_for_context_inner(
    _context: &RequestContext,
    request: LocalShareGrantRequest,
    admin_mode: bool,
) -> Result<Value, WebError> {
    let mut store = load_local_share_grants_store()?;
    let owner_user_id = request.owner_user_id.trim();
    let target_user_id = request.target_user_id.trim();
    if owner_user_id.is_empty() || owner_user_id == "anonymous" {
        return Err(WebError::bad_request_code(
            "local_share_grant_owner_required",
            "必须提供有效 ownerUserId",
        ));
    }
    if target_user_id.is_empty() || target_user_id == "anonymous" {
        return Err(WebError::bad_request_code(
            "local_share_grant_target_required",
            "必须提供有效 targetUserId",
        ));
    }
    let canonical = canonical_root_from_admin_request(&request.root_uri, &request.root_path)?;
    let share_id = request.share_id.trim();
    let share_id = if share_id.is_empty() {
        generate_local_folder_share_id(owner_user_id, target_user_id, &canonical)
    } else {
        share_id.to_string()
    };
    if !admin_mode {
        ensure_path_inside_owned_local_workspace(owner_user_id, &canonical)?;
    }
    let permission = normalize_share_grant_permission(&request.permission)?;
    let capabilities = normalize_share_grant_capabilities(&request.capabilities)?;
    let root_uri = file_uri_for_path(&canonical);
    let allowed_resource_ids = request
        .allowed_resource_ids
        .iter()
        .map(|item| item.trim().to_string())
        .filter(|item| !item.is_empty())
        .collect::<Vec<_>>();
    let grant_id = request.id.trim();
    let grant_id = if grant_id.is_empty() {
        generate_local_share_grant_id(&share_id, target_user_id, &canonical)
    } else {
        grant_id.to_string()
    };
    if store
        .grants
        .iter()
        .any(|grant| grant.id.trim() == grant_id || grant.share_id.trim() == share_id)
    {
        return Err(WebError::bad_request_code(
            "local_share_grant_exists",
            "分享授权已存在",
        ));
    }
    let grant = LocalShareGrant {
        id: grant_id,
        share_id,
        owner_user_id: owner_user_id.to_string(),
        target_user_id: target_user_id.to_string(),
        root_uri,
        root_path: canonical.display().to_string(),
        document_id: request.document_id.trim().to_string(),
        allowed_resource_ids,
        permission,
        capabilities,
        created_at: now_ms().to_string(),
        revoked_at: None,
        active: true,
    };
    store.grants.push(grant.clone());
    write_local_share_grants_store(&store)?;
    Ok(json!({
        "ok": true,
        "grantsPath": local_share_grants_path().display().to_string(),
        "grant": share_grant_payload(&grant),
        "store": store,
    }))
}

fn revoke_local_share_grant_for_context(
    context: &RequestContext,
    share_id: &str,
) -> Result<Value, WebError> {
    require_share_grants_admin(context)?;
    revoke_local_share_grant_for_context_inner(context, share_id, true)
}

fn revoke_user_share_grant_for_context(
    context: &RequestContext,
    share_id: &str,
) -> Result<Value, WebError> {
    let actor_id = context.auth.actor_id.trim();
    if actor_id.is_empty() || actor_id == "anonymous" {
        return Err(WebError::new(
            StatusCode::UNAUTHORIZED,
            "local_share_grants_auth_required",
            "撤销分享授权需要先登录",
        ));
    }
    revoke_local_share_grant_for_context_inner(context, share_id, false)
}

fn revoke_local_share_grant_for_context_inner(
    context: &RequestContext,
    share_id: &str,
    admin_mode: bool,
) -> Result<Value, WebError> {
    let mut store = load_local_share_grants_store()?;
    let share_id = share_id.trim();
    if share_id.is_empty() {
        return Err(WebError::bad_request_code(
            "local_share_grant_share_id_required",
            "必须提供 shareId",
        ));
    }
    let now = now_ms().to_string();
    let mut updated = None;
    for grant in &mut store.grants {
        if grant.share_id.trim() == share_id || grant.id.trim() == share_id {
            if !admin_mode && grant.owner_user_id.trim() != context.auth.actor_id.trim() {
                return Err(WebError::new(
                    StatusCode::FORBIDDEN,
                    "local_share_grant_owner_required",
                    "只能撤销自己创建的分享授权",
                ));
            }
            grant.active = false;
            grant.revoked_at = Some(now.clone());
            updated = Some(grant.clone());
            break;
        }
    }
    let grant = updated.ok_or_else(|| {
        WebError::new(
            StatusCode::NOT_FOUND,
            "local_share_grant_not_found",
            "未找到要撤销的分享授权",
        )
    })?;
    write_local_share_grants_store(&store)?;
    Ok(json!({
        "ok": true,
        "grantsPath": local_share_grants_path().display().to_string(),
        "revokedShareId": share_id,
        "grant": share_grant_payload(&grant),
        "store": store,
    }))
}

fn require_active_share_grant_for_actor(
    context: &RequestContext,
    root_uri: &str,
    share_id: &str,
) -> Result<LocalShareGrant, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_shared_cache_root_unavailable",
            format!("无法访问共享缓存 root: {error}"),
        )
    })?;
    let actor_id = context.auth.actor_id.trim();
    if actor_id.is_empty() || actor_id == "anonymous" {
        return Err(WebError::new(
            StatusCode::UNAUTHORIZED,
            "local_share_grant_auth_required",
            "访问共享缓存需要先登录",
        ));
    }
    let store = load_local_share_grants_store()?;
    let share_id = share_id.trim();
    let grant = store
        .grants
        .into_iter()
        .find(|grant| grant.share_id.trim() == share_id || grant.id.trim() == share_id)
        .ok_or_else(|| {
            WebError::new(
                StatusCode::FORBIDDEN,
                "local_share_grant_not_found",
                "未找到共享授权",
            )
        })?;
    if !grant.active || grant.revoked_at.as_deref().unwrap_or("").trim().len() > 0 {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_share_grant_revoked",
            "共享授权已撤销",
        ));
    }
    if grant.target_user_id.trim() != actor_id && grant.owner_user_id.trim() != actor_id {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_share_grant_actor_mismatch",
            "当前用户不在该共享授权范围内",
        ));
    }
    let Some(grant_root) = local_share_grant_root(&grant) else {
        return Err(WebError::bad_request_code(
            "local_share_grant_root_invalid",
            "共享授权 root 无效",
        ));
    };
    if grant_root != canonical_root {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_share_grant_root_mismatch",
            "共享授权 root 与请求 root 不一致",
        ));
    }
    Ok(grant)
}

fn local_share_grant_root(grant: &LocalShareGrant) -> Option<PathBuf> {
    let root = grant.root_uri.trim();
    let path = if !root.is_empty() {
        parse_file_root_uri(root).ok()?
    } else {
        let root_path = grant.root_path.trim();
        if root_path.is_empty() {
            return None;
        }
        PathBuf::from(root_path)
    };
    path.canonicalize().ok()
}

fn shared_cache_json_path(root: &Path) -> PathBuf {
    root.join(".mnote").join("share-cache.json")
}

fn sync_state_json_path(root: &Path) -> PathBuf {
    root.join(".mnote").join("sync-state.json")
}

fn sync_report_dir(root: &Path) -> PathBuf {
    root.join(".mnote").join("sync-reports")
}

fn read_json_or_default(path: &Path, default_value: Value) -> Value {
    fs::read_to_string(path)
        .ok()
        .and_then(|content| serde_json::from_str::<Value>(&content).ok())
        .unwrap_or(default_value)
}

fn write_json_file(path: &Path, value: &Value) -> Result<(), WebError> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).map_err(|error| {
            WebError::bad_request_code(
                "local_sidecar_dir_create_failed",
                format!("无法创建本地 sidecar 目录 {}: {error}", parent.display()),
            )
        })?;
    }
    let content = serde_json::to_string_pretty(value)
        .map_err(|error| WebError::internal(format!("无法序列化本地 sidecar JSON: {error}")))?;
    fs::write(path, format!("{content}\n")).map_err(|error| {
        WebError::bad_request_code(
            "local_sidecar_write_failed",
            format!("无法写入本地 sidecar 文件 {}: {error}", path.display()),
        )
    })
}

fn record_shared_cache_for_context(
    context: &RequestContext,
    request: SharedCacheRecordRequest,
) -> Result<Value, WebError> {
    let grant =
        require_active_share_grant_for_actor(context, &request.root_uri, &request.share_id)?;
    let root = parse_file_root_uri(&request.root_uri)?
        .canonicalize()
        .map_err(|error| {
            WebError::bad_request_code(
                "local_shared_cache_root_unavailable",
                format!("无法访问共享缓存 root: {error}"),
            )
        })?;
    let permission = normalize_share_grant_permission(&request.permission)?;
    if permission != grant.permission.trim() {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_shared_cache_permission_mismatch",
            "共享缓存权限必须来自 canonical share grant",
        ));
    }
    let cache_entry = json!({
        "schema": "mnote.share_cache.v1",
        "shareId": grant.share_id,
        "permission": permission,
        "rootUri": grant.root_uri,
        "allowedResourceIds": grant.allowed_resource_ids,
        "remoteVersion": request.remote_version,
        "baseVersion": request.base_version,
        "sourceActor": request.source_actor,
        "syncedAt": now_ms(),
    });
    let path = shared_cache_json_path(&root);
    let mut cache = read_json_or_default(
        &path,
        json!({"schema": "mnote.share_cache.v1", "shares": {}}),
    );
    cache["shares"][grant.share_id.clone()] = cache_entry.clone();
    write_json_file(&path, &cache)?;
    Ok(json!({
        "ok": true,
        "path": path.display().to_string(),
        "share": cache_entry,
    }))
}

fn record_sync_pending_change_for_context(
    context: &RequestContext,
    request: SyncPendingChangeRequest,
) -> Result<Value, WebError> {
    let grant =
        require_active_share_grant_for_actor(context, &request.root_uri, &request.share_id)?;
    if normalize_share_grant_permission(&grant.permission)? != "write" {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_sync_shared_read_write_forbidden",
            "shared_read 不能记录待同步写入",
        ));
    }
    if !grant
        .allowed_resource_ids
        .iter()
        .any(|resource_id| resource_id == &request.resource_id)
    {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_sync_resource_not_shared",
            "待同步资源不在 share grant 授权范围内",
        ));
    }
    if !request.remote_version.trim().is_empty()
        && !request.base_version.trim().is_empty()
        && request.remote_version.trim() != request.base_version.trim()
    {
        return Err(WebError::new(
            StatusCode::CONFLICT,
            "local_sync_base_version_conflict",
            "同步前远端版本已变化，需要生成冲突报告",
        ));
    }
    let root = parse_file_root_uri(&request.root_uri)?
        .canonicalize()
        .map_err(|error| {
            WebError::bad_request_code(
                "local_sync_root_unavailable",
                format!("无法访问同步 root: {error}"),
            )
        })?;
    let path = sync_state_json_path(&root);
    let mut state = read_json_or_default(
        &path,
        json!({"schema": "mnote.sync_state.v1", "pendingChanges": []}),
    );
    let pending = json!({
        "shareId": grant.share_id,
        "resourceId": request.resource_id,
        "localVersion": request.local_version,
        "baseVersion": request.base_version,
        "remoteVersion": request.remote_version,
        "actorId": context.auth.actor_id,
        "changeSummary": request.change_summary,
        "createdAt": now_ms(),
    });
    state["pendingChanges"]
        .as_array_mut()
        .ok_or_else(|| {
            WebError::bad_request_code(
                "local_sync_state_invalid",
                "sync-state pendingChanges 非数组",
            )
        })?
        .push(pending.clone());
    write_json_file(&path, &state)?;
    Ok(json!({
        "ok": true,
        "path": path.display().to_string(),
        "pendingChange": pending,
    }))
}

fn write_sync_conflict_report_for_context(
    context: &RequestContext,
    request: SyncConflictReportRequest,
) -> Result<Value, WebError> {
    let grant =
        require_active_share_grant_for_actor(context, &request.root_uri, &request.share_id)?;
    if !grant
        .allowed_resource_ids
        .iter()
        .any(|resource_id| resource_id == &request.resource_id)
    {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_sync_report_resource_not_shared",
            "冲突报告资源不在 share grant 授权范围内",
        ));
    }
    let root = parse_file_root_uri(&request.root_uri)?
        .canonicalize()
        .map_err(|error| {
            WebError::bad_request_code(
                "local_sync_report_root_unavailable",
                format!("无法访问同步报告 root: {error}"),
            )
        })?;
    let report = json!({
        "schema": "mnote.sync_conflict_report.v1",
        "source": "shared_sync",
        "shareId": grant.share_id,
        "resourceId": request.resource_id,
        "localVersion": request.local_version,
        "remoteVersion": request.remote_version,
        "baseVersion": request.base_version,
        "actorId": if request.actor_id.trim().is_empty() { context.auth.actor_id.clone() } else { request.actor_id },
        "summary": request.summary,
        "createdAt": now_ms(),
        "suggestedActions": ["accept_local", "accept_remote", "open_diff_merge"],
    });
    let dir = sync_report_dir(&root);
    fs::create_dir_all(&dir).map_err(|error| {
        WebError::bad_request_code(
            "local_sync_report_dir_create_failed",
            format!("无法创建同步报告目录 {}: {error}", dir.display()),
        )
    })?;
    let path = dir.join(format!(
        "sync-conflict-{}-{}.json",
        sanitize_file_name(&grant.share_id, "share"),
        now_ms()
    ));
    write_json_file(&path, &report)?;
    Ok(json!({
        "ok": true,
        "path": path.display().to_string(),
        "report": report,
    }))
}

fn control_plane_grant_payload(grant: &DirectoryGrantRecord) -> Value {
    let capabilities = serde_json::from_str::<Vec<String>>(&grant.capabilities_json)
        .unwrap_or_else(|_| Vec::new());
    json!({
        "id": grant.id,
        "userId": grant.user_id,
        "workspaceId": grant.workspace_id,
        "rootUri": grant.root_uri,
        "rootPath": grant.root_path,
        "permission": grant.permission,
        "recursive": grant.recursive,
        "capabilities": capabilities,
        "source": grant.source,
        "status": grant.status,
        "createdBy": grant.created_by,
        "createdAt": grant.created_at,
        "updatedAt": grant.updated_at,
        "revision": grant.revision,
    })
}

fn is_default_workspace_auto_grant(grant: &DirectoryGrantRecord) -> bool {
    grant.source.trim() == "auto"
        && grant.permission.trim() == "write"
        && grant.recursive
        && grant.created_by.as_deref().map(str::trim) == Some(grant.user_id.as_str())
        && grant.workspace_id.is_some()
        && grant.root_uri.starts_with("local://users/")
        && grant.root_uri.ends_with("/workspaces/my-space")
}

fn sqlite_access_policy_payload(
    state: &AppState,
    context: &RequestContext,
) -> Result<Value, WebError> {
    let policy = require_local_access_policy_admin(context)?;
    let grants = state
        .control_plane()
        .list_directory_grants()
        .map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?;
    let grant_values = grants
        .iter()
        .map(control_plane_grant_payload)
        .collect::<Vec<_>>();
    Ok(json!({
        "ok": true,
        "controlPlane": "sqlite",
        "policyPath": local_access_policy_path().display().to_string(),
        "policy": {
            "admins": policy.admins,
            "grants": grant_values,
        },
        "effectiveAdmins": {
            "policy": policy.admins,
            "env": env_local_admin_ids(),
        }
    }))
}

fn sqlite_user_access_policy_payload(
    state: &AppState,
    context: &RequestContext,
) -> Result<Value, WebError> {
    let actor_id = context.auth.actor_id.trim();
    if actor_id.is_empty() || actor_id == "anonymous" {
        return Err(WebError::new(
            StatusCode::UNAUTHORIZED,
            "local_access_policy_auth_required",
            "查看文件夹授权需要先登录",
        ));
    }
    let grants = state
        .control_plane()
        .list_directory_grants()
        .map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?;
    let grant_values = grants
        .iter()
        .filter(|grant| {
            grant.user_id.trim() == actor_id
                || grant.created_by.as_deref().map(str::trim) == Some(actor_id)
        })
        .map(control_plane_grant_payload)
        .collect::<Vec<_>>();
    Ok(json!({
        "ok": true,
        "controlPlane": "sqlite",
        "policy": {
            "grants": grant_values,
        },
        "grants": grant_values,
    }))
}

fn validate_local_access_root_for_context(
    context: &RequestContext,
    request: LocalAccessValidateRootRequest,
) -> Result<Value, WebError> {
    let policy = require_local_access_policy_admin(context)?;
    let canonical = canonical_root_from_admin_request(&request.root_uri, &request.root_path)?;
    Ok(json!({
        "ok": true,
        "policyPath": local_access_policy_path().display().to_string(),
        "rootPath": canonical.display().to_string(),
        "rootUri": file_uri_for_path(&canonical),
        "admin": is_local_admin(&context.auth.actor_id, &context.auth.actor_type, &policy),
    }))
}

fn add_sqlite_user_access_grant_for_context(
    state: &AppState,
    context: &RequestContext,
    mut request: LocalAccessGrantRequest,
) -> Result<Value, WebError> {
    let actor_id = context.auth.actor_id.trim();
    if actor_id.is_empty() || actor_id == "anonymous" {
        return Err(WebError::new(
            StatusCode::UNAUTHORIZED,
            "local_access_policy_auth_required",
            "创建文件夹授权需要先登录",
        ));
    }
    let canonical = canonical_root_from_admin_request(&request.root_uri, &request.root_path)?;
    ensure_path_inside_owned_local_workspace(actor_id, &canonical)?;
    request.root_uri = file_uri_for_path(&canonical);
    request.root_path = canonical.display().to_string();
    add_sqlite_local_access_grant_for_context_inner(state, context, request, "user")
}

fn add_sqlite_local_access_grant_for_context(
    state: &AppState,
    context: &RequestContext,
    request: LocalAccessGrantRequest,
) -> Result<Value, WebError> {
    let _policy = require_local_access_policy_admin(context)?;
    add_sqlite_local_access_grant_for_context_inner(state, context, request, "admin")
}

fn add_sqlite_local_access_grant_for_context_inner(
    state: &AppState,
    context: &RequestContext,
    request: LocalAccessGrantRequest,
    source: &str,
) -> Result<Value, WebError> {
    let user_id = request.user_id.trim();
    if user_id.is_empty() || user_id == "anonymous" {
        return Err(WebError::bad_request_code(
            "local_access_policy_user_required",
            "必须提供有效 userId",
        ));
    }
    let canonical = canonical_root_from_admin_request(&request.root_uri, &request.root_path)?;
    let permission = normalize_local_access_permission(&request.permission)?;
    let capabilities = normalize_local_access_capabilities(&request.capabilities)?;
    let root_uri = file_uri_for_path(&canonical);
    let duplicates = state
        .control_plane()
        .find_directory_grants(DirectoryGrantLookup {
            grant_id: None,
            user_id: Some(user_id.to_string()),
            root_uri: Some(root_uri.clone()),
            include_revoked: false,
        })
        .map_err(|error| WebError::internal(format!("SQLite 控制面授权查重失败: {error}")))?;
    if duplicates.iter().any(|grant| {
        grant.permission.trim() == permission
            && grant.recursive == request.recursive
            && serde_json::from_str::<Vec<String>>(&grant.capabilities_json).unwrap_or_default()
                == capabilities
    }) {
        return Err(WebError::bad_request_code(
            "local_access_policy_grant_duplicate",
            "相同目录授权已存在",
        ));
    }
    let grant = state
        .control_plane()
        .grant_directory_access(DirectoryGrantInput {
            user_id: user_id.to_string(),
            workspace_id: None,
            root_uri,
            root_path: canonical.display().to_string(),
            permission,
            recursive: request.recursive,
            capabilities,
            source: source.to_string(),
            created_by: Some(context.auth.actor_id.trim().to_string()),
        })
        .map_err(|error| WebError::internal(format!("SQLite 控制面授权写入失败: {error}")))?;
    let _ = state.control_plane().append_audit(AppendAuditInput {
        actor_user_id: Some(context.auth.actor_id.trim().to_string()),
        action: "control.grant.created".to_string(),
        target_kind: "directory_grant".to_string(),
        target_id: Some(grant.id.clone()),
        metadata_json: serde_json::to_string(&json!({
            "userId": grant.user_id,
            "rootUri": grant.root_uri,
            "permission": grant.permission,
        }))
        .unwrap_or_else(|_| "{}".to_string()),
    });
    append_control_plane_outbox_event(
        state,
        "control.grant.created",
        json!({
            "grant": control_plane_grant_payload(&grant),
        }),
    );
    let grants = state
        .control_plane()
        .list_directory_grants()
        .map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?;
    let grant_values = grants
        .iter()
        .map(control_plane_grant_payload)
        .collect::<Vec<_>>();
    Ok(json!({
        "ok": true,
        "controlPlane": "sqlite",
        "policyPath": local_access_policy_path().display().to_string(),
        "grant": control_plane_grant_payload(&grant),
        "policy": {
            "grants": grant_values,
        },
    }))
}

fn delete_sqlite_user_access_grant_for_context(
    state: &AppState,
    context: &RequestContext,
    grant_id: &str,
) -> Result<Value, WebError> {
    let actor_id = context.auth.actor_id.trim();
    if actor_id.is_empty() || actor_id == "anonymous" {
        return Err(WebError::new(
            StatusCode::UNAUTHORIZED,
            "local_access_policy_auth_required",
            "撤销文件夹授权需要先登录",
        ));
    }
    let grant_id = grant_id.trim();
    if grant_id.is_empty() {
        return Err(WebError::bad_request_code(
            "local_access_policy_grant_id_required",
            "必须提供 grantId",
        ));
    }
    let grants = state
        .control_plane()
        .find_directory_grants(DirectoryGrantLookup {
            grant_id: Some(grant_id.to_string()),
            user_id: None,
            root_uri: None,
            include_revoked: false,
        })
        .map_err(|error| WebError::internal(format!("SQLite 控制面授权查找失败: {error}")))?;
    let grant = grants.first().ok_or_else(|| {
        WebError::new(
            StatusCode::NOT_FOUND,
            "local_access_policy_grant_not_found",
            "目录授权不存在或不属于当前用户",
        )
    })?;
    if is_default_workspace_auto_grant(grant) {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_access_policy_system_grant_readonly",
            "默认空间的系统授权不能撤销",
        ));
    }
    if grant.created_by.as_deref().map(str::trim) != Some(actor_id) {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_access_policy_grant_owner_required",
            "普通用户只能撤销自己创建的文件夹授权",
        ));
    }
    state
        .control_plane()
        .revoke_directory_grant(grant_id, None)
        .map_err(|error| match error {
            control_plane::ControlPlaneError::NotFound(message) => WebError::new(
                StatusCode::NOT_FOUND,
                "local_access_policy_grant_not_found",
                message,
            ),
            other => WebError::internal(format!("SQLite 控制面授权撤销失败: {other}")),
        })?;
    let _ = state.control_plane().append_audit(AppendAuditInput {
        actor_user_id: Some(actor_id.to_string()),
        action: "control.grant.revoked".to_string(),
        target_kind: "directory_grant".to_string(),
        target_id: Some(grant_id.to_string()),
        metadata_json: "{}".to_string(),
    });
    append_control_plane_outbox_event(
        state,
        "control.grant.revoked",
        json!({
            "grantId": grant_id,
        }),
    );
    Ok(json!({
        "ok": true,
        "controlPlane": "sqlite",
        "deletedGrantId": grant_id,
        "policy": {
            "grants": state
                .control_plane()
                .list_directory_grants()
                .map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?
                .iter()
                .filter(|grant| grant.created_by.as_deref().map(str::trim) == Some(actor_id))
                .map(control_plane_grant_payload)
                .collect::<Vec<_>>(),
        },
    }))
}

fn delete_sqlite_local_access_grant_for_context(
    state: &AppState,
    context: &RequestContext,
    grant_id: &str,
) -> Result<Value, WebError> {
    let _policy = require_local_access_policy_admin(context)?;
    let grant_id = grant_id.trim();
    if grant_id.is_empty() {
        return Err(WebError::bad_request_code(
            "local_access_policy_grant_id_required",
            "必须提供 grantId",
        ));
    }
    let grants = state
        .control_plane()
        .find_directory_grants(DirectoryGrantLookup {
            grant_id: Some(grant_id.to_string()),
            user_id: None,
            root_uri: None,
            include_revoked: false,
        })
        .map_err(|error| WebError::internal(format!("SQLite 控制面授权查找失败: {error}")))?;
    if grants
        .first()
        .map(is_default_workspace_auto_grant)
        .unwrap_or(false)
    {
        return Err(WebError::new(
            StatusCode::FORBIDDEN,
            "local_access_policy_system_grant_readonly",
            "默认空间的系统授权不能撤销",
        ));
    }
    state
        .control_plane()
        .revoke_directory_grant(grant_id, None)
        .map_err(|error| match error {
            control_plane::ControlPlaneError::NotFound(message) => WebError::new(
                StatusCode::NOT_FOUND,
                "local_access_policy_grant_not_found",
                message,
            ),
            other => WebError::internal(format!("SQLite 控制面授权撤销失败: {other}")),
        })?;
    let _ = state.control_plane().append_audit(AppendAuditInput {
        actor_user_id: Some(context.auth.actor_id.trim().to_string()),
        action: "control.grant.revoked".to_string(),
        target_kind: "directory_grant".to_string(),
        target_id: Some(grant_id.to_string()),
        metadata_json: "{}".to_string(),
    });
    append_control_plane_outbox_event(
        state,
        "control.grant.revoked",
        json!({
            "grantId": grant_id,
        }),
    );
    Ok(json!({
        "ok": true,
        "controlPlane": "sqlite",
        "policyPath": local_access_policy_path().display().to_string(),
        "deletedGrantId": grant_id,
        "policy": {
            "grants": state
                .control_plane()
                .list_directory_grants()
                .map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?
                .iter()
                .map(control_plane_grant_payload)
                .collect::<Vec<_>>(),
        },
    }))
}

fn is_local_admin(actor_id: &str, actor_type: &str, policy: &LocalAccessPolicy) -> bool {
    if actor_type.trim() == "admin" {
        return true;
    }
    let actor_id = actor_id.trim();
    if actor_id.is_empty() {
        return false;
    }
    if policy.admins.iter().any(|admin| admin.trim() == actor_id) {
        return true;
    }
    std::env::var(ENV_LOCAL_ADMIN_USER_IDS)
        .ok()
        .map(|value| {
            value
                .split([',', ';', '\n', ' '])
                .map(str::trim)
                .filter(|item| !item.is_empty())
                .any(|item| item == actor_id)
        })
        .unwrap_or(false)
}

fn local_access_policy_allows(
    policy: &LocalAccessPolicy,
    actor_id: &str,
    canonical_root: &Path,
    mode: LocalAccessMode,
) -> bool {
    let actor_id = actor_id.trim();
    if actor_id.is_empty() {
        return false;
    }
    policy.grants.iter().any(|grant| {
        if grant.user_id.trim() != actor_id {
            return false;
        }
        if !local_access_permission_allows(grant.permission.as_str(), mode) {
            return false;
        }
        let Some(grant_root) = local_access_grant_root(grant) else {
            return false;
        };
        if grant.recursive {
            canonical_root.starts_with(&grant_root)
        } else {
            canonical_root == grant_root
        }
    })
}

fn local_access_permission_allows(permission: &str, mode: LocalAccessMode) -> bool {
    let permission = permission.trim();
    if permission == "admin" {
        return true;
    }
    match mode {
        LocalAccessMode::Read => permission == "read" || permission == "write",
        LocalAccessMode::Write => permission == "write",
    }
}

fn local_access_grant_root(grant: &LocalAccessGrant) -> Option<PathBuf> {
    let root = grant.root_uri.trim();
    let path = if !root.is_empty() {
        parse_file_root_uri(root).ok()?
    } else {
        let root_path = grant.root_path.trim();
        if root_path.is_empty() {
            return None;
        }
        PathBuf::from(root_path)
    };
    path.canonicalize().ok()
}

pub(crate) fn create_default_local_workspace_for_actor(
    actor_id: &str,
    actor_type: &str,
) -> Result<Value, WebError> {
    create_default_local_workspace_for_actor_at_base(
        actor_id,
        actor_type,
        &default_local_workspace_base_dir(),
    )
}

fn create_default_local_workspace_for_actor_at_base(
    actor_id: &str,
    actor_type: &str,
    base_dir: &Path,
) -> Result<Value, WebError> {
    let actor_id = actor_id.trim();
    let actor_type = actor_type.trim();
    if actor_id.is_empty()
        || actor_id == "anonymous"
        || actor_type.is_empty()
        || actor_type == "anonymous"
    {
        return Err(WebError::new(
            StatusCode::UNAUTHORIZED,
            "local_workspace_auth_required",
            "创建本地工作区需要先登录",
        ));
    }

    fs::create_dir_all(base_dir).map_err(|error| {
        WebError::bad_request_code(
            "local_workspace_base_create_failed",
            format!(
                "无法创建本地工作区数据根目录 {}: {error}",
                base_dir.display()
            ),
        )
    })?;
    let canonical_base = base_dir.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_workspace_base_unavailable",
            format!("无法访问本地工作区数据根目录: {error}"),
        )
    })?;
    let actor_segment = encode_local_id_segment(actor_id);
    let workspace_root = canonical_base
        .join("users")
        .join(&actor_segment)
        .join("workspaces")
        .join(DEFAULT_LOCAL_WORKSPACE_SLUG);
    fs::create_dir_all(&workspace_root).map_err(|error| {
        WebError::bad_request_code(
            "local_workspace_create_failed",
            format!(
                "无法创建默认本地工作区 {}: {error}",
                workspace_root.display()
            ),
        )
    })?;
    let canonical_root = workspace_root.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_workspace_unavailable",
            format!("无法访问默认本地工作区: {error}"),
        )
    })?;
    if !canonical_root.starts_with(&canonical_base) {
        return Err(WebError::bad_request_code(
            "local_workspace_root_escape",
            "默认本地工作区不能越过受管数据根目录",
        ));
    }

    let manifest = ensure_default_workspace_manifest(actor_id, &canonical_root)?;
    ensure_default_workspace_directories(&canonical_root)?;
    Ok(json!({
        "ok": true,
        "workspace": {
            "rootUri": file_uri_for_path(&canonical_root),
            "rootPath": canonical_root.display().to_string(),
            "baseDir": canonical_base.display().to_string(),
            "manifest": local_workspace_manifest_json(&manifest),
        }
    }))
}

pub async fn create_default_local_workspace(
    Extension(context): Extension<RequestContext>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload =
        create_default_local_workspace_for_actor(&context.auth.actor_id, &context.auth.actor_type)
            .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn get_local_access_policy(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = sqlite_access_policy_payload(&state, &context)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn get_user_access_policy(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = sqlite_user_access_policy_payload(&state, &context)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn validate_local_access_root(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<LocalAccessValidateRootRequest>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = validate_local_access_root_for_context(&context, request)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn create_local_access_grant(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Json(request): Json<LocalAccessGrantRequest>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = add_sqlite_local_access_grant_for_context(&state, &context, request)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn create_user_access_grant(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Json(request): Json<LocalAccessGrantRequest>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = add_sqlite_user_access_grant_for_context(&state, &context, request)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn delete_local_access_grant(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    AxumPath(grant_id): AxumPath<String>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = delete_sqlite_local_access_grant_for_context(&state, &context, &grant_id)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn delete_user_access_grant(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    AxumPath(grant_id): AxumPath<String>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = delete_sqlite_user_access_grant_for_context(&state, &context, &grant_id)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn get_share_grants(
    Extension(context): Extension<RequestContext>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = list_local_share_grants_for_context(&context, true)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn get_user_share_grants(
    Extension(context): Extension<RequestContext>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = list_local_share_grants_for_context(&context, false)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn get_share_links(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Query(query): Query<ShareLinkListQuery>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = list_sqlite_share_links_for_context(&state, &context, query)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn create_share_link(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Json(request): Json<LocalShareLinkRequest>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = create_sqlite_share_link_for_context(&state, &context, request)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn delete_share_link(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    AxumPath(link_id): AxumPath<String>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = revoke_sqlite_share_link_for_context(&state, &context, &link_id)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn create_share_grant(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<LocalShareGrantRequest>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = add_local_share_grant_for_context(&context, request)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn delete_share_grant(
    Extension(context): Extension<RequestContext>,
    AxumPath(share_id): AxumPath<String>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = revoke_local_share_grant_for_context(&context, &share_id)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn create_user_share_grant(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<LocalShareGrantRequest>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = add_user_share_grant_for_context(&context, request)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn delete_user_share_grant(
    Extension(context): Extension<RequestContext>,
    AxumPath(share_id): AxumPath<String>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = revoke_user_share_grant_for_context(&context, &share_id)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn record_shared_cache(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<SharedCacheRecordRequest>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = record_shared_cache_for_context(&context, request)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn record_sync_pending_change(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<SyncPendingChangeRequest>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = record_sync_pending_change_for_context(&context, request)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

pub async fn write_sync_conflict_report(
    Extension(context): Extension<RequestContext>,
    Json(request): Json<SyncConflictReportRequest>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let payload = write_sync_conflict_report_for_context(&context, request)
        .map_err(|error| error.with_context(&context))?;
    Ok((StatusCode::OK, Json(payload)))
}

#[cfg(test)]
pub(crate) fn initialize_local_workspace_for_actor(
    actor_id: &str,
    root_uri: &str,
) -> Result<Value, WebError> {
    let actor_id = actor_id.trim();
    if actor_id.is_empty() || actor_id == "anonymous" {
        return Err(WebError::new(
            StatusCode::UNAUTHORIZED,
            "local_workspace_auth_required",
            "创建本地工作区需要先登录",
        ));
    }
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    if !canonical_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_not_directory",
            "本地 rootUri 必须指向目录",
        ));
    }
    let manifest_path = local_workspace_manifest_path(&canonical_root);
    if manifest_path.exists() {
        let manifest = load_local_workspace_manifest(&canonical_root)?;
        if manifest.owner_id.trim() != actor_id {
            return Err(WebError::new(
                StatusCode::FORBIDDEN,
                "local_workspace_access_denied",
                "当前用户无权接管该本地工作区",
            ));
        }
        return Ok(local_workspace_manifest_json(&manifest));
    }
    let manifest = LocalWorkspaceManifest {
        workspace_id: format!(
            "local-ws:{}",
            encode_local_id_segment(&format!("{}:{}", actor_id, canonical_root.display()))
        ),
        owner_id: actor_id.to_string(),
        created_at: now_ms().to_string(),
        capabilities: vec![
            "local_files".to_string(),
            "tree_commands".to_string(),
            "markdown_edit".to_string(),
            "asset_upload".to_string(),
        ],
    };
    write_local_workspace_manifest(&canonical_root, &manifest)?;
    Ok(local_workspace_manifest_json(&manifest))
}

fn default_local_workspace_base_dir() -> PathBuf {
    std::env::var("MNOTE_LOCAL_WORKSPACE_BASE_DIR")
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from(DEFAULT_LOCAL_WORKSPACE_BASE_DIR))
}

fn ensure_default_workspace_manifest(
    actor_id: &str,
    root: &Path,
) -> Result<LocalWorkspaceManifest, WebError> {
    let manifest_path = local_workspace_manifest_path(root);
    if manifest_path.exists() {
        let manifest = load_local_workspace_manifest(root)?;
        if manifest.owner_id.trim() != actor_id {
            return Err(WebError::new(
                StatusCode::FORBIDDEN,
                "local_workspace_access_denied",
                "当前用户无权接管该本地工作区",
            ));
        }
        return Ok(manifest);
    }
    let manifest = LocalWorkspaceManifest {
        workspace_id: format!(
            "local-ws:{}:{}",
            encode_local_id_segment(actor_id),
            DEFAULT_LOCAL_WORKSPACE_SLUG
        ),
        owner_id: actor_id.to_string(),
        created_at: now_ms().to_string(),
        capabilities: vec![
            "local_files".to_string(),
            "tree_commands".to_string(),
            "markdown_edit".to_string(),
            "asset_upload".to_string(),
        ],
    };
    write_local_workspace_manifest(root, &manifest)?;
    Ok(manifest)
}

fn ensure_default_workspace_directories(root: &Path) -> Result<(), WebError> {
    for relative in [".mnote/trash"] {
        let path = root.join(relative);
        if !path.starts_with(root) {
            return Err(WebError::bad_request_code(
                "local_workspace_root_escape",
                "默认本地工作区目录不能越过 root",
            ));
        }
        fs::create_dir_all(&path).map_err(|error| {
            WebError::bad_request_code(
                "local_workspace_create_failed",
                format!("无法创建默认本地工作区目录 {}: {error}", path.display()),
            )
        })?;
    }
    Ok(())
}

fn local_workspace_manifest_path(root: &Path) -> PathBuf {
    root.join(".mnote").join("workspace.json")
}

fn load_local_workspace_manifest(root: &Path) -> Result<LocalWorkspaceManifest, WebError> {
    let manifest_path = local_workspace_manifest_path(root);
    let content = fs::read_to_string(&manifest_path).map_err(|error| {
        WebError::new(
            StatusCode::FORBIDDEN,
            "local_workspace_manifest_missing",
            format!("缺少本地工作区清单 {}: {error}", manifest_path.display()),
        )
    })?;
    let manifest = serde_json::from_str::<LocalWorkspaceManifest>(&content).map_err(|error| {
        WebError::bad_request_code(
            "local_workspace_manifest_invalid",
            format!("本地工作区清单格式非法: {error}"),
        )
    })?;
    if manifest.workspace_id.trim().is_empty() || manifest.owner_id.trim().is_empty() {
        return Err(WebError::bad_request_code(
            "local_workspace_manifest_invalid",
            "本地工作区清单缺少 workspaceId / ownerId",
        ));
    }
    Ok(manifest)
}

fn write_local_workspace_manifest(
    root: &Path,
    manifest: &LocalWorkspaceManifest,
) -> Result<(), WebError> {
    let metadata_dir = root.join(".mnote");
    fs::create_dir_all(&metadata_dir).map_err(|error| {
        WebError::bad_request_code(
            "local_workspace_manifest_write_failed",
            format!(
                "无法创建本地工作区元数据目录 {}: {error}",
                metadata_dir.display()
            ),
        )
    })?;
    let manifest_path = local_workspace_manifest_path(root);
    let content = serde_json::to_string_pretty(manifest)
        .map_err(|error| WebError::internal(format!("本地工作区清单序列化失败: {error}")))?;
    fs::write(&manifest_path, content).map_err(|error| {
        WebError::bad_request_code(
            "local_workspace_manifest_write_failed",
            format!(
                "无法写入本地工作区清单 {}: {error}",
                manifest_path.display()
            ),
        )
    })
}

fn local_workspace_manifest_json(manifest: &LocalWorkspaceManifest) -> Value {
    json!({
        "workspaceId": manifest.workspace_id,
        "ownerId": manifest.owner_id,
        "createdAt": manifest.created_at,
        "capabilities": manifest.capabilities,
    })
}

pub fn load_local_folder_file_tree_snapshot(
    root_uri: &str,
) -> Result<ProjectionSnapshot, WebError> {
    load_local_folder_file_tree_scope_snapshot(root_uri, None, None)
}

pub fn load_local_folder_file_tree_snapshot_with_reveal(
    root_uri: &str,
    reveal_document_id: Option<&str>,
) -> Result<ProjectionSnapshot, WebError> {
    let reveal_relative_path = reveal_document_id
        .and_then(local_markdown_relative_path_from_document_id)
        .map(|path| path.replace('\\', "/"));
    load_local_folder_file_tree_scope_snapshot(root_uri, None, reveal_relative_path.as_deref())
}

pub fn load_local_folder_file_tree_children_snapshot(
    root_uri: &str,
    parent_relative_path: &str,
) -> Result<ProjectionSnapshot, WebError> {
    load_local_folder_file_tree_scope_snapshot(root_uri, Some(parent_relative_path), None)
}

fn load_local_folder_file_tree_scope_snapshot(
    root_uri: &str,
    parent_relative_path: Option<&str>,
    reveal_relative_path: Option<&str>,
) -> Result<ProjectionSnapshot, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    if !canonical_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_not_directory",
            "本地 rootUri 必须指向目录",
        ));
    }

    let workspace_id = local_workspace_id(&canonical_root);
    let root_source_uri = file_uri_for_path(&canonical_root);
    let metadata = load_local_folder_metadata(&canonical_root)?;
    let parent_relative_path = parent_relative_path
        .map(str::trim)
        .filter(|value| !value.is_empty() && *value != ".")
        .unwrap_or("");
    let scan_root = if parent_relative_path.is_empty() {
        canonical_root.clone()
    } else {
        resolve_metadata_relative_path(&canonical_root, parent_relative_path)?
    };
    if !scan_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_parent_not_directory",
            "FileTree 懒加载 parentRelativePath 必须指向目录",
        ));
    }
    let parent_node_id = if parent_relative_path.is_empty() {
        None
    } else {
        Some(local_node_id(parent_relative_path))
    };
    let depth = local_folder_relative_depth(parent_relative_path);
    let mut scan_result = scan_directory_shallow_with_revision(
        &canonical_root,
        &scan_root,
        parent_node_id,
        depth,
        &root_source_uri,
        &workspace_id,
        &metadata,
    )?;
    if parent_relative_path.is_empty() {
        append_file_tree_reveal_rows(
            &canonical_root,
            reveal_relative_path,
            &root_source_uri,
            &workspace_id,
            &metadata,
            &mut scan_result.rows,
        )?;
    }

    let items = scan_result
        .rows
        .iter()
        .map(local_folder_row_to_projection_item)
        .collect::<Vec<_>>();
    let dataset = json!({
        "workspace": {
            "id": &workspace_id,
            "sourceKind": "local_folder",
            "rootUri": root_source_uri,
        },
        "nodes": [],
        "edges": [],
        "documents": [],
        "media_assets": [],
        "mindmap_assets": [],
        "table_assets": [],
        "mindmap_asset_children": {},
    });
    let projection = json!({
        "projection": "file_tree",
        "sourceKind": "local_folder",
        "rootUri": root_source_uri,
        "parentRelativePath": parent_relative_path,
        "lazy": true,
        "watchRevision": scan_result.watch_revision,
        "items": items,
    });

    Ok(ProjectionSnapshot {
        dataset,
        projection,
    })
}

pub fn load_local_folder_page_tree_snapshot(
    root_uri: &str,
) -> Result<ProjectionSnapshot, WebError> {
    load_local_folder_page_tree_snapshot_for_scope(root_uri, None)
}

pub fn load_local_folder_page_tree_scope_snapshot(
    root_uri: &str,
    parent_relative_path: &str,
) -> Result<ProjectionSnapshot, WebError> {
    load_local_folder_page_tree_snapshot_for_scope(root_uri, Some(parent_relative_path))
}

fn load_local_folder_page_tree_snapshot_for_scope(
    root_uri: &str,
    parent_relative_path: Option<&str>,
) -> Result<ProjectionSnapshot, WebError> {
    #[cfg(test)]
    LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);

    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    if !canonical_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_not_directory",
            "本地 rootUri 必须指向目录",
        ));
    }

    let workspace_id = local_workspace_id(&canonical_root);
    let root_source_uri = file_uri_for_path(&canonical_root);
    let parent_relative_path = parent_relative_path
        .map(str::trim)
        .filter(|value| !value.is_empty() && *value != ".")
        .unwrap_or("");
    let scan_root = if parent_relative_path.is_empty() {
        canonical_root.clone()
    } else {
        resolve_metadata_relative_path(&canonical_root, parent_relative_path)?
    };
    if !scan_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_parent_not_directory",
            "PageTree scope parentRelativePath 必须指向目录",
        ));
    }
    let watch_revision = local_folder_watch_revision_for_root(&canonical_root, &root_source_uri)?;
    let cache_key = format!("{root_source_uri}\n{parent_relative_path}");
    if let Ok(cache) = local_page_tree_snapshot_cache().lock() {
        if let Some(entry) = cache.get(&cache_key) {
            if entry.watch_revision.revision == watch_revision.revision
                && entry.watch_revision.entry_count == watch_revision.entry_count
                && entry.watch_revision.latest_modified_ms == watch_revision.latest_modified_ms
            {
                return Ok(entry.snapshot.clone());
            }
        }
    }
    let metadata = load_local_folder_metadata(&canonical_root)?;
    let mut rows = Vec::new();
    #[cfg(test)]
    LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
    scan_markdown_page_tree(
        &canonical_root,
        &scan_root,
        None,
        0,
        None,
        &metadata,
        &mut rows,
        &workspace_id,
        &root_source_uri,
    )?;
    let child_counts = rows
        .iter()
        .filter_map(|row| row.parent_node_id.as_ref())
        .fold(
            std::collections::BTreeMap::<String, u32>::new(),
            |mut acc, parent| {
                *acc.entry(parent.clone()).or_default() += 1;
                acc
            },
        );
    for row in &mut rows {
        row.child_count = child_counts.get(&row.node_id).copied().unwrap_or(0);
        row.expandable = row.child_count > 0;
        row.expanded_by_default = row.expandable && row.depth < 2;
    }

    let items = rows
        .iter()
        .map(local_folder_row_to_projection_item)
        .collect::<Vec<_>>();
    let snapshot = ProjectionSnapshot {
        dataset: json!({
            "workspace": {
                "id": local_workspace_id(&canonical_root),
                "sourceKind": "local_folder",
                "rootUri": root_source_uri,
            },
            "documents": rows
                .iter()
                .filter_map(|row| {
                    row.document_id.as_ref().map(|document_id| {
                        json!({
                            "id": document_id,
                            "workspace_id": local_workspace_id(&canonical_root),
                            "title": row.title,
                            "parent_id": row.parent_node_id,
                            "sort_order": row.position,
                        })
                    })
                })
                .collect::<Vec<_>>(),
        }),
        projection: json!({
            "projection": "page_tree",
            "sourceKind": "local_folder",
            "rootUri": root_source_uri,
            "parentRelativePath": parent_relative_path,
            "watchRevision": watch_revision,
            "items": items,
        }),
    };
    if let Ok(mut cache) = local_page_tree_snapshot_cache().lock() {
        if cache.len() > 64 {
            cache.clear();
        }
        cache.insert(
            cache_key,
            LocalPageTreeSnapshotCacheEntry {
                watch_revision,
                snapshot: snapshot.clone(),
            },
        );
    }
    Ok(snapshot)
}

pub fn local_folder_watch_revision(root_uri: &str) -> Result<LocalFolderWatchRevision, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    if !canonical_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_not_directory",
            "本地 rootUri 必须指向目录",
        ));
    }
    let root_source_uri = file_uri_for_path(&canonical_root);
    local_folder_watch_revision_for_root(&canonical_root, &root_source_uri)
}

pub fn resolve_local_markdown_page_aggregate(
    root_uri: &str,
    document_id: &str,
) -> Result<PageAggregate, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    if !canonical_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_not_directory",
            "本地 rootUri 必须指向目录",
        ));
    }
    let metadata = load_local_folder_metadata(&canonical_root)?;
    let markdown_file = find_markdown_by_page_id(&canonical_root, &metadata, document_id)?
        .ok_or_else(|| {
            WebError::bad_request_code(
                "local_markdown_not_found",
                "找不到本地 Markdown 页面对应的文件",
            )
        })?;
    let markdown = fs::read_to_string(&markdown_file.path).map_err(|error| {
        WebError::bad_request_code(
            "local_markdown_read_failed",
            format!(
                "无法读取本地 Markdown 文件 {}: {error}",
                markdown_file.path.display()
            ),
        )
    })?;
    let parsed = parse_markdown_page(&markdown, &markdown_file.file_name);
    let title = parsed.title;
    let attachment_paths = uploaded_asset_markdown_relative_paths_for_document(
        &canonical_root,
        &metadata,
        document_id,
        &markdown_file.path,
    );
    let content = crate::routes::local_markdown_parser::markdown_to_blocks_with_attachment_paths(
        &parsed.body,
        &attachment_paths,
    );
    let block_count = content.as_array().map(|blocks| blocks.len()).unwrap_or(0) as u64;
    let page_subtree = markdown_page_subtree(document_id, &title, &content);
    let workspace_id = local_workspace_id(&canonical_root);
    let page_options_value = metadata
        .page_options
        .get(document_id)
        .or_else(|| metadata.page_options.get(&markdown_file.relative_path));
    let mut page_options = page_options_value
        .map(page_options_from_metadata)
        .unwrap_or_default();
    if page_options_value
        .and_then(page_options_hide_title_header_value)
        .is_none()
    {
        // 我的空间（有 workspace.json 的托管工作区，包括用户本人的及分享的）默认显示页头标题。
        // 普通本地文件夹（无托管清单）默认隐藏页头标题，避免文件名与正文 H1 重复。
        let is_managed = load_local_workspace_manifest(&canonical_root).is_ok();
        page_options.hide_title_header = !is_managed;
    }
    let character_count = parsed.body.chars().count() as u64;
    let word_count = parsed
        .body
        .split_whitespace()
        .filter(|word| !word.trim().is_empty())
        .count() as u64;
    let read_only = markdown_file.is_readonly;

    let block_document =
        project_legacy_content_to_block_document(document_id, &content, &Value::Number(0.into()))
            .map_err(|error| WebError::internal(format!("{error:?}")))?;

    let conflict_detection_key =
        local_markdown_conflict_detection_key(document_id, &markdown_file.path)?;

    Ok(PageAggregate {
        schema: PageAggregate::SCHEMA.into(),
        projection_version: PageAggregate::VERSION,
        source: PageAggregateSource::KernelProjection,
        page_id: document_id.to_string(),
        parent_id: None,
        title: title.clone(),
        path: vec![document_id.to_string()],
        sidebar_tree_membership: vec!["page-tree".into(), "file-tree".into()],
        body_ref: Some(format!("local-md:{document_id}")),
        layout_options: serde_json::to_value(&page_options).unwrap_or_else(|_| json!({})),
        updated_at: None,
        identity: PageIdentity {
            document_id: document_id.to_string(),
            workspace_id,
        },
        head: PageHead {
            title,
            updated_at: Value::Null,
            permissions: PagePermissions {
                read_only,
                disable_download: false,
                disable_copy: false,
            },
        },
        layout: PageLayout { page_options },
        body: PageBody {
            content,
            revision: Value::Number(0.into()),
            conflict_detection_key: Value::String(conflict_detection_key.clone()),
            file_version: Value::String(conflict_detection_key),
            block_document,
            block_projection_version: 1,
            projection_source: "local_markdown.content".into(),
        },
        tree: PageTree { page_subtree },
        stats: PageStats {
            word_count,
            character_count,
            block_count,
            todo_total: 0,
            todo_done: 0,
        },
    })
}

pub fn save_local_markdown_page(
    root_uri: &str,
    document_id: &str,
    expected_conflict_detection_key: Option<&str>,
    content: &Value,
) -> Result<Value, WebError> {
    save_local_markdown_page_inner(
        root_uri,
        document_id,
        expected_conflict_detection_key,
        content,
        None,
    )
}

fn save_local_markdown_page_inner(
    root_uri: &str,
    document_id: &str,
    expected_conflict_detection_key: Option<&str>,
    content: &Value,
    buffer_store: Option<&crate::document_buffer_store::BufferStore>,
) -> Result<Value, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    let metadata = load_local_folder_metadata(&canonical_root)?;
    let markdown_file = find_markdown_by_page_id(&canonical_root, &metadata, document_id)?
        .ok_or_else(|| {
            WebError::bad_request_code(
                "local_markdown_not_found",
                "找不到要保存的本地 Markdown 页面",
            )
        })?;
    let workspace_id = local_workspace_id(&canonical_root);
    let relative_path_for_buffer = markdown_file
        .path
        .strip_prefix(&canonical_root)
        .ok()
        .map(|p| p.to_string_lossy().replace('\\', "/"))
        .unwrap_or_default();
    let current = fs::read_to_string(&markdown_file.path).map_err(|error| {
        WebError::bad_request_code(
            "local_markdown_read_failed",
            format!(
                "无法读取本地 Markdown 文件 {}: {error}",
                markdown_file.path.display()
            ),
        )
    })?;
    let current_conflict_key =
        local_markdown_conflict_detection_key(document_id, &markdown_file.path)?;
    if let Some(expected_key) = expected_conflict_detection_key
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        if expected_key != current_conflict_key {
            let buffer_state = buffer_store.and_then(|store| {
                let ws_path = crate::document_buffer_store::build_local_folder_workspace_path(
                    &workspace_id,
                    root_uri,
                    &relative_path_for_buffer,
                    document_id,
                );
                store.get_by_path(&ws_path)
            });
            return Err(local_markdown_conflict_error(
                root_uri,
                document_id,
                expected_key,
                &current_conflict_key,
                buffer_state.as_ref(),
            ));
        }
    }
    let (frontmatter, _) = split_frontmatter(&current);
    let body = editor_blocks_to_markdown_for_file(content, &canonical_root, &markdown_file.path);
    let next_markdown = if let Some(frontmatter) = frontmatter {
        format!("---\n{frontmatter}\n---\n{body}")
    } else {
        body
    };
    fs::write(&markdown_file.path, next_markdown).map_err(|error| {
        WebError::bad_request_code(
            "local_markdown_write_failed",
            format!(
                "无法保存本地 Markdown 文件 {}: {error}",
                markdown_file.path.display()
            ),
        )
    })?;
    refresh_local_search_index_best_effort(&canonical_root, root_uri, &workspace_id);
    let next_conflict_key =
        local_markdown_conflict_detection_key(document_id, &markdown_file.path)?;
    Ok(json!({
        "ok": true,
        "documentId": document_id,
        "workspaceId": workspace_id,
        "relativePath": relative_path_for_buffer,
        "revision": now_ms(),
        "conflict_detection_key": next_conflict_key,
        "fileVersion": next_conflict_key,
        "executedCommand": "page.body.save",
        "canonicalCommand": "page.body.save",
        "sourceKind": "local_folder",
    }))
}

fn refresh_local_search_index_best_effort(root: &Path, root_uri: &str, workspace_id: &str) {
    let _ = local_search_index::refresh_local_search_index(root, root_uri, workspace_id);
}

fn local_markdown_conflict_error(
    root_uri: &str,
    document_id: &str,
    editor_base_version: &str,
    current_disk_version: &str,
    buffer_state: Option<&core_protocol::DocumentBuffer>,
) -> WebError {
    let mut details = json!({
        "conflict": {
            "code": "local_markdown_external_change",
            "documentId": document_id,
            "rootUri": root_uri,
            "currentDiskVersion": current_disk_version,
            "editorBaseVersion": editor_base_version,
            "externalActor": Value::Null,
            "dirtyState": Value::Null,
            "bufferFileVersion": Value::Null,
            "suggestedActions": [
                "accept_disk",
                "keep_editor",
                "open_diff",
                "merge"
            ]
        }
    });
    if let Some(buf) = buffer_state {
        if let Some(conflict) = details.get_mut("conflict").and_then(Value::as_object_mut) {
            conflict.insert("externalActor".into(), json!(&buf.external_actor));
            conflict.insert("dirtyState".into(), json!(format!("{:?}", buf.dirty_state)));
            conflict.insert("bufferFileVersion".into(), json!(&buf.file_version));
        }
    }
    WebError::new(
        StatusCode::CONFLICT,
        "local_markdown_external_change",
        "本地 Markdown 文件已被外部修改，请刷新后再保存",
    )
    .with_details(details)
}

pub fn write_local_markdown_page_body(
    request: &core_protocol::PageBodyWriteRequest,
    buffer_store: Option<&crate::document_buffer_store::BufferStore>,
) -> Result<Value, WebError> {
    if request.source_kind != core_protocol::WorkspaceSourceKind::LocalFolder {
        return Err(WebError::bad_request_code(
            "page_body_write_source_unsupported",
            "page.body.write 当前只支持 local_folder 本地写入",
        ));
    }
    if request.content_format.trim() != "editorBlocks" {
        return Err(WebError::bad_request_code(
            "page_body_write_content_format_unsupported",
            "page.body.write 当前只支持 editorBlocks 内容格式",
        ));
    }

    let mut result = if buffer_store.is_some() {
        save_local_markdown_page_inner(
            &request.root_uri,
            &request.document_id,
            request.expected_file_version.as_deref(),
            &request.content,
            buffer_store,
        )?
    } else {
        save_local_markdown_page(
            &request.root_uri,
            &request.document_id,
            request.expected_file_version.as_deref(),
            &request.content,
        )?
    };

    // 保存成功后更新 BufferStore
    if let Some(store) = buffer_store {
        if let (Some(workspace_id), Some(relative_path), Some(file_version)) = (
            result.get("workspaceId").and_then(Value::as_str),
            result.get("relativePath").and_then(Value::as_str),
            result.get("fileVersion").and_then(Value::as_str),
        ) {
            let ws_path = crate::document_buffer_store::build_local_folder_workspace_path(
                workspace_id,
                &request.root_uri,
                relative_path,
                &request.document_id,
            );
            store.mark_saved(
                &ws_path,
                file_version.to_string(),
                format!("sha256:{file_version}"),
            );
        }
    }

    if let Value::Object(map) = &mut result {
        map.insert("canonicalCommand".into(), json!("page.body.write"));
        map.insert("compatCommand".into(), json!("page.body.save"));
        map.insert("contentFormat".into(), json!(request.content_format));
        map.insert(
            "editorSource".into(),
            request
                .editor_source
                .as_deref()
                .map(Value::from)
                .unwrap_or(Value::Null),
        );
        map.insert(
            "baseContentHash".into(),
            request
                .base_content_hash
                .as_deref()
                .map(Value::from)
                .unwrap_or(Value::Null),
        );
    }
    Ok(result)
}

pub async fn upload_local_markdown_asset(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    multipart: Multipart,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let fields = read_local_asset_upload_multipart(multipart).await?;
    ensure_local_workspace_write_access_with_state(&state, &context, &fields.root_uri)
        .map_err(|error| error.with_context(&context))?;
    let asset = match fields.upload_intent.as_str() {
        "editor.markdown.attach" => {
            if fields.document_id.trim().is_empty() || fields.target_relative_path.is_some() {
                return Err(WebError::bad_request_code(
                    "local_asset_upload_intent_invalid",
                    "editor.markdown.attach 必须提供 documentId 且不能提供 targetRelativePath",
                ));
            }
            write_local_markdown_asset(
                &fields.root_uri,
                &fields.document_id,
                &fields.kind,
                fields.file,
            )?
        }
        "filetree.folder.drop" => {
            let Some(target_relative_path) = fields.target_relative_path.as_deref() else {
                return Err(WebError::bad_request_code(
                    "local_asset_upload_intent_invalid",
                    "filetree.folder.drop 必须提供 targetRelativePath",
                ));
            };
            write_local_folder_file_upload(
                &fields.root_uri,
                target_relative_path,
                &fields.kind,
                fields.file,
            )?
        }
        _ => {
            return Err(WebError::bad_request_code(
                "local_asset_upload_intent_unsupported",
                "不支持的本地上传 intent",
            ));
        }
    };
    Ok((StatusCode::OK, Json(json!({ "ok": true, "asset": asset }))))
}

pub async fn open_local_file(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Query(query): Query<LocalFileOpenQuery>,
) -> Result<(StatusCode, HeaderMap, Vec<u8>), WebError> {
    ensure_local_workspace_read_access_with_state(&state, &context, &query.root_uri)
        .map_err(|error| error.with_context(&context))?;
    let target = resolve_local_open_path(&query.root_uri, &query.path)?;
    let mut headers = HeaderMap::new();
    if target.is_dir() {
        if !query.download.unwrap_or(false) {
            return Err(WebError::bad_request_code(
                "local_file_open_is_directory",
                "不能直接读取本地目录",
            )
            .with_context(&context));
        }
        let bytes = build_local_directory_tar_archive(&target).map_err(|error| {
            WebError::bad_request_code(
                "local_directory_download_failed",
                format!("无法打包本地目录 {}: {error}", target.display()),
            )
            .with_context(&context)
        })?;
        headers.insert(
            header::CONTENT_TYPE,
            HeaderValue::from_static("application/x-tar"),
        );
        headers.insert(
            header::CONTENT_DISPOSITION,
            content_disposition_attachment_for_filename(&local_directory_tar_filename(&target)),
        );
        return Ok((StatusCode::OK, headers, bytes));
    }
    let bytes = fs::read(&target).map_err(|error| {
        WebError::bad_request_code(
            "local_file_open_read_failed",
            format!("无法读取本地文件 {}: {error}", target.display()),
        )
        .with_context(&context)
    })?;
    headers.insert(header::CONTENT_TYPE, content_type_for_path(&target));
    if query.download.unwrap_or(false) {
        headers.insert(
            header::CONTENT_DISPOSITION,
            content_disposition_attachment_for_path(&target),
        );
    }
    Ok((StatusCode::OK, headers, bytes))
}

pub async fn stat_local_file(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Query(query): Query<LocalFileOpenQuery>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
    ensure_local_workspace_read_access_with_state(&state, &context, &query.root_uri)
        .map_err(|error| error.with_context(&context))?;
    let root_path = parse_file_root_uri(&query.root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
        .with_context(&context)
    })?;
    let requested = Path::new(&query.path);
    if requested.is_absolute()
        || requested
            .components()
            .any(|component| matches!(component, std::path::Component::ParentDir))
    {
        return Err(WebError::bad_request_code(
            "local_file_stat_root_escape",
            "本地文件路径不能越过 root",
        )
        .with_context(&context));
    }
    let target = canonical_root.join(requested);
    let metadata = match fs::symlink_metadata(&target) {
        Ok(metadata) => metadata,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            return Ok((
                StatusCode::OK,
                HeaderMap::new(),
                Json(json!({
                    "ok": true,
                    "result": {
                        "rootUri": query.root_uri,
                        "path": query.path,
                        "exists": false,
                        "sourceKind": "local_folder",
                    }
                })),
            ));
        }
        Err(error) => {
            return Err(WebError::bad_request_code(
                "local_file_stat_failed",
                format!("无法读取本地文件状态 {}: {error}", target.display()),
            )
            .with_context(&context));
        }
    };
    let canonical_target = target.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_file_stat_failed",
            format!("无法解析本地文件路径 {}: {error}", target.display()),
        )
        .with_context(&context)
    })?;
    if !canonical_target.starts_with(&canonical_root) {
        return Err(WebError::bad_request_code(
            "local_file_stat_root_escape",
            "本地文件路径不能越过 root",
        )
        .with_context(&context));
    }
    let file_name = canonical_target
        .file_name()
        .and_then(|value| value.to_str())
        .unwrap_or("资源")
        .to_string();
    Ok((
        StatusCode::OK,
        HeaderMap::new(),
        Json(json!({
            "ok": true,
            "result": {
                "rootUri": query.root_uri,
                "path": query.path,
                "exists": true,
                "isDirectory": metadata.is_dir(),
                "fileName": file_name,
                "contentType": content_type_for_path(&canonical_target).to_str().unwrap_or("application/octet-stream"),
                "sourceKind": "local_folder",
            }
        })),
    ))
}

pub async fn read_local_resource(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Query(query): Query<LocalResourceReadQuery>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
    ensure_local_workspace_read_access_with_state(&state, &context, &query.root_uri)
        .map_err(|error| error.with_context(&context))?;
    let root = parse_file_root_uri(&query.root_uri)?
        .canonicalize()
        .map_err(|error| {
            WebError::bad_request_code(
                "local_folder_unavailable",
                format!("无法访问本地文件夹: {error}"),
            )
            .with_context(&context)
        })?;
    let target = resolve_local_file_open_path(&query.root_uri, &query.path)?;
    let text = fs::read_to_string(&target).map_err(|error| {
        WebError::bad_request_code(
            "local_resource_read_failed",
            format!("无法读取本地资源文件 {}: {error}", target.display()),
        )
        .with_context(&context)
    })?;
    let file_name = target
        .file_name()
        .and_then(|value| value.to_str())
        .unwrap_or("资源")
        .to_string();
    let content = if is_markdown_file(&file_name) {
        let metadata = load_local_folder_metadata(&root)?;
        let target_relative_path = normalize_relative_path(&root, &target)?;
        let attachment_paths = metadata
            .uploaded_assets
            .get(&target_relative_path)
            .map(|entry| {
                uploaded_asset_markdown_relative_paths_for_document(
                    &root,
                    &metadata,
                    &entry.document_id,
                    &target,
                )
            })
            .unwrap_or_default();
        if attachment_paths.is_empty() {
            crate::routes::local_markdown_parser::markdown_to_blocks(&text)
        } else {
            crate::routes::local_markdown_parser::markdown_to_blocks_with_attachment_paths(
                &text,
                &attachment_paths,
            )
        }
    } else {
        text_to_editor_blocks(&text, &file_name)
    };
    let file_version = local_resource_conflict_detection_key(&root, &target)?;
    Ok(local_folder_ok_response(
        &context,
        json!({
            "rootUri": query.root_uri,
            "path": query.path,
            "fileName": file_name,
            "contentType": content_type_for_path(&target).to_str().unwrap_or("application/octet-stream"),
            "text": text,
            "content": content,
            "contentFormat": "editorBlocks",
            "fileVersion": file_version,
            "conflictDetectionKey": file_version,
            "sourceKind": "local_folder",
        }),
    ))
}

pub async fn write_local_resource(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Json(request): Json<LocalResourceWriteRequest>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
    let root_uri = request.root_uri.trim();
    let path = request.path.trim();
    if root_uri.is_empty() || path.is_empty() {
        return Err(WebError::bad_request_code(
            "local_resource_write_required_missing",
            "缺少 rootUri 或 path",
        )
        .with_context(&context));
    }
    ensure_local_workspace_write_access_with_state(&state, &context, root_uri)
        .map_err(|error| error.with_context(&context))?;
    let root = parse_file_root_uri(root_uri)?
        .canonicalize()
        .map_err(|error| {
            WebError::bad_request_code(
                "local_folder_unavailable",
                format!("无法访问本地文件夹: {error}"),
            )
            .with_context(&context)
        })?;
    let target = resolve_local_file_open_path(root_uri, path)?;
    let current_key = local_resource_conflict_detection_key(&root, &target)?;
    if let Some(expected_key) = request
        .expected_file_version
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        if expected_key != current_key {
            return Err(WebError::new(
                StatusCode::CONFLICT,
                "local_resource_external_change",
                "本地资源文件已被外部修改，请刷新后再保存",
            )
            .with_details(json!({
                "conflict": {
                    "code": "local_resource_external_change",
                    "rootUri": root_uri,
                    "path": path,
                    "currentDiskVersion": current_key,
                    "editorBaseVersion": expected_key,
                }
            }))
            .with_context(&context));
        }
    }
    let content = local_resource_write_editor_blocks(&request);
    let file_name = target
        .file_name()
        .and_then(|value| value.to_str())
        .unwrap_or("资源")
        .to_string();
    let next_text = if is_markdown_file(&file_name) {
        editor_blocks_to_markdown_for_file(&content, &root, &target)
    } else {
        editor_blocks_to_plain_text(&content)
    };
    fs::write(&target, next_text).map_err(|error| {
        WebError::bad_request_code(
            "local_resource_write_failed",
            format!("无法保存本地资源文件 {}: {error}", target.display()),
        )
        .with_context(&context)
    })?;
    let next_key = local_resource_conflict_detection_key(&root, &target)?;
    Ok(local_folder_ok_response(
        &context,
        json!({
            "ok": true,
            "rootUri": root_uri,
            "path": path,
            "fileName": file_name,
            "fileVersion": next_key,
            "conflictDetectionKey": next_key,
            "contentFormat": request.content_format.unwrap_or_else(|| "editorBlocks".into()),
            "executedCommand": "resource.file.write",
            "canonicalCommand": "resource.file.write",
            "sourceKind": "local_folder",
        }),
    ))
}

fn resolve_local_open_path(root_uri: &str, relative_path: &str) -> Result<PathBuf, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    let requested = Path::new(relative_path);
    if requested.is_absolute()
        || requested
            .components()
            .any(|component| matches!(component, std::path::Component::ParentDir))
    {
        return Err(WebError::bad_request_code(
            "local_file_open_root_escape",
            "本地文件路径不能越过 root",
        ));
    }
    let target = canonical_root
        .join(requested)
        .canonicalize()
        .map_err(|error| {
            WebError::bad_request_code(
                "local_file_open_not_found",
                format!("找不到本地文件: {error}"),
            )
        })?;
    if !target.starts_with(&canonical_root) {
        return Err(WebError::bad_request_code(
            "local_file_open_root_escape",
            "本地文件路径不能越过 root",
        ));
    }
    Ok(target)
}

fn resolve_local_file_open_path(root_uri: &str, relative_path: &str) -> Result<PathBuf, WebError> {
    let target = resolve_local_open_path(root_uri, relative_path)?;
    if target.is_dir() {
        return Err(WebError::bad_request_code(
            "local_file_open_is_directory",
            "不能直接读取本地目录",
        ));
    }
    Ok(target)
}

async fn read_local_asset_upload_multipart(
    mut multipart: Multipart,
) -> Result<LocalAssetUploadFields, WebError> {
    let mut file: Option<LocalUploadFile> = None;
    let mut root_uri = String::new();
    let mut document_id = String::new();
    let mut target_relative_path: Option<String> = None;
    let mut upload_intent = String::new();
    let mut kind = String::new();

    while let Some(field) = multipart.next_field().await.map_err(|error| {
        WebError::bad_request_code(
            "local_asset_upload_bad_multipart",
            format!("上传表单解析失败: {error}"),
        )
    })? {
        let name = field.name().unwrap_or_default().to_string();
        if name == "file" {
            let file_name = field
                .file_name()
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .unwrap_or("附件")
                .to_string();
            let content_type = field
                .content_type()
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .unwrap_or("application/octet-stream")
                .to_string();
            let bytes = field
                .bytes()
                .await
                .map_err(|error| {
                    WebError::bad_request_code(
                        "local_asset_upload_file_read_failed",
                        format!("读取上传文件失败: {error}"),
                    )
                })?
                .to_vec();
            file = Some(LocalUploadFile {
                name: file_name,
                content_type,
                bytes,
            });
            continue;
        }

        let value = field.text().await.map_err(|error| {
            WebError::bad_request_code(
                "local_asset_upload_field_read_failed",
                format!("读取上传字段失败: {error}"),
            )
        })?;
        match name.as_str() {
            "rootUri" => root_uri = value.trim().to_string(),
            "documentId" => document_id = value.trim().to_string(),
            "targetRelativePath" | "targetDirectoryPath" => {
                target_relative_path = Some(value.trim().to_string())
            }
            "uploadIntent" | "upload_intent" => upload_intent = value.trim().to_string(),
            "kind" => kind = value.trim().to_string(),
            _ => {}
        }
    }

    let file = file.ok_or_else(|| {
        WebError::bad_request_code("local_asset_upload_file_missing", "缺少 file")
    })?;
    if file.bytes.is_empty() || root_uri.is_empty() || upload_intent.is_empty() {
        return Err(WebError::bad_request_code(
            "local_asset_upload_required_missing",
            "缺少必要参数",
        ));
    }
    Ok(LocalAssetUploadFields {
        file,
        root_uri,
        document_id,
        target_relative_path,
        upload_intent,
        kind,
    })
}

pub(crate) fn write_local_folder_file_upload(
    root_uri: &str,
    target_relative_path: &str,
    kind: &str,
    file: LocalUploadFile,
) -> Result<Value, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    if !canonical_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_not_directory",
            "本地 rootUri 必须指向目录",
        ));
    }
    let target_relative_path = target_relative_path.trim();
    let requested = if target_relative_path.is_empty() {
        Path::new(".")
    } else {
        Path::new(target_relative_path)
    };
    if requested.is_absolute()
        || requested
            .components()
            .any(|component| matches!(component, std::path::Component::ParentDir))
    {
        return Err(WebError::bad_request_code(
            "local_folder_upload_root_escape",
            "上传目标目录不能越过 root",
        ));
    }
    let target_dir = canonical_root
        .join(requested)
        .canonicalize()
        .map_err(|error| {
            WebError::bad_request_code(
                "local_folder_upload_target_missing",
                format!("找不到上传目标目录: {error}"),
            )
        })?;
    if !target_dir.starts_with(&canonical_root) || !target_dir.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_upload_target_invalid",
            "上传目标必须是本地 root 内的目录",
        ));
    }
    let sanitized_name = sanitize_file_name(&file.name, "附件");
    let target = next_available_raw_path(&target_dir, &sanitized_name);
    fs::write(&target, &file.bytes).map_err(|error| {
        WebError::bad_request_code(
            "local_folder_upload_write_failed",
            format!("无法写入本地文件 {}: {error}", target.display()),
        )
    })?;
    let root_relative_path = normalize_relative_path(&canonical_root, &target)?;
    let asset_type = local_upload_asset_type(kind, &file.content_type);
    Ok(json!({
        "id": format!("local-file:{root_relative_path}"),
        "uploadIntent": "filetree.folder.drop",
        "asset_type": asset_type,
        "file_name": target.file_name().and_then(|value| value.to_str()).unwrap_or(&sanitized_name),
        "mime_type": file.content_type,
        "file_size": file.bytes.len(),
        "file_url": root_relative_path,
        "sourcePath": root_relative_path,
        "sourceKind": "local_folder",
        "rootUri": file_uri_for_path(&canonical_root),
        "rootRelativePath": root_relative_path,
        "targetRelativePath": target_relative_path,
        "markdownRelativePath": Value::Null,
        "ownerDocumentId": Value::Null,
    }))
}

pub(crate) fn write_local_markdown_asset(
    root_uri: &str,
    document_id: &str,
    kind: &str,
    file: LocalUploadFile,
) -> Result<Value, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    if !canonical_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_not_directory",
            "本地 rootUri 必须指向目录",
        ));
    }

    let metadata = load_local_folder_metadata(&canonical_root)?;
    let markdown_file = find_markdown_by_page_id(&canonical_root, &metadata, document_id)?
        .ok_or_else(|| {
            WebError::bad_request_code(
                "local_markdown_not_found",
                "找不到本地 Markdown 页面对应的文件",
            )
        })?;
    let markdown_dir = markdown_file.path.parent().ok_or_else(|| {
        WebError::bad_request_code(
            "local_asset_upload_bad_markdown_path",
            "本地 Markdown 文件路径无父目录",
        )
    })?;
    if !markdown_dir.starts_with(&canonical_root) {
        return Err(WebError::bad_request_code(
            "local_folder_root_escape",
            "本地 Markdown 文件路径不在 root 内",
        ));
    }

    let page_resource_dir =
        markdown_page_resource_directory(&markdown_file.path).ok_or_else(|| {
            WebError::bad_request_code(
                "local_asset_upload_bad_markdown_path",
                "无法解析本地页面资源目录",
            )
        })?;
    let asset_dir = page_resource_dir;
    if !asset_dir.starts_with(&canonical_root) {
        return Err(WebError::bad_request_code(
            "local_folder_root_escape",
            "本地资源目录不能越过 root",
        ));
    }
    fs::create_dir_all(&asset_dir).map_err(|error| {
        WebError::bad_request_code(
            "local_asset_upload_create_dir_failed",
            format!("无法创建本地资源目录 {}: {error}", asset_dir.display()),
        )
    })?;

    let sanitized_name = sanitize_file_name(&file.name, "附件");
    let target = next_available_asset_path(&asset_dir, &sanitized_name);
    if !target.starts_with(&canonical_root) {
        return Err(WebError::bad_request_code(
            "local_folder_root_escape",
            "本地资源文件不能越过 root",
        ));
    }
    fs::write(&target, &file.bytes).map_err(|error| {
        WebError::bad_request_code(
            "local_asset_upload_write_failed",
            format!("无法写入本地资源文件 {}: {error}", target.display()),
        )
    })?;

    let root_relative_path = normalize_relative_path(&canonical_root, &target)?;
    let markdown_relative_path = normalize_markdown_relative_asset_path(markdown_dir, &target)?;
    let asset_type = local_upload_asset_type(kind, &file.content_type);
    let mut uploaded_assets = metadata.uploaded_assets;
    uploaded_assets.insert(
        root_relative_path.clone(),
        LocalUploadedAssetEntry {
            document_id: document_id.to_string(),
            relative_path: root_relative_path.clone(),
            file_name: target
                .file_name()
                .and_then(|value| value.to_str())
                .unwrap_or(&sanitized_name)
                .to_string(),
            created_at_ms: now_ms(),
        },
    );
    write_uploaded_asset_index_metadata(&canonical_root, &uploaded_assets)?;
    Ok(json!({
        "id": format!("local:asset:{root_relative_path}"),
        "uploadIntent": "editor.markdown.attach",
        "asset_type": asset_type,
        "file_name": target.file_name().and_then(|value| value.to_str()).unwrap_or(&sanitized_name),
        "mime_type": file.content_type,
        "file_size": file.bytes.len(),
        "file_url": markdown_relative_path,
        "sourcePath": markdown_relative_path,
        "document_id": document_id,
        "documentId": document_id,
        "ownerDocumentId": document_id,
        "sourceKind": "local_folder",
        "rootUri": file_uri_for_path(&canonical_root),
        "rootRelativePath": root_relative_path,
        "markdownRelativePath": markdown_relative_path,
    }))
}

fn local_mindmap_relative_path_from_id(mindmap_id: &str) -> Option<String> {
    let value = mindmap_id.trim();
    value
        .strip_prefix("local-file:")
        .or_else(|| value.strip_prefix("local:asset:"))
        .map(str::trim)
        .filter(|path| !path.is_empty())
        .map(ToOwned::to_owned)
}

fn ensure_relative_path_stays_in_root(
    root: &Path,
    relative_path: &str,
) -> Result<PathBuf, WebError> {
    let requested = Path::new(relative_path);
    if requested.is_absolute()
        || requested
            .components()
            .any(|component| matches!(component, std::path::Component::ParentDir))
    {
        return Err(WebError::bad_request_code(
            "local_mindmap_root_escape",
            "本地思维导图路径不能越过 root",
        ));
    }
    let target = root.join(requested);
    if !target.starts_with(root) {
        return Err(WebError::bad_request_code(
            "local_mindmap_root_escape",
            "本地思维导图路径不能越过 root",
        ));
    }
    Ok(target)
}

fn local_mindmap_file_name(mindmap_id: &str) -> String {
    let sanitized = sanitize_file_name(mindmap_id, "mindmap");
    if sanitized.to_ascii_lowercase().ends_with(".json") {
        sanitized
    } else {
        format!("{}.json", sanitize_file_stem(&sanitized, "mindmap"))
    }
}

fn is_local_mindmap_file_name(file_name: &str) -> bool {
    let trimmed = file_name.trim();
    let lower = trimmed.to_ascii_lowercase();
    lower.ends_with(".mindmap.json")
        || (trimmed.starts_with("思维导图") && lower.ends_with(".json"))
}

fn resolve_local_mindmap_path(
    root: &Path,
    metadata: &LocalFolderMetadata,
    document_id: &str,
    mindmap_id: &str,
) -> Result<PathBuf, WebError> {
    if let Some(relative_path) = local_mindmap_relative_path_from_id(mindmap_id) {
        return ensure_relative_path_stays_in_root(root, &relative_path);
    }

    let markdown_file =
        find_markdown_by_page_id(root, metadata, document_id)?.ok_or_else(|| {
            WebError::bad_request_code(
                "local_markdown_not_found",
                "找不到本地思维导图所属的 Markdown 页面",
            )
        })?;
    let page_resource_dir =
        markdown_page_resource_directory(&markdown_file.path).ok_or_else(|| {
            WebError::bad_request_code(
                "local_mindmap_bad_markdown_path",
                "无法解析本地页面思维导图目录",
            )
        })?;
    Ok(page_resource_dir.join(local_mindmap_file_name(mindmap_id)))
}

fn local_mindmap_default_data() -> Value {
    json!({
        "data": {"uid": "root", "text": "KMIND"},
        "children": []
    })
}

pub(crate) fn read_local_mindmap_data(
    root_uri: &str,
    document_id: &str,
    mindmap_id: &str,
) -> Result<Value, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    let metadata = load_local_folder_metadata(&canonical_root)?;
    let target = resolve_local_mindmap_path(&canonical_root, &metadata, document_id, mindmap_id)?;
    if !target.exists() {
        return Ok(local_mindmap_default_data());
    }
    if !target.is_file() {
        return Err(WebError::bad_request_code(
            "local_mindmap_not_file",
            "本地思维导图目标必须是文件",
        ));
    }
    let raw = fs::read_to_string(&target).map_err(|error| {
        WebError::bad_request_code(
            "local_mindmap_read_failed",
            format!("无法读取本地思维导图 {}: {error}", target.display()),
        )
    })?;
    serde_json::from_str(&raw).map_err(|error| {
        WebError::bad_request_code(
            "local_mindmap_invalid",
            format!("本地思维导图 JSON 损坏 {}: {error}", target.display()),
        )
    })
}

pub(crate) fn write_local_mindmap_data(
    root_uri: &str,
    document_id: &str,
    mindmap_id: &str,
    data: Value,
    create_only: bool,
) -> Result<Value, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    let metadata = load_local_folder_metadata(&canonical_root)?;
    let target = resolve_local_mindmap_path(&canonical_root, &metadata, document_id, mindmap_id)?;
    if !target.starts_with(&canonical_root) {
        return Err(WebError::bad_request_code(
            "local_mindmap_root_escape",
            "本地思维导图路径不能越过 root",
        ));
    }
    let parent = target.parent().ok_or_else(|| {
        WebError::bad_request_code("local_mindmap_bad_path", "无法解析本地思维导图父目录")
    })?;
    fs::create_dir_all(parent).map_err(|error| {
        WebError::bad_request_code(
            "local_mindmap_create_dir_failed",
            format!("无法创建本地思维导图目录 {}: {error}", parent.display()),
        )
    })?;
    if !(create_only && target.exists()) {
        write_json_atomic(&target, &data)?;
    }
    let relative_path = normalize_relative_path(&canonical_root, &target)?;
    Ok(json!({
        "ok": true,
        "sourceKind": "local_folder",
        "documentId": document_id,
        "mindmapId": mindmap_id,
        "assetId": format!("local-file:{relative_path}"),
        "relativePath": relative_path,
        "fileName": target.file_name().and_then(|value| value.to_str()).unwrap_or("思维导图.json"),
    }))
}

pub fn update_local_markdown_title(
    root_uri: &str,
    document_id: &str,
    title: &str,
) -> Result<Value, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    let result = rename_local_markdown_page(&canonical_root, document_id, title)?;
    let workspace_id = local_workspace_id(&canonical_root);
    refresh_local_search_index_best_effort(&canonical_root, root_uri, &workspace_id);
    Ok(result)
}

pub fn execute_local_tree_command(
    root_uri: &str,
    action: &str,
    document_id: &str,
    parent_id: Option<&str>,
    title: Option<&str>,
) -> Result<Value, WebError> {
    execute_local_tree_command_with_sort(root_uri, action, document_id, parent_id, title, None)
}

pub fn execute_local_tree_command_with_sort(
    root_uri: &str,
    action: &str,
    document_id: &str,
    parent_id: Option<&str>,
    title: Option<&str>,
    sort_order: Option<i64>,
) -> Result<Value, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    let result = match action {
        "create" => {
            create_local_markdown_page(&canonical_root, parent_id, title.unwrap_or("新页面"))
        }
        "createFolder" | "create_folder" | "folder.create" => {
            create_local_folder(&canonical_root, parent_id, title.unwrap_or("新建文件夹"))
        }
        "rename" => rename_local_entry(&canonical_root, document_id, title.unwrap_or("无标题")),
        "copy" => {
            copy_local_markdown_page(&canonical_root, document_id, parent_id, title.unwrap_or(""))
        }
        "dropFiles" | "drop_files" => drop_external_files_into_local_folder(
            &canonical_root,
            parent_id,
            title.unwrap_or("外部文件"),
        ),
        "move" => move_local_entry(&canonical_root, document_id, parent_id, sort_order),
        "delete" | "trash" => trash_local_entry(&canonical_root, document_id),
        "restore" => restore_local_entry(&canonical_root, document_id),
        "purge" => purge_local_entry(&canonical_root, document_id),
        other => Err(WebError::bad_request_code(
            "local_tree_command_unsupported",
            format!("local_folder 暂不支持 tree action: {other}"),
        )),
    }?;
    let result = add_local_file_operation_contract(root_uri, action, result);
    let workspace_id = local_workspace_id(&canonical_root);
    refresh_local_search_index_best_effort(&canonical_root, root_uri, &workspace_id);
    Ok(result)
}

fn local_tree_parent_relative_path_for_path(relative_path: &str) -> String {
    let normalized = relative_path.trim().trim_matches('/').replace('\\', "/");
    if normalized.is_empty() || normalized == "." {
        return String::new();
    }
    normalized
        .rsplit_once('/')
        .map(|(parent, _)| parent.to_string())
        .unwrap_or_default()
}

fn push_local_tree_affected_parent(
    parents: &mut Vec<AffectedParent>,
    seen: &mut BTreeSet<String>,
    relative_path: String,
    reason: &str,
) {
    let normalized = relative_path.trim().trim_matches('/').replace('\\', "/");
    let key = if normalized == "." {
        String::new()
    } else {
        normalized
    };
    if !seen.insert(key.clone()) {
        return;
    }
    parents.push(AffectedParent {
        relative_path: key,
        reason: reason.to_string(),
    });
}

fn local_tree_result_string(result: &Value, key: &str) -> Option<String> {
    result
        .get(key)
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
}

fn local_file_operation_hash(values: &[&str]) -> u64 {
    let mut hasher = DefaultHasher::new();
    for value in values {
        value.hash(&mut hasher);
    }
    hasher.finish()
}

fn local_file_operation_row_id(
    relative_path: &str,
    document_id: Option<&str>,
    resource_kind: Option<&str>,
) -> String {
    let normalized = relative_path.trim().trim_matches('/').replace('\\', "/");
    if normalized.is_empty() {
        return String::new();
    }
    let document_id = document_id.unwrap_or_default();
    let resource_kind = resource_kind.unwrap_or_default();
    if document_id.starts_with("local-md:") || is_markdown_file(&normalized) {
        return format!("local:markdown:{normalized}");
    }
    if document_id.starts_with("local-dir:")
        || resource_kind == "local_directory"
        || resource_kind == "folder"
    {
        return format!("local:folder:{normalized}");
    }
    format!("local:asset:{normalized}")
}

fn local_file_operation_resource(
    relative_path: &str,
    document_id: Option<String>,
    resource_kind: Option<&str>,
) -> Option<LocalFileOperationResource> {
    let normalized = relative_path.trim().trim_matches('/').replace('\\', "/");
    if normalized.is_empty() {
        return None;
    }
    let resource_kind = resource_kind.unwrap_or_default();
    let object_kind = if document_id
        .as_deref()
        .is_some_and(|value| value.starts_with("local-md:"))
        || is_markdown_file(&normalized)
    {
        KernelObjectKind::Page
    } else if document_id
        .as_deref()
        .is_some_and(|value| value.starts_with("local-dir:"))
        || resource_kind == "local_directory"
        || resource_kind == "folder"
    {
        KernelObjectKind::Index
    } else {
        KernelObjectKind::Attachment
    };
    Some(LocalFileOperationResource {
        document_id: document_id.clone(),
        relative_path: normalized,
        object_identity: KernelObjectIdentity {
            object_kind,
            document_id,
            block_id: None,
            asset_id: None,
        },
    })
}

fn local_tree_previous_relative_path(result: &Value) -> Option<String> {
    local_tree_result_string(result, "previousRelativePath")
        .or_else(|| local_tree_result_string(result, "originalRelativePath"))
        .or_else(|| local_tree_result_string(result, "originalFilePath"))
}

fn add_local_tree_command_affected_parents(result: &Value, action: &str) -> Vec<AffectedParent> {
    let mut parents = Vec::new();
    let mut seen = BTreeSet::new();
    let normalized_action = action.trim();
    if let Some(previous_path) = local_tree_previous_relative_path(result) {
        push_local_tree_affected_parent(
            &mut parents,
            &mut seen,
            local_tree_parent_relative_path_for_path(&previous_path),
            "old-parent",
        );
    }
    if let Some(parent_path) = result.get("parentRelativePath").and_then(Value::as_str) {
        push_local_tree_affected_parent(&mut parents, &mut seen, parent_path.to_string(), "parent");
    }
    if normalized_action != "delete"
        && normalized_action != "trash"
        && normalized_action != "archive"
        && normalized_action != "purge"
    {
        if let Some(relative_path) = local_tree_result_string(result, "relativePath") {
            push_local_tree_affected_parent(
                &mut parents,
                &mut seen,
                local_tree_parent_relative_path_for_path(&relative_path),
                "new-parent",
            );
        }
    }
    if let Some(order_path) = local_tree_result_string(result, "orderRelativePath") {
        push_local_tree_affected_parent(
            &mut parents,
            &mut seen,
            local_tree_parent_relative_path_for_path(&order_path),
            "order-parent",
        );
    }
    parents
}

fn add_local_file_operation_contract(root_uri: &str, action: &str, mut result: Value) -> Value {
    let resolved_action =
        local_tree_result_string(&result, "action").unwrap_or_else(|| action.trim().to_string());
    let relative_path = local_tree_result_string(&result, "relativePath");
    let previous_relative_path = local_tree_previous_relative_path(&result);
    let document_id = local_tree_result_string(&result, "documentId")
        .or_else(|| local_tree_result_string(&result, "id"));
    let previous_document_id =
        local_tree_result_string(&result, "previousDocumentId").or_else(|| {
            previous_relative_path
                .as_deref()
                .map(local_markdown_path_page_id)
        });
    let resource_kind = local_tree_result_string(&result, "resourceKind");
    let affected_parents = add_local_tree_command_affected_parents(&result, &resolved_action);
    let resource = relative_path.as_deref().and_then(|path| {
        local_file_operation_resource(path, document_id.clone(), resource_kind.as_deref())
    });
    let previous_resource = previous_relative_path.as_deref().and_then(|path| {
        local_file_operation_resource(path, previous_document_id.clone(), resource_kind.as_deref())
    });
    let is_remove = matches!(
        resolved_action.as_str(),
        "delete" | "trash" | "archive" | "purge"
    );
    let reveal_target = if is_remove {
        None
    } else {
        relative_path.as_deref().map(|path| RevealTarget {
            relative_path: path.to_string(),
            row_id: local_file_operation_row_id(
                path,
                document_id.as_deref(),
                resource_kind.as_deref(),
            ),
        })
    };
    let select_target = if is_remove {
        previous_relative_path.as_deref().map(|path| {
            let parent = local_tree_parent_relative_path_for_path(path);
            RevealTarget {
                row_id: local_file_operation_row_id(&parent, None, Some("folder")),
                relative_path: parent,
            }
        })
    } else {
        reveal_target.clone()
    };
    let operation_hash = local_file_operation_hash(&[
        &resolved_action,
        relative_path.as_deref().unwrap_or_default(),
        previous_relative_path.as_deref().unwrap_or_default(),
        document_id.as_deref().unwrap_or_default(),
    ]);
    let contract = LocalFileOperationResult {
        schema: "mnote.local_file_operation_result.v1".to_string(),
        operation_id: format!("local-op-{resolved_action}-{operation_hash:016x}"),
        batch_id: None,
        action: resolved_action,
        root_uri: root_uri.to_string(),
        resource,
        previous_resource,
        affected_parents,
        reveal_target,
        select_target,
    };
    if let (Some(object), Ok(Value::Object(contract_object))) =
        (result.as_object_mut(), serde_json::to_value(contract))
    {
        for (key, value) in contract_object {
            object.insert(key, value);
        }
    }
    result
}

/// 生成新页面文件名主体。
/// 默认标题“新页面”会追加本地时间 HHMMSS，例如“新页面061240”。
/// 候选名必须同时检查 `.md` 文件和同名目录，避免页面 bundle 错配。
fn determine_page_stem(directory: &Path, title: &str) -> String {
    let base = if title.trim() == "新页面" {
        let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
        format!(
            "新页面{:02}{:02}{:02}",
            now.hour(),
            now.minute(),
            now.second()
        )
    } else {
        sanitize_file_stem(title, "新页面")
    };
    // 同时检查 .md 文件和同名目录，确保页面 bundle 使用同一个 stem。
    let mut candidate = base.clone();
    let mut index = 2u32;
    while directory.join(format!("{}.md", candidate)).exists()
        || directory.join(&candidate).exists()
    {
        candidate = format!("{}-{}", base, index);
        index += 1;
    }
    candidate
}

/// 生成新文件夹名。
/// 默认标题“新建文件夹”会追加本地时间 HHMMSS，与默认新建页面保持同类命名节奏。
fn determine_folder_stem(directory: &Path, title: &str) -> String {
    let base = if title.trim() == "新建文件夹" {
        let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
        format!(
            "新建文件夹{:02}{:02}{:02}",
            now.hour(),
            now.minute(),
            now.second()
        )
    } else {
        sanitize_file_stem(title, "新建文件夹")
    };
    let mut candidate = base.clone();
    let mut index = 2u32;
    while directory.join(&candidate).exists()
        || directory.join(format!("{}.md", candidate)).exists()
    {
        candidate = format!("{}-{}", base, index);
        index += 1;
    }
    candidate
}

fn determine_page_stem_for_rename(
    directory: &Path,
    title: &str,
    current_file: &Path,
    current_directory: Option<&Path>,
) -> String {
    let base = sanitize_file_stem(title, "新页面");
    let mut candidate = base.clone();
    let mut index = 2u32;
    loop {
        let file_candidate = directory.join(format!("{candidate}.md"));
        let directory_candidate = directory.join(&candidate);
        let file_conflict = file_candidate.exists() && file_candidate != current_file;
        let directory_conflict = directory_candidate.exists()
            && current_directory
                .map(|current| directory_candidate != current)
                .unwrap_or(true);
        if !file_conflict && !directory_conflict {
            return candidate;
        }
        candidate = format!("{}-{}", base, index);
        index += 1;
    }
}

fn determine_page_bundle_stem_for_rename(
    directory: &Path,
    title: &str,
    current_bundle_directory: &Path,
) -> String {
    let base = sanitize_file_stem(title, "新页面");
    let mut candidate = base.clone();
    let mut index = 2u32;
    loop {
        let bundle_candidate = directory.join(&candidate);
        let loose_markdown_candidate = directory.join(format!("{candidate}.md"));
        let bundle_conflict =
            bundle_candidate.exists() && bundle_candidate != current_bundle_directory;
        let loose_markdown_conflict = loose_markdown_candidate.exists();
        if !bundle_conflict && !loose_markdown_conflict {
            return candidate;
        }
        candidate = format!("{}-{}", base, index);
        index += 1;
    }
}

fn markdown_file_stem(path: &Path) -> Option<String> {
    path.file_stem()
        .and_then(|stem| stem.to_str())
        .map(str::trim)
        .filter(|stem| !stem.is_empty())
        .map(ToOwned::to_owned)
}

fn is_nested_page_bundle_markdown(path: &Path) -> bool {
    let Some(parent_name) = path
        .parent()
        .and_then(|parent| parent.file_name())
        .and_then(|name| name.to_str())
    else {
        return false;
    };
    markdown_file_stem(path)
        .map(|stem| stem == parent_name)
        .unwrap_or(false)
}

fn nested_bundle_main_markdown(directory: &Path) -> Option<PathBuf> {
    let name = directory.file_name().and_then(|value| value.to_str())?;
    let candidate = directory.join(format!("{name}.md"));
    if candidate.is_file() {
        Some(candidate)
    } else {
        None
    }
}

fn markdown_page_bundle_directory(markdown_path: &Path) -> Option<PathBuf> {
    let parent = markdown_path.parent()?;
    let stem = markdown_file_stem(markdown_path)?;
    if parent
        .file_name()
        .and_then(|name| name.to_str())
        .map(|name| name == stem)
        .unwrap_or(false)
    {
        return Some(parent.to_path_buf());
    }
    None
}

fn markdown_page_resource_directory(markdown_path: &Path) -> Option<PathBuf> {
    let parent = markdown_path.parent()?;
    let stem = markdown_file_stem(markdown_path)?;
    if parent
        .file_name()
        .and_then(|name| name.to_str())
        .map(|name| name == stem)
        .unwrap_or(false)
    {
        return Some(parent.to_path_buf());
    }
    let sibling_dir = parent.join(&stem);
    if sibling_dir.is_dir() {
        Some(sibling_dir)
    } else {
        Some(parent.join(sanitize_file_name(&stem, "page")))
    }
}

fn create_local_markdown_page(
    root: &Path,
    parent_id: Option<&str>,
    title: &str,
) -> Result<Value, WebError> {
    let metadata = load_local_folder_metadata(root)?;
    let parent_directory = resolve_local_parent_directory(root, &metadata, parent_id)?;
    let stem = determine_page_stem(&parent_directory, title);
    let dir_target = parent_directory.join(&stem);
    let target = dir_target.join(format!("{}.md", stem));
    let parent_relative_path = normalize_relative_path(root, &parent_directory)?;
    let relative_path = normalize_relative_path(root, &target)?;
    let page_id = local_markdown_path_page_id(&relative_path);
    let display_title = file_stem_title(&format!("{stem}.md"));
    let markdown = "";
    fs::create_dir_all(&dir_target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法创建本地页面目录 {}: {error}", dir_target.display()),
        )
    })?;
    fs::write(&target, markdown).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法创建本地 Markdown 文件 {}: {error}", target.display()),
        )
    })?;
    Ok(json!({
        "ok": true,
        "id": page_id,
        "documentId": page_id,
        "title": display_title,
        "parentRelativePath": parent_relative_path,
        "relativePath": relative_path,
        "action": "create",
        "sourceKind": "local_folder",
    }))
}

fn create_local_folder(
    root: &Path,
    parent_id: Option<&str>,
    title: &str,
) -> Result<Value, WebError> {
    let metadata = load_local_folder_metadata(root)?;
    let parent_directory = resolve_local_parent_directory(root, &metadata, parent_id)?;
    let target = parent_directory.join(determine_folder_stem(&parent_directory, title));
    let parent_relative_path = normalize_relative_path(root, &parent_directory)?;
    fs::create_dir_all(&target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法创建本地文件夹 {}: {error}", target.display()),
        )
    })?;
    Ok(json!({
        "ok": true,
        "id": local_directory_group_id(&normalize_relative_path(root, &target)?),
        "parentRelativePath": parent_relative_path,
        "relativePath": normalize_relative_path(root, &target)?,
        "action": "createFolder",
        "sourceKind": "local_folder",
    }))
}

fn rename_local_entry(root: &Path, document_id: &str, title: &str) -> Result<Value, WebError> {
    if let Some(directory) = resolve_local_directory_id(root, document_id)? {
        return rename_local_directory(root, &directory, title);
    }
    if let Some(file) = resolve_local_raw_file_id(root, document_id)? {
        return rename_local_raw_file(root, &file, title);
    }
    rename_local_markdown_page(root, document_id, title)
}

fn rename_local_raw_file(root: &Path, file: &Path, title: &str) -> Result<Value, WebError> {
    let parent = file.parent().ok_or_else(|| {
        WebError::bad_request_code("local_tree_command_failed", "无法解析文件父目录")
    })?;
    let original_extension = file
        .extension()
        .and_then(|value| value.to_str())
        .unwrap_or("");
    let requested_name = sanitize_file_name(title, "文件");
    let requested_has_extension = Path::new(&requested_name).extension().is_some();
    let target_name = if requested_has_extension || original_extension.is_empty() {
        requested_name
    } else {
        format!(
            "{}.{}",
            sanitize_file_stem(&requested_name, "文件"),
            original_extension
        )
    };
    let target = next_available_raw_path(parent, &target_name);
    fs::rename(file, &target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法重命名本地文件 {}: {error}", file.display()),
        )
    })?;
    Ok(json!({
        "ok": true,
        "id": local_node_id(&normalize_relative_path(root, &target)?),
        "documentId": format!("local:asset:{}", normalize_relative_path(root, &target)?),
        "resourceKind": "local_file",
        "relativePath": normalize_relative_path(root, &target)?,
        "previousRelativePath": normalize_relative_path(root, file)?,
        "action": "rename",
        "sourceKind": "local_folder",
    }))
}

fn rename_local_markdown_page(
    root: &Path,
    document_id: &str,
    title: &str,
) -> Result<Value, WebError> {
    let metadata = load_local_folder_metadata(root)?;
    let markdown_file =
        find_markdown_by_page_id(root, &metadata, document_id)?.ok_or_else(|| {
            WebError::bad_request_code(
                "local_markdown_not_found",
                "找不到要重命名的本地 Markdown 页面",
            )
        })?;
    let parent = markdown_file.path.parent().ok_or_else(|| {
        WebError::bad_request_code("local_tree_command_failed", "无法解析 Markdown 父目录")
    })?;
    if is_nested_page_bundle_markdown(&markdown_file.path) {
        return rename_nested_bundle_markdown_page(root, document_id, &markdown_file, title);
    }
    let new_stem = determine_page_stem_for_rename(parent, title, &markdown_file.path, None);
    let target = parent.join(format!("{new_stem}.md"));
    if target.exists() && target != markdown_file.path {
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            format!(
                "目标 Markdown 文件 {} 已存在，禁止覆盖重命名",
                target.display()
            ),
        ));
    }
    let old_relative_path = markdown_file.relative_path;

    fs::rename(&markdown_file.path, &target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!(
                "无法重命名本地 Markdown 文件 {}: {error}",
                markdown_file.path.display()
            ),
        )
    })?;

    let new_relative_path = normalize_relative_path(root, &target)?;
    apply_file_order_path_rewrite(root, &old_relative_path, &new_relative_path)?;
    let new_document_id = local_markdown_path_page_id(&new_relative_path);
    Ok(json!({
        "ok": true,
        "id": new_document_id,
        "documentId": new_document_id,
        "title": file_stem_title(&format!("{new_stem}.md")),
        "relativePath": new_relative_path,
        "previousDocumentId": document_id,
        "previousRelativePath": old_relative_path,
        "action": "rename",
        "sourceKind": "local_folder",
    }))
}

fn rename_nested_bundle_markdown_page(
    root: &Path,
    document_id: &str,
    markdown_file: &LocalFolderEntry,
    title: &str,
) -> Result<Value, WebError> {
    let bundle_dir = markdown_file.path.parent().ok_or_else(|| {
        WebError::bad_request_code("local_tree_command_failed", "无法解析页面目录")
    })?;
    if bundle_dir == root {
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "页面目录不能是 workspace root",
        ));
    }
    let bundle_parent = bundle_dir.parent().ok_or_else(|| {
        WebError::bad_request_code("local_tree_command_failed", "无法解析页面目录父级")
    })?;
    let new_stem = determine_page_bundle_stem_for_rename(bundle_parent, title, bundle_dir);
    let new_bundle_dir = bundle_parent.join(&new_stem);
    let new_markdown = new_bundle_dir.join(format!("{new_stem}.md"));
    if new_bundle_dir.exists() && new_bundle_dir != bundle_dir {
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            format!(
                "目标页面目录 {} 已存在，禁止覆盖重命名",
                new_bundle_dir.display()
            ),
        ));
    }
    let old_relative_path = markdown_file.relative_path.clone();
    if new_bundle_dir == bundle_dir {
        if new_markdown != markdown_file.path {
            fs::rename(&markdown_file.path, &new_markdown).map_err(|error| {
                WebError::bad_request_code(
                    "local_tree_command_failed",
                    format!(
                        "无法重命名本地 Markdown 文件 {}: {error}",
                        markdown_file.path.display()
                    ),
                )
            })?;
        }
    } else {
        fs::rename(bundle_dir, &new_bundle_dir).map_err(|error| {
            WebError::bad_request_code(
                "local_tree_command_failed",
                format!("无法重命名本地页面目录 {}: {error}", bundle_dir.display()),
            )
        })?;
        let moved_old_markdown = new_bundle_dir.join(
            markdown_file
                .path
                .file_name()
                .and_then(|name| name.to_str())
                .unwrap_or("page.md"),
        );
        if moved_old_markdown != new_markdown {
            fs::rename(&moved_old_markdown, &new_markdown).map_err(|error| {
                WebError::bad_request_code(
                    "local_tree_command_failed",
                    format!(
                        "无法重命名本地 Markdown 文件 {}: {error}",
                        moved_old_markdown.display()
                    ),
                )
            })?;
        }
    }
    let new_relative_path = normalize_relative_path(root, &new_markdown)?;
    let old_bundle_relative_path = markdown_file
        .path
        .parent()
        .and_then(|parent| normalize_relative_path(root, parent).ok())
        .unwrap_or_else(|| old_relative_path.clone());
    let new_bundle_relative_path = normalize_relative_path(root, &new_bundle_dir)?;
    apply_file_order_path_rewrite(root, &old_bundle_relative_path, &new_bundle_relative_path)?;
    let new_document_id = local_markdown_path_page_id(&new_relative_path);
    Ok(json!({
        "ok": true,
        "id": new_document_id,
        "documentId": new_document_id,
        "title": file_stem_title(&format!("{new_stem}.md")),
        "relativePath": new_relative_path,
        "previousDocumentId": document_id,
        "previousRelativePath": old_relative_path,
        "action": "rename",
        "sourceKind": "local_folder",
    }))
}

fn rename_local_directory(root: &Path, directory: &Path, title: &str) -> Result<Value, WebError> {
    if directory == root {
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "不能重命名本地 workspace root",
        ));
    }
    let parent = directory.parent().ok_or_else(|| {
        WebError::bad_request_code("local_tree_command_failed", "无法解析文件夹父目录")
    })?;
    let target = next_available_directory_path(parent, &sanitize_file_stem(title, "文件夹"));
    fs::rename(directory, &target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法重命名本地文件夹 {}: {error}", directory.display()),
        )
    })?;
    let new_relative_path = normalize_relative_path(root, &target)?;
    let old_relative_path = normalize_relative_path(root, directory)?;
    apply_file_order_path_rewrite(root, &old_relative_path, &new_relative_path)?;
    Ok(json!({
        "ok": true,
        "id": local_directory_group_id(&new_relative_path),
        "relativePath": new_relative_path,
        "previousRelativePath": old_relative_path,
        "action": "rename",
        "sourceKind": "local_folder",
    }))
}

fn copy_local_markdown_page(
    root: &Path,
    document_id: &str,
    parent_id: Option<&str>,
    title: &str,
) -> Result<Value, WebError> {
    let metadata = load_local_folder_metadata(root)?;
    let markdown_file =
        find_markdown_by_page_id(root, &metadata, document_id)?.ok_or_else(|| {
            WebError::bad_request_code(
                "local_markdown_not_found",
                "找不到要复制的本地 Markdown 页面",
            )
        })?;
    let target_directory = resolve_local_parent_directory(root, &metadata, parent_id)?;
    let source_stem = markdown_file
        .path
        .file_stem()
        .and_then(|stem| stem.to_str())
        .unwrap_or("页面");
    let copy_stem = if title.trim().is_empty() {
        source_stem
    } else {
        title.trim()
    };
    let copy_stem = sanitize_file_stem(copy_stem, "页面");
    let target;
    if let Some(source_bundle) = markdown_page_bundle_directory(&markdown_file.path) {
        let target_bundle = next_available_directory_path(&target_directory, &copy_stem);
        let target_stem = target_bundle
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or(&copy_stem)
            .to_string();
        target = target_bundle.join(format!("{target_stem}.md"));
        copy_directory_recursively(&source_bundle, &target_bundle).map_err(|error| {
            WebError::bad_request_code(
                "local_tree_command_failed",
                format!(
                    "无法复制本地页面目录 {} 到 {}: {error}",
                    source_bundle.display(),
                    target_bundle.display()
                ),
            )
        })?;
        let copied_main = target_bundle.join(
            markdown_file
                .path
                .file_name()
                .and_then(|name| name.to_str())
                .unwrap_or("page.md"),
        );
        if copied_main != target {
            fs::rename(&copied_main, &target).map_err(|error| {
                WebError::bad_request_code(
                    "local_tree_command_failed",
                    format!(
                        "无法重命名复制后的 Markdown 文件 {}: {error}",
                        copied_main.display()
                    ),
                )
            })?;
        }
    } else {
        target = next_available_path(&target_directory, &copy_stem, "md");
        let source_markdown = fs::read_to_string(&markdown_file.path).map_err(|error| {
            WebError::bad_request_code(
                "local_tree_command_failed",
                format!(
                    "无法读取待复制 Markdown 文件 {}: {error}",
                    markdown_file.path.display()
                ),
            )
        })?;
        fs::write(&target, source_markdown).map_err(|error| {
            WebError::bad_request_code(
                "local_tree_command_failed",
                format!("无法复制本地 Markdown 文件到 {}: {error}", target.display()),
            )
        })?;
    }
    let new_relative_path = normalize_relative_path(root, &target)?;
    let new_page_id = local_markdown_path_page_id(&new_relative_path);
    Ok(json!({
        "ok": true,
        "id": new_page_id,
        "documentId": new_page_id,
        "relativePath": new_relative_path,
        "action": "copy",
        "sourceKind": "local_folder",
    }))
}

fn move_local_entry(
    root: &Path,
    document_id: &str,
    parent_id: Option<&str>,
    sort_order: Option<i64>,
) -> Result<Value, WebError> {
    let original_relative_path =
        if let Some(directory) = resolve_local_directory_id(root, document_id)? {
            Some(normalize_relative_path(root, &directory)?)
        } else {
            load_local_folder_metadata(root)
                .ok()
                .and_then(|metadata| {
                    find_markdown_by_page_id(root, &metadata, document_id)
                        .ok()
                        .flatten()
                })
                .and_then(|entry| {
                    markdown_page_bundle_directory(&entry.path)
                        .and_then(|directory| normalize_relative_path(root, &directory).ok())
                        .or(Some(entry.relative_path))
                })
        };
    let result = if let Some(directory) = resolve_local_directory_id(root, document_id)? {
        move_local_directory(root, &directory, parent_id)?
    } else if let Some(file) = resolve_local_raw_file_id(root, document_id)? {
        move_local_raw_file(root, document_id, &file, parent_id)?
    } else {
        move_local_markdown_page(root, document_id, parent_id)?
    };
    if let Some(sort_order) = sort_order.filter(|value| *value >= 0) {
        if let Some(relative_path) = result.get("orderRelativePath").and_then(Value::as_str) {
            update_local_file_order_after_move(
                root,
                original_relative_path.as_deref(),
                relative_path,
                sort_order,
            )?;
        }
    }
    Ok(result)
}

fn move_local_raw_file(
    root: &Path,
    entry_id: &str,
    file: &Path,
    parent_id: Option<&str>,
) -> Result<Value, WebError> {
    let relative_path = normalize_relative_path(root, file)?;
    if is_markdown_file(&relative_path) {
        return Err(WebError::bad_request_code(
            "local_file_resource_not_supported",
            "Markdown 文件必须走页面生命周期，不能走 local_file 资源移动",
        ));
    }
    let metadata = load_local_folder_metadata(root)?;
    let target_directory = resolve_local_parent_directory(root, &metadata, parent_id)?;
    if file.parent() == Some(target_directory.as_path()) {
        return Ok(json!({
            "ok": true,
            "id": entry_id,
            "documentId": entry_id,
            "resourceKind": "local_file",
            "relativePath": relative_path,
            "orderRelativePath": relative_path,
            "action": "move",
            "sourceKind": "local_folder",
        }));
    }
    let file_name = file
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("local-file");
    let target = next_available_raw_path(&target_directory, file_name);
    fs::rename(file, &target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法移动本地文件 {}: {error}", file.display()),
        )
    })?;
    let new_relative_path = normalize_relative_path(root, &target)?;
    let new_id = format!("local:asset:{new_relative_path}");
    Ok(json!({
        "ok": true,
        "id": new_id,
        "documentId": new_id,
        "resourceKind": "local_file",
        "relativePath": new_relative_path,
        "orderRelativePath": new_relative_path,
        "previousDocumentId": entry_id,
        "previousRelativePath": relative_path,
        "action": "move",
        "sourceKind": "local_folder",
    }))
}

fn move_local_markdown_page(
    root: &Path,
    document_id: &str,
    parent_id: Option<&str>,
) -> Result<Value, WebError> {
    let metadata = load_local_folder_metadata(root)?;
    let markdown_file =
        find_markdown_by_page_id(root, &metadata, document_id)?.ok_or_else(|| {
            WebError::bad_request_code(
                "local_markdown_not_found",
                "找不到要移动的本地 Markdown 页面",
            )
        })?;
    let target_directory = resolve_local_parent_directory(root, &metadata, parent_id)?;
    if let Some(bundle_dir) = markdown_page_bundle_directory(&markdown_file.path) {
        return move_local_markdown_bundle(
            root,
            document_id,
            &markdown_file,
            &bundle_dir,
            &target_directory,
        );
    }
    if markdown_file.path.parent() == Some(target_directory.as_path()) {
        return Ok(json!({
            "ok": true,
            "id": document_id,
            "documentId": document_id,
            "relativePath": markdown_file.relative_path,
            "orderRelativePath": markdown_file.relative_path,
            "action": "move",
            "sourceKind": "local_folder",
        }));
    }
    let file_name = markdown_file
        .path
        .file_stem()
        .and_then(|stem| stem.to_str())
        .map(|stem| sanitize_file_stem(stem, "页面"))
        .unwrap_or_else(|| "页面".to_string());
    let target = next_available_path(&target_directory, &file_name, "md");
    if target == markdown_file.path {
        return Ok(json!({
            "ok": true,
            "id": document_id,
            "documentId": document_id,
            "relativePath": markdown_file.relative_path,
            "orderRelativePath": markdown_file.relative_path,
            "action": "move",
            "sourceKind": "local_folder",
        }));
    }
    if target.starts_with(&markdown_file.path) {
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "禁止把本地页面移动到自身或后代",
        ));
    }
    let old_relative_path = markdown_file.relative_path;
    fs::rename(&markdown_file.path, &target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!(
                "无法移动本地 Markdown 文件 {}: {error}",
                markdown_file.path.display()
            ),
        )
    })?;
    let new_relative_path = normalize_relative_path(root, &target)?;
    let new_document_id = local_markdown_path_page_id(&new_relative_path);
    Ok(json!({
        "ok": true,
        "id": new_document_id,
        "documentId": new_document_id,
        "relativePath": new_relative_path,
        "orderRelativePath": new_relative_path,
        "previousDocumentId": document_id,
        "previousRelativePath": old_relative_path,
        "action": "move",
        "sourceKind": "local_folder",
    }))
}

fn move_local_markdown_bundle(
    root: &Path,
    document_id: &str,
    markdown_file: &LocalFolderEntry,
    bundle_dir: &Path,
    target_directory: &Path,
) -> Result<Value, WebError> {
    if bundle_dir.parent() == Some(target_directory) {
        return Ok(json!({
            "ok": true,
            "id": document_id,
            "documentId": document_id,
            "relativePath": markdown_file.relative_path,
            "orderRelativePath": normalize_relative_path(root, bundle_dir)?,
            "action": "move",
            "sourceKind": "local_folder",
        }));
    }
    if target_directory == bundle_dir || target_directory.starts_with(bundle_dir) {
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "禁止把本地页面移动到自身或后代",
        ));
    }
    let stem = bundle_dir
        .file_name()
        .and_then(|name| name.to_str())
        .map(|name| sanitize_file_stem(name, "页面"))
        .unwrap_or_else(|| "页面".to_string());
    let target_bundle = next_available_directory_path(target_directory, &stem);
    fs::rename(bundle_dir, &target_bundle).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法移动本地页面目录 {}: {error}", bundle_dir.display()),
        )
    })?;
    let main_file_name = markdown_file
        .path
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("page.md");
    let moved_markdown = target_bundle.join(main_file_name);
    let target_stem = target_bundle
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or(&stem);
    let target_markdown = target_bundle.join(format!("{target_stem}.md"));
    if moved_markdown != target_markdown {
        fs::rename(&moved_markdown, &target_markdown).map_err(|error| {
            WebError::bad_request_code(
                "local_tree_command_failed",
                format!(
                    "无法重命名移动后的 Markdown 文件 {}: {error}",
                    moved_markdown.display()
                ),
            )
        })?;
    }
    let new_relative_path = normalize_relative_path(root, &target_markdown)?;
    let new_bundle_relative_path = normalize_relative_path(root, &target_bundle)?;
    let new_document_id = local_markdown_path_page_id(&new_relative_path);
    Ok(json!({
        "ok": true,
        "id": new_document_id,
        "documentId": new_document_id,
        "relativePath": new_relative_path,
        "orderRelativePath": new_bundle_relative_path,
        "previousDocumentId": document_id,
        "previousRelativePath": markdown_file.relative_path,
        "action": "move",
        "sourceKind": "local_folder",
    }))
}

fn move_local_directory(
    root: &Path,
    directory: &Path,
    parent_id: Option<&str>,
) -> Result<Value, WebError> {
    if directory == root {
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "不能移动本地 workspace root",
        ));
    }
    let metadata = load_local_folder_metadata(root)?;
    let target_directory = resolve_local_parent_directory(root, &metadata, parent_id)?;
    if target_directory == directory || target_directory.starts_with(directory) {
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "禁止把本地文件夹移动到自身或后代",
        ));
    }
    if directory.parent() == Some(target_directory.as_path()) {
        return Ok(json!({
            "ok": true,
            "id": local_directory_group_id(&normalize_relative_path(root, directory)?),
            "relativePath": normalize_relative_path(root, directory)?,
            "orderRelativePath": normalize_relative_path(root, directory)?,
            "action": "move",
            "sourceKind": "local_folder",
        }));
    }
    let stem = directory
        .file_name()
        .and_then(|name| name.to_str())
        .map(|name| sanitize_file_stem(name, "文件夹"))
        .unwrap_or_else(|| "文件夹".to_string());
    let target = next_available_directory_path(&target_directory, &stem);
    fs::rename(directory, &target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法移动本地文件夹 {}: {error}", directory.display()),
        )
    })?;
    let new_relative_path = normalize_relative_path(root, &target)?;
    Ok(json!({
        "ok": true,
        "id": local_directory_group_id(&new_relative_path),
        "relativePath": new_relative_path,
        "orderRelativePath": new_relative_path,
        "previousRelativePath": normalize_relative_path(root, directory)?,
        "action": "move",
        "sourceKind": "local_folder",
    }))
}

fn trash_local_entry(root: &Path, entry_id: &str) -> Result<Value, WebError> {
    if let Some(directory) = resolve_local_directory_id(root, entry_id)? {
        // 若目录是页面 bundle（包含同名 .md），按 Markdown 页面生命周期入 trash
        if let Some(main_md) = nested_bundle_main_markdown(&directory) {
            let relative = normalize_relative_path(root, &main_md)?;
            let page_id = local_markdown_path_page_id(&relative);
            return trash_local_markdown_page(root, &page_id);
        }
        return trash_local_directory(root, entry_id, &directory);
    }
    if let Some(file) = resolve_local_raw_file_id(root, entry_id)? {
        return trash_local_raw_file(root, entry_id, &file);
    }
    trash_local_markdown_page(root, entry_id)
}

fn restore_local_entry(root: &Path, entry_id: &str) -> Result<Value, WebError> {
    let metadata = load_local_folder_metadata(root)?;
    if find_local_directory_trash_entry_key(&metadata, entry_id).is_some() {
        return restore_local_directory(root, entry_id);
    }
    if find_local_file_trash_entry_key(&metadata, entry_id).is_some() {
        return restore_local_raw_file(root, entry_id);
    }
    restore_local_markdown_page(root, entry_id)
}

fn purge_local_entry(root: &Path, entry_id: &str) -> Result<Value, WebError> {
    let metadata = load_local_folder_metadata(root)?;
    if find_local_directory_trash_entry_key(&metadata, entry_id).is_some() {
        return purge_local_directory(root, entry_id);
    }
    if find_local_file_trash_entry_key(&metadata, entry_id).is_some() {
        return purge_local_raw_file(root, entry_id);
    }
    if entry_id.starts_with("local:asset:") || entry_id.starts_with("local:node:") {
        return Err(WebError::bad_request_code(
            "local_trash_entry_not_found",
            "本地资源必须先进入回收站后才能永久删除",
        ));
    }
    purge_local_markdown_page(root, entry_id)
}

fn trash_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, WebError> {
    let mut metadata = load_local_folder_metadata(root)?;
    let markdown_file =
        find_markdown_by_page_id(root, &metadata, document_id)?.ok_or_else(|| {
            WebError::bad_request_code(
                "local_markdown_not_found",
                "找不到要删除的本地 Markdown 页面",
            )
        })?;
    let trash_dir = root.join(".mnote").join("trash");
    fs::create_dir_all(&trash_dir).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法创建本地回收站 {}: {error}", trash_dir.display()),
        )
    })?;
    let bundle_dir = markdown_page_bundle_directory(&markdown_file.path);
    let (source_path, target, resource_kind) = if let Some(bundle_dir) = bundle_dir.as_ref() {
        if bundle_dir == root {
            return Err(WebError::bad_request_code(
                "local_tree_command_failed",
                "不能删除本地 workspace root",
            ));
        }
        let directory_name = bundle_dir
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("page");
        (
            bundle_dir.clone(),
            next_available_directory_path(&trash_dir, directory_name),
            "markdown_bundle",
        )
    } else {
        let file_name = markdown_file
            .path
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("page.md");
        (
            markdown_file.path.clone(),
            next_available_path(&trash_dir, &file_stem_title(file_name), "md"),
            "markdown",
        )
    };
    fs::rename(&source_path, &target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法移动到本地回收站 {}: {error}", source_path.display()),
        )
    })?;
    let original_relative_path = if let Some(bundle_dir) = bundle_dir.as_ref() {
        normalize_relative_path(root, bundle_dir)?
    } else {
        markdown_file.relative_path.clone()
    };
    let trash_relative_path = normalize_relative_path(root, &target)?;
    remove_file_order_path(&mut metadata.file_order, &original_relative_path);
    metadata.trash_entries.insert(
        document_id.to_string(),
        LocalTrashEntry {
            document_id: document_id.to_string(),
            resource_kind: resource_kind.to_string(),
            resource_scope: "local_folder".to_string(),
            original_file_path: original_relative_path.clone(),
            trashed_file_path: trash_relative_path.clone(),
            trash_entry_id: document_id.to_string(),
            original_relative_path: original_relative_path.clone(),
            trash_relative_path: trash_relative_path.clone(),
            deleted_at_ms: now_ms(),
            archived_at: now_ms(),
            purged_at: None,
        },
    );
    write_trash_index_metadata(root, &metadata.trash_entries)?;
    write_file_order_metadata(root, &metadata.file_order)?;
    Ok(json!({
        "ok": true,
        "id": document_id,
        "documentId": document_id,
        "resourceKind": resource_kind,
        "originalRelativePath": original_relative_path,
        "trashPath": trash_relative_path,
        "action": "delete",
        "sourceKind": "local_folder",
    }))
}

fn trash_local_raw_file(root: &Path, entry_id: &str, file: &Path) -> Result<Value, WebError> {
    let mut metadata = load_local_folder_metadata(root)?;
    let relative_path = normalize_relative_path(root, file)?;
    if is_markdown_file(&relative_path) {
        return Err(WebError::bad_request_code(
            "local_file_resource_not_supported",
            "Markdown 文件必须走页面生命周期，不能走 local_file 资源回收站",
        ));
    }
    let trash_dir = root.join(".mnote").join("trash");
    fs::create_dir_all(&trash_dir).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法创建本地回收站 {}: {error}", trash_dir.display()),
        )
    })?;
    let file_name = file
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("local-file");
    let target = next_available_raw_path(&trash_dir, file_name);
    fs::rename(file, &target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法移动本地资源到回收站 {}: {error}", file.display()),
        )
    })?;
    let trash_relative_path = normalize_relative_path(root, &target)?;
    let trash_entry_id = local_file_trash_entry_id(&relative_path);
    let now = now_ms();
    remove_file_order_path(&mut metadata.file_order, &relative_path);
    metadata.trash_entries.insert(
        trash_entry_id.clone(),
        LocalTrashEntry {
            document_id: entry_id.to_string(),
            resource_kind: "local_file".to_string(),
            resource_scope: "local_folder".to_string(),
            original_file_path: relative_path.clone(),
            trashed_file_path: trash_relative_path.clone(),
            trash_entry_id: trash_entry_id.clone(),
            original_relative_path: relative_path.clone(),
            trash_relative_path: trash_relative_path.clone(),
            deleted_at_ms: now,
            archived_at: now,
            purged_at: None,
        },
    );
    write_trash_index_metadata(root, &metadata.trash_entries)?;
    write_file_order_metadata(root, &metadata.file_order)?;
    Ok(json!({
        "ok": true,
        "id": entry_id,
        "documentId": entry_id,
        "resourceKind": "local_file",
        "resourceScope": "local_folder",
        "originalRelativePath": relative_path,
        "originalFilePath": relative_path,
        "trashEntryId": trash_entry_id,
        "trashPath": trash_relative_path,
        "action": "delete",
        "canonicalCommand": "tree.resource.archive",
        "sourceKind": "local_folder",
    }))
}

fn trash_local_directory(root: &Path, entry_id: &str, directory: &Path) -> Result<Value, WebError> {
    if directory == root {
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "不能删除本地 workspace root",
        ));
    }
    let mut metadata = load_local_folder_metadata(root)?;
    let relative_path = normalize_relative_path(root, directory)?;
    if relative_path == ".mnote" || relative_path.starts_with(".mnote/") {
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "不能删除 MNote 元数据目录",
        ));
    }
    let trash_dir = root.join(".mnote").join("trash");
    fs::create_dir_all(&trash_dir).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法创建本地回收站 {}: {error}", trash_dir.display()),
        )
    })?;
    let directory_name = directory
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("local-folder");
    let target = next_available_directory_path(&trash_dir, directory_name);
    fs::rename(directory, &target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!(
                "无法移动本地文件夹到回收站 {}: {error}",
                directory.display()
            ),
        )
    })?;
    let trash_relative_path = normalize_relative_path(root, &target)?;
    let trash_entry_id = local_directory_trash_entry_id(&relative_path);
    let now = now_ms();
    remove_file_order_path(&mut metadata.file_order, &relative_path);
    metadata.trash_entries.insert(
        trash_entry_id.clone(),
        LocalTrashEntry {
            document_id: entry_id.to_string(),
            resource_kind: "local_directory".to_string(),
            resource_scope: "local_folder".to_string(),
            original_file_path: relative_path.clone(),
            trashed_file_path: trash_relative_path.clone(),
            trash_entry_id: trash_entry_id.clone(),
            original_relative_path: relative_path.clone(),
            trash_relative_path: trash_relative_path.clone(),
            deleted_at_ms: now,
            archived_at: now,
            purged_at: None,
        },
    );
    write_trash_index_metadata(root, &metadata.trash_entries)?;
    write_file_order_metadata(root, &metadata.file_order)?;
    Ok(json!({
        "ok": true,
        "id": entry_id,
        "documentId": entry_id,
        "resourceKind": "local_directory",
        "resourceScope": "local_folder",
        "originalRelativePath": relative_path,
        "originalFilePath": relative_path,
        "trashEntryId": trash_entry_id,
        "trashPath": trash_relative_path,
        "action": "delete",
        "canonicalCommand": "tree.resource.archive",
        "sourceKind": "local_folder",
    }))
}

fn restore_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, WebError> {
    let mut metadata = load_local_folder_metadata(root)?;
    let trash_entry = metadata
        .trash_entries
        .get(document_id)
        .cloned()
        .ok_or_else(|| {
            WebError::bad_request_code(
                "local_trash_entry_not_found",
                "找不到要恢复的本地回收站记录",
            )
        })?;
    let trash_path = resolve_metadata_relative_path(root, &trash_entry.trash_relative_path)?;
    if trash_entry.resource_kind == "markdown_bundle" {
        return restore_local_markdown_bundle(
            root,
            document_id,
            &mut metadata,
            trash_entry,
            &trash_path,
        );
    }
    if !trash_path.is_file() {
        return Err(WebError::bad_request_code(
            "local_trash_entry_not_found",
            "本地回收站文件不存在，无法恢复",
        ));
    }
    let original_path = resolve_metadata_relative_path(root, &trash_entry.original_relative_path)?;
    let parent = original_path.parent().ok_or_else(|| {
        WebError::bad_request_code("local_tree_command_failed", "无法解析恢复目标目录")
    })?;
    fs::create_dir_all(parent).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法创建恢复目标目录 {}: {error}", parent.display()),
        )
    })?;
    let stem = original_path
        .file_stem()
        .and_then(|stem| stem.to_str())
        .map(|stem| sanitize_file_stem(stem, "页面"))
        .unwrap_or_else(|| "页面".to_string());
    let target = if original_path.exists() {
        next_available_path(parent, &stem, "md")
    } else {
        original_path
    };
    fs::rename(&trash_path, &target).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法从本地回收站恢复 {}: {error}", trash_path.display()),
        )
    })?;
    let restored_relative_path = normalize_relative_path(root, &target)?;
    let restored_document_id = local_markdown_path_page_id(&restored_relative_path);
    metadata.trash_entries.remove(document_id);
    write_trash_index_metadata(root, &metadata.trash_entries)?;
    Ok(json!({
        "ok": true,
        "id": restored_document_id,
        "documentId": restored_document_id,
        "relativePath": restored_relative_path,
        "previousDocumentId": document_id,
        "action": "restore",
        "sourceKind": "local_folder",
    }))
}

fn restore_local_markdown_bundle(
    root: &Path,
    document_id: &str,
    metadata: &mut LocalFolderMetadata,
    trash_entry: LocalTrashEntry,
    trash_path: &Path,
) -> Result<Value, WebError> {
    if !trash_path.is_dir() {
        return Err(WebError::bad_request_code(
            "local_trash_entry_not_found",
            "本地回收站页面目录不存在，无法恢复",
        ));
    }
    let original_bundle_path =
        resolve_metadata_relative_path(root, &trash_entry.original_relative_path)?;
    let parent = original_bundle_path.parent().ok_or_else(|| {
        WebError::bad_request_code("local_tree_command_failed", "无法解析页面恢复目标目录")
    })?;
    fs::create_dir_all(parent).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法创建页面恢复目标目录 {}: {error}", parent.display()),
        )
    })?;
    let stem = original_bundle_path
        .file_name()
        .and_then(|name| name.to_str())
        .map(|name| sanitize_file_stem(name, "页面"))
        .unwrap_or_else(|| "页面".to_string());
    let target_bundle = if original_bundle_path.exists() {
        next_available_directory_path(parent, &stem)
    } else {
        original_bundle_path
    };
    fs::rename(trash_path, &target_bundle).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!(
                "无法从本地回收站恢复页面目录 {}: {error}",
                trash_path.display()
            ),
        )
    })?;
    let target_stem = target_bundle
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or(&stem);
    let restored_markdown = target_bundle.join(format!("{target_stem}.md"));
    if !restored_markdown.is_file() {
        return Err(WebError::bad_request_code(
            "local_trash_entry_not_found",
            "恢复后的页面目录缺少同名 Markdown 文件",
        ));
    }
    let restored_relative_path = normalize_relative_path(root, &restored_markdown)?;
    let restored_document_id = local_markdown_path_page_id(&restored_relative_path);
    metadata.trash_entries.remove(document_id);
    write_trash_index_metadata(root, &metadata.trash_entries)?;
    Ok(json!({
        "ok": true,
        "id": restored_document_id,
        "documentId": restored_document_id,
        "relativePath": restored_relative_path,
        "previousDocumentId": document_id,
        "action": "restore",
        "sourceKind": "local_folder",
    }))
}

fn restore_local_raw_file(root: &Path, entry_id: &str) -> Result<Value, WebError> {
    let mut metadata = load_local_folder_metadata(root)?;
    let trash_key = find_local_file_trash_entry_key(&metadata, entry_id).ok_or_else(|| {
        WebError::bad_request_code(
            "local_trash_entry_not_found",
            "找不到要恢复的本地资源回收站记录",
        )
    })?;
    let trash_entry = metadata
        .trash_entries
        .get(&trash_key)
        .cloned()
        .ok_or_else(|| {
            WebError::bad_request_code(
                "local_trash_entry_not_found",
                "找不到要恢复的本地资源回收站记录",
            )
        })?;
    let original_relative_path = local_trash_original_path(&trash_entry);
    let trash_relative_path = local_trash_file_path(&trash_entry);
    let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?;
    if !trash_path.is_file() {
        return Err(WebError::bad_request_code(
            "local_trash_entry_not_found",
            "本地资源回收站文件不存在，无法恢复",
        ));
    }
    let original_path = resolve_metadata_relative_path(root, &original_relative_path)?;
    if original_path.exists() {
        return Err(WebError::new(
            StatusCode::CONFLICT,
            "local_file_restore_conflict",
            "本地资源原路径已存在，恢复会覆盖用户文件",
        ));
    }
    let parent = original_path.parent().ok_or_else(|| {
        WebError::bad_request_code("local_tree_command_failed", "无法解析资源恢复目标目录")
    })?;
    fs::create_dir_all(parent).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法创建资源恢复目标目录 {}: {error}", parent.display()),
        )
    })?;
    fs::rename(&trash_path, &original_path).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法从本地回收站恢复资源 {}: {error}", trash_path.display()),
        )
    })?;
    metadata.trash_entries.remove(&trash_key);
    write_trash_index_metadata(root, &metadata.trash_entries)?;
    Ok(json!({
        "ok": true,
        "id": entry_id,
        "documentId": entry_id,
        "resourceKind": "local_file",
        "resourceScope": "local_folder",
        "originalFilePath": original_relative_path,
        "relativePath": normalize_relative_path(root, &original_path)?,
        "trashEntryId": trash_key,
        "action": "restore",
        "canonicalCommand": "tree.resource.restore",
        "sourceKind": "local_folder",
    }))
}

fn restore_local_directory(root: &Path, entry_id: &str) -> Result<Value, WebError> {
    let mut metadata = load_local_folder_metadata(root)?;
    let trash_key = find_local_directory_trash_entry_key(&metadata, entry_id).ok_or_else(|| {
        WebError::bad_request_code(
            "local_trash_entry_not_found",
            "找不到要恢复的本地文件夹回收站记录",
        )
    })?;
    let trash_entry = metadata
        .trash_entries
        .get(&trash_key)
        .cloned()
        .ok_or_else(|| {
            WebError::bad_request_code(
                "local_trash_entry_not_found",
                "找不到要恢复的本地文件夹回收站记录",
            )
        })?;
    let original_relative_path = local_trash_original_path(&trash_entry);
    let trash_relative_path = local_trash_file_path(&trash_entry);
    let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?;
    if !trash_path.is_dir() {
        return Err(WebError::bad_request_code(
            "local_trash_entry_not_found",
            "本地文件夹回收站目录不存在，无法恢复",
        ));
    }
    let original_path = resolve_metadata_relative_path(root, &original_relative_path)?;
    if original_path.exists() {
        return Err(WebError::new(
            StatusCode::CONFLICT,
            "local_directory_restore_conflict",
            "本地文件夹原路径已存在，恢复会覆盖用户文件",
        ));
    }
    let parent = original_path.parent().ok_or_else(|| {
        WebError::bad_request_code("local_tree_command_failed", "无法解析文件夹恢复目标目录")
    })?;
    fs::create_dir_all(parent).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("无法创建文件夹恢复目标目录 {}: {error}", parent.display()),
        )
    })?;
    fs::rename(&trash_path, &original_path).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!(
                "无法从本地回收站恢复文件夹 {}: {error}",
                trash_path.display()
            ),
        )
    })?;
    metadata.trash_entries.remove(&trash_key);
    write_trash_index_metadata(root, &metadata.trash_entries)?;
    Ok(json!({
        "ok": true,
        "id": entry_id,
        "documentId": entry_id,
        "resourceKind": "local_directory",
        "resourceScope": "local_folder",
        "relativePath": normalize_relative_path(root, &original_path)?,
        "trashEntryId": trash_key,
        "action": "restore",
        "canonicalCommand": "tree.resource.restore",
        "sourceKind": "local_folder",
    }))
}

fn purge_local_markdown_page(root: &Path, document_id: &str) -> Result<Value, WebError> {
    let mut metadata = load_local_folder_metadata(root)?;
    if let Some(markdown_file) = find_markdown_by_page_id(root, &metadata, document_id)? {
        let order_relative_path = markdown_page_bundle_directory(&markdown_file.path)
            .and_then(|bundle_dir| normalize_relative_path(root, &bundle_dir).ok())
            .unwrap_or_else(|| markdown_file.relative_path.clone());
        if let Some(bundle_dir) = markdown_page_bundle_directory(&markdown_file.path) {
            fs::remove_dir_all(&bundle_dir).map_err(|error| {
                WebError::bad_request_code(
                    "local_tree_command_failed",
                    format!("无法永久删除本地页面目录 {}: {error}", bundle_dir.display()),
                )
            })?;
        } else {
            fs::remove_file(&markdown_file.path).map_err(|error| {
                WebError::bad_request_code(
                    "local_tree_command_failed",
                    format!(
                        "无法永久删除本地 Markdown 文件 {}: {error}",
                        markdown_file.path.display()
                    ),
                )
            })?;
        }
        remove_file_order_path(&mut metadata.file_order, &order_relative_path);
        write_file_order_metadata(root, &metadata.file_order)?;
        return Ok(json!({
            "ok": true,
            "id": document_id,
            "documentId": document_id,
            "originalRelativePath": order_relative_path,
            "action": "purge",
            "sourceKind": "local_folder",
        }));
    }
    let trash_entry = metadata.trash_entries.remove(document_id).ok_or_else(|| {
        WebError::bad_request_code(
            "local_markdown_not_found",
            "找不到要永久删除的本地 Markdown 页面",
        )
    })?;
    remove_file_order_path(
        &mut metadata.file_order,
        &trash_entry.original_relative_path,
    );
    let trash_path = resolve_metadata_relative_path(root, &trash_entry.trash_relative_path)?;
    if trash_path.exists() {
        let remove_result = if trash_path.is_dir() {
            fs::remove_dir_all(&trash_path)
        } else {
            fs::remove_file(&trash_path)
        };
        remove_result.map_err(|error| {
            WebError::bad_request_code(
                "local_tree_command_failed",
                format!(
                    "无法永久删除本地回收站对象 {}: {error}",
                    trash_path.display()
                ),
            )
        })?;
    }
    write_trash_index_metadata(root, &metadata.trash_entries)?;
    write_file_order_metadata(root, &metadata.file_order)?;
    Ok(json!({
        "ok": true,
        "id": document_id,
        "documentId": document_id,
        "originalRelativePath": trash_entry.original_relative_path,
        "action": "purge",
        "sourceKind": "local_folder",
    }))
}

fn purge_local_directory(root: &Path, entry_id: &str) -> Result<Value, WebError> {
    let mut metadata = load_local_folder_metadata(root)?;
    let trash_key = find_local_directory_trash_entry_key(&metadata, entry_id).ok_or_else(|| {
        WebError::bad_request_code(
            "local_trash_entry_not_found",
            "找不到要永久删除的本地文件夹回收站记录",
        )
    })?;
    let trash_entry = metadata.trash_entries.remove(&trash_key).ok_or_else(|| {
        WebError::bad_request_code(
            "local_trash_entry_not_found",
            "找不到要永久删除的本地文件夹回收站记录",
        )
    })?;
    remove_file_order_path(
        &mut metadata.file_order,
        &trash_entry.original_relative_path,
    );
    let trash_relative_path = local_trash_file_path(&trash_entry);
    let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?;
    if trash_path.exists() {
        fs::remove_dir_all(&trash_path).map_err(|error| {
            WebError::bad_request_code(
                "local_tree_command_failed",
                format!(
                    "无法永久删除本地回收站文件夹 {}: {error}",
                    trash_path.display()
                ),
            )
        })?;
    }
    write_trash_index_metadata(root, &metadata.trash_entries)?;
    write_file_order_metadata(root, &metadata.file_order)?;
    Ok(json!({
        "ok": true,
        "id": entry_id,
        "documentId": entry_id,
        "resourceKind": "local_directory",
        "resourceScope": "local_folder",
        "originalRelativePath": trash_entry.original_relative_path,
        "trashEntryId": trash_key,
        "action": "purge",
        "canonicalCommand": "tree.resource.purge",
        "sourceKind": "local_folder",
    }))
}

fn purge_local_raw_file(root: &Path, entry_id: &str) -> Result<Value, WebError> {
    let mut metadata = load_local_folder_metadata(root)?;
    let trash_key = find_local_file_trash_entry_key(&metadata, entry_id).ok_or_else(|| {
        WebError::bad_request_code(
            "local_trash_entry_not_found",
            "找不到要永久删除的本地资源回收站记录",
        )
    })?;
    let trash_entry = metadata.trash_entries.remove(&trash_key).ok_or_else(|| {
        WebError::bad_request_code(
            "local_trash_entry_not_found",
            "找不到要永久删除的本地资源回收站记录",
        )
    })?;
    remove_file_order_path(
        &mut metadata.file_order,
        &trash_entry.original_relative_path,
    );
    let trash_relative_path = local_trash_file_path(&trash_entry);
    let trash_path = resolve_metadata_relative_path(root, &trash_relative_path)?;
    if trash_path.exists() {
        fs::remove_file(&trash_path).map_err(|error| {
            WebError::bad_request_code(
                "local_tree_command_failed",
                format!(
                    "无法永久删除本地资源回收站文件 {}: {error}",
                    trash_path.display()
                ),
            )
        })?;
    }
    write_trash_index_metadata(root, &metadata.trash_entries)?;
    Ok(json!({
        "ok": true,
        "id": entry_id,
        "documentId": entry_id,
        "resourceKind": "local_file",
        "resourceScope": "local_folder",
        "originalRelativePath": trash_entry.original_relative_path,
        "trashEntryId": trash_key,
        "action": "purge",
        "canonicalCommand": "tree.resource.purge",
        "sourceKind": "local_folder",
    }))
}

fn resolve_local_parent_directory(
    root: &Path,
    metadata: &LocalFolderMetadata,
    parent_id: Option<&str>,
) -> Result<PathBuf, WebError> {
    let Some(parent_id) = parent_id.map(str::trim).filter(|value| !value.is_empty()) else {
        return Ok(root.to_path_buf());
    };
    if let Some(encoded) = parent_id.strip_prefix("local-dir:") {
        let relative_path = decode_local_id_segment(encoded)?;
        let directory = root.join(relative_path);
        if directory.starts_with(root) && directory.is_dir() {
            return Ok(directory);
        }
    }
    if let Some(relative_path) = parent_id.strip_prefix("local:node:") {
        let directory = resolve_metadata_relative_path(root, relative_path)?;
        if directory.is_dir() {
            return Ok(directory);
        }
    }
    if let Some(relative_path) = parent_id.strip_prefix("local:folder:") {
        let directory = resolve_metadata_relative_path(root, relative_path)?;
        if directory.is_dir() {
            return Ok(directory);
        }
    }
    if let Some(markdown_file) = find_markdown_by_page_id(root, metadata, parent_id)? {
        if let Some(parent) = markdown_file.path.parent() {
            return Ok(parent.to_path_buf());
        }
    }
    Err(WebError::bad_request_code(
        "local_tree_command_failed",
        "无法解析本地新建目标目录",
    ))
}

fn drop_external_files_into_local_folder(
    root: &Path,
    parent_id: Option<&str>,
    files_json: &str,
) -> Result<Value, WebError> {
    let metadata = load_local_folder_metadata(root)?;
    let target_directory = resolve_local_parent_directory(root, &metadata, parent_id)?;
    let files = serde_json::from_str::<Vec<LocalDropFilePayload>>(files_json).map_err(|error| {
        WebError::bad_request_code(
            "local_tree_command_failed",
            format!("外部拖入文件 payload 非法: {error}"),
        )
    })?;
    if files.is_empty() {
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "外部拖入文件不能为空",
        ));
    }
    let mut written = Vec::new();
    for file in files {
        let name = sanitize_file_name(&file.name, "dropped-file");
        let path = next_available_raw_path(&target_directory, &name);
        fs::write(&path, file.text.unwrap_or_default()).map_err(|error| {
            WebError::bad_request_code(
                "local_tree_command_failed",
                format!("无法写入外部拖入文件 {}: {error}", path.display()),
            )
        })?;
        written.push(json!({
            "name": name,
            "relativePath": normalize_relative_path(root, &path)?,
        }));
    }
    Ok(json!({
        "ok": true,
        "action": "dropFiles",
        "files": written,
        "sourceKind": "local_folder",
    }))
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LocalDropFilePayload {
    name: String,
    text: Option<String>,
}

fn resolve_local_directory_id(root: &Path, entry_id: &str) -> Result<Option<PathBuf>, WebError> {
    let trimmed = entry_id.trim();
    if let Some(relative_path) = trimmed
        .strip_prefix("local:node:")
        .or_else(|| trimmed.strip_prefix("local:folder:"))
    {
        let directory = resolve_metadata_relative_path(root, relative_path)?;
        if directory.is_dir() {
            return Ok(Some(directory));
        }
        if directory.is_file() {
            return Ok(None);
        }
        return Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "本地文件夹不存在",
        ));
    }
    let Some(encoded) = trimmed.strip_prefix("local-dir:") else {
        return Ok(None);
    };
    let relative_path = decode_local_id_segment(encoded)?;
    let directory = resolve_metadata_relative_path(root, &relative_path)?;
    if directory.is_dir() {
        Ok(Some(directory))
    } else {
        Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "本地文件夹不存在",
        ))
    }
}

fn resolve_local_raw_file_id(root: &Path, entry_id: &str) -> Result<Option<PathBuf>, WebError> {
    let trimmed = entry_id.trim();
    let Some(relative_path) = trimmed
        .strip_prefix("local-file:")
        .or_else(|| trimmed.strip_prefix("local:asset:"))
        .or_else(|| trimmed.strip_prefix("local:node:"))
    else {
        return Ok(None);
    };
    let file = resolve_metadata_relative_path(root, relative_path)?;
    if file.is_file() {
        Ok(Some(file))
    } else {
        Err(WebError::bad_request_code(
            "local_tree_command_failed",
            "本地文件不存在",
        ))
    }
}

fn local_file_trash_entry_id(relative_path: &str) -> String {
    format!("local-file:{relative_path}")
}

fn local_directory_trash_entry_id(relative_path: &str) -> String {
    format!("local-dir-trash:{relative_path}")
}

fn is_local_file_trash_entry(entry: &LocalTrashEntry) -> bool {
    entry.resource_kind == "local_file" || entry.trash_entry_id.starts_with("local-file:")
}

fn is_local_directory_trash_entry(entry: &LocalTrashEntry) -> bool {
    entry.resource_kind == "local_directory" || entry.trash_entry_id.starts_with("local-dir-trash:")
}

fn local_trash_original_path(entry: &LocalTrashEntry) -> String {
    if !entry.original_file_path.trim().is_empty() {
        entry.original_file_path.clone()
    } else {
        entry.original_relative_path.clone()
    }
}

fn local_trash_file_path(entry: &LocalTrashEntry) -> String {
    if !entry.trashed_file_path.trim().is_empty() {
        entry.trashed_file_path.clone()
    } else {
        entry.trash_relative_path.clone()
    }
}

fn find_local_file_trash_entry_key(
    metadata: &LocalFolderMetadata,
    entry_id: &str,
) -> Option<String> {
    let trimmed = entry_id.trim();
    if metadata
        .trash_entries
        .get(trimmed)
        .map(is_local_file_trash_entry)
        .unwrap_or(false)
    {
        return Some(trimmed.to_string());
    }
    let relative_path = trimmed
        .strip_prefix("local:asset:")
        .or_else(|| trimmed.strip_prefix("local:node:"))
        .unwrap_or(trimmed);
    let expected_key = local_file_trash_entry_id(relative_path);
    if metadata
        .trash_entries
        .get(&expected_key)
        .map(is_local_file_trash_entry)
        .unwrap_or(false)
    {
        return Some(expected_key);
    }
    metadata
        .trash_entries
        .iter()
        .find(|(_, entry)| {
            is_local_file_trash_entry(entry) && local_trash_original_path(entry) == relative_path
        })
        .map(|(key, _)| key.clone())
}

fn find_local_directory_trash_entry_key(
    metadata: &LocalFolderMetadata,
    entry_id: &str,
) -> Option<String> {
    let trimmed = entry_id.trim();
    if metadata
        .trash_entries
        .get(trimmed)
        .map(is_local_directory_trash_entry)
        .unwrap_or(false)
    {
        return Some(trimmed.to_string());
    }
    let relative_path = if let Some(encoded) = trimmed.strip_prefix("local-dir:") {
        decode_local_id_segment(encoded).ok()?
    } else {
        trimmed
            .strip_prefix("local:node:")
            .or_else(|| trimmed.strip_prefix("local:folder:"))
            .unwrap_or(trimmed)
            .to_string()
    };
    let expected_key = local_directory_trash_entry_id(&relative_path);
    if metadata
        .trash_entries
        .get(&expected_key)
        .map(is_local_directory_trash_entry)
        .unwrap_or(false)
    {
        return Some(expected_key);
    }
    metadata
        .trash_entries
        .iter()
        .find(|(_, entry)| {
            is_local_directory_trash_entry(entry)
                && local_trash_original_path(entry) == relative_path
        })
        .map(|(key, _)| key.clone())
}

pub fn local_workspace_id_from_root_uri(root_uri: &str) -> Result<String, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    Ok(local_workspace_id(&canonical_root))
}

pub(crate) fn local_root_has_workspace_manifest(root_uri: &str) -> Result<bool, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    Ok(load_local_workspace_manifest(&canonical_root).is_ok())
}

fn parse_file_root_uri(root_uri: &str) -> Result<PathBuf, WebError> {
    let trimmed = root_uri.trim();
    if trimmed.is_empty() {
        return Err(WebError::bad_request_code(
            "local_folder_root_required",
            "缺少本地文件夹 rootUri",
        ));
    }
    let Some(path) = trimmed.strip_prefix("file://") else {
        return Err(WebError::bad_request_code(
            "local_folder_root_invalid",
            "本地文件夹 rootUri 必须使用 file://",
        ));
    };
    if path.trim().is_empty() {
        return Err(WebError::bad_request_code(
            "local_folder_root_invalid",
            "本地文件夹 rootUri 不能为空",
        ));
    }
    Ok(PathBuf::from(percent_decode_file_uri_path(path)?))
}

fn percent_decode_file_uri_path(value: &str) -> Result<String, WebError> {
    let bytes = value.as_bytes();
    let mut decoded = Vec::with_capacity(bytes.len());
    let mut index = 0;
    while index < bytes.len() {
        if bytes[index] == b'%' {
            if index + 2 >= bytes.len() {
                return Err(WebError::bad_request_code(
                    "local_folder_root_invalid",
                    "file:// rootUri 包含不完整的百分号编码",
                ));
            }
            let hex = &value[index + 1..index + 3];
            let byte = u8::from_str_radix(hex, 16).map_err(|_| {
                WebError::bad_request_code(
                    "local_folder_root_invalid",
                    "file:// rootUri 包含无效的百分号编码",
                )
            })?;
            decoded.push(byte);
            index += 3;
        } else {
            decoded.push(bytes[index]);
            index += 1;
        }
    }
    String::from_utf8(decoded).map_err(|_| {
        WebError::bad_request_code(
            "local_folder_root_invalid",
            "file:// rootUri 不是 UTF-8 路径",
        )
    })
}

fn load_local_folder_metadata(root: &Path) -> Result<LocalFolderMetadata, WebError> {
    Ok(LocalFolderMetadata {
        page_options: load_metadata_value_map(&root.join(".mnote").join("page-options.json"))?,
        trash_entries: load_trash_index_map(&root.join(".mnote").join("trash-index.json"))?,
        uploaded_assets: load_uploaded_asset_index_map(
            &root.join(".mnote").join("uploaded-assets.json"),
        )?,
        file_order: load_file_order_metadata(&root.join(".mnote").join("file-order.json"))?,
    })
}

pub(crate) fn load_local_trash_entries(
    root_uri: &str,
) -> Result<BTreeMap<String, LocalTrashEntry>, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    if !canonical_root.is_dir() {
        return Err(WebError::bad_request_code(
            "local_folder_not_directory",
            "本地 rootUri 必须指向目录",
        ));
    }
    Ok(load_local_folder_metadata(&canonical_root)?.trash_entries)
}

fn load_metadata_value_map(path: &Path) -> Result<BTreeMap<String, Value>, WebError> {
    if !path.exists() {
        return Ok(BTreeMap::new());
    }
    let value = read_metadata_json(path)?;
    let source = value
        .get("pages")
        .or_else(|| value.get("options"))
        .unwrap_or(&value);
    let Some(map) = source.as_object() else {
        return Err(metadata_invalid(path, "页面选项元数据必须是对象"));
    };
    Ok(map
        .iter()
        .map(|(key, value)| (key.clone(), value.clone()))
        .collect())
}

fn read_metadata_json(path: &Path) -> Result<Value, WebError> {
    let raw = fs::read_to_string(path).map_err(|error| {
        WebError::bad_request_code(
            "local_metadata_read_failed",
            format!("无法读取本地元数据 {}: {error}", path.display()),
        )
    })?;
    serde_json::from_str(&raw)
        .map_err(|error| metadata_invalid(path, format!("元数据 JSON 损坏: {error}")))
}

fn load_trash_index_map(path: &Path) -> Result<BTreeMap<String, LocalTrashEntry>, WebError> {
    if !path.exists() {
        return Ok(BTreeMap::new());
    }
    let value = read_metadata_json(path)?;
    let source = value.get("entries").unwrap_or(&value);
    let entries: BTreeMap<String, LocalTrashEntry> = serde_json::from_value(source.clone())
        .map_err(|error| metadata_invalid(path, format!("回收站索引损坏: {error}")))?;
    Ok(entries)
}

fn load_uploaded_asset_index_map(
    path: &Path,
) -> Result<BTreeMap<String, LocalUploadedAssetEntry>, WebError> {
    if !path.exists() {
        return Ok(BTreeMap::new());
    }
    let value = read_metadata_json(path)?;
    let source = value.get("entries").unwrap_or(&value);
    let entries: BTreeMap<String, LocalUploadedAssetEntry> = serde_json::from_value(source.clone())
        .map_err(|error| metadata_invalid(path, format!("上传资源索引损坏: {error}")))?;
    Ok(entries)
}

fn load_file_order_metadata(path: &Path) -> Result<BTreeMap<String, Vec<String>>, WebError> {
    if !path.exists() {
        return Ok(BTreeMap::new());
    }
    let value = read_metadata_json(path)?;
    let source = value.get("parents").unwrap_or(&value);
    let orders: BTreeMap<String, Vec<String>> = serde_json::from_value(source.clone())
        .map_err(|error| metadata_invalid(path, format!("文件树排序索引损坏: {error}")))?;
    Ok(orders
        .into_iter()
        .map(|(parent, children)| {
            (
                normalize_file_order_parent_key(&parent),
                normalize_file_order_children(children),
            )
        })
        .collect())
}

fn metadata_invalid(path: &Path, message: impl Into<String>) -> WebError {
    WebError::bad_request_code(
        "local_metadata_invalid",
        format!("{}；请修复或移走 {} 后重试", message.into(), path.display()),
    )
}

#[allow(dead_code)]
pub fn initialize_local_page_id(root_uri: &str, relative_path: &str) -> Result<String, WebError> {
    let root_path = parse_file_root_uri(root_uri)?;
    let canonical_root = root_path.canonicalize().map_err(|error| {
        WebError::bad_request_code(
            "local_folder_unavailable",
            format!("无法访问本地文件夹: {error}"),
        )
    })?;
    let target = canonical_root
        .join(relative_path)
        .canonicalize()
        .map_err(|error| {
            WebError::bad_request_code(
                "local_page_id_target_invalid",
                format!("无法解析本地 Markdown 文件: {error}"),
            )
        })?;
    if !target.starts_with(&canonical_root) || !target.is_file() || !is_markdown_file(relative_path)
    {
        return Err(WebError::bad_request_code(
            "local_page_id_target_invalid",
            "只能为 root 内的 Markdown 文件初始化 page id",
        ));
    }
    Ok(local_markdown_path_page_id(relative_path))
}

fn write_trash_index_metadata(
    root: &Path,
    entries: &BTreeMap<String, LocalTrashEntry>,
) -> Result<(), WebError> {
    let mnote_dir = root.join(".mnote");
    fs::create_dir_all(&mnote_dir).map_err(|error| {
        WebError::bad_request_code(
            "local_metadata_write_failed",
            format!("无法创建本地元数据目录 {}: {error}", mnote_dir.display()),
        )
    })?;
    let path = mnote_dir.join("trash-index.json");
    let value = json!({
        "version": 1,
        "entries": entries,
    });
    write_json_atomic(&path, &value)
}

fn write_uploaded_asset_index_metadata(
    root: &Path,
    entries: &BTreeMap<String, LocalUploadedAssetEntry>,
) -> Result<(), WebError> {
    let mnote_dir = root.join(".mnote");
    fs::create_dir_all(&mnote_dir).map_err(|error| {
        WebError::bad_request_code(
            "local_metadata_write_failed",
            format!("无法创建本地元数据目录 {}: {error}", mnote_dir.display()),
        )
    })?;
    let path = mnote_dir.join("uploaded-assets.json");
    let value = json!({
        "version": 1,
        "entries": entries,
    });
    write_json_atomic(&path, &value)
}

fn write_file_order_metadata(
    root: &Path,
    file_order: &BTreeMap<String, Vec<String>>,
) -> Result<(), WebError> {
    let mnote_dir = root.join(".mnote");
    fs::create_dir_all(&mnote_dir).map_err(|error| {
        WebError::bad_request_code(
            "local_metadata_write_failed",
            format!("无法创建本地元数据目录 {}: {error}", mnote_dir.display()),
        )
    })?;
    let path = mnote_dir.join("file-order.json");
    let value = json!({
        "version": 1,
        "parents": file_order,
    });
    write_json_atomic(&path, &value)
}

#[allow(dead_code)]
fn write_json_atomic(path: &Path, value: &Value) -> Result<(), WebError> {
    let tmp_path = path.with_extension("json.tmp");
    let bytes = serde_json::to_vec_pretty(value)
        .map_err(|error| WebError::internal(format!("本地元数据序列化失败: {error}")))?;
    fs::write(&tmp_path, bytes).map_err(|error| {
        WebError::bad_request_code(
            "local_metadata_write_failed",
            format!("无法写入临时元数据 {}: {error}", tmp_path.display()),
        )
    })?;
    fs::rename(&tmp_path, path).map_err(|error| {
        let _ = fs::remove_file(&tmp_path);
        WebError::bad_request_code(
            "local_metadata_write_failed",
            format!("无法替换本地元数据 {}: {error}", path.display()),
        )
    })
}

fn scan_directory(
    root: &Path,
    directory: &Path,
    parent_node_id: Option<String>,
    depth: u32,
    max_depth: Option<u32>,
    root_source_uri: &str,
    workspace_id: &str,
    metadata: &LocalFolderMetadata,
    rows: &mut Vec<LocalFolderRow>,
) -> Result<(), WebError> {
    let mut entries = read_sorted_entries(directory, root)?;
    let parent_key = file_order_parent_key_for_directory(root, directory)?;
    sort_entries_with_file_order(&mut entries, metadata, &parent_key);
    for (position, entry) in entries.into_iter().enumerate() {
        let node_id = local_node_id(if entry.relative_path.is_empty() {
            "."
        } else {
            &entry.relative_path
        });
        let child_count = if entry.is_dir && !entry.is_symlink {
            if has_visible_child(&entry.path, root)? {
                1
            } else {
                0
            }
        } else {
            0
        };
        let uploaded_asset = metadata.uploaded_assets.get(&entry.relative_path);
        let row_kind = if entry.is_dir {
            "folder".to_string()
        } else if uploaded_asset.is_some() {
            "asset".to_string()
        } else if is_markdown_file(&entry.file_name) {
            "markdown".to_string()
        } else {
            "asset".to_string()
        };
        let document_id = local_folder_entry_document_id(root, &entry, uploaded_asset);
        let icon_hint = icon_hint_for_entry(&entry);
        rows.push(LocalFolderRow {
            node_id: node_id.clone(),
            row_id: format!("local:{row_kind}:{}", entry.relative_path),
            parent_node_id: parent_node_id.clone(),
            title: entry.file_name.clone(),
            depth,
            position: position as u32,
            row_kind,
            icon_hint,
            relative_path: entry.relative_path.clone(),
            source_uri: file_uri_for_path(&entry.path),
            child_count,
            expandable: entry.is_dir && child_count > 0,
            expanded_by_default: false,
            document_id,
            capabilities: local_entry_capabilities(&entry),
            workspace_id: workspace_id.to_string(),
            root_source_uri: root_source_uri.to_string(),
        });
        if entry.is_dir && !entry.is_symlink && max_depth.map_or(true, |limit| depth < limit) {
            scan_directory(
                root,
                &entry.path,
                Some(node_id),
                depth + 1,
                max_depth,
                root_source_uri,
                workspace_id,
                metadata,
                rows,
            )?;
        }
    }

    Ok(())
}

fn scan_directory_shallow_with_revision(
    root: &Path,
    directory: &Path,
    parent_node_id: Option<String>,
    depth: u32,
    root_source_uri: &str,
    workspace_id: &str,
    metadata: &LocalFolderMetadata,
) -> Result<LocalFolderScanResult, WebError> {
    let mut entries = read_sorted_entries(directory, root)?;
    let parent_key = file_order_parent_key_for_directory(root, directory)?;
    sort_entries_with_file_order(&mut entries, metadata, &parent_key);

    let mut rows = Vec::new();
    let mut hasher = DefaultHasher::new();
    let mut latest_modified_ms = 0u128;

    for (position, entry) in entries.iter().enumerate() {
        entry.relative_path.hash(&mut hasher);
        entry.file_name.hash(&mut hasher);
        entry.is_dir.hash(&mut hasher);
        entry.is_symlink.hash(&mut hasher);
        entry.is_readonly.hash(&mut hasher);
        if let Ok(meta) = fs::symlink_metadata(&entry.path) {
            meta.len().hash(&mut hasher);
            if let Ok(modified) = meta.modified() {
                if let Ok(delta) = modified.duration_since(UNIX_EPOCH) {
                    let modified_ms = delta.as_millis();
                    modified_ms.hash(&mut hasher);
                    if modified_ms > latest_modified_ms {
                        latest_modified_ms = modified_ms;
                    }
                }
            }
        }

        let node_id = local_node_id(if entry.relative_path.is_empty() {
            "."
        } else {
            &entry.relative_path
        });
        let child_count = if entry.is_dir && !entry.is_symlink {
            if has_visible_child(&entry.path, root)? {
                1
            } else {
                0
            }
        } else {
            0
        };
        let uploaded_asset = metadata.uploaded_assets.get(&entry.relative_path);
        let row_kind = if entry.is_dir {
            "folder".to_string()
        } else if uploaded_asset.is_some() {
            "asset".to_string()
        } else if is_markdown_file(&entry.file_name) {
            "markdown".to_string()
        } else {
            "asset".to_string()
        };
        let document_id = local_folder_entry_document_id(root, entry, uploaded_asset);
        let icon_hint = icon_hint_for_entry(entry);
        rows.push(LocalFolderRow {
            node_id: node_id.clone(),
            row_id: format!("local:{row_kind}:{}", entry.relative_path),
            parent_node_id: parent_node_id.clone(),
            title: entry.file_name.clone(),
            depth,
            position: position as u32,
            row_kind,
            icon_hint,
            relative_path: entry.relative_path.clone(),
            source_uri: file_uri_for_path(&entry.path),
            child_count,
            expandable: entry.is_dir && child_count > 0,
            expanded_by_default: false,
            document_id,
            capabilities: local_entry_capabilities(entry),
            workspace_id: workspace_id.to_string(),
            root_source_uri: root_source_uri.to_string(),
        });
    }

    Ok(LocalFolderScanResult {
        watch_revision: LocalFolderWatchRevision {
            root_uri: root_source_uri.to_string(),
            revision: format!("{:016x}", hasher.finish()),
            entry_count: rows.len(),
            latest_modified_ms,
        },
        rows,
    })
}

fn local_folder_entry_document_id(
    root: &Path,
    entry: &LocalFolderEntry,
    uploaded_asset: Option<&LocalUploadedAssetEntry>,
) -> Option<String> {
    if let Some(asset) = uploaded_asset {
        return Some(asset.document_id.clone());
    }
    if is_markdown_file(&entry.file_name) {
        return Some(local_markdown_path_page_id(&entry.relative_path));
    }
    if entry.is_dir && !entry.is_symlink {
        if let Some(markdown_path) = nested_bundle_main_markdown(&entry.path) {
            if let Ok(relative_path) = normalize_relative_path(root, &markdown_path) {
                return Some(local_markdown_path_page_id(&relative_path));
            }
        }
    }
    None
}

fn local_markdown_relative_path_from_document_id(document_id: &str) -> Option<String> {
    let encoded = document_id.trim().strip_prefix("local-md:")?;
    decode_local_id_segment(encoded).ok()
}

fn ancestor_directories_for_relative_path(relative_path: &str) -> Vec<String> {
    let normalized = relative_path.trim().trim_matches('/');
    if normalized.is_empty() || normalized == "." {
        return Vec::new();
    }
    let mut ancestors = Vec::new();
    let mut current = Path::new(normalized).parent();
    while let Some(parent) = current {
        let value = parent
            .components()
            .map(|component| component.as_os_str().to_string_lossy().to_string())
            .collect::<Vec<_>>()
            .join("/");
        if value.is_empty() || value == "." {
            break;
        }
        ancestors.push(value);
        current = parent.parent();
    }
    ancestors.reverse();
    ancestors
}

fn append_file_tree_reveal_rows(
    root: &Path,
    reveal_relative_path: Option<&str>,
    root_source_uri: &str,
    workspace_id: &str,
    metadata: &LocalFolderMetadata,
    rows: &mut Vec<LocalFolderRow>,
) -> Result<(), WebError> {
    let Some(reveal_relative_path) = reveal_relative_path
        .map(str::trim)
        .filter(|value| !value.is_empty())
    else {
        return Ok(());
    };
    let mut ancestors = ancestor_directories_for_relative_path(reveal_relative_path);
    if let Some(bundle_parent) = same_name_markdown_bundle_parent(reveal_relative_path) {
        ancestors.retain(|ancestor| ancestor != &bundle_parent);
    }
    if ancestors.is_empty() {
        return Ok(());
    }
    let ancestor_set = ancestors.iter().cloned().collect::<BTreeSet<_>>();
    let mut seen = rows
        .iter()
        .map(|row| row.row_id.clone())
        .collect::<BTreeSet<_>>();

    for ancestor in &ancestors {
        let directory = resolve_metadata_relative_path(root, ancestor)?;
        if !directory.is_dir() {
            continue;
        }
        let depth = local_folder_relative_depth(ancestor);
        let mut scoped_rows = Vec::new();
        scan_directory(
            root,
            &directory,
            Some(local_node_id(ancestor)),
            depth,
            Some(depth),
            root_source_uri,
            workspace_id,
            metadata,
            &mut scoped_rows,
        )?;
        for row in scoped_rows {
            if seen.insert(row.row_id.clone()) {
                rows.push(row);
            }
        }
    }

    for row in rows.iter_mut() {
        if ancestor_set.contains(&row.relative_path) {
            row.expanded_by_default = true;
        }
    }
    Ok(())
}

fn same_name_markdown_bundle_parent(relative_path: &str) -> Option<String> {
    let normalized = relative_path.trim().trim_matches('/').replace('\\', "/");
    if !normalized.to_ascii_lowercase().ends_with(".md") {
        return None;
    }
    let (parent, file_name) = normalized.rsplit_once('/')?;
    if parent.is_empty() {
        return None;
    }
    let parent_name = parent.rsplit('/').next().unwrap_or(parent);
    let file_stem = &file_name[..file_name.len().saturating_sub(3)];
    if file_stem == parent_name {
        Some(parent.to_string())
    } else {
        None
    }
}

fn read_sorted_entries(directory: &Path, root: &Path) -> Result<Vec<LocalFolderEntry>, WebError> {
    let read_dir = fs::read_dir(directory).map_err(|error| {
        WebError::bad_request_code(
            "local_folder_scan_failed",
            format!("无法读取本地目录 {}: {error}", directory.display()),
        )
    })?;
    let mut entries = Vec::new();
    for entry in read_dir {
        let entry = entry.map_err(|error| {
            WebError::bad_request_code(
                "local_folder_scan_failed",
                format!("读取目录项失败: {error}"),
            )
        })?;
        let path = entry.path();
        let metadata = fs::symlink_metadata(&path).map_err(|error| {
            WebError::bad_request_code(
                "local_folder_scan_failed",
                format!("无法读取目录项元数据 {}: {error}", path.display()),
            )
        })?;
        let file_name = entry.file_name().to_string_lossy().to_string();
        let relative_path = normalize_relative_path(root, &path)?;
        if should_ignore_entry(&relative_path, &file_name) {
            continue;
        }
        let is_symlink = metadata.file_type().is_symlink();
        let is_dir = if is_symlink { false } else { metadata.is_dir() };
        let is_readonly = metadata.permissions().readonly();
        entries.push(LocalFolderEntry {
            path,
            relative_path,
            file_name,
            is_dir,
            is_symlink,
            is_readonly,
        });
    }
    entries.sort_by(compare_local_entries);
    Ok(entries)
}

fn has_visible_child(directory: &Path, root: &Path) -> Result<bool, WebError> {
    let read_dir = fs::read_dir(directory).map_err(|error| {
        WebError::bad_request_code(
            "local_folder_scan_failed",
            format!("无法读取本地目录 {}: {error}", directory.display()),
        )
    })?;
    for entry in read_dir {
        let entry = entry.map_err(|error| {
            WebError::bad_request_code(
                "local_folder_scan_failed",
                format!("读取目录项失败: {error}"),
            )
        })?;
        let path = entry.path();
        let file_name = entry.file_name().to_string_lossy().to_string();
        let relative_path = normalize_relative_path(root, &path)?;
        if !should_ignore_entry(&relative_path, &file_name) {
            return Ok(true);
        }
    }
    Ok(false)
}

fn local_entry_capabilities(entry: &LocalFolderEntry) -> Vec<String> {
    let mut capabilities = Vec::new();
    if entry.is_readonly {
        capabilities.push("readonly".to_string());
    }
    if entry.is_symlink {
        capabilities.push("symlink".to_string());
    }
    capabilities
}

fn normalize_relative_path(root: &Path, path: &Path) -> Result<String, WebError> {
    if !path.starts_with(root) {
        return Err(WebError::bad_request_code(
            "local_folder_root_escape",
            "本地文件夹扫描不能越过 root",
        ));
    }
    let relative = path.strip_prefix(root).map_err(|_| {
        WebError::bad_request_code("local_folder_root_escape", "本地路径不在 root 内")
    })?;
    Ok(relative
        .components()
        .map(|component| component.as_os_str().to_string_lossy().to_string())
        .collect::<Vec<_>>()
        .join("/"))
}

fn local_folder_relative_depth(relative_path: &str) -> u32 {
    let trimmed = relative_path.trim().trim_matches('/');
    if trimmed.is_empty() || trimmed == "." {
        return 0;
    }
    trimmed
        .split('/')
        .filter(|segment| !segment.trim().is_empty())
        .count() as u32
}

fn resolve_metadata_relative_path(root: &Path, relative_path: &str) -> Result<PathBuf, WebError> {
    let relative = Path::new(relative_path);
    if relative.is_absolute()
        || relative
            .components()
            .any(|component| matches!(component, std::path::Component::ParentDir))
    {
        return Err(WebError::bad_request_code(
            "local_folder_root_escape",
            "本地元数据路径不能越过 root",
        ));
    }
    let target = root.join(relative);
    if !target.starts_with(root) {
        return Err(WebError::bad_request_code(
            "local_folder_root_escape",
            "本地元数据路径不在 root 内",
        ));
    }
    Ok(target)
}

fn should_ignore_entry(relative_path: &str, file_name: &str) -> bool {
    if matches!(file_name, ".git" | "node_modules" | ".mnote") {
        return true;
    }
    relative_path == ".mnote/trash" || relative_path.starts_with(".mnote/trash/")
}

fn compare_local_entries(a: &LocalFolderEntry, b: &LocalFolderEntry) -> Ordering {
    match (a.is_dir, b.is_dir) {
        (true, false) => Ordering::Less,
        (false, true) => Ordering::Greater,
        _ => a
            .file_name
            .to_ascii_lowercase()
            .cmp(&b.file_name.to_ascii_lowercase())
            .then_with(|| a.file_name.cmp(&b.file_name)),
    }
}

fn normalize_file_order_parent_key(parent: &str) -> String {
    let trimmed = parent.trim().trim_matches('/');
    if trimmed.is_empty() || trimmed == "." {
        ".".to_string()
    } else {
        trimmed.to_string()
    }
}

fn normalize_file_order_children(children: Vec<String>) -> Vec<String> {
    let mut seen = std::collections::BTreeSet::new();
    children
        .into_iter()
        .map(|child| child.trim().trim_matches('/').to_string())
        .filter(|child| !child.is_empty() && seen.insert(child.clone()))
        .collect()
}

fn file_order_parent_key_for_directory(root: &Path, directory: &Path) -> Result<String, WebError> {
    if directory == root {
        return Ok(".".to_string());
    }
    Ok(normalize_file_order_parent_key(&normalize_relative_path(
        root, directory,
    )?))
}

fn parent_key_for_relative_path(relative_path: &str) -> String {
    Path::new(relative_path)
        .parent()
        .and_then(|parent| {
            let value = parent
                .components()
                .map(|component| component.as_os_str().to_string_lossy().to_string())
                .collect::<Vec<_>>()
                .join("/");
            if value.is_empty() {
                None
            } else {
                Some(value)
            }
        })
        .map(|value| normalize_file_order_parent_key(&value))
        .unwrap_or_else(|| ".".to_string())
}

fn sort_entries_with_file_order(
    entries: &mut [LocalFolderEntry],
    metadata: &LocalFolderMetadata,
    parent_key: &str,
) {
    let Some(order) = metadata.file_order.get(parent_key) else {
        return;
    };
    let index = order
        .iter()
        .enumerate()
        .map(|(position, relative_path)| (relative_path.as_str(), position))
        .collect::<BTreeMap<_, _>>();
    entries.sort_by(|a, b| {
        let a_index = index.get(a.relative_path.as_str()).copied();
        let b_index = index.get(b.relative_path.as_str()).copied();
        match (a_index, b_index) {
            (Some(left), Some(right)) => left.cmp(&right),
            (Some(_), None) => Ordering::Less,
            (None, Some(_)) => Ordering::Greater,
            (None, None) => compare_local_entries(a, b),
        }
    });
}

fn ordered_child_paths_for_parent(
    root: &Path,
    parent_directory: &Path,
) -> Result<Vec<String>, WebError> {
    let mut entries = read_sorted_entries(parent_directory, root)?;
    let parent_key = file_order_parent_key_for_directory(root, parent_directory)?;
    let metadata = load_local_folder_metadata(root)?;
    sort_entries_with_file_order(&mut entries, &metadata, &parent_key);
    Ok(entries
        .into_iter()
        .map(|entry| entry.relative_path)
        .collect::<Vec<_>>())
}

fn reorder_child_paths(children: &mut Vec<String>, child_path: &str, sort_order: i64) {
    children.retain(|candidate| candidate != child_path);
    let index = usize::try_from(sort_order)
        .unwrap_or(usize::MAX)
        .min(children.len());
    children.insert(index, child_path.to_string());
}

fn update_local_file_order_after_move(
    root: &Path,
    original_relative_path: Option<&str>,
    new_relative_path: &str,
    sort_order: i64,
) -> Result<(), WebError> {
    let mut metadata = load_local_folder_metadata(root)?;
    if let Some(original) = original_relative_path
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        let original_parent = parent_key_for_relative_path(original);
        if original_parent != parent_key_for_relative_path(new_relative_path) {
            if let Some(children) = metadata.file_order.get_mut(&original_parent) {
                children.retain(|candidate| candidate != original);
            }
        }
    }
    let parent_key = parent_key_for_relative_path(new_relative_path);
    let parent_directory = if parent_key == "." {
        root.to_path_buf()
    } else {
        resolve_metadata_relative_path(root, &parent_key)?
    };
    let mut children = ordered_child_paths_for_parent(root, &parent_directory)?;
    reorder_child_paths(&mut children, new_relative_path, sort_order);
    metadata.file_order.insert(parent_key, children);
    write_file_order_metadata(root, &metadata.file_order)
}

fn rewrite_file_order_path(
    file_order: &mut BTreeMap<String, Vec<String>>,
    old_relative_path: &str,
    new_relative_path: &str,
) {
    for children in file_order.values_mut() {
        for child in children.iter_mut() {
            if child == old_relative_path {
                *child = new_relative_path.to_string();
            } else if child.starts_with(&format!("{old_relative_path}/")) {
                *child = format!("{}{}", new_relative_path, &child[old_relative_path.len()..]);
            }
        }
    }
    let old_parent_prefix = format!("{old_relative_path}/");
    let parent_rewrites = file_order
        .keys()
        .filter_map(|parent| {
            if parent == old_relative_path {
                Some((parent.clone(), new_relative_path.to_string()))
            } else if parent.starts_with(&old_parent_prefix) {
                Some((
                    parent.clone(),
                    format!(
                        "{}{}",
                        new_relative_path,
                        &parent[old_relative_path.len()..]
                    ),
                ))
            } else {
                None
            }
        })
        .collect::<Vec<_>>();
    for (old_parent, new_parent) in parent_rewrites {
        if let Some(children) = file_order.remove(&old_parent) {
            file_order.insert(new_parent, children);
        }
    }
}

fn remove_file_order_path(file_order: &mut BTreeMap<String, Vec<String>>, relative_path: &str) {
    let child_prefix = format!("{relative_path}/");
    for children in file_order.values_mut() {
        children.retain(|child| child != relative_path && !child.starts_with(&child_prefix));
    }
    let parent_keys = file_order
        .keys()
        .filter(|parent| parent.as_str() == relative_path || parent.starts_with(&child_prefix))
        .cloned()
        .collect::<Vec<_>>();
    for parent in parent_keys {
        file_order.remove(&parent);
    }
}

fn apply_file_order_path_rewrite(
    root: &Path,
    old_relative_path: &str,
    new_relative_path: &str,
) -> Result<(), WebError> {
    let mut metadata = load_local_folder_metadata(root)?;
    rewrite_file_order_path(
        &mut metadata.file_order,
        old_relative_path,
        new_relative_path,
    );
    write_file_order_metadata(root, &metadata.file_order)
}

fn scan_markdown_page_tree(
    root: &Path,
    directory: &Path,
    parent_node_id: Option<String>,
    depth: u32,
    skip_markdown_relative_path: Option<String>,
    metadata: &LocalFolderMetadata,
    rows: &mut Vec<LocalFolderRow>,
    workspace_id: &str,
    root_source_uri: &str,
) -> Result<bool, WebError> {
    let mut entries = read_sorted_entries(directory, root)?;
    let parent_key = file_order_parent_key_for_directory(root, directory)?;
    sort_entries_with_file_order(&mut entries, metadata, &parent_key);
    let mut directory_rows = Vec::<LocalFolderRow>::new();
    let mut contains_markdown = false;
    for (position, entry) in entries.into_iter().enumerate() {
        if skip_markdown_relative_path
            .as_deref()
            .map(|skip| skip == entry.relative_path)
            .unwrap_or(false)
        {
            continue;
        }
        if entry.is_dir && !entry.is_symlink {
            if let Some(nested_main) = nested_bundle_main_markdown(&entry.path) {
                let nested_relative = normalize_relative_path(root, &nested_main)?;
                let markdown = fs::read_to_string(&nested_main).unwrap_or_default();
                let parsed = parse_markdown_page(
                    &markdown,
                    nested_main
                        .file_name()
                        .and_then(|name| name.to_str())
                        .unwrap_or("page.md"),
                );
                let page_id = local_markdown_path_page_id(&nested_relative);
                let mut child_rows = Vec::new();
                let child_contains_markdown = scan_markdown_page_tree(
                    root,
                    &entry.path,
                    Some(page_id.clone()),
                    depth + 1,
                    Some(nested_relative.clone()),
                    metadata,
                    &mut child_rows,
                    workspace_id,
                    root_source_uri,
                )?;
                let child_count = child_rows.len() as u32;
                directory_rows.push(LocalFolderRow {
                    node_id: page_id.clone(),
                    row_id: format!("local:page:{nested_relative}"),
                    parent_node_id: parent_node_id.clone(),
                    title: parsed.title,
                    depth,
                    position: position as u32,
                    row_kind: "document".to_string(),
                    icon_hint: "markdown".to_string(),
                    relative_path: nested_relative,
                    source_uri: file_uri_for_path(&nested_main),
                    child_count,
                    expandable: child_contains_markdown,
                    expanded_by_default: child_contains_markdown && depth < 2,
                    document_id: Some(page_id),
                    capabilities: local_entry_capabilities(&entry),
                    workspace_id: workspace_id.to_string(),
                    root_source_uri: root_source_uri.to_string(),
                });
                directory_rows.extend(child_rows);
                contains_markdown = true;
                continue;
            }
            // VSCode 风格 loose Markdown 也可作为页面节点管理；
            // 若同级存在同名目录，该目录中的 Markdown 作为该页面的子页面投影。
            let sibling_md = directory.join(format!("{}.md", entry.file_name));
            let has_sibling_md =
                sibling_md.exists() && sibling_md.starts_with(root) && sibling_md.is_file();
            let parent_for_children = if has_sibling_md {
                let sibling_relative = normalize_relative_path(root, &sibling_md)?;
                Some(local_markdown_path_page_id(&sibling_relative))
            } else {
                Some(local_directory_group_id(&entry.relative_path))
            };
            let mut child_rows = Vec::new();
            let child_contains_markdown = scan_markdown_page_tree(
                root,
                &entry.path,
                parent_for_children,
                depth + 1,
                None,
                metadata,
                &mut child_rows,
                workspace_id,
                root_source_uri,
            )?;
            if child_contains_markdown {
                if !has_sibling_md {
                    directory_rows.push(LocalFolderRow {
                        node_id: local_directory_group_id(&entry.relative_path),
                        row_id: format!("local:page-group:{}", entry.relative_path),
                        parent_node_id: parent_node_id.clone(),
                        title: entry.file_name.clone(),
                        depth,
                        position: position as u32,
                        row_kind: "document".to_string(),
                        icon_hint: "folder".to_string(),
                        relative_path: entry.relative_path.clone(),
                        source_uri: file_uri_for_path(&entry.path),
                        child_count: 0,
                        expandable: true,
                        expanded_by_default: depth < 2,
                        document_id: None,
                        capabilities: local_entry_capabilities(&entry),
                        workspace_id: workspace_id.to_string(),
                        root_source_uri: root_source_uri.to_string(),
                    });
                }
                directory_rows.extend(child_rows);
                contains_markdown = true;
            }
            continue;
        }
        let uploaded_asset = metadata.uploaded_assets.get(&entry.relative_path);
        if let Some(asset) = uploaded_asset {
            directory_rows.push(LocalFolderRow {
                node_id: local_node_id(&entry.relative_path),
                row_id: format!("local:asset:{}", entry.relative_path),
                parent_node_id: parent_node_id.clone(),
                title: entry.file_name.clone(),
                depth,
                position: position as u32,
                row_kind: "asset".to_string(),
                icon_hint: icon_hint_for_entry(&entry),
                relative_path: entry.relative_path.clone(),
                source_uri: file_uri_for_path(&entry.path),
                child_count: 0,
                expandable: false,
                expanded_by_default: false,
                document_id: Some(asset.document_id.clone()),
                capabilities: local_entry_capabilities(&entry),
                workspace_id: workspace_id.to_string(),
                root_source_uri: root_source_uri.to_string(),
            });
            continue;
        }
        if is_markdown_file(&entry.file_name) {
            let markdown = fs::read_to_string(&entry.path).unwrap_or_default();
            let parsed = parse_markdown_page(&markdown, &entry.file_name);
            let page_id = local_markdown_path_page_id(&entry.relative_path);
            let md_stem = entry
                .path
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("")
                .to_string();
            let sibling_dir = directory.join(&md_stem);
            let has_sibling_dir = sibling_dir.is_dir() && sibling_dir.starts_with(root);
            directory_rows.push(LocalFolderRow {
                node_id: page_id.clone(),
                row_id: format!("local:page:{}", entry.relative_path),
                parent_node_id: parent_node_id.clone(),
                title: parsed.title,
                depth,
                position: position as u32,
                row_kind: "document".to_string(),
                icon_hint: "markdown".to_string(),
                relative_path: entry.relative_path.clone(),
                source_uri: file_uri_for_path(&entry.path),
                child_count: 0,
                expandable: has_sibling_dir,
                expanded_by_default: has_sibling_dir && depth < 2,
                document_id: Some(page_id),
                capabilities: local_entry_capabilities(&entry),
                workspace_id: workspace_id.to_string(),
                root_source_uri: root_source_uri.to_string(),
            });
            contains_markdown = true;
        }
    }
    rows.extend(directory_rows);
    Ok(contains_markdown)
}

fn find_markdown_by_page_id(
    root: &Path,
    metadata: &LocalFolderMetadata,
    document_id: &str,
) -> Result<Option<LocalFolderEntry>, WebError> {
    if let Some(entry) = find_markdown_by_path_document_id(root, document_id)? {
        return Ok(Some(entry));
    }

    fn walk(
        root: &Path,
        directory: &Path,
        metadata: &LocalFolderMetadata,
        document_id: &str,
    ) -> Result<Option<LocalFolderEntry>, WebError> {
        for entry in read_sorted_entries(directory, root)? {
            if entry.is_dir && !entry.is_symlink {
                if let Some(found) = walk(root, &entry.path, metadata, document_id)? {
                    return Ok(Some(found));
                }
                continue;
            }
            if !is_markdown_file(&entry.file_name) {
                continue;
            }
            let path_page_id = local_markdown_path_page_id(&entry.relative_path);
            if path_page_id == document_id {
                return Ok(Some(entry));
            }
        }
        Ok(None)
    }

    walk(root, root, metadata, document_id)
}

fn find_markdown_by_path_document_id(
    root: &Path,
    document_id: &str,
) -> Result<Option<LocalFolderEntry>, WebError> {
    let Some(relative_path) = local_markdown_relative_path_from_document_id(document_id) else {
        return Ok(None);
    };
    let target = resolve_metadata_relative_path(root, &relative_path)?;
    let metadata = match fs::symlink_metadata(&target) {
        Ok(metadata) => metadata,
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(error) => {
            return Err(WebError::bad_request_code(
                "local_folder_scan_failed",
                format!("无法读取本地 Markdown 元数据 {}: {error}", target.display()),
            ));
        }
    };
    let file_name = target
        .file_name()
        .and_then(|value| value.to_str())
        .unwrap_or_default()
        .to_string();
    let normalized_relative_path = normalize_relative_path(root, &target)?;
    if metadata.is_dir()
        || should_ignore_entry(&normalized_relative_path, &file_name)
        || !is_markdown_file(&file_name)
        || local_markdown_path_page_id(&normalized_relative_path) != document_id
    {
        return Ok(None);
    }
    Ok(Some(LocalFolderEntry {
        path: target,
        relative_path: normalized_relative_path,
        file_name,
        is_dir: false,
        is_symlink: metadata.file_type().is_symlink(),
        is_readonly: metadata.permissions().readonly(),
    }))
}

fn uploaded_asset_markdown_relative_paths_for_document(
    root: &Path,
    metadata: &LocalFolderMetadata,
    document_id: &str,
    markdown_path: &Path,
) -> BTreeSet<String> {
    let Some(markdown_dir) = markdown_path.parent() else {
        return BTreeSet::new();
    };
    metadata
        .uploaded_assets
        .values()
        .filter(|entry| entry.document_id == document_id)
        .filter_map(|entry| {
            let path = root.join(&entry.relative_path);
            normalize_markdown_relative_asset_path(markdown_dir, &path).ok()
        })
        .collect()
}

fn local_folder_row_to_projection_item(row: &LocalFolderRow) -> Value {
    let mut item = json!({
        "rowId": row.row_id,
        "rowKind": row.row_kind,
        "nodeId": row.node_id,
        "parentNodeId": row.parent_node_id,
        "title": row.title,
        "depth": row.depth,
        "position": row.position,
        "childCount": row.child_count,
        "expandable": row.expandable,
        "expandedByDefault": row.expanded_by_default,
        "capabilities": row.capabilities.clone(),
        "iconHint": row.icon_hint,
        "resourceMeta": {
            "resourceKind": row.row_kind,
            "iconHint": row.icon_hint,
            "extra": {
                "source": {
                    "sourceKind": "local_folder",
                    "sourceUri": row.source_uri,
                    "relativePath": row.relative_path,
                    "storageIdentity": {
                        "kind": "file_path",
                        "path": row.relative_path,
                    },
                    "operationProfile": "local_readonly",
                }
            }
        }
    });
    if let Some(document_id) = row.document_id.as_ref() {
        item["documentId"] = Value::String(document_id.clone());
        item["resourceMeta"]["documentId"] = Value::String(document_id.clone());
    }
    if row.row_kind == "asset" && !row.relative_path.trim().is_empty() {
        let asset_id = format!("local-file:{}", row.relative_path);
        item["assetId"] = Value::String(asset_id.clone());
        item["resourceMeta"]["assetId"] = Value::String(asset_id);
    }

    // Phase A1: 为 File/Page/Resource tree 输出统一 workspacePath
    let object_kind = match row.row_kind.as_str() {
        "markdown" | "document" => KernelObjectKind::Page,
        "folder" | "index" => {
            if row
                .document_id
                .as_deref()
                .is_some_and(|value| value.starts_with("local-md:"))
            {
                KernelObjectKind::Page
            } else {
                KernelObjectKind::Index
            }
        }
        "asset" => match row.icon_hint.as_str() {
            "mindmap" => KernelObjectKind::Mindmap,
            "onlyoffice" | "office" => KernelObjectKind::OnlyOffice,
            _ => KernelObjectKind::Attachment,
        },
        _ => KernelObjectKind::Attachment,
    };
    let workspace_path = ObjectWorkspacePath {
        workspace_id: row.workspace_id.clone(),
        source_kind: WorkspaceSourceKind::LocalFolder,
        root_uri: row.root_source_uri.clone(),
        relative_path: row.relative_path.clone(),
        object_identity: KernelObjectIdentity {
            object_kind,
            document_id: row.document_id.clone(),
            block_id: None,
            asset_id: None,
        },
        resource_kind: Some(row.row_kind.clone()),
    };
    if let Ok(path_value) = serde_json::to_value(&workspace_path) {
        item["resourceMeta"]["workspacePath"] = path_value;
    }
    item
}

fn local_node_id(relative_path: &str) -> String {
    format!("local:node:{relative_path}")
}

fn local_directory_group_id(relative_path: &str) -> String {
    format!("local-dir:{}", encode_local_id_segment(relative_path))
}

fn local_markdown_path_page_id(relative_path: &str) -> String {
    format!("local-md:{}", encode_local_id_segment(relative_path))
}

fn page_options_from_metadata(value: &Value) -> PageOptions {
    let mut options = PageOptions::default();
    if let Some(wide_layout) = value
        .get("wideLayout")
        .or_else(|| value.get("wide_layout"))
        .and_then(Value::as_bool)
    {
        options.wide_layout = wide_layout;
    }
    if let Some(small_text) = value
        .get("smallText")
        .or_else(|| value.get("small_text"))
        .and_then(Value::as_bool)
    {
        options.small_text = small_text;
    }
    if let Some(layout_density) = value
        .get("layoutDensity")
        .or_else(|| value.get("layout_density"))
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|item| !item.is_empty())
    {
        options.layout_density = layout_density.to_string();
    }
    if let Some(show_toc) = value
        .get("showToc")
        .or_else(|| value.get("show_toc"))
        .and_then(Value::as_bool)
    {
        options.show_toc = show_toc;
    }
    if let Some(show_structure) = value
        .get("showStructure")
        .or_else(|| value.get("show_structure"))
        .and_then(Value::as_bool)
    {
        options.show_structure = show_structure;
    }
    if let Some(show_heading_numbers) = value
        .get("showHeadingNumbers")
        .or_else(|| value.get("show_heading_numbers"))
        .and_then(Value::as_bool)
    {
        options.show_heading_numbers = show_heading_numbers;
    }
    if let Some(protect_editing) = value
        .get("protectEditing")
        .or_else(|| value.get("protect_editing"))
        .and_then(Value::as_bool)
    {
        options.protect_editing = protect_editing;
    }
    if let Some(hide_title_header) = page_options_hide_title_header_value(value) {
        options.hide_title_header = hide_title_header;
    }
    options
}

fn page_options_hide_title_header_value(value: &Value) -> Option<bool> {
    value
        .get("hideTitleHeader")
        .or_else(|| value.get("hide_title_header"))
        .and_then(Value::as_bool)
}

pub(crate) fn local_workspace_id(root: &Path) -> String {
    if let Ok(manifest) = load_local_workspace_manifest(root) {
        if !manifest.workspace_id.trim().is_empty() {
            return manifest.workspace_id;
        }
    }
    format!(
        "local:{}",
        root.to_string_lossy()
            .chars()
            .map(|character| match character {
                '/' | '\\' | ':' | ' ' => '_',
                value if value.is_ascii_alphanumeric() || matches!(value, '_' | '-' | '.') => value,
                _ => '_',
            })
            .collect::<String>()
    )
}

fn local_folder_watch_revision_for_root(
    root: &Path,
    root_source_uri: &str,
) -> Result<LocalFolderWatchRevision, WebError> {
    let mut hasher = DefaultHasher::new();
    let mut entry_count = 0usize;
    let mut latest_modified_ms = 0u128;

    fn visit(
        root: &Path,
        directory: &Path,
        hasher: &mut DefaultHasher,
        entry_count: &mut usize,
        latest_modified_ms: &mut u128,
    ) -> Result<(), WebError> {
        for entry in read_sorted_entries(directory, root)? {
            entry.relative_path.hash(hasher);
            entry.file_name.hash(hasher);
            entry.is_dir.hash(hasher);
            entry.is_symlink.hash(hasher);
            entry.is_readonly.hash(hasher);
            if let Ok(meta) = fs::symlink_metadata(&entry.path) {
                meta.len().hash(hasher);
                if let Ok(modified) = meta.modified() {
                    if let Ok(delta) = modified.duration_since(UNIX_EPOCH) {
                        let modified_ms = delta.as_millis();
                        modified_ms.hash(hasher);
                        if modified_ms > *latest_modified_ms {
                            *latest_modified_ms = modified_ms;
                        }
                    }
                }
            }
            *entry_count += 1;
            if entry.is_dir && !entry.is_symlink {
                visit(root, &entry.path, hasher, entry_count, latest_modified_ms)?;
            }
        }
        Ok(())
    }

    visit(
        root,
        root,
        &mut hasher,
        &mut entry_count,
        &mut latest_modified_ms,
    )?;
    Ok(LocalFolderWatchRevision {
        root_uri: root_source_uri.to_string(),
        revision: format!("{:016x}", hasher.finish()),
        entry_count,
        latest_modified_ms,
    })
}

fn file_uri_for_path(path: &Path) -> String {
    format!("file://{}", path.display())
}

fn is_markdown_file(file_name: &str) -> bool {
    file_name.to_ascii_lowercase().ends_with(".md")
}

fn icon_hint_for_entry(entry: &LocalFolderEntry) -> String {
    if entry.is_dir {
        return "folder".to_string();
    }
    if entry.is_symlink {
        return "symlink".to_string();
    }
    let lower = entry.file_name.to_ascii_lowercase();
    if is_local_mindmap_file_name(&entry.file_name) {
        "mindmap"
    } else if lower.ends_with(".md") || lower.ends_with(".markdown") {
        "markdown"
    } else if matches!(
        extension(&lower).as_deref(),
        Some("png" | "jpg" | "jpeg" | "gif" | "webp" | "svg" | "bmp")
    ) {
        "image"
    } else if matches!(
        extension(&lower).as_deref(),
        Some("mp4" | "webm" | "mov" | "mkv")
    ) {
        "video"
    } else if matches!(
        extension(&lower).as_deref(),
        Some("mp3" | "wav" | "flac" | "ogg")
    ) {
        "audio"
    } else if lower.ends_with(".pdf") {
        "pdf"
    } else if matches!(
        extension(&lower).as_deref(),
        Some(
            "doc"
                | "docx"
                | "odt"
                | "rtf"
                | "ppt"
                | "pptx"
                | "odp"
                | "xls"
                | "xlsx"
                | "ods"
                | "csv"
        )
    ) {
        "office"
    } else if matches!(extension(&lower).as_deref(), Some("epub" | "mobi")) {
        "book"
    } else {
        "unknown"
    }
    .to_string()
}

fn extension(file_name: &str) -> Option<String> {
    Path::new(file_name)
        .extension()
        .and_then(|extension| extension.to_str())
        .map(ToOwned::to_owned)
}

pub(crate) fn encode_local_id_segment(value: &str) -> String {
    let mut encoded = String::with_capacity(value.len());
    for byte in value.as_bytes() {
        let character = *byte as char;
        if character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '-') {
            encoded.push(character);
        } else {
            encoded.push('~');
            encoded.push_str(&format!("{byte:02X}"));
        }
    }
    encoded
}

pub(crate) fn decode_local_id_segment(value: &str) -> Result<String, WebError> {
    let bytes = value.as_bytes();
    let mut decoded = Vec::with_capacity(bytes.len());
    let mut index = 0;
    while index < bytes.len() {
        if bytes[index] == b'~' {
            if index + 2 >= bytes.len() {
                return Err(WebError::bad_request_code(
                    "local_id_invalid",
                    "本地 page id 编码不完整",
                ));
            }
            let hex = &value[index + 1..index + 3];
            let byte = u8::from_str_radix(hex, 16).map_err(|_| {
                WebError::bad_request_code("local_id_invalid", "本地 page id 编码非法")
            })?;
            decoded.push(byte);
            index += 3;
        } else {
            decoded.push(bytes[index]);
            index += 1;
        }
    }
    String::from_utf8(decoded)
        .map_err(|_| WebError::bad_request_code("local_id_invalid", "本地 page id 不是 UTF-8"))
}

fn sanitize_file_stem(value: &str, fallback: &str) -> String {
    let sanitized = value
        .trim()
        .chars()
        .map(|character| {
            if matches!(
                character,
                '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|'
            ) {
                '_'
            } else {
                character
            }
        })
        .collect::<String>();
    let trimmed = sanitized.trim().trim_matches('.').trim();
    if trimmed.is_empty() {
        fallback.to_string()
    } else {
        trimmed.to_string()
    }
}

fn sanitize_file_name(value: &str, fallback: &str) -> String {
    let sanitized = value
        .trim()
        .chars()
        .map(|character| {
            if matches!(
                character,
                '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|'
            ) {
                '_'
            } else {
                character
            }
        })
        .collect::<String>();
    let trimmed = sanitized.trim().trim_matches('.').trim();
    if trimmed.is_empty() {
        fallback.to_string()
    } else {
        trimmed.to_string()
    }
}

fn next_available_path(directory: &Path, stem: &str, extension: &str) -> PathBuf {
    let first = directory.join(format!("{stem}.{extension}"));
    if !first.exists() {
        return first;
    }
    for index in 2..10_000 {
        let candidate = directory.join(format!("{stem} {index}.{extension}"));
        if !candidate.exists() {
            return candidate;
        }
    }
    directory.join(format!("{stem}-{}.{}", std::process::id(), extension))
}

fn copy_directory_recursively(source: &Path, target: &Path) -> std::io::Result<()> {
    fs::create_dir_all(target)?;
    for entry in fs::read_dir(source)? {
        let entry = entry?;
        let source_path = entry.path();
        let target_path = target.join(entry.file_name());
        let metadata = fs::symlink_metadata(&source_path)?;
        if metadata.file_type().is_symlink() {
            continue;
        }
        if metadata.is_dir() {
            copy_directory_recursively(&source_path, &target_path)?;
        } else if metadata.is_file() {
            fs::copy(&source_path, &target_path)?;
        }
    }
    Ok(())
}

fn next_available_raw_path(directory: &Path, file_name: &str) -> PathBuf {
    let first = directory.join(file_name);
    if !first.exists() {
        return first;
    }
    let path = Path::new(file_name);
    let stem = path
        .file_stem()
        .and_then(|value| value.to_str())
        .unwrap_or(file_name);
    let extension = path.extension().and_then(|value| value.to_str());
    for index in 2..10_000 {
        let candidate_name = if let Some(extension) = extension {
            format!("{stem} {index}.{extension}")
        } else {
            format!("{stem} {index}")
        };
        let candidate = directory.join(candidate_name);
        if !candidate.exists() {
            return candidate;
        }
    }
    directory.join(format!("{stem}-{}", std::process::id()))
}

fn next_available_asset_path(directory: &Path, file_name: &str) -> PathBuf {
    let first = directory.join(file_name);
    if !first.exists() {
        return first;
    }
    let path = Path::new(file_name);
    let stem = path
        .file_stem()
        .and_then(|value| value.to_str())
        .unwrap_or(file_name);
    let extension = path.extension().and_then(|value| value.to_str());
    for index in 1..10_000 {
        let candidate_name = if let Some(extension) = extension {
            format!("{stem}-{index}.{extension}")
        } else {
            format!("{stem}-{index}")
        };
        let candidate = directory.join(candidate_name);
        if !candidate.exists() {
            return candidate;
        }
    }
    directory.join(format!("{stem}-{}", std::process::id()))
}

fn normalize_markdown_relative_asset_path(
    markdown_dir: &Path,
    asset_path: &Path,
) -> Result<String, WebError> {
    let relative = asset_path.strip_prefix(markdown_dir).map_err(|_| {
        WebError::bad_request_code(
            "local_asset_upload_relative_path_failed",
            "本地资源文件不在 Markdown 文件目录内",
        )
    })?;
    Ok(relative
        .components()
        .map(|component| component.as_os_str().to_string_lossy().to_string())
        .collect::<Vec<_>>()
        .join("/"))
}

fn local_upload_asset_type(kind: &str, mime_type: &str) -> &'static str {
    let normalized_kind = kind.trim().to_ascii_lowercase();
    if normalized_kind == "image" || mime_type.trim().to_ascii_lowercase().starts_with("image/") {
        "image"
    } else {
        "file"
    }
}

fn next_available_directory_path(directory: &Path, stem: &str) -> PathBuf {
    let first = directory.join(stem);
    if !first.exists() {
        return first;
    }
    for index in 2..10_000 {
        let candidate = directory.join(format!("{stem} {index}"));
        if !candidate.exists() {
            return candidate;
        }
    }
    directory.join(format!("{stem}-{}", std::process::id()))
}

// 过渡实现：写侧手写 Markdown 回写入口函数。当前通过 editor_blocks_to_markdown_with_rewrite
// 将 editor block document 转为 Markdown 文本，AST 迁移 complete 后应替换为
// AST→Markdown 反向映射（见 design/03-rust-web/process/3-13 方案 Step 3）。
fn editor_blocks_to_markdown_for_file(
    content: &Value,
    root: &Path,
    markdown_path: &Path,
) -> String {
    editor_blocks_to_markdown_with_rewrite(content, Some((root, markdown_path)))
}

fn local_resource_write_editor_blocks(request: &LocalResourceWriteRequest) -> Value {
    let format = request
        .content_format
        .as_deref()
        .unwrap_or_default()
        .trim()
        .to_ascii_lowercase();
    let source = if request.content.is_null() {
        request.tiptap_document.as_ref().unwrap_or(&Value::Null)
    } else {
        &request.content
    };
    if format == "tiptapdocument" || is_tiptap_document(source) {
        tiptap_document_to_editor_blocks(source)
    } else {
        source.clone()
    }
}

fn is_tiptap_document(value: &Value) -> bool {
    value.get("type").and_then(Value::as_str) == Some("doc")
        && value.get("content").and_then(Value::as_array).is_some()
}

fn tiptap_document_to_editor_blocks(value: &Value) -> Value {
    let nodes = value
        .get("content")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();
    let blocks = nodes
        .iter()
        .enumerate()
        .map(|(index, node)| {
            let node_type = node
                .get("type")
                .and_then(Value::as_str)
                .unwrap_or("paragraph");
            let content = node.get("content").cloned().unwrap_or_else(|| json!([]));
            let text = extract_block_text(&content);
            let block_type = match node_type {
                "heading" => "heading",
                "codeBlock" | "code_block" | "code" => "codeBlock",
                "blockquote" => "quote",
                "bulletListItem" | "listItem" => "bulletListItem",
                "orderedListItem" | "numberedListItem" => "numberedListItem",
                _ => "paragraph",
            };
            let mut block = json!({
                "id": format!("resource-block-{}-{}", index + 1, stable_hash_hex(&format!("{node_type}:{text}"))),
                "type": block_type,
                "content": content,
            });
            if node_type == "heading" {
                let level = node
                    .get("attrs")
                    .and_then(|attrs| attrs.get("level"))
                    .and_then(Value::as_u64)
                    .unwrap_or(1)
                    .clamp(1, 6);
                block["props"] = json!({ "level": level });
            }
            block
        })
        .collect::<Vec<_>>();
    Value::Array(blocks)
}

fn text_to_editor_blocks(text: &str, file_name: &str) -> Value {
    if is_code_like_file(file_name) {
        return json!([{
            "id": "resource-block-1",
            "type": "code_block",
            "content": text,
            "props": {
                "language": file_extension(file_name).unwrap_or_default()
            }
        }]);
    }
    let blocks = text
        .split('\n')
        .enumerate()
        .map(|(index, line)| {
            json!({
                "id": format!("resource-block-{}-{}", index + 1, stable_hash_hex(line)),
                "type": "paragraph",
                "content": line
            })
        })
        .collect::<Vec<_>>();
    Value::Array(blocks)
}

fn editor_blocks_to_plain_text(content: &Value) -> String {
    let blocks = if let Some(array) = content.as_array() {
        array.clone()
    } else {
        content
            .get("blocks")
            .and_then(Value::as_array)
            .cloned()
            .unwrap_or_default()
    };
    let text = blocks
        .iter()
        .map(extract_block_text)
        .collect::<Vec<_>>()
        .join("\n");
    if text.ends_with('\n') {
        text
    } else {
        format!("{text}\n")
    }
}

fn file_extension(file_name: &str) -> Option<String> {
    Path::new(file_name)
        .extension()
        .and_then(|value| value.to_str())
        .map(|value| value.to_ascii_lowercase())
}

fn is_code_like_file(file_name: &str) -> bool {
    matches!(
        file_extension(file_name).as_deref(),
        Some(
            "rs" | "ts"
                | "tsx"
                | "js"
                | "jsx"
                | "json"
                | "css"
                | "scss"
                | "html"
                | "xml"
                | "py"
                | "go"
                | "java"
                | "kt"
                | "swift"
                | "c"
                | "h"
                | "cpp"
                | "hpp"
                | "sh"
                | "bash"
                | "zsh"
                | "toml"
                | "yaml"
                | "yml"
                | "sql"
        )
    )
}

fn stable_hash_hex(value: &str) -> String {
    let mut hasher = DefaultHasher::new();
    value.hash(&mut hasher);
    format!("{:016x}", hasher.finish())
}

// 过渡实现：写侧手写 Markdown 回写核心函数。逐 block 遍历，通过
// inline_nodes_to_markdown 将行内节点转为 Markdown 文本。
// AST 迁移 complete 后应替换为 block→Markdown AST→文本 的两次映射。
fn editor_blocks_to_markdown_with_rewrite(
    content: &Value,
    local_file_context: Option<(&Path, &Path)>,
) -> String {
    let blocks = if let Some(array) = content.as_array() {
        array.clone()
    } else {
        content
            .get("blocks")
            .and_then(Value::as_array)
            .cloned()
            .unwrap_or_default()
    };
    let mut lines = Vec::new();
    for block in blocks {
        let block_type = block
            .get("type")
            .or_else(|| block.get("blockType"))
            .and_then(Value::as_str)
            .unwrap_or("paragraph");
        let inline_markdown = block_content_value(&block)
            .map(|content| inline_nodes_to_markdown(content, local_file_context))
            .unwrap_or_default()
            .trim()
            .to_string();
        let text = if inline_markdown.is_empty() {
            extract_block_text(&block).trim().to_string()
        } else {
            inline_markdown
        };
        if text.is_empty()
            && !matches!(
                block_type,
                "divider" | "media" | "image" | "mindmap" | "table"
            )
        {
            lines.push(String::new());
            continue;
        }
        match block_type {
            "image" => {
                let props = block.get("props").and_then(Value::as_object);
                let src = props
                    .and_then(|props| props.get("src").or_else(|| props.get("sourcePath")))
                    .and_then(Value::as_str)
                    .unwrap_or("")
                    .trim();
                let alt = props
                    .and_then(|props| props.get("alt").or_else(|| props.get("title")))
                    .and_then(Value::as_str)
                    .unwrap_or(text.as_str())
                    .trim();
                if src.is_empty() {
                    lines.push(text);
                } else {
                    let src = rewrite_local_open_url_to_markdown_relative(src, local_file_context)
                        .unwrap_or_else(|| src.to_string());
                    lines.push(format!(
                        "![{}]({})",
                        alt.replace(']', r"\]"),
                        markdown_link_target(&src)
                    ));
                }
            }
            "media" => {
                let props = block.get("props").and_then(Value::as_object);
                let url = props
                    .and_then(|props| {
                        props
                            .get("sourcePath")
                            .or_else(|| props.get("url"))
                            .or_else(|| props.get("src"))
                    })
                    .and_then(Value::as_str)
                    .unwrap_or("")
                    .trim();
                let name = props
                    .and_then(|props| props.get("name").or_else(|| props.get("fileName")))
                    .and_then(Value::as_str)
                    .unwrap_or(text.as_str())
                    .trim();
                if url.is_empty() {
                    lines.push(text);
                } else {
                    let url = rewrite_local_open_url_to_markdown_relative(url, local_file_context)
                        .unwrap_or_else(|| url.to_string());
                    let label = if name.is_empty() { url.as_str() } else { name };
                    lines.push(format!("[{}]({})", label, markdown_link_target(&url)));
                }
            }
            "mindmap" => {
                let props = block.get("props").and_then(Value::as_object);
                let source = props
                    .and_then(|props| {
                        props
                            .get("sourcePath")
                            .or_else(|| props.get("mindmapId"))
                            .or_else(|| props.get("mindmap_id"))
                    })
                    .and_then(Value::as_str)
                    .unwrap_or("")
                    .trim();
                let label = props
                    .and_then(|props| props.get("name").or_else(|| props.get("title")))
                    .and_then(Value::as_str)
                    .unwrap_or("思维导图")
                    .trim();
                if source.is_empty() {
                    lines.push(text);
                } else {
                    let source = rewrite_local_mindmap_source_to_markdown_relative(
                        source,
                        local_file_context,
                    )
                    .unwrap_or_else(|| {
                        if is_local_mindmap_file_name(source) || source.contains('/') {
                            source.to_string()
                        } else {
                            local_mindmap_file_name(source)
                        }
                    });
                    let label = if label.is_empty() {
                        "思维导图"
                    } else {
                        label
                    };
                    lines.push(format!("[{}]({})", label, markdown_link_target(&source)));
                }
            }
            "heading" => {
                let level = block
                    .get("props")
                    .and_then(|props| props.get("level"))
                    .and_then(Value::as_u64)
                    .unwrap_or(1)
                    .clamp(1, 6);
                lines.push(format!("{} {text}", "#".repeat(level as usize)));
            }
            "bulletListItem" | "bullet_list_item" | "list_item" => {
                lines.push(format!("- {text}"));
            }
            "numberedListItem" | "numbered_list_item" => {
                lines.push(format!("1. {text}"));
            }
            "todo" | "checkListItem" | "advancedTodo" => {
                let checked = block
                    .get("props")
                    .and_then(|props| props.get("checked"))
                    .and_then(Value::as_bool)
                    .unwrap_or(false);
                let checkbox = if checked { "[x]" } else { "[ ]" };
                lines.push(format!("- {checkbox} {text}"));
            }
            "quote" | "blockquote" => {
                lines.push(format!("> {text}"));
            }
            "codeBlock" | "code_block" | "code" => {
                lines.push(format!("```\n{text}\n```"));
            }
            "divider" => {
                lines.push("---".to_string());
            }
            "table" => {
                let table_markdown = editor_block_table_to_markdown(&block);
                if table_markdown.is_empty() {
                    lines.push(text);
                } else {
                    lines.extend(table_markdown.lines().map(ToOwned::to_owned));
                }
            }
            _ => lines.push(text),
        }
        lines.push(String::new());
    }
    let markdown = lines.join("\n").trim_end().to_string();
    if markdown.is_empty() {
        "\n".to_string()
    } else {
        format!("{markdown}\n")
    }
}

fn markdown_link_target(target: &str) -> String {
    let trimmed = target.trim();
    if trimmed.is_empty() {
        return String::new();
    }
    if trimmed
        .chars()
        .any(|ch| ch.is_whitespace() || matches!(ch, '(' | ')' | '<' | '>'))
    {
        format!("<{}>", trimmed.replace('>', "%3E"))
    } else {
        trimmed.to_string()
    }
}

fn rewrite_local_mindmap_source_to_markdown_relative(
    value: &str,
    local_file_context: Option<(&Path, &Path)>,
) -> Option<String> {
    if let Some(relative_path) = local_mindmap_relative_path_from_id(value) {
        let (root, markdown_path) = local_file_context?;
        let asset_path = root.canonicalize().ok()?.join(relative_path);
        let markdown_dir = markdown_path.parent()?;
        return normalize_markdown_relative_asset_path(markdown_dir, &asset_path).ok();
    }
    rewrite_local_open_url_to_markdown_relative(value, local_file_context)
}

fn rewrite_local_open_url_to_markdown_relative(
    value: &str,
    local_file_context: Option<(&Path, &Path)>,
) -> Option<String> {
    let (root, markdown_path) = local_file_context?;
    let parsed = if value.starts_with('/') {
        Url::parse(&format!("http://localhost{}", value)).ok()?
    } else {
        Url::parse(value).ok()?
    };
    if parsed.path() != "/api/local-folder/files/open" {
        return None;
    }
    let root_uri = parsed
        .query_pairs()
        .find(|(key, _)| key == "rootUri")
        .map(|(_, value)| value.into_owned())?;
    let asset_relative_path = parsed
        .query_pairs()
        .find(|(key, _)| key == "path")
        .map(|(_, value)| value.into_owned())?;
    let parsed_root = parse_file_root_uri(&root_uri).ok()?.canonicalize().ok()?;
    let canonical_root = root.canonicalize().ok()?;
    if parsed_root != canonical_root {
        return None;
    }
    let asset_path = canonical_root.join(asset_relative_path);
    let markdown_dir = markdown_path.parent()?;
    normalize_markdown_relative_asset_path(markdown_dir, &asset_path).ok()
}

fn content_type_for_path(path: &Path) -> HeaderValue {
    let extension = path
        .extension()
        .and_then(|value| value.to_str())
        .unwrap_or("")
        .to_ascii_lowercase();
    let content_type = match extension.as_str() {
        "jpg" | "jpeg" => "image/jpeg",
        "png" => "image/png",
        "gif" => "image/gif",
        "webp" => "image/webp",
        "svg" => "image/svg+xml; charset=utf-8",
        "md" | "markdown" => "text/markdown; charset=utf-8",
        "txt" | "log" => "text/plain; charset=utf-8",
        "json" => "application/json; charset=utf-8",
        "pdf" => "application/pdf",
        "doc" => "application/msword",
        "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "ppt" => "application/vnd.ms-powerpoint",
        "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
        "xls" => "application/vnd.ms-excel",
        "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "odt" => "application/vnd.oasis.opendocument.text",
        "odp" => "application/vnd.oasis.opendocument.presentation",
        "ods" => "application/vnd.oasis.opendocument.spreadsheet",
        _ => "application/octet-stream",
    };
    HeaderValue::from_static(content_type)
}

fn percent_encode_content_disposition_filename(value: &str) -> String {
    let mut encoded = String::new();
    for byte in value.as_bytes() {
        match *byte {
            b'A'..=b'Z'
            | b'a'..=b'z'
            | b'0'..=b'9'
            | b'!'
            | b'#'
            | b'$'
            | b'&'
            | b'+'
            | b'-'
            | b'.'
            | b'^'
            | b'_'
            | b'`'
            | b'|'
            | b'~' => encoded.push(*byte as char),
            _ => encoded.push_str(&format!("%{byte:02X}")),
        }
    }
    encoded
}

fn content_disposition_attachment_for_path(path: &Path) -> HeaderValue {
    let filename = path
        .file_name()
        .and_then(|value| value.to_str())
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .unwrap_or("download");
    content_disposition_attachment_for_filename(filename)
}

fn content_disposition_attachment_for_filename(filename: &str) -> HeaderValue {
    let filename = filename.trim();
    let filename = if filename.is_empty() {
        "download"
    } else {
        filename
    };
    let ascii_fallback = filename
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_' | ' ') {
                ch
            } else {
                '_'
            }
        })
        .collect::<String>();
    let ascii_fallback = if ascii_fallback.trim().is_empty() {
        "download".to_string()
    } else {
        ascii_fallback
    };
    let quoted = ascii_fallback.replace('\\', "\\\\").replace('"', "\\\"");
    let encoded = percent_encode_content_disposition_filename(filename);
    HeaderValue::from_str(&format!(
        "attachment; filename=\"{quoted}\"; filename*=UTF-8''{encoded}"
    ))
    .unwrap_or_else(|_| HeaderValue::from_static("attachment"))
}

fn local_directory_tar_filename(path: &Path) -> String {
    let name = path
        .file_name()
        .and_then(|value| value.to_str())
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .unwrap_or("folder");
    format!("{name}.tar")
}

fn tar_octal(value: u64, width: usize) -> Vec<u8> {
    let mut encoded = format!("{value:o}").into_bytes();
    let max_digits = width.saturating_sub(1);
    if encoded.len() > max_digits {
        encoded = vec![b'7'; max_digits];
    }
    let mut out = vec![b'0'; max_digits.saturating_sub(encoded.len())];
    out.extend(encoded);
    out.push(0);
    out
}

fn split_tar_path(path: &str) -> Result<(&str, &str), std::io::Error> {
    let bytes = path.as_bytes();
    if bytes.len() <= 100 {
        return Ok(("", path));
    }
    let mut best: Option<usize> = None;
    for (index, ch) in path.char_indices() {
        if ch == '/' {
            let prefix_len = path[..index].as_bytes().len();
            let name_len = path[index + 1..].as_bytes().len();
            if prefix_len <= 155 && name_len <= 100 {
                best = Some(index);
            }
        }
    }
    if let Some(index) = best {
        return Ok((&path[..index], &path[index + 1..]));
    }
    Err(std::io::Error::new(
        std::io::ErrorKind::InvalidInput,
        "tar entry path is too long",
    ))
}

fn append_tar_header(
    out: &mut Vec<u8>,
    path: &str,
    size: u64,
    typeflag: u8,
    mode: u64,
    mtime: u64,
) -> Result<(), std::io::Error> {
    let normalized = path.trim_start_matches('/').replace('\\', "/");
    let (prefix, name) = split_tar_path(&normalized)?;
    let mut header = [0u8; 512];
    header[0..name.as_bytes().len()].copy_from_slice(name.as_bytes());
    let mode = tar_octal(mode, 8);
    header[100..108].copy_from_slice(&mode);
    header[108..116].copy_from_slice(&tar_octal(0, 8));
    header[116..124].copy_from_slice(&tar_octal(0, 8));
    header[124..136].copy_from_slice(&tar_octal(size, 12));
    header[136..148].copy_from_slice(&tar_octal(mtime, 12));
    header[148..156].fill(b' ');
    header[156] = typeflag;
    header[257..263].copy_from_slice(b"ustar\0");
    header[263..265].copy_from_slice(b"00");
    if !prefix.is_empty() {
        header[345..345 + prefix.as_bytes().len()].copy_from_slice(prefix.as_bytes());
    }
    let checksum = header.iter().map(|byte| u32::from(*byte)).sum::<u32>();
    let checksum_bytes = format!("{checksum:06o}\0 ").into_bytes();
    header[148..156].copy_from_slice(&checksum_bytes);
    out.extend_from_slice(&header);
    Ok(())
}

fn append_tar_padding(out: &mut Vec<u8>, size: usize) {
    let remainder = size % 512;
    if remainder > 0 {
        out.extend(std::iter::repeat(0).take(512 - remainder));
    }
}

fn append_local_directory_tar_entries(
    out: &mut Vec<u8>,
    root: &Path,
    current: &Path,
    base_name: &str,
) -> Result<(), std::io::Error> {
    let metadata = fs::symlink_metadata(current)?;
    if metadata.file_type().is_symlink() {
        return Ok(());
    }
    let relative = current.strip_prefix(root).unwrap_or(current);
    let mut entry_name = if relative.as_os_str().is_empty() {
        base_name.to_string()
    } else {
        format!(
            "{base_name}/{}",
            relative.to_string_lossy().replace('\\', "/")
        )
    };
    let mtime = metadata
        .modified()
        .ok()
        .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
        .map(|duration| duration.as_secs())
        .unwrap_or(0);
    if metadata.is_dir() {
        if !entry_name.ends_with('/') {
            entry_name.push('/');
        }
        append_tar_header(out, &entry_name, 0, b'5', 0o755, mtime)?;
        let mut children = fs::read_dir(current)?.collect::<Result<Vec<_>, _>>()?;
        children.sort_by_key(|entry| entry.file_name());
        for child in children {
            append_local_directory_tar_entries(out, root, &child.path(), base_name)?;
        }
        return Ok(());
    }
    if metadata.is_file() {
        append_tar_header(out, &entry_name, metadata.len(), b'0', 0o644, mtime)?;
        let bytes = fs::read(current)?;
        out.extend_from_slice(&bytes);
        append_tar_padding(out, bytes.len());
    }
    Ok(())
}

fn build_local_directory_tar_archive(directory: &Path) -> Result<Vec<u8>, std::io::Error> {
    let canonical = directory.canonicalize()?;
    let base_name = canonical
        .file_name()
        .and_then(|value| value.to_str())
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .unwrap_or("folder")
        .to_string();
    let mut out = Vec::new();
    append_local_directory_tar_entries(&mut out, &canonical, &canonical, &base_name)?;
    out.extend_from_slice(&[0u8; 1024]);
    Ok(out)
}

fn editor_block_table_to_markdown(block: &Value) -> String {
    let table = block
        .get("props")
        .and_then(|props| props.get("tiptapTable"))
        .or_else(|| block.get("tiptapTable"));
    let rows = table
        .and_then(|value| value.get("content"))
        .and_then(Value::as_array);
    let Some(rows) = rows else {
        return String::new();
    };
    let mut parsed_rows = Vec::<Vec<(String, bool)>>::new();
    let mut max_columns = 0usize;
    for row in rows {
        let cells = row
            .get("content")
            .and_then(Value::as_array)
            .map(|cells| {
                cells
                    .iter()
                    .map(|cell| {
                        let cell_text = cell
                            .get("content")
                            .map(|content| inline_nodes_to_markdown(content, None))
                            .unwrap_or_default()
                            .replace('\n', " ")
                            .replace('|', r"\|")
                            .trim()
                            .to_string();
                        let is_header =
                            cell.get("type").and_then(Value::as_str) == Some("tableHeader");
                        (cell_text, is_header)
                    })
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();
        max_columns = max_columns.max(cells.len());
        parsed_rows.push(cells);
    }
    if parsed_rows.is_empty() || max_columns == 0 {
        return String::new();
    }
    let alignments = alignments_from_table(block);
    let mut lines = Vec::<String>::new();
    for (index, row) in parsed_rows.iter().enumerate() {
        let mut cells = row.iter().map(|(text, _)| text.clone()).collect::<Vec<_>>();
        while cells.len() < max_columns {
            cells.push(String::new());
        }
        lines.push(format!("| {} |", cells.join(" | ")));
        if index == 0 {
            let align_row = (0..max_columns)
                .map(
                    |column_index| match table_cell_alignment(&alignments, column_index) {
                        "left" => ":---",
                        "center" => ":---:",
                        "right" => "---:",
                        _ => "---",
                    },
                )
                .collect::<Vec<_>>()
                .join(" | ");
            lines.push(format!("| {} |", align_row));
        }
    }
    if lines.len() < 2 {
        String::new()
    } else {
        lines.join("\n")
    }
}

fn alignments_from_table(block: &Value) -> Vec<String> {
    block
        .get("props")
        .and_then(|props| props.get("tiptapTable"))
        .or_else(|| block.get("tiptapTable"))
        .and_then(|value| value.get("content"))
        .and_then(Value::as_array)
        .and_then(|rows| rows.get(0))
        .and_then(|row| row.get("content"))
        .and_then(Value::as_array)
        .map(|cells| {
            cells
                .iter()
                .map(|cell| {
                    cell.get("attrs")
                        .and_then(|attrs| attrs.get("textAlign"))
                        .and_then(Value::as_str)
                        .unwrap_or("")
                        .to_string()
                })
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

fn table_cell_alignment(alignments: &[String], index: usize) -> &str {
    alignments.get(index).map(String::as_str).unwrap_or("")
}

fn extract_block_text(value: &Value) -> String {
    if let Some(text) = value.as_str() {
        return text.to_string();
    }
    if let Some(array) = value.as_array() {
        return array
            .iter()
            .map(extract_block_text)
            .collect::<Vec<_>>()
            .join("");
    }
    if let Some(object) = value.as_object() {
        if let Some(text) = object.get("text").and_then(Value::as_str) {
            return text.to_string();
        }
        if let Some(content) = object.get("content") {
            return extract_block_text(content);
        }
        if let Some(children) = object.get("children") {
            return extract_block_text(children);
        }
    }
    String::new()
}

fn block_content_value(block: &Value) -> Option<&Value> {
    block.get("content").or_else(|| block.get("contentNodes"))
}

// 过渡实现：手写行内节点→Markdown 回写函数。从 editor block 的 content 数组中
// 逐个节点提取 text + styles，按 legacy 样式格式输出为内联 Markdown。
// AST + 中间 IR 迁移 complete 后应统一走 MarkdownInline→Markdown 反向映射。
fn inline_nodes_to_markdown(value: &Value, local_file_context: Option<(&Path, &Path)>) -> String {
    if let Some(text) = value.as_str() {
        return escape_markdown_inline_text(text);
    }
    if let Some(array) = value.as_array() {
        return array
            .iter()
            .map(|node| inline_node_to_markdown(node, local_file_context))
            .collect::<Vec<_>>()
            .join("");
    }
    if let Some(object) = value.as_object() {
        if let Some(text) = object.get("text").and_then(Value::as_str) {
            return markdown_text_with_styles(
                text,
                &inline_styles_from_object(object),
                local_file_context,
            );
        }
        if let Some(content) = object.get("content").or_else(|| object.get("contentNodes")) {
            return inline_nodes_to_markdown(content, local_file_context);
        }
    }
    String::new()
}

fn inline_node_to_markdown(node: &Value, local_file_context: Option<(&Path, &Path)>) -> String {
    if let Some(text) = node.as_str() {
        return escape_markdown_inline_text(text);
    }
    if let Some(object) = node.as_object() {
        let text = object
            .get("text")
            .and_then(Value::as_str)
            .unwrap_or_default();
        if !text.is_empty() {
            return markdown_text_with_styles(
                text,
                &inline_styles_from_object(object),
                local_file_context,
            );
        }
        if let Some(content) = object.get("content").or_else(|| object.get("contentNodes")) {
            return inline_nodes_to_markdown(content, local_file_context);
        }
    }
    String::new()
}

fn inline_styles_from_object(object: &Map<String, Value>) -> Value {
    let mut styles = object
        .get("styles")
        .and_then(Value::as_object)
        .cloned()
        .unwrap_or_default();
    for mark in object
        .get("marks")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
    {
        let mark_type = mark.get("type").and_then(Value::as_str).unwrap_or_default();
        match mark_type {
            "bold" | "strong" => {
                styles.insert("bold".to_string(), Value::Bool(true));
            }
            "italic" | "em" => {
                styles.insert("italic".to_string(), Value::Bool(true));
            }
            "underline" => {
                styles.insert("underline".to_string(), Value::Bool(true));
            }
            "strike" | "strikethrough" => {
                styles.insert("strike".to_string(), Value::Bool(true));
            }
            "code" => {
                styles.insert("code".to_string(), Value::Bool(true));
            }
            "link" => {
                if let Some(href) = mark
                    .get("attrs")
                    .and_then(|attrs| attrs.get("href"))
                    .and_then(Value::as_str)
                    .map(str::trim)
                    .filter(|href| !href.is_empty())
                {
                    styles.insert("link".to_string(), Value::String(href.to_string()));
                }
            }
            _ => {}
        }
    }
    Value::Object(styles)
}

fn markdown_text_with_styles(
    text: &str,
    styles: &Value,
    local_file_context: Option<(&Path, &Path)>,
) -> String {
    let mut value = escape_markdown_inline_text(text);
    let link = styles
        .get("link")
        .or_else(|| styles.get("href"))
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|href| !href.is_empty())
        .map(ToOwned::to_owned);
    if styles
        .get("code")
        .or_else(|| styles.get("inlineCode"))
        .and_then(Value::as_bool)
        .unwrap_or(false)
    {
        value = format!("`{}`", text.replace('`', r"\`"));
    }
    if styles.get("bold").and_then(Value::as_bool).unwrap_or(false) {
        value = format!("**{value}**");
    }
    if styles
        .get("italic")
        .and_then(Value::as_bool)
        .unwrap_or(false)
    {
        value = format!("*{value}*");
    }
    if styles
        .get("strike")
        .or_else(|| styles.get("strikethrough"))
        .and_then(Value::as_bool)
        .unwrap_or(false)
    {
        value = format!("~~{value}~~");
    }
    if let Some(href) = link {
        let href =
            rewrite_local_open_url_to_markdown_relative(&href, local_file_context).unwrap_or(href);
        value = format!("[{}]({href})", value.replace(']', r"\]"));
    }
    value
}

fn escape_markdown_inline_text(text: &str) -> String {
    text.replace('\\', r"\\")
}

fn now_ms() -> u128 {
    system_time_ms(SystemTime::now())
}

fn system_time_ms(time: SystemTime) -> u128 {
    time.duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis())
        .unwrap_or(0)
}

fn local_markdown_conflict_detection_key(
    document_id: &str,
    markdown_path: &Path,
) -> Result<String, WebError> {
    let content = fs::read(markdown_path).map_err(|error| {
        WebError::bad_request_code(
            "local_markdown_read_failed",
            format!(
                "无法读取本地 Markdown 文件 {}: {error}",
                markdown_path.display()
            ),
        )
    })?;
    let meta = fs::metadata(markdown_path).map_err(|error| {
        WebError::bad_request_code(
            "local_markdown_stat_failed",
            format!(
                "无法读取本地 Markdown 文件状态 {}: {error}",
                markdown_path.display()
            ),
        )
    })?;
    let modified_ms = system_time_ms(meta.modified().unwrap_or(SystemTime::UNIX_EPOCH));
    let mut hasher = DefaultHasher::new();
    content.hash(&mut hasher);
    let content_hash = hasher.finish();
    Ok(format!(
        "local-md:{document_id}:{modified_ms}:{}:{content_hash:016x}",
        meta.len()
    ))
}

fn local_resource_conflict_detection_key(root: &Path, target: &Path) -> Result<String, WebError> {
    let content = fs::read(target).map_err(|error| {
        WebError::bad_request_code(
            "local_resource_read_failed",
            format!("无法读取本地资源文件 {}: {error}", target.display()),
        )
    })?;
    let meta = fs::metadata(target).map_err(|error| {
        WebError::bad_request_code(
            "local_resource_stat_failed",
            format!("无法读取本地资源文件状态 {}: {error}", target.display()),
        )
    })?;
    let modified_ms = system_time_ms(meta.modified().unwrap_or(SystemTime::UNIX_EPOCH));
    let relative_path = target
        .strip_prefix(root)
        .ok()
        .map(|path| path.to_string_lossy().replace('\\', "/"))
        .unwrap_or_else(|| target.to_string_lossy().to_string());
    let mut hasher = DefaultHasher::new();
    content.hash(&mut hasher);
    let content_hash = hasher.finish();
    Ok(format!(
        "local-resource:{relative_path}:{modified_ms}:{}:{content_hash:016x}",
        meta.len()
    ))
}

fn markdown_page_subtree(document_id: &str, title: &str, content: &Value) -> Value {
    let outline = content
        .as_array()
        .unwrap_or(&Vec::new())
        .iter()
        .filter(|block| block.get("type").and_then(Value::as_str) == Some("heading"))
        .enumerate()
        .map(|(index, block)| {
            json!({
                "id": format!("outline-{index}"),
                "nodeId": document_id,
                "blockId": block.get("id").and_then(Value::as_str).unwrap_or_default(),
                "title": block
                    .get("content")
                    .and_then(Value::as_array)
                    .and_then(|items| items.first())
                    .and_then(|item| item.get("text"))
                    .and_then(Value::as_str)
                    .unwrap_or(title),
                "depth": block
                    .get("props")
                    .and_then(|props| props.get("level"))
                    .and_then(Value::as_u64)
                    .unwrap_or(1),
            })
        })
        .collect::<Vec<_>>();
    json!({
        "projectionId": format!("page-subtree:{document_id}"),
        "projection": "page_tree",
        "rootNodeId": document_id,
        "rootNode": {
            "id": document_id,
            "title": title,
            "nodeType": "page",
            "depth": 0,
        },
        "subtree": {
            "rootNodeId": document_id,
            "nodes": [{
                "id": document_id,
                "title": title,
                "nodeType": "page",
                "depth": 0,
            }],
        },
        "outline": outline,
        "evidence": [{
            "id": format!("evidence:{document_id}"),
            "nodeId": document_id,
            "kind": "page",
            "snippet": title,
        }],
        "stats": {
            "blockCount": content.as_array().map(|blocks| blocks.len()).unwrap_or(0),
            "headingCount": outline.len(),
            "evidenceCount": 1,
            "maxDepth": 1,
        },
    })
}

#[cfg(test)]
mod tests {
    use super::{
        add_sqlite_local_access_grant_for_context,
        create_default_local_workspace_for_actor_at_base, create_local_access_grant,
        create_share_grant, create_share_link, create_user_access_grant, create_user_share_grant,
        delete_share_link, delete_user_access_grant, editor_blocks_to_markdown_for_file,
        encode_local_id_segment, ensure_local_path_read_access,
        ensure_local_workspace_access_for_actor, ensure_local_workspace_read_access_for_actor,
        ensure_local_workspace_read_access_with_state,
        ensure_local_workspace_write_access_with_state, execute_local_tree_command,
        execute_local_tree_command_with_sort, get_local_access_policy, get_share_grants,
        get_share_links, get_user_access_policy, initialize_local_page_id,
        initialize_local_workspace_for_actor, load_local_folder_file_tree_children_snapshot,
        load_local_folder_file_tree_snapshot, load_local_folder_file_tree_snapshot_with_reveal,
        load_local_folder_page_tree_snapshot, local_folder_watch_revision,
        local_markdown_path_page_id, local_resource_write_editor_blocks, local_workspace_id,
        open_local_file, read_local_resource, record_shared_cache, record_sync_pending_change,
        resolve_local_markdown_page_aggregate, save_local_markdown_page,
        update_local_markdown_title, validate_local_access_root, write_local_markdown_asset,
        write_local_markdown_page_body, write_local_mindmap_data, write_local_resource,
        write_sync_conflict_report, LocalAccessGrantRequest, LocalAccessValidateRootRequest,
        LocalFileOpenQuery, LocalResourceReadQuery, LocalResourceWriteRequest,
        LocalShareGrantRequest, LocalShareLinkRequest, LocalUploadFile, ShareLinkListQuery,
        SharedCacheRecordRequest, SyncConflictReportRequest, SyncPendingChangeRequest,
    };
    use crate::app::{AppConfig, AppState};
    use crate::context::RequestContext;
    use axum::extract::{Extension, Path as AxumPath, Query, State};
    use axum::http::{HeaderMap, Method, StatusCode};
    use axum::Json;
    use control_plane::{DirectoryGrantInput, UpsertUserInput};
    use serde_json::{json, Value};
    use std::sync::Mutex;

    fn env_lock() -> &'static Mutex<()> {
        crate::test_support::hermes_env_lock()
    }

    fn temp_root(name: &str) -> std::path::PathBuf {
        let root = std::env::temp_dir().join(format!("{name}-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&root);
        std::fs::create_dir_all(&root).expect("create temp root");
        root
    }

    fn request_context(actor_id: &str, actor_type: &str) -> RequestContext {
        let mut headers = HeaderMap::new();
        headers.insert("x-mnote-actor-id", actor_id.parse().unwrap());
        headers.insert("x-mnote-actor-type", actor_type.parse().unwrap());
        RequestContext::from_http_parts(
            &Method::POST,
            &"/api/admin/access-policy".parse().expect("uri"),
            &headers,
        )
    }

    fn test_state() -> AppState {
        AppState::new(AppConfig {
            service_name: "mnote-web".into(),
            service_version: "0.1.0".into(),
            bind_addr: "127.0.0.1:0".into(),
            public_bind_addr: "127.0.0.1:3000".into(),
            legacy_next_base_url: None,
            enable_legacy_next_compat: false,
            enable_debug_shell_routes: false,
            enable_editor_actor: true,
            hermes_base_path: "/api/hermes".into(),
            compat_next_base_path: "/api/compat/next".into(),
            convex_url: None,
            convex_admin_key: None,
            allow_dev_fixtures: false,
            query_fixtures_json: None,
            mutation_fixtures_json: None,
            dev_user_id: "dev-user".into(),
            dev_user_name: "开发用户".into(),
            dev_user_email: "dev@mnote.local".into(),
        })
    }

    fn init_workspace(root: &std::path::Path) {
        initialize_local_workspace_for_actor("user_test", &format!("file://{}", root.display()))
            .expect("init local workspace");
    }

    #[test]
    fn local_folder_file_tree_children_snapshot_scans_parent_once() {
        let root = temp_root("mnote-filetree-children-shallow-scan");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("design").join("03-rust-web")).expect("create rust web");
        std::fs::create_dir_all(root.join("design").join("05-editor-mainline"))
            .expect("create editor");
        std::fs::write(root.join("design").join("README.md"), "# Design\n").expect("write readme");
        std::fs::write(
            root.join("design").join("03-rust-web").join("plan.md"),
            "# Plan\n",
        )
        .expect("write nested");
        let root_uri = format!("file://{}", root.display());

        let snapshot =
            load_local_folder_file_tree_children_snapshot(&root_uri, "design").expect("snapshot");
        let items = snapshot
            .projection
            .get("items")
            .and_then(Value::as_array)
            .expect("items");
        let titles = items
            .iter()
            .filter_map(|item| item.get("title").and_then(Value::as_str))
            .collect::<Vec<_>>();

        assert_eq!(
            snapshot
                .projection
                .get("parentRelativePath")
                .and_then(Value::as_str),
            Some("design")
        );
        assert_eq!(items.len(), 3);
        assert!(titles.contains(&"03-rust-web"));
        assert!(titles.contains(&"05-editor-mainline"));
        assert!(titles.contains(&"README.md"));
        assert!(!titles.contains(&"plan.md"));
        assert_eq!(
            snapshot
                .projection
                .get("watchRevision")
                .and_then(|revision| revision.get("entryCount"))
                .and_then(Value::as_u64),
            Some(items.len() as u64)
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_folder_page_tree_snapshot_reuses_cache_until_watch_revision_changes() {
        let root = temp_root("mnote-page-tree-snapshot-cache");
        init_workspace(&root);
        std::fs::write(root.join("page.md"), "# Page\n").expect("write md");
        let root_uri = format!("file://{}", root.display());
        super::reset_local_page_tree_snapshot_test_loads();

        let first = load_local_folder_page_tree_snapshot(&root_uri).expect("first snapshot");
        let second = load_local_folder_page_tree_snapshot(&root_uri).expect("second snapshot");
        assert_eq!(first.projection, second.projection);
        assert_eq!(super::local_page_tree_snapshot_scan_test_loads(), 1);

        std::fs::write(root.join("next.md"), "# Next\n").expect("write next");
        let third = load_local_folder_page_tree_snapshot(&root_uri).expect("third snapshot");
        assert_ne!(second.projection, third.projection);
        assert_eq!(super::local_page_tree_snapshot_scan_test_loads(), 2);

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_identity_uses_path_even_when_frontmatter_has_mnote_id() {
        let root = temp_root("mnote-local-frontmatter-path-id");
        init_workspace(&root);
        std::fs::write(
            root.join("page.md"),
            "---\nmnote_id: stable-frontmatter-id\ntitle: Stable\n---\n正文\n",
        )
        .expect("write md");
        let root_uri = format!("file://{}", root.display());

        let first = load_local_folder_page_tree_snapshot(&root_uri).expect("first snapshot");
        let first_json = first.projection.to_string();
        assert!(first_json.contains("local-md:page.md"));
        assert!(!first_json.contains("local-mdid:stable-frontmatter-id"));

        std::fs::create_dir_all(root.join("docs")).expect("create docs");
        std::fs::rename(root.join("page.md"), root.join("docs").join("renamed.md"))
            .expect("move md");
        let second = load_local_folder_page_tree_snapshot(&root_uri).expect("second snapshot");
        let second_json = second.projection.to_string();
        assert!(second_json.contains("local-md:docs~2Frenamed.md"));
        assert!(!second_json.contains("local-mdid:stable-frontmatter-id"));
        assert!(second_json.contains("renamed.md"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_page_ids_metadata_does_not_override_path_derived_id() {
        let root = temp_root("mnote-local-page-ids");
        init_workspace(&root);
        std::fs::create_dir_all(root.join(".mnote")).expect("create metadata dir");
        std::fs::create_dir_all(root.join("docs")).expect("create docs");
        std::fs::write(root.join("docs").join("page.md"), "# Metadata Page\n").expect("write md");
        std::fs::write(
            root.join(".mnote").join("page-ids.json"),
            r#"{"version":1,"pages":{"docs/page.md":"stable-from-page-ids"}}"#,
        )
        .expect("write page ids");
        let root_uri = format!("file://{}", root.display());

        let snapshot = load_local_folder_page_tree_snapshot(&root_uri).expect("snapshot");
        let html_json = snapshot.projection.to_string();
        assert!(html_json.contains("local-md:docs~2Fpage.md"));
        assert!(!html_json.contains("local-mdid:stable-from-page-ids"));
        assert!(!html_json.contains("page-ids.json"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_page_id_initialization_returns_path_id_without_writing_page_ids() {
        let root = temp_root("mnote-local-page-id-init");
        init_workspace(&root);
        std::fs::write(root.join("draft.md"), "# Draft\n").expect("write md");
        let root_uri = format!("file://{}", root.display());

        let page_id = initialize_local_page_id(&root_uri, "draft.md").expect("init page id");
        assert_eq!(page_id, "local-md:draft.md");
        assert!(!root.join(".mnote").join("page-ids.json").exists());
        assert!(!root.join(".mnote").join("page-ids.json.tmp").exists());

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_metadata_damage_returns_explainable_error() {
        let root = temp_root("mnote-local-bad-metadata");
        std::fs::create_dir_all(root.join(".mnote")).expect("create metadata");
        std::fs::write(root.join("README.md"), "# Broken Metadata\n").expect("write md");
        std::fs::write(root.join(".mnote").join("page-options.json"), "{not-json")
            .expect("write bad metadata");
        let root_uri = format!("file://{}", root.display());

        let error = load_local_folder_page_tree_snapshot(&root_uri).expect_err("metadata error");
        assert!(error.message().contains("元数据 JSON 损坏"));
        assert!(error.message().contains("请修复或移走"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_page_options_metadata_flows_into_page_aggregate() {
        let root = temp_root("mnote-local-page-options");
        std::fs::create_dir_all(root.join(".mnote")).expect("create metadata");
        std::fs::write(root.join("README.md"), "# Local Options\n").expect("write md");
        std::fs::write(
            root.join(".mnote").join("page-options.json"),
            r#"{"version":1,"pages":{"local-md:README.md":{"wideLayout":true,"showToc":true,"showHeadingNumbers":true,"hideTitleHeader":false}}}"#,
        )
        .expect("write options");
        let root_uri = format!("file://{}", root.display());

        let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:README.md")
            .expect("aggregate");
        assert!(aggregate.layout.page_options.wide_layout);
        assert!(aggregate.layout.page_options.show_toc);
        assert!(aggregate.layout.page_options.show_heading_numbers);
        assert!(!aggregate.layout.page_options.hide_title_header);

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_page_aggregate_accepts_path_id_when_frontmatter_has_mnote_id() {
        let root = temp_root("mnote-local-path-id-frontmatter");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("pages")).expect("create pages");
        std::fs::write(
            root.join("pages").join("初始化的新页面.md"),
            "---\ntitle: 初始化的新页面\nmnote_id: initial-page\n---\n# 初始化的新页面\n\n正文\n",
        )
        .expect("write md");
        let root_uri = format!("file://{}", root.display());
        let path_id = format!(
            "local-md:{}",
            encode_local_id_segment("pages/初始化的新页面.md")
        );

        let aggregate =
            resolve_local_markdown_page_aggregate(&root_uri, &path_id).expect("aggregate");
        assert_eq!(aggregate.title, "初始化的新页面");

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_parser_preserves_pipe_table_as_table_block() {
        let blocks = crate::routes::local_markdown_parser::markdown_to_blocks(
            "| 左 | 右 |\n| --- | --- |\n| `A` | **B** |\n",
        );
        let array = blocks.as_array().expect("blocks");
        let table = array
            .iter()
            .find(|block| block["type"].as_str() == Some("table"))
            .expect("pipe table 应保留为 table block");
        assert_eq!(table["props"]["tiptapTable"]["type"], "table");
        assert_eq!(
            table["props"]["tiptapTable"]["content"][0]["content"][0]["type"],
            "tableHeader"
        );
        assert_eq!(
            table["props"]["tiptapTable"]["content"][1]["content"][0]["type"],
            "tableCell"
        );
        assert_eq!(
            table["props"]["tiptapTable"]["content"][0]["content"][0]["content"][0]["content"][0]
                ["text"],
            "左"
        );
        assert_eq!(
            table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]
                ["text"],
            "A"
        );
        assert_eq!(
            table["props"]["tiptapTable"]["content"][1]["content"][0]["content"][0]["content"][0]
                ["marks"][0]["type"],
            "code"
        );
        assert_eq!(
            table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]
                ["text"],
            "B"
        );
        assert_eq!(
            table["props"]["tiptapTable"]["content"][1]["content"][1]["content"][0]["content"][0]
                ["marks"][0]["type"],
            "bold"
        );
    }

    #[test]
    fn local_markdown_parser_preserves_task_list_and_inline_marks() {
        let blocks = crate::routes::local_markdown_parser::markdown_to_blocks(
            "- [x] `3000` 默认由 **mnote-web** 监听。\n- [ ] *待办* ~~删除~~ [链接](https://example.com)\n",
        );
        let array = blocks.as_array().expect("blocks");
        assert_eq!(array[0]["type"], "todo");
        assert_eq!(array[0]["props"]["checked"], true);
        assert_eq!(array[1]["type"], "todo");
        assert_eq!(array[1]["props"]["checked"], false);

        let first_content = array[0]["content"].as_array().expect("first content");
        assert!(first_content.iter().any(|node| {
            node["text"].as_str() == Some("3000") && node["styles"]["code"].as_bool() == Some(true)
        }));
        assert!(first_content.iter().any(|node| {
            node["text"].as_str() == Some("mnote-web")
                && node["styles"]["bold"].as_bool() == Some(true)
        }));

        let second_content = array[1]["content"].as_array().expect("second content");
        assert!(second_content.iter().any(|node| {
            node["text"].as_str() == Some("待办")
                && node["styles"]["italic"].as_bool() == Some(true)
        }));
        assert!(second_content.iter().any(|node| {
            node["text"].as_str() == Some("删除")
                && node["styles"]["strike"].as_bool() == Some(true)
        }));
        assert!(second_content.iter().any(|node| {
            node["text"].as_str() == Some("链接")
                && node["styles"]["link"].as_str() == Some("https://example.com")
        }));
    }

    #[test]
    fn local_markdown_aggregate_follows_file_permissions_for_editability() {
        let root = temp_root("mnote-local-readonly-permission");
        std::fs::write(root.join("README.md"), "# Editable?\n").expect("write md");
        let root_uri = format!("file://{}", root.display());

        let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:README.md")
            .expect("aggregate");
        assert!(!aggregate.head.permissions.read_only);

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_folder_watch_revision_changes_for_visible_files_only() {
        let root = temp_root("mnote-local-watch-revision");
        std::fs::create_dir_all(root.join(".mnote")).expect("create metadata");
        std::fs::create_dir_all(root.join("docs")).expect("create docs");
        std::fs::write(root.join("docs").join("page.md"), "# Watch\n").expect("write md");
        let root_uri = format!("file://{}", root.display());

        let initial = local_folder_watch_revision(&root_uri).expect("initial revision");
        std::fs::write(root.join(".mnote").join("ignored.txt"), "ignored").expect("write ignored");
        let ignored = local_folder_watch_revision(&root_uri).expect("ignored revision");
        assert_eq!(initial.revision, ignored.revision);
        std::fs::write(root.join("docs").join("page.md"), "# Watch Again\n").expect("update md");
        let updated = local_folder_watch_revision(&root_uri).expect("updated revision");
        assert_ne!(initial.revision, updated.revision);
        assert!(updated.entry_count >= 1);

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_conflict_detection_key_changes_when_content_changes_with_same_size() {
        let root = temp_root("mnote-local-conflict-key-content");
        let file = root.join("README.md");
        std::fs::write(&file, "aaaa\n").expect("write first content");

        let first = super::local_markdown_conflict_detection_key("local-md:README.md", &file)
            .expect("first conflict key");

        std::fs::write(&file, "bbbb\n").expect("write second content");

        let second = super::local_markdown_conflict_detection_key("local-md:README.md", &file)
            .expect("second conflict key");

        assert_ne!(first, second);

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_aggregate_exposes_file_version_alias() {
        let root = temp_root("mnote-local-file-version-aggregate");
        std::fs::write(root.join("README.md"), "# Versioned\n").expect("write md");
        let root_uri = format!("file://{}", root.display());

        let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:README.md")
            .expect("aggregate");
        let body = serde_json::to_value(&aggregate.body).expect("body json");

        assert_eq!(body["fileVersion"], body["conflictDetectionKey"]);
        assert!(body["fileVersion"]
            .as_str()
            .expect("file version")
            .starts_with("local-md:local-md:README.md:"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_returns_file_version_alias() {
        let root = temp_root("mnote-local-file-version-save");
        std::fs::write(root.join("README.md"), "# Old\n").expect("write md");
        let root_uri = format!("file://{}", root.display());

        let result = save_local_markdown_page(
            &root_uri,
            "local-md:README.md",
            None,
            &serde_json::json!([
                {"type":"heading","props":{"level":1},"content":[{"type":"text","text":"New"}]}
            ]),
        )
        .expect("save");

        assert_eq!(result["fileVersion"], result["conflict_detection_key"]);
        assert!(result["fileVersion"]
            .as_str()
            .expect("file version")
            .starts_with("local-md:local-md:README.md:"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_write_contract_returns_page_body_write_command() {
        let root = temp_root("mnote-local-page-body-write-contract");
        std::fs::write(root.join("README.md"), "---\ntitle: Contract\n---\n# Old\n")
            .expect("write md");
        let root_uri = format!("file://{}", root.display());
        let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:README.md")
            .expect("aggregate");
        let request = core_protocol::PageBodyWriteRequest {
            document_id: "local-md:README.md".into(),
            workspace_id: "local-ws:test".into(),
            source_kind: core_protocol::WorkspaceSourceKind::LocalFolder,
            root_uri: root_uri.clone(),
            expected_file_version: aggregate.body.file_version.as_str().map(ToOwned::to_owned),
            base_content_hash: Some("sha256:test-base".into()),
            content_format: "editorBlocks".into(),
            content: serde_json::json!([
                {"type":"heading","props":{"level":1},"content":[{"type":"text","text":"Written"}]}
            ]),
            editor_source: Some("unit-test".into()),
        };

        let result = write_local_markdown_page_body(&request, None).expect("write");

        assert_eq!(result["canonicalCommand"], "page.body.write");
        assert_eq!(result["compatCommand"], "page.body.save");
        assert_eq!(result["contentFormat"], "editorBlocks");
        assert_eq!(result["editorSource"], "unit-test");
        assert_eq!(result["fileVersion"], result["conflict_detection_key"]);
        let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
        assert!(saved.contains("# Written"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_parser_covers_basic_blocks_and_attachment_refs() {
        let blocks = crate::routes::local_markdown_parser::markdown_to_blocks(
            r#"# Title

paragraph

## Section
- bullet
1. numbered
> quote
```rust
fn main() {}
```
---
[Spec](attachments/spec.pdf)
| unsupported | table |
"#,
        );
        let block_types = blocks
            .as_array()
            .expect("blocks")
            .iter()
            .map(|block| block["type"].as_str().unwrap_or_default().to_string())
            .collect::<Vec<_>>();
        assert!(block_types.contains(&"heading".to_string()));
        assert!(block_types.contains(&"paragraph".to_string()));
        assert!(block_types.contains(&"bulletListItem".to_string()));
        assert!(block_types.contains(&"numberedListItem".to_string()));
        assert!(block_types.contains(&"quote".to_string()));
        assert!(block_types.contains(&"codeBlock".to_string()));
        assert!(block_types.contains(&"divider".to_string()));
        assert!(block_types.contains(&"media".to_string()));
        assert!(blocks.to_string().contains("attachments/spec.pdf"));
        assert!(blocks.to_string().contains("unsupported"));
    }

    #[test]
    fn local_markdown_parser_preserves_attachment_media_block() {
        let blocks = crate::routes::local_markdown_parser::markdown_to_blocks(
            "[Spec](attachments/spec.pdf)\n",
        );
        let array = blocks.as_array().expect("blocks");
        let media = array
            .iter()
            .find(|block| block["type"].as_str() == Some("media"))
            .expect("media block");
        assert_eq!(media["props"]["name"], "Spec");
        assert_eq!(media["props"]["sourcePath"], "attachments/spec.pdf");
    }

    #[test]
    fn local_markdown_page_aggregate_preserves_uploaded_markdown_assets_as_media_blocks() {
        let root = temp_root("mnote-local-uploaded-md-assets-media");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create page dir");
        std::fs::write(root.join("Page").join("Page.md"), "# Page\n").expect("write page");
        let root_uri = format!("file://{}", root.display());
        let document_id = "local-md:Page~2FPage.md";

        let first = write_local_markdown_asset(
            &root_uri,
            document_id,
            "attachment",
            LocalUploadFile {
                name: "notes.md".to_string(),
                content_type: "text/markdown".to_string(),
                bytes: b"# Notes one\n".to_vec(),
            },
        )
        .expect("upload first md asset");
        let second = write_local_markdown_asset(
            &root_uri,
            document_id,
            "attachment",
            LocalUploadFile {
                name: "notes.md".to_string(),
                content_type: "text/markdown".to_string(),
                bytes: b"# Notes two\n".to_vec(),
            },
        )
        .expect("upload second md asset");
        assert_eq!(first["sourcePath"], "notes.md");
        assert_eq!(second["sourcePath"], "notes-1.md");

        save_local_markdown_page(
            &root_uri,
            document_id,
            None,
            &json!([
                {"type":"heading","props":{"level":1},"content":[{"type":"text","text":"Page"}]},
                {"type":"media","props":{"name":"notes.md","sourcePath":"notes.md"}},
                {"type":"media","props":{"name":"notes-1.md","sourcePath":"notes-1.md"}}
            ]),
        )
        .expect("save uploaded md links");

        let aggregate =
            resolve_local_markdown_page_aggregate(&root_uri, document_id).expect("aggregate");
        let media_paths = aggregate
            .body
            .content
            .as_array()
            .expect("blocks")
            .iter()
            .filter(|block| block["type"].as_str() == Some("media"))
            .map(|block| {
                block["props"]["sourcePath"]
                    .as_str()
                    .unwrap_or_default()
                    .to_string()
            })
            .collect::<Vec<_>>();
        assert_eq!(media_paths, vec!["notes.md", "notes-1.md"]);

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_rewrites_uploaded_markdown_inline_link_as_media_path() {
        let root = temp_root("mnote-local-uploaded-md-inline-link");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create page dir");
        std::fs::write(root.join("Page").join("Page.md"), "# Page\n").expect("write page");
        let root_uri = format!("file://{}", root.display());
        let document_id = "local-md:Page~2FPage.md";
        let asset = write_local_markdown_asset(
            &root_uri,
            document_id,
            "attachment",
            LocalUploadFile {
                name: "notes.md".to_string(),
                content_type: "text/markdown".to_string(),
                bytes: b"# Notes\n".to_vec(),
            },
        )
        .expect("upload md asset");
        let href = format!(
            "http://127.0.0.1:3000/api/local-folder/files/open?rootUri={}&path=Page%2Fnotes.md",
            root_uri
        );

        save_local_markdown_page(
            &root_uri,
            document_id,
            None,
            &json!([
                {
                    "type": "paragraph",
                    "content": [{
                        "type": "text",
                        "text": "notes.md",
                        "marks": [{
                            "type": "link",
                            "attrs": {
                                "href": href,
                                "class": "mnote-uploaded-attachment"
                            }
                        }]
                    }]
                }
            ]),
        )
        .expect("save inline attachment link");

        assert_eq!(asset["sourcePath"], "notes.md");
        let saved = std::fs::read_to_string(root.join("Page").join("Page.md")).expect("read md");
        assert!(saved.contains("[notes.md](notes.md)"));
        assert!(!saved.contains("/api/local-folder/files/open"));

        let aggregate =
            resolve_local_markdown_page_aggregate(&root_uri, document_id).expect("aggregate");
        let media = aggregate
            .body
            .content
            .as_array()
            .expect("blocks")
            .iter()
            .find(|block| block["type"].as_str() == Some("media"))
            .expect("uploaded md inline link should reload as media block");
        assert_eq!(media["props"]["sourcePath"], "notes.md");

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_rewrites_uploaded_markdown_relative_open_url_as_media_path() {
        let root = temp_root("mnote-local-uploaded-md-relative-open-link");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create page dir");
        std::fs::write(root.join("Page").join("Page.md"), "# Page\n").expect("write page");
        let root_uri = format!("file://{}", root.display());
        let document_id = "local-md:Page~2FPage.md";
        write_local_markdown_asset(
            &root_uri,
            document_id,
            "attachment",
            LocalUploadFile {
                name: "notes.md".to_string(),
                content_type: "text/markdown".to_string(),
                bytes: b"# Notes\n".to_vec(),
            },
        )
        .expect("upload md asset");
        let href = format!(
            "/api/local-folder/files/open?rootUri={}&path=Page%2Fnotes.md",
            root_uri
        );

        save_local_markdown_page(
            &root_uri,
            document_id,
            None,
            &json!([
                {
                    "type": "paragraph",
                    "content": [{
                        "type": "text",
                        "text": "notes.md",
                        "marks": [{
                            "type": "link",
                            "attrs": {
                                "href": href,
                                "class": "mnote-uploaded-attachment-row"
                            }
                        }]
                    }]
                }
            ]),
        )
        .expect("save relative open url");

        let saved = std::fs::read_to_string(root.join("Page").join("Page.md")).expect("read md");
        assert!(saved.contains("[notes.md](notes.md)"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_preserves_frontmatter_and_writes_basic_blocks() {
        let root = temp_root("mnote-local-markdown-save-basic-blocks");
        std::fs::write(
            root.join("README.md"),
            "---\ntitle: Preserved\nmnote_id: stable\n---\n# Old\n",
        )
        .expect("write md");
        let root_uri = format!("file://{}", root.display());
        save_local_markdown_page(
            &root_uri,
            "local-md:README.md",
            None,
            &serde_json::json!([
                {"type":"heading","props":{"level":2},"content":[{"type":"text","text":"Heading"}]},
                {"type":"bulletListItem","content":[{"type":"text","text":"Bullet"}]},
                {"type":"numberedListItem","content":[{"type":"text","text":"Numbered"}]},
                {"type":"quote","content":[{"type":"text","text":"Quoted"}]},
                {"type":"codeBlock","content":[{"type":"text","text":"let x = 1;"}]},
                {"type":"divider"},
                {"type":"media","props":{"name":"Spec","sourcePath":"attachments/spec.pdf"}},
                {"type":"table","props":{"tiptapTable":{"type":"table","content":[
                    {"type":"tableRow","content":[
                        {"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"左","styles":{}}]}]},
                        {"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"右","styles":{}}]}]}
                    ]},
                    {"type":"tableRow","content":[
                        {"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"A","styles":{}}]}]},
                        {"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"B","styles":{}}]}]}
                    ]}
                ]}}}
            ]),
        )
        .expect("save");

        let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
        assert!(saved.starts_with("---\ntitle: Preserved\nmnote_id: stable\n---\n"));
        assert!(saved.contains("## Heading"));
        assert!(saved.contains("- Bullet"));
        assert!(saved.contains("1. Numbered"));
        assert!(saved.contains("> Quoted"));
        assert!(saved.contains("```\nlet x = 1;\n```"));
        assert!(saved.contains("[Spec](attachments/spec.pdf)"));
        assert!(saved.contains("| 左 | 右 |"));
        assert!(saved.contains("| --- | --- |"));
        assert!(saved.contains("| A | B |"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_writes_task_list_and_inline_marks() {
        let root = temp_root("mnote-local-markdown-save-inline-marks");
        std::fs::write(root.join("README.md"), "---\ntitle: Inline\n---\n# Old\n")
            .expect("write md");
        let root_uri = format!("file://{}", root.display());
        save_local_markdown_page(
            &root_uri,
            "local-md:README.md",
            None,
            &serde_json::json!([
                {
                    "type":"todo",
                    "props":{"checked":true},
                    "content":[
                        {"type":"text","text":"3000","styles":{"code":true}},
                        {"type":"text","text":" 默认由 "},
                        {"type":"text","text":"mnote-web","styles":{"bold":true}},
                        {"type":"text","text":" 监听"}
                    ]
                },
                {
                    "type":"todo",
                    "props":{"checked":false},
                    "content":[
                        {"type":"text","text":"待办","styles":{"italic":true}},
                        {"type":"text","text":" "},
                        {"type":"text","text":"删除","styles":{"strike":true}},
                        {"type":"text","text":" "},
                        {"type":"text","text":"链接","styles":{"link":"https://example.com"}}
                    ]
                }
            ]),
        )
        .expect("save");

        let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
        assert!(saved.contains("- [x] `3000` 默认由 **mnote-web** 监听"));
        assert!(saved.contains("- [ ] *待办* ~~删除~~ [链接](https://example.com)"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_rejects_stale_external_file_change() {
        let root = temp_root("mnote-local-markdown-save-stale-conflict");
        std::fs::write(root.join("README.md"), "---\ntitle: Stale\n---\n# Old\n")
            .expect("write md");
        let root_uri = format!("file://{}", root.display());
        let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:README.md")
            .expect("aggregate");
        let stale_key = aggregate
            .body
            .conflict_detection_key
            .as_str()
            .expect("conflict key")
            .to_string();

        std::thread::sleep(std::time::Duration::from_millis(5));
        std::fs::write(
            root.join("README.md"),
            "---\ntitle: Stale\n---\n# External\n",
        )
        .expect("external write");

        let error = save_local_markdown_page(
            &root_uri,
            "local-md:README.md",
            Some(&stale_key),
            &serde_json::json!([
                {"type":"heading","props":{"level":1},"content":[{"type":"text","text":"Editor"}]}
            ]),
        )
        .expect_err("stale save should fail");
        assert!(error.message().contains("本地 Markdown 文件已被外部修改"));

        let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
        assert!(saved.contains("# External"));
        assert!(!saved.contains("# Editor"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_fails_with_error_code_on_readonly_file() {
        let root = temp_root("mnote-local-markdown-save-readonly-error");
        std::fs::write(
            root.join("README.md"),
            "---\ntitle: Error Test\n---\n# Original\n",
        )
        .expect("write md");
        let root_uri = format!("file://{}", root.display());

        // 把文件设为只读，使 fs::write 失败。
        let mut perms = std::fs::metadata(root.join("README.md"))
            .expect("metadata")
            .permissions();
        perms.set_readonly(true);
        std::fs::set_permissions(root.join("README.md"), perms).expect("set readonly");

        let error = save_local_markdown_page(
            &root_uri,
            "local-md:README.md",
            None,
            &serde_json::json!([
                {"type":"paragraph","content":[{"type":"text","text":"新内容","styles":{}}]}
            ]),
        )
        .expect_err("save 应在只读文件上失败");
        assert_eq!(
            error.code(),
            "local_markdown_write_failed",
            "错误码应为 local_markdown_write_failed，实际: {}",
            error.code()
        );
        assert!(
            error.message().contains("无法保存本地 Markdown 文件"),
            "错误信息应包含中文解释和文件路径，实际: {}",
            error.message()
        );

        // 验证磁盘上的原始内容未被清除。
        let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
        assert!(
            saved.contains("# Original"),
            "原文应保留，实际内容: {}",
            saved
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_writes_table_inline_marks() {
        let root = temp_root("mnote-local-markdown-save-table-inline-marks");
        std::fs::write(root.join("README.md"), "---\ntitle: Table\n---\n# Old\n")
            .expect("write md");
        let root_uri = format!("file://{}", root.display());
        save_local_markdown_page(
            &root_uri,
            "local-md:README.md",
            None,
            &serde_json::json!([
                {"type":"table","props":{"tiptapTable":{"type":"table","content":[
                    {"type":"tableRow","content":[
                        {"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"左","styles":{}}]}]},
                        {"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"右","styles":{}}]}]}
                    ]},
                    {"type":"tableRow","content":[
                        {"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"A","styles":{"code":true}}]}]},
                        {"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"B","styles":{"bold":true}}]}]}
                    ]}
                ]}}}
            ]),
        )
        .expect("save");

        let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
        assert!(saved.contains("| `A` | **B** |"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_round_trips_tiptap_table_marks() {
        let root = temp_root("mnote-local-markdown-save-table-marks-roundtrip");
        std::fs::write(
            root.join("README.md"),
            "---\ntitle: Table Marks\n---\n# Old\n",
        )
        .expect("write md");
        let root_uri = format!("file://{}", root.display());
        let parsed = crate::routes::local_markdown_parser::markdown_to_blocks(
            "| 左 | 右 |\n| --- | --- |\n| `A` | **B** |\n",
        );
        save_local_markdown_page(&root_uri, "local-md:README.md", None, &parsed).expect("save");

        let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
        assert!(saved.contains("| `A` | **B** |"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_writes_table_alignment_markers() {
        let root = temp_root("mnote-local-markdown-save-table-alignments");
        std::fs::write(
            root.join("README.md"),
            "---\ntitle: Table Align\n---\n# Old\n",
        )
        .expect("write md");
        let root_uri = format!("file://{}", root.display());
        save_local_markdown_page(
            &root_uri,
            "local-md:README.md",
            None,
            &serde_json::json!([
                {"type":"table","props":{"tiptapTable":{"type":"table","content":[
                    {"type":"tableRow","content":[
                        {"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"textAlign":"left"},"content":[{"type":"paragraph","content":[{"type":"text","text":"左","styles":{}}]}]},
                        {"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"textAlign":"center"},"content":[{"type":"paragraph","content":[{"type":"text","text":"中","styles":{}}]}]},
                        {"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"textAlign":"right"},"content":[{"type":"paragraph","content":[{"type":"text","text":"右","styles":{}}]}]}
                    ]},
                    {"type":"tableRow","content":[
                        {"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"textAlign":"left"},"content":[{"type":"paragraph","content":[{"type":"text","text":"A","styles":{}}]}]},
                        {"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"textAlign":"center"},"content":[{"type":"paragraph","content":[{"type":"text","text":"B","styles":{}}]}]},
                        {"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"textAlign":"right"},"content":[{"type":"paragraph","content":[{"type":"text","text":"C","styles":{}}]}]}
                    ]}
                ]}}}
            ]),
        )
        .expect("save");

        let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
        assert!(saved.contains("| :--- | :---: | ---: |"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_table_handles_colspan_degradation() {
        // Tiptap table cell 可能包含 colspan/rowspan 等 GFM pipe table
        // 无法表达的结构。降级策略：提取文本内容，忽略合并单元格属性，
        // 输出标准 pipe table，不丢文字。
        let root = temp_root("mnote-local-markdown-save-table-colspan");
        std::fs::write(root.join("README.md"), "---\ntitle: Colspan\n---\n# Old\n")
            .expect("write md");
        let root_uri = format!("file://{}", root.display());

        // 第二行第二个单元格有 colspan=2，GFM pipe table 不支持，应降级为普通单元格。
        save_local_markdown_page(
            &root_uri,
            "local-md:README.md",
            None,
            &serde_json::json!([
                {"type":"table","props":{"tiptapTable":{"type":"table","content":[
                    {"type":"tableRow","content":[
                        {"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"A","styles":{}}]}]},
                        {"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"B","styles":{}}]}]},
                        {"type":"tableHeader","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"C","styles":{}}]}]}
                    ]},
                    {"type":"tableRow","content":[
                        {"type":"tableCell","attrs":{"colspan":1,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"x","styles":{}}]}]},
                        {"type":"tableCell","attrs":{"colspan":2,"rowspan":1,"colwidth":null},"content":[{"type":"paragraph","content":[{"type":"text","text":"y+z","styles":{}}]}]}
                    ]}
                ]}}}
            ]),
        )
        .expect("save");

        let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
        // 降级后 colspan=2 的单元格变成普通单列，GFM 表头行 3 列但数据行 2 列 → 右列留空。
        // 重点：文字不丢。
        assert!(
            saved.contains("y+z"),
            "colspan 单元格文字不应丢失: {}",
            saved
        );
        // 不会 panic，表格行数对齐由 editor_block_table_to_markdown 的 padding 逻辑保证。

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_writes_image_blocks_as_markdown_images() {
        let root = temp_root("mnote-local-markdown-save-image-block");
        std::fs::write(root.join("README.md"), "---\ntitle: Image\n---\n# Old\n")
            .expect("write md");
        let root_uri = format!("file://{}", root.display());

        save_local_markdown_page(
            &root_uri,
            "local-md:README.md",
            None,
            &serde_json::json!([
                {
                    "type": "image",
                    "props": {
                        "src": "README.assets/photo.png",
                        "alt": "示例图片",
                        "title": "示例图片"
                    }
                }
            ]),
        )
        .expect("save");

        let saved = std::fs::read_to_string(root.join("README.md")).expect("read md");
        assert!(saved.contains("![示例图片](README.assets/photo.png)"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_rewrites_local_file_open_url_to_markdown_relative_path() {
        let root = temp_root("mnote-local-markdown-save-image-open-url");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create page dir");
        std::fs::write(root.join("Page").join("Page.md"), "").expect("write md");
        std::fs::write(root.join("Page").join("屏幕截图 2025.png"), b"jpg").expect("write image");
        let root_uri = format!("file://{}", root.display());
        let image_url = format!(
            "http://localhost:3000/api/local-folder/files/open?rootUri={}&path=Page%2F%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE%202025.png",
            root_uri
        );

        save_local_markdown_page(
            &root_uri,
            "local-md:Page~2FPage.md",
            None,
            &serde_json::json!([
                {
                    "type": "image",
                    "props": {
                        "src": image_url,
                        "alt": "示例图片"
                    }
                }
            ]),
        )
        .expect("save");

        let saved = std::fs::read_to_string(root.join("Page").join("Page.md")).expect("read md");
        assert!(saved.contains("![示例图片](<屏幕截图 2025.png>)"));
        assert!(!saved.contains("/api/local-folder/files/open"));
        let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:Page~2FPage.md")
            .expect("aggregate");
        let image = aggregate.body.content.as_array().expect("blocks")[0].clone();
        assert_eq!(image["type"], "image");
        assert_eq!(image["props"]["src"], "屏幕截图 2025.png");

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_mindmap_blocks_roundtrip_next_to_page() {
        let root = temp_root("mnote-local-markdown-mindmap-roundtrip");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create page dir");
        std::fs::write(root.join("Page").join("Page.md"), "").expect("write md");
        let root_uri = format!("file://{}", root.display());

        let write_result = write_local_mindmap_data(
            &root_uri,
            "local-md:Page~2FPage.md",
            "思维导图123456.json",
            serde_json::json!({"data":{"text":"中心主题"},"children":[]}),
            false,
        )
        .expect("write mindmap");
        assert_eq!(write_result["relativePath"], "Page/思维导图123456.json");
        assert!(root.join("Page").join("思维导图123456.json").is_file());

        save_local_markdown_page(
            &root_uri,
            "local-md:Page~2FPage.md",
            None,
            &serde_json::json!([
                {
                    "type": "mindmap",
                    "props": {
                        "name": "主页思维导图",
                        "mindmapId": "思维导图123456.json",
                        "rootNodeId": "root"
                    }
                }
            ]),
        )
        .expect("save");

        let saved = std::fs::read_to_string(root.join("Page").join("Page.md")).expect("read md");
        assert!(saved.contains("[主页思维导图](思维导图123456.json)"));

        let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:Page~2FPage.md")
            .expect("aggregate");
        let mindmap = aggregate.body.content.as_array().expect("blocks")[0].clone();
        assert_eq!(mindmap["type"], "mindmap");
        assert_eq!(mindmap["props"]["mindmapId"], "思维导图123456.json");
        assert_eq!(mindmap["props"]["sourcePath"], "思维导图123456.json");

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_workspace_access_rejects_owner_mismatch() {
        let root = temp_root("mnote-local-workspace-owner-mismatch");
        std::fs::create_dir_all(root.join(".mnote")).expect("create metadata dir");
        std::fs::write(
            root.join(".mnote").join("workspace.json"),
            r#"{"workspaceId":"local-ws-demo","ownerId":"user_a","createdAt":"2026-05-18T00:00:00Z","capabilities":["local_files"]}"#,
        )
        .expect("write workspace manifest");
        let root_uri = format!("file://{}", root.display());

        let error = ensure_local_workspace_access_for_actor("user_b", "user", &root_uri)
            .expect_err("owner mismatch must be rejected");

        assert_eq!(error.status(), axum::http::StatusCode::FORBIDDEN);
        assert!(error.message().contains("无权"));
        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_workspace_access_allows_admin_without_manifest() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-local-workspace-admin-any-root");
        let root_uri = format!("file://{}", root.display());
        std::env::set_var("MNOTE_ADMIN_USER_IDS", "user_admin");

        let allowed = ensure_local_workspace_access_for_actor("user_admin", "user", &root_uri)
            .expect("admin can write any local directory");

        assert_eq!(allowed, root.canonicalize().expect("canonical root"));
        std::env::remove_var("MNOTE_ADMIN_USER_IDS");
        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_workspace_access_policy_grants_read_and_write_separately() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-local-workspace-policy-root");
        let policy_root = temp_root("mnote-local-workspace-policy-config");
        let policy_file = policy_root.join("access-policy.json");
        let root_uri = format!("file://{}", root.display());
        std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
        std::fs::write(
            &policy_file,
            serde_json::json!({
                "grants": [
                    {
                        "userId": "user_reader",
                        "rootUri": root_uri,
                        "permission": "read",
                        "recursive": true
                    }
                ]
            })
            .to_string(),
        )
        .expect("write policy");

        ensure_local_workspace_read_access_for_actor("user_reader", "user", &root_uri)
            .expect("read grant can read local directory");
        let write_error = ensure_local_workspace_access_for_actor("user_reader", "user", &root_uri)
            .expect_err("read grant must not write");
        assert_eq!(write_error.status(), axum::http::StatusCode::FORBIDDEN);

        std::fs::write(
            &policy_file,
            serde_json::json!({
                "grants": [
                    {
                        "userId": "user_writer",
                        "rootUri": root_uri,
                        "permission": "write",
                        "recursive": true
                    }
                ]
            })
            .to_string(),
        )
        .expect("write policy");
        ensure_local_workspace_access_for_actor("user_writer", "user", &root_uri)
            .expect("write grant can write local directory");

        std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
        let _ = std::fs::remove_dir_all(&root);
        let _ = std::fs::remove_dir_all(&policy_root);
    }

    #[test]
    fn local_path_read_access_rejects_root_escape() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-local-path-read-root");
        let outside = temp_root("mnote-local-path-read-outside");
        init_workspace(&root);
        std::fs::write(root.join("inside.md"), "inside").expect("write inside");
        std::fs::write(outside.join("outside.md"), "outside").expect("write outside");
        let context = request_context("user_test", "user");
        let root_uri = format!("file://{}", root.display());

        let inside = ensure_local_path_read_access(&context, &root_uri, "inside.md")
            .expect("relative file inside root can be read");
        assert_eq!(
            inside,
            root.join("inside.md").canonicalize().expect("inside")
        );

        let escape_error = ensure_local_path_read_access(
            &context,
            &root_uri,
            &outside.join("outside.md").display().to_string(),
        )
        .expect_err("absolute path outside root must be rejected");
        assert_eq!(escape_error.status(), StatusCode::BAD_REQUEST);

        let _ = std::fs::remove_dir_all(&root);
        let _ = std::fs::remove_dir_all(&outside);
    }

    #[tokio::test]
    async fn local_access_policy_admin_api_rejects_non_admin_and_validates_root() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-local-access-policy-validate-root");
        let policy_root = temp_root("mnote-local-access-policy-validate-config");
        let policy_file = policy_root.join("access-policy.json");
        std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);

        let non_admin_error = get_local_access_policy(
            State(test_state()),
            Extension(request_context("user_1", "user")),
        )
        .await
        .expect_err("non-admin cannot read access policy");
        assert_eq!(non_admin_error.status(), StatusCode::FORBIDDEN);

        std::env::set_var("MNOTE_ADMIN_USER_IDS", "admin_1");
        let (_, Json(payload)) = validate_local_access_root(
            Extension(request_context("admin_1", "user")),
            Json(LocalAccessValidateRootRequest {
                root_uri: format!("file://{}", root.display()),
                root_path: String::new(),
            }),
        )
        .await
        .expect("admin can validate root");
        assert_eq!(payload["ok"], true);
        assert_eq!(
            payload["rootPath"],
            root.canonicalize()
                .expect("canonical root")
                .display()
                .to_string()
        );

        let invalid_error = validate_local_access_root(
            Extension(request_context("admin_1", "user")),
            Json(LocalAccessValidateRootRequest {
                root_uri: format!("file://{}", root.join("missing").display()),
                root_path: String::new(),
            }),
        )
        .await
        .expect_err("missing root should be rejected");
        assert_eq!(invalid_error.status(), StatusCode::BAD_REQUEST);

        std::env::remove_var("MNOTE_ADMIN_USER_IDS");
        std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
        let _ = std::fs::remove_dir_all(&root);
        let _ = std::fs::remove_dir_all(&policy_root);
    }

    #[tokio::test]
    async fn local_access_policy_admin_api_creates_and_deletes_grant() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-local-access-policy-grant-root");
        let policy_root = temp_root("mnote-local-access-policy-grant-config");
        let policy_file = policy_root.join("access-policy.json");
        let root_uri = format!("file://{}", root.display());
        let state = test_state();
        state
            .control_plane()
            .upsert_user(UpsertUserInput {
                id: Some("admin_1".into()),
                email: Some("admin-1@example.com".into()),
                username: "admin_1".into(),
                display_name: "admin_1".into(),
                role: Some("admin".into()),
                password_hash: None,
            })
            .expect("upsert admin");
        state
            .control_plane()
            .upsert_user(UpsertUserInput {
                id: Some("user_reader".into()),
                email: Some("user-reader@example.com".into()),
                username: "user_reader".into(),
                display_name: "user_reader".into(),
                role: None,
                password_hash: None,
            })
            .expect("upsert reader");
        std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
        std::env::set_var("MNOTE_ADMIN_USER_IDS", "admin_1");
        let mut control_delta_rx = state.stream_delta_tx.subscribe();

        let (_, Json(created)) = create_local_access_grant(
            State(state.clone()),
            Extension(request_context("admin_1", "user")),
            Json(LocalAccessGrantRequest {
                id: "grant_reader".into(),
                user_id: "user_reader".into(),
                root_uri: root_uri.clone(),
                root_path: String::new(),
                permission: "read".into(),
                recursive: true,
                capabilities: vec!["ai".into(), "share".into()],
            }),
        )
        .await
        .expect("admin can create read grant");
        let created_grant_id = created["grant"]["id"]
            .as_str()
            .expect("created grant id")
            .to_string();
        assert_eq!(created["grant"]["permission"], "read");
        assert_eq!(created["controlPlane"], "sqlite");
        let created_events = state
            .control_plane()
            .drain_outbox(10)
            .expect("drain created event");
        assert_eq!(created_events.len(), 1);
        assert_eq!(created_events[0].event_type, "control.grant.created");
        let created_delta = control_delta_rx
            .try_recv()
            .expect("grant created broadcast delta");
        assert_eq!(created_delta["kind"], "control_plane_event");
        assert_eq!(created_delta["eventType"], "control.grant.created");
        ensure_local_workspace_read_access_with_state(
            &state,
            &request_context("user_reader", "user"),
            &root_uri,
        )
        .expect("created read grant can read");
        let write_error = ensure_local_workspace_write_access_with_state(
            &state,
            &request_context("user_reader", "user"),
            &root_uri,
        )
        .expect_err("created read grant cannot write");
        assert_eq!(write_error.status(), StatusCode::FORBIDDEN);

        let duplicate_error = add_sqlite_local_access_grant_for_context(
            &state,
            &request_context("admin_1", "user"),
            LocalAccessGrantRequest {
                id: "grant_reader".into(),
                user_id: "user_reader".into(),
                root_uri: root_uri.clone(),
                root_path: String::new(),
                permission: "read".into(),
                recursive: true,
                capabilities: vec!["ai".into(), "share".into()],
            },
        )
        .expect_err("duplicate grant id should be rejected");
        assert_eq!(duplicate_error.status(), StatusCode::BAD_REQUEST);

        let (_, Json(deleted)) = super::delete_local_access_grant(
            State(state.clone()),
            Extension(request_context("admin_1", "user")),
            AxumPath(created_grant_id.clone()),
        )
        .await
        .expect("admin can delete grant");
        assert_eq!(deleted["deletedGrantId"], created_grant_id);
        let revoked_events = state
            .control_plane()
            .drain_outbox(10)
            .expect("drain revoked event");
        assert_eq!(revoked_events.len(), 1);
        assert_eq!(revoked_events[0].event_type, "control.grant.revoked");
        let revoked_delta = control_delta_rx
            .try_recv()
            .expect("grant revoked broadcast delta");
        assert_eq!(revoked_delta["kind"], "control_plane_event");
        assert_eq!(revoked_delta["eventType"], "control.grant.revoked");
        let read_error = ensure_local_workspace_read_access_with_state(
            &state,
            &request_context("user_reader", "user"),
            &root_uri,
        )
        .expect_err("deleted grant should remove read access");
        assert_eq!(read_error.status(), StatusCode::FORBIDDEN);

        std::env::remove_var("MNOTE_ADMIN_USER_IDS");
        std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
        let _ = std::fs::remove_dir_all(&root);
        let _ = std::fs::remove_dir_all(&policy_root);
    }

    #[tokio::test]
    async fn share_grant_admin_api_creates_reads_and_revokes_without_local_access() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-share-grant-workspace-root");
        let config_root = temp_root("mnote-share-grant-config-root");
        let access_policy_file = config_root.join("access-policy.json");
        let share_grants_file = config_root.join("share-grants.json");
        let root_uri = format!("file://{}", root.display());
        std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &access_policy_file);
        std::env::set_var("MNOTE_SHARE_GRANTS_FILE", &share_grants_file);
        std::env::set_var("MNOTE_ADMIN_USER_IDS", "admin_1");

        let non_admin_error = get_share_grants(Extension(request_context("user_1", "user")))
            .await
            .expect_err("non-admin cannot read share grants");
        assert_eq!(non_admin_error.status(), StatusCode::FORBIDDEN);

        let (_, Json(created)) = create_share_grant(
            Extension(request_context("admin_1", "user")),
            Json(LocalShareGrantRequest {
                id: "share_grant_1".into(),
                share_id: "share_1".into(),
                owner_user_id: "owner_1".into(),
                target_user_id: "target_1".into(),
                root_uri: root_uri.clone(),
                root_path: String::new(),
                document_id: "doc_1".into(),
                allowed_resource_ids: vec!["doc_1".into()],
                permission: "read".into(),
                capabilities: vec!["ai".into(), "share".into()],
            }),
        )
        .await
        .expect("admin can create share grant");
        assert_eq!(created["grant"]["id"], "share_grant_1");
        assert_eq!(created["grant"]["shareId"], "share_1");
        assert_eq!(created["grant"]["active"], true);

        let read_error =
            ensure_local_workspace_read_access_for_actor("target_1", "user", &root_uri)
                .expect_err("share grant must not grant local filesystem read access");
        assert_eq!(read_error.status(), StatusCode::FORBIDDEN);
        let write_error = ensure_local_workspace_access_for_actor("target_1", "user", &root_uri)
            .expect_err("share grant must not grant local filesystem write access");
        assert_eq!(write_error.status(), StatusCode::FORBIDDEN);

        let (_, Json(listed)) = get_share_grants(Extension(request_context("admin_1", "user")))
            .await
            .expect("admin can list share grants");
        assert_eq!(listed["grants"][0]["id"], "share_grant_1");
        assert_eq!(listed["grants"][0]["active"], true);

        let (_, Json(revoked)) = super::delete_share_grant(
            Extension(request_context("admin_1", "user")),
            AxumPath("share_1".into()),
        )
        .await
        .expect("admin can revoke by share id");
        assert_eq!(revoked["revokedShareId"], "share_1");
        assert_eq!(revoked["grant"]["active"], false);
        assert!(
            revoked["grant"]["revokedAt"]
                .as_str()
                .unwrap_or_default()
                .len()
                > 0
        );

        let stored = std::fs::read_to_string(&share_grants_file).expect("read share grants file");
        let stored_json: Value = serde_json::from_str(&stored).expect("share grants json");
        assert_eq!(stored_json["grants"][0]["active"], false);

        std::env::remove_var("MNOTE_ADMIN_USER_IDS");
        std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
        std::env::remove_var("MNOTE_SHARE_GRANTS_FILE");
        let _ = std::fs::remove_dir_all(&root);
        let _ = std::fs::remove_dir_all(&config_root);
    }

    #[tokio::test]
    async fn user_share_grant_only_allows_owned_workspace_folder() {
        let _guard = env_lock().lock().expect("env lock");
        let owned_root = temp_root("mnote-user-folder-grant-owned-root");
        let outside_root = temp_root("mnote-user-folder-grant-outside-root");
        let config_root = temp_root("mnote-user-folder-grant-config-root");
        let share_grants_file = config_root.join("share-grants.json");
        std::env::set_var("MNOTE_SHARE_GRANTS_FILE", &share_grants_file);
        std::fs::create_dir_all(owned_root.join("docs")).expect("owned docs");
        initialize_local_workspace_for_actor(
            "user_owner",
            &format!("file://{}", owned_root.display()),
        )
        .expect("init owned workspace");

        let (_, Json(created)) = create_user_share_grant(
            Extension(request_context("user_owner", "user")),
            Json(LocalShareGrantRequest {
                target_user_id: "user_target".into(),
                root_path: owned_root.join("docs").display().to_string(),
                permission: "write".into(),
                ..Default::default()
            }),
        )
        .await
        .expect("user can grant owned folder");
        assert_eq!(created["grant"]["ownerUserId"], "user_owner");
        assert_eq!(created["grant"]["targetUserId"], "user_target");
        assert_eq!(created["grant"]["permission"], "write");
        assert!(created["grant"]["shareId"]
            .as_str()
            .unwrap_or_default()
            .starts_with("folder_"));

        let outside_error = create_user_share_grant(
            Extension(request_context("user_owner", "user")),
            Json(LocalShareGrantRequest {
                target_user_id: "user_target_2".into(),
                root_path: outside_root.display().to_string(),
                permission: "read".into(),
                ..Default::default()
            }),
        )
        .await
        .expect_err("user cannot grant outside owned workspace");
        assert_eq!(outside_error.status(), StatusCode::FORBIDDEN);

        std::env::remove_var("MNOTE_SHARE_GRANTS_FILE");
        let _ = std::fs::remove_dir_all(&owned_root);
        let _ = std::fs::remove_dir_all(&outside_root);
        let _ = std::fs::remove_dir_all(&config_root);
    }

    #[tokio::test]
    async fn user_access_policy_grant_uses_sqlite_directory_grants_for_owned_folder() {
        let _guard = env_lock().lock().expect("env lock");
        let owned_root = temp_root("mnote-user-access-policy-owned-root");
        let outside_root = temp_root("mnote-user-access-policy-outside-root");
        std::fs::create_dir_all(owned_root.join("docs")).expect("owned docs");
        initialize_local_workspace_for_actor(
            "user_owner",
            &format!("file://{}", owned_root.display()),
        )
        .expect("init owned workspace");
        let state = test_state();
        for user_id in ["user_owner", "user_target"] {
            state
                .control_plane()
                .upsert_user(UpsertUserInput {
                    id: Some(user_id.into()),
                    email: Some(format!("{user_id}@example.com")),
                    username: user_id.into(),
                    display_name: user_id.into(),
                    role: None,
                    password_hash: None,
                })
                .expect("upsert test user");
        }

        let (_, Json(created)) = create_user_access_grant(
            State(state.clone()),
            Extension(request_context("user_owner", "user")),
            Json(LocalAccessGrantRequest {
                user_id: "user_target".into(),
                root_path: owned_root.join("docs").display().to_string(),
                permission: "write".into(),
                recursive: true,
                ..Default::default()
            }),
        )
        .await
        .expect("user can grant owned folder to target user");
        let grant_id = created["grant"]["id"]
            .as_str()
            .expect("grant id")
            .to_string();
        assert_eq!(created["controlPlane"], "sqlite");
        assert_eq!(created["grant"]["userId"], "user_target");
        assert_eq!(created["grant"]["createdBy"], "user_owner");
        assert_eq!(created["grant"]["permission"], "write");
        assert_eq!(created["grant"]["source"], "user");

        let docs_uri = format!("file://{}", owned_root.join("docs").display());
        ensure_local_workspace_write_access_with_state(
            &state,
            &request_context("user_target", "user"),
            &docs_uri,
        )
        .expect("target user can write granted folder");

        let (_, Json(listed)) = get_user_access_policy(
            State(state.clone()),
            Extension(request_context("user_owner", "user")),
        )
        .await
        .expect("owner can list self-created directory grants");
        assert_eq!(listed["controlPlane"], "sqlite");
        assert_eq!(listed["grants"][0]["id"], grant_id);
        assert_eq!(listed["grants"][0]["permission"], "write");

        let (_, Json(target_listed)) = get_user_access_policy(
            State(state.clone()),
            Extension(request_context("user_target", "user")),
        )
        .await
        .expect("target can list incoming directory grants");
        assert_eq!(target_listed["controlPlane"], "sqlite");
        assert_eq!(target_listed["grants"][0]["id"], grant_id);
        assert_eq!(target_listed["grants"][0]["userId"], "user_target");

        let target_delete_error = delete_user_access_grant(
            State(state.clone()),
            Extension(request_context("user_target", "user")),
            AxumPath(grant_id.clone()),
        )
        .await
        .expect_err("target cannot revoke grant created by owner");
        assert_eq!(target_delete_error.status(), StatusCode::FORBIDDEN);

        let (_, Json(deleted)) = delete_user_access_grant(
            State(state.clone()),
            Extension(request_context("user_owner", "user")),
            AxumPath(grant_id.clone()),
        )
        .await
        .expect("owner can revoke self-created grant");
        assert_eq!(deleted["deletedGrantId"], grant_id);
        let revoked_error = ensure_local_workspace_write_access_with_state(
            &state,
            &request_context("user_target", "user"),
            &docs_uri,
        )
        .expect_err("revoked grant removes target write access");
        assert_eq!(revoked_error.status(), StatusCode::FORBIDDEN);

        let outside_error = create_user_access_grant(
            State(state.clone()),
            Extension(request_context("user_owner", "user")),
            Json(LocalAccessGrantRequest {
                user_id: "user_target".into(),
                root_path: outside_root.display().to_string(),
                permission: "read".into(),
                recursive: true,
                ..Default::default()
            }),
        )
        .await
        .expect_err("user cannot grant folder outside owned workspace");
        assert_eq!(outside_error.status(), StatusCode::FORBIDDEN);

        let _ = std::fs::remove_dir_all(&owned_root);
        let _ = std::fs::remove_dir_all(&outside_root);
    }

    #[tokio::test]
    async fn user_access_policy_cannot_revoke_default_workspace_auto_grant() {
        let _guard = env_lock().lock().expect("env lock");
        let state = test_state();
        state
            .control_plane()
            .upsert_user(UpsertUserInput {
                id: Some("shujuan".into()),
                email: Some("shujuan@example.com".into()),
                username: "shujuan".into(),
                display_name: "shujuan".into(),
                role: None,
                password_hash: None,
            })
            .expect("upsert user");
        state
            .control_plane()
            .ensure_default_workspace("shujuan")
            .expect("default workspace");
        let grant_id = state
            .control_plane()
            .list_directory_grants()
            .expect("list grants")
            .into_iter()
            .find(|grant| grant.user_id == "shujuan" && grant.source == "auto")
            .map(|grant| grant.id)
            .expect("default auto grant");

        let error = delete_user_access_grant(
            State(state.clone()),
            Extension(request_context("shujuan", "user")),
            AxumPath(grant_id),
        )
        .await
        .expect_err("default grant must be readonly");
        assert_eq!(error.status(), StatusCode::FORBIDDEN);
    }

    #[tokio::test]
    async fn admin_share_grant_allows_any_local_folder_with_minimal_fields() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-admin-folder-grant-root");
        let config_root = temp_root("mnote-admin-folder-grant-config-root");
        let share_grants_file = config_root.join("share-grants.json");
        std::env::set_var("MNOTE_SHARE_GRANTS_FILE", &share_grants_file);
        std::env::set_var("MNOTE_ADMIN_USER_IDS", "admin_1");

        let (_, Json(created)) = create_share_grant(
            Extension(request_context("admin_1", "user")),
            Json(LocalShareGrantRequest {
                target_user_id: "user_target".into(),
                root_path: root.display().to_string(),
                permission: "read".into(),
                ..Default::default()
            }),
        )
        .await
        .expect("admin can grant any local folder");
        assert_eq!(created["grant"]["ownerUserId"], "admin_1");
        assert_eq!(created["grant"]["targetUserId"], "user_target");
        assert_eq!(created["grant"]["permission"], "read");

        std::env::remove_var("MNOTE_ADMIN_USER_IDS");
        std::env::remove_var("MNOTE_SHARE_GRANTS_FILE");
        let _ = std::fs::remove_dir_all(&root);
        let _ = std::fs::remove_dir_all(&config_root);
    }

    #[tokio::test]
    async fn share_link_api_creates_lists_and_revokes_sqlite_record() {
        let _guard = env_lock().lock().expect("env lock");
        let state = test_state();
        std::env::set_var("MNOTE_ADMIN_USER_IDS", "admin_1");
        state
            .control_plane()
            .upsert_user(UpsertUserInput {
                id: Some("admin_1".into()),
                email: Some("admin-1@example.com".into()),
                username: "admin_1".into(),
                display_name: "admin_1".into(),
                role: Some("admin".into()),
                password_hash: None,
            })
            .expect("upsert admin");
        let workspace = state
            .control_plane()
            .ensure_default_workspace("admin_1")
            .expect("default workspace");
        let mut control_delta_rx = state.stream_delta_tx.subscribe();

        let (_, Json(created)) = create_share_link(
            State(state.clone()),
            Extension(request_context("admin_1", "user")),
            Json(LocalShareLinkRequest {
                workspace_id: workspace.id.clone(),
                resource_kind: "page".into(),
                resource_id: "local-md:README.md".into(),
                token: Some("visible-token".into()),
                permission: "read".into(),
                expires_at: None,
            }),
        )
        .await
        .expect("admin can create share link");
        let link_id = created["link"]["id"].as_str().expect("link id").to_string();
        assert_eq!(created["controlPlane"], "sqlite");
        assert_eq!(created["token"], "visible-token");
        assert!(created["link"].get("tokenHash").is_none());
        assert_eq!(created["link"]["permission"], "read");
        let created_events = state
            .control_plane()
            .drain_outbox(10)
            .expect("drain created event");
        assert_eq!(created_events.len(), 1);
        assert_eq!(created_events[0].event_type, "control.share.created");
        let created_delta = control_delta_rx
            .try_recv()
            .expect("share created broadcast delta");
        assert_eq!(created_delta["kind"], "control_plane_event");
        assert_eq!(created_delta["eventType"], "control.share.created");

        let stored = state
            .control_plane()
            .list_share_links(&workspace.id)
            .expect("list sqlite share links");
        assert_eq!(stored.len(), 1);
        assert_eq!(stored[0].id, link_id);
        assert_ne!(stored[0].token_hash, "visible-token");
        assert!(state
            .control_plane()
            .resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
            .expect("resolve share link")
            .is_some());

        let (_, Json(listed)) = get_share_links(
            State(state.clone()),
            Extension(request_context("admin_1", "user")),
            Query(ShareLinkListQuery {
                workspace_id: workspace.id.clone(),
            }),
        )
        .await
        .expect("admin can list share links");
        assert_eq!(listed["links"][0]["id"], link_id);
        assert!(listed["links"][0].get("tokenHash").is_none());

        let (_, Json(revoked)) = delete_share_link(
            State(state.clone()),
            Extension(request_context("admin_1", "user")),
            AxumPath(link_id.clone()),
        )
        .await
        .expect("admin can revoke share link");
        assert_eq!(revoked["revokedLinkId"], link_id);
        let revoked_events = state
            .control_plane()
            .drain_outbox(10)
            .expect("drain revoked event");
        assert_eq!(revoked_events.len(), 1);
        assert_eq!(revoked_events[0].event_type, "control.share.revoked");
        let revoked_delta = control_delta_rx
            .try_recv()
            .expect("share revoked broadcast delta");
        assert_eq!(revoked_delta["kind"], "control_plane_event");
        assert_eq!(revoked_delta["eventType"], "control.share.revoked");
        assert!(state
            .control_plane()
            .resolve_share_link(&control_plane::share_token_hash_v1("visible-token"))
            .expect("resolve revoked share link")
            .is_none());
        assert_eq!(
            state
                .control_plane()
                .list_audit_log(10)
                .expect("audit log")
                .iter()
                .filter(|entry| entry.action.starts_with("control.share."))
                .count(),
            2
        );

        std::env::remove_var("MNOTE_ADMIN_USER_IDS");
    }

    #[tokio::test]
    async fn shared_cache_and_sync_state_record_permissions_and_conflict_report() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-shared-cache-sync-root");
        let config_root = temp_root("mnote-shared-cache-sync-config");
        let access_policy_file = config_root.join("access-policy.json");
        let share_grants_file = config_root.join("share-grants.json");
        let root_uri = format!("file://{}", root.display());
        std::fs::create_dir_all(root.join(".mnote")).expect("metadata");
        std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &access_policy_file);
        std::env::set_var("MNOTE_SHARE_GRANTS_FILE", &share_grants_file);
        std::env::set_var("MNOTE_ADMIN_USER_IDS", "admin_1");

        let (read_status, Json(read_share_payload)) = create_share_grant(
            Extension(request_context("admin_1", "user")),
            Json(LocalShareGrantRequest {
                id: "share_grant_sync_read".into(),
                share_id: "share_sync_read".into(),
                owner_user_id: "owner_1".into(),
                target_user_id: "target_1".into(),
                root_uri: root_uri.clone(),
                root_path: String::new(),
                document_id: "local-md:README.md".into(),
                allowed_resource_ids: vec!["local-md:README.md".into()],
                permission: "read".into(),
                capabilities: vec!["ai".into(), "share".into()],
            }),
        )
        .await
        .expect("create read share");
        assert_eq!(read_status, StatusCode::OK);
        assert_eq!(read_share_payload["grant"]["shareId"], "share_sync_read");
        let (write_status, Json(write_share_payload)) = create_share_grant(
            Extension(request_context("admin_1", "user")),
            Json(LocalShareGrantRequest {
                id: "share_grant_sync_write".into(),
                share_id: "share_sync_write".into(),
                owner_user_id: "owner_1".into(),
                target_user_id: "target_1".into(),
                root_uri: root_uri.clone(),
                root_path: String::new(),
                document_id: "local-md:README.md".into(),
                allowed_resource_ids: vec!["local-md:README.md".into()],
                permission: "write".into(),
                capabilities: vec!["ai".into(), "share".into()],
            }),
        )
        .await
        .expect("create write share");
        assert_eq!(write_status, StatusCode::OK);
        assert_eq!(write_share_payload["grant"]["shareId"], "share_sync_write");

        let (_, Json(cache_payload)) = record_shared_cache(
            Extension(request_context("target_1", "user")),
            Json(SharedCacheRecordRequest {
                root_uri: root_uri.clone(),
                share_id: "share_sync_read".into(),
                permission: "read".into(),
                remote_version: "remote-1".into(),
                base_version: "base-1".into(),
                source_actor: "owner_1".into(),
            }),
        )
        .await
        .expect("record cache");
        assert_eq!(cache_payload["share"]["remoteVersion"], "remote-1");
        let cache_json =
            std::fs::read_to_string(root.join(".mnote").join("share-cache.json")).expect("cache");
        assert!(cache_json.contains("share_sync_read"));
        assert!(cache_json.contains("remote-1"));

        let read_pending_error = record_sync_pending_change(
            Extension(request_context("target_1", "user")),
            Json(SyncPendingChangeRequest {
                root_uri: root_uri.clone(),
                share_id: "share_sync_read".into(),
                resource_id: "local-md:README.md".into(),
                local_version: "local-1".into(),
                base_version: "remote-1".into(),
                remote_version: "remote-1".into(),
                change_summary: "read share write attempt".into(),
            }),
        )
        .await
        .expect_err("shared_read cannot write pending changes");
        assert_eq!(read_pending_error.status(), StatusCode::FORBIDDEN);

        let (_, Json(pending_payload)) = record_sync_pending_change(
            Extension(request_context("target_1", "user")),
            Json(SyncPendingChangeRequest {
                root_uri: root_uri.clone(),
                share_id: "share_sync_write".into(),
                resource_id: "local-md:README.md".into(),
                local_version: "local-2".into(),
                base_version: "remote-1".into(),
                remote_version: "remote-1".into(),
                change_summary: "写入共享页面".into(),
            }),
        )
        .await
        .expect("shared_write can record pending change");
        assert_eq!(
            pending_payload["pendingChange"]["resourceId"],
            "local-md:README.md"
        );
        let sync_state =
            std::fs::read_to_string(root.join(".mnote").join("sync-state.json")).expect("sync");
        assert!(sync_state.contains("share_sync_write"));
        assert!(sync_state.contains("写入共享页面"));

        let conflict_error = record_sync_pending_change(
            Extension(request_context("target_1", "user")),
            Json(SyncPendingChangeRequest {
                root_uri: root_uri.clone(),
                share_id: "share_sync_write".into(),
                resource_id: "local-md:README.md".into(),
                local_version: "local-3".into(),
                base_version: "remote-1".into(),
                remote_version: "remote-2".into(),
                change_summary: "冲突写入".into(),
            }),
        )
        .await
        .expect_err("remote changed since base");
        assert_eq!(conflict_error.status(), StatusCode::CONFLICT);

        let (_, Json(report_payload)) = write_sync_conflict_report(
            Extension(request_context("target_1", "user")),
            Json(SyncConflictReportRequest {
                root_uri,
                share_id: "share_sync_write".into(),
                resource_id: "local-md:README.md".into(),
                local_version: "local-3".into(),
                remote_version: "remote-2".into(),
                base_version: "remote-1".into(),
                actor_id: "target_1".into(),
                summary: "同步冲突".into(),
            }),
        )
        .await
        .expect("write conflict report");
        assert_eq!(report_payload["report"]["source"], "shared_sync");
        assert_eq!(report_payload["report"]["shareId"], "share_sync_write");
        assert!(root.join(".mnote").join("sync-reports").is_dir());

        std::env::remove_var("MNOTE_ADMIN_USER_IDS");
        std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
        std::env::remove_var("MNOTE_SHARE_GRANTS_FILE");
        let _ = std::fs::remove_dir_all(&root);
        let _ = std::fs::remove_dir_all(&config_root);
    }

    #[test]
    fn create_default_local_workspace_uses_managed_data_root() {
        let base = temp_root("mnote-local-managed-data-root");
        let payload =
            create_default_local_workspace_for_actor_at_base("user@example.com", "user", &base)
                .expect("create default workspace");
        let root_uri = payload["workspace"]["rootUri"]
            .as_str()
            .expect("root uri")
            .to_string();
        let root_path = payload["workspace"]["rootPath"]
            .as_str()
            .expect("root path");
        let expected_root = base
            .join("users")
            .join("user~40example.com")
            .join("workspaces")
            .join("my-space");

        assert_eq!(std::path::Path::new(root_path), expected_root.as_path());
        assert!(expected_root.join(".mnote").join("workspace.json").exists());
        assert!(
            !expected_root.join("初始化的新页面").exists(),
            "默认工作区不应创建'初始化的新页面'目录"
        );
        assert!(!expected_root.join("pages").exists());
        assert!(!expected_root.join("assets").exists());
        assert!(!expected_root.join("mindmaps").exists());
        assert!(!expected_root.join("ai-sessions").exists());
        assert_eq!(
            payload["workspace"]["manifest"]["workspaceId"],
            "local-ws:user~40example.com:my-space"
        );
        assert_eq!(
            payload["workspace"]["manifest"]["ownerId"],
            "user@example.com"
        );
        assert!(payload["workspace"]["manifest"]["capabilities"]
            .as_array()
            .expect("capabilities")
            .iter()
            .any(|value| value.as_str() == Some("markdown_edit")));
        ensure_local_workspace_access_for_actor("user@example.com", "user", &root_uri)
            .expect("owner can access managed workspace");

        let second =
            create_default_local_workspace_for_actor_at_base("user@example.com", "user", &base)
                .expect("idempotent create");
        assert_eq!(
            second["workspace"]["rootUri"],
            payload["workspace"]["rootUri"]
        );

        let _ = std::fs::remove_dir_all(&base);
    }

    #[test]
    fn local_file_tree_empty_root_does_not_emit_workspace_folder_row() {
        let root = temp_root("mnote-local-empty-root-file-tree");
        init_workspace(&root);
        let root_uri = format!("file://{}", root.display());

        let snapshot = load_local_folder_file_tree_snapshot(&root_uri).expect("load file tree");
        let items = snapshot.projection["items"].as_array().expect("items");

        assert!(
            items.is_empty(),
            "只剩 .mnote 的空 workspace 不应投影出 root 文件夹行: {items:?}"
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_file_tree_snapshot_loads_root_level_only() {
        let root = temp_root("mnote-local-file-tree-root-only");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("docs").join("nested")).expect("create nested dirs");
        std::fs::write(root.join("docs").join("README.md"), "# README\n").expect("write readme");
        std::fs::write(root.join("docs").join("nested").join("deep.md"), "# Deep\n")
            .expect("write deep");
        std::fs::write(root.join("root.md"), "# Root\n").expect("write root");
        let root_uri = format!("file://{}", root.display());

        let snapshot = load_local_folder_file_tree_snapshot(&root_uri).expect("load file tree");
        let items = snapshot.projection["items"].as_array().expect("items");
        let titles = items
            .iter()
            .filter_map(|item| item["title"].as_str())
            .collect::<Vec<_>>();

        assert!(titles.contains(&"docs"));
        assert!(titles.contains(&"root.md"));
        assert!(!titles.contains(&"README.md"));
        assert!(!titles.contains(&"deep.md"));
        let docs = items
            .iter()
            .find(|item| item["title"].as_str() == Some("docs"))
            .expect("docs row");
        assert_eq!(docs["parentNodeId"].as_str(), None);
        assert_eq!(docs["expandable"].as_bool(), Some(true));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_refreshes_search_index_after_rename() {
        let root = temp_root("mnote-local-tree-refresh-search-index");
        init_workspace(&root);
        std::fs::write(
            root.join("search-target.md"),
            "# Search Target\nrename-token\n",
        )
        .expect("write md");
        let root_uri = format!("file://{}", root.display());

        crate::routes::local_search_index::refresh_local_search_index(
            &root,
            &root_uri,
            "local-ws-search-index-test",
        )
        .expect("initial index");
        let before =
            std::fs::read_to_string(root.join(".mnote").join("index").join("search-index.json"))
                .expect("before index");
        assert!(before.contains("search-target.md"));

        execute_local_tree_command(
            &root_uri,
            "rename",
            "local-md:search-target.md",
            None,
            Some("Renamed Target"),
        )
        .expect("rename");

        let after =
            std::fs::read_to_string(root.join(".mnote").join("index").join("search-index.json"))
                .expect("after index");
        assert!(after.contains("Renamed Target.md"), "{after}");
        assert!(!after.contains("search-target.md"), "{after}");

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_page_title_comes_from_file_name_not_body_heading() {
        let root = temp_root("mnote-local-title-file-name");
        init_workspace(&root);
        std::fs::write(root.join("File Name.md"), "# Body Heading\n").expect("write md");
        let root_uri = format!("file://{}", root.display());

        let snapshot = load_local_folder_page_tree_snapshot(&root_uri).expect("page tree");
        let items = snapshot.projection["items"].as_array().expect("items");
        let row = items
            .iter()
            .find(|item| {
                item["resourceMeta"]["documentId"].as_str() == Some("local-md:File~20Name.md")
            })
            .expect("page row");

        // 本地 Markdown 标题来自文件名，正文 H1 只作为正文内容。
        assert_eq!(row["title"].as_str(), Some("File Name"));

        let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:File~20Name.md")
            .expect("aggregate");
        assert_eq!(aggregate.title, "File Name");
        // init_workspace 创建的是托管工作区（我的空间），默认显示页头标题。
        assert!(!aggregate.layout.page_options.hide_title_header);

        let _ = std::fs::remove_dir_all(&root);
    }

    #[cfg(unix)]
    #[test]
    fn local_markdown_path_id_resolves_without_scanning_sibling_directories() {
        use std::os::unix::fs::PermissionsExt;

        let root = temp_root("mnote-local-path-id-direct-resolve");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("aaa-unreadable")).expect("create unreadable dir");
        std::fs::write(root.join("Target.md"), "# Target\n").expect("write target");
        std::fs::set_permissions(
            root.join("aaa-unreadable"),
            std::fs::Permissions::from_mode(0o000),
        )
        .expect("chmod unreadable");
        let root_uri = format!("file://{}", root.display());

        let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:Target.md")
            .expect("path id should resolve directly");

        std::fs::set_permissions(
            root.join("aaa-unreadable"),
            std::fs::Permissions::from_mode(0o700),
        )
        .expect("restore unreadable dir permissions");
        let _ = std::fs::remove_dir_all(&root);

        assert_eq!(aggregate.title, "Target");
    }

    #[test]
    fn local_markdown_non_managed_folder_default_hides_title_header() {
        // 非托管本地文件夹：没有 .mnote/workspace.json，应该默认隐藏标题。
        let root = temp_root("mnote-local-non-managed-title-default");
        let _ = std::fs::create_dir_all(root.join(".mnote"));
        std::fs::write(root.join("Page.md"), "# Page Heading\n").expect("write md");
        let root_uri = format!("file://{}", root.display());

        let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:Page.md")
            .expect("aggregate");
        assert_eq!(aggregate.title, "Page");
        assert!(aggregate.layout.page_options.hide_title_header);

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_document_title_save_renames_nested_bundle_directory_and_markdown() {
        let root = temp_root("mnote-local-title-save-renames-file");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Old")).expect("create Old dir");
        std::fs::write(root.join("Old").join("Old.md"), "# Body Heading\n").expect("write Old.md");
        std::fs::write(root.join("Old").join("Child.md"), "# Child\n").expect("write child");
        let root_uri = format!("file://{}", root.display());

        let result = update_local_markdown_title(&root_uri, "local-md:Old~2FOld.md", "New")
            .expect("save title");

        assert!(
            root.join("New").join("New.md").is_file(),
            "New/New.md should exist"
        );
        assert!(
            root.join("New").join("Child.md").is_file(),
            "New/Child.md should exist"
        );
        assert!(!root.join("Old").exists(), "Old/ should not exist");
        assert_eq!(result["documentId"].as_str(), Some("local-md:New~2FNew.md"));
        assert_eq!(
            result["previousDocumentId"].as_str(),
            Some("local-md:Old~2FOld.md")
        );
        assert_eq!(result["title"].as_str(), Some("New"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_manages_loose_markdown_like_regular_file() {
        let root = temp_root("mnote-local-loose-markdown-file-management");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("docs")).expect("create docs");
        std::fs::write(root.join("docs").join("README.md"), "# Loose\n").expect("write loose md");
        let root_uri = format!("file://{}", root.display());

        let renamed = execute_local_tree_command(
            &root_uri,
            "rename",
            "local-md:docs~2FREADME.md",
            None,
            Some("Renamed"),
        )
        .expect("rename loose markdown");
        assert!(root.join("docs").join("Renamed.md").is_file());
        assert!(!root.join("docs").join("README.md").exists());
        assert_eq!(
            renamed["documentId"].as_str(),
            Some("local-md:docs~2FRenamed.md")
        );

        let moved =
            execute_local_tree_command(&root_uri, "move", "local-md:docs~2FRenamed.md", None, None)
                .expect("move loose markdown");
        assert!(root.join("Renamed.md").is_file());
        assert!(!root.join("docs").join("Renamed.md").exists());
        assert_eq!(moved["documentId"].as_str(), Some("local-md:Renamed.md"));

        let copied =
            execute_local_tree_command(&root_uri, "copy", "local-md:Renamed.md", None, None)
                .expect("copy loose markdown");
        assert!(root.join("Renamed 2.md").is_file());
        assert_eq!(
            copied["documentId"].as_str(),
            Some("local-md:Renamed~202.md")
        );

        let deleted =
            execute_local_tree_command(&root_uri, "delete", "local-md:Renamed.md", None, None)
                .expect("delete loose markdown");
        assert!(!root.join("Renamed.md").exists());
        assert!(root
            .join(".mnote")
            .join("trash")
            .join("Renamed.md")
            .is_file());
        assert_eq!(deleted["resourceKind"].as_str(), Some("markdown"));

        let restored =
            execute_local_tree_command(&root_uri, "restore", "local-md:Renamed.md", None, None)
                .expect("restore loose markdown");
        assert!(root.join("Renamed.md").is_file());
        assert_eq!(restored["documentId"].as_str(), Some("local-md:Renamed.md"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_resource_markdown_write_targets_asset_not_page_body() {
        let root = temp_root("mnote-local-resource-md-write");
        init_workspace(&root);
        std::fs::write(root.join("Page.md"), "# Page\n\n正文\n").expect("write page");
        std::fs::create_dir_all(root.join("Page.assets")).expect("create assets");
        let asset_path = root.join("Page.assets").join("note.md");
        std::fs::write(&asset_path, "# Asset\n\n旧内容\n").expect("write asset");
        let blocks = serde_json::json!([
            {"type":"heading","props":{"level":1},"content":[{"type":"text","text":"Asset"}]},
            {"type":"paragraph","content":[{"type":"text","text":"新内容"}]}
        ]);

        let next = editor_blocks_to_markdown_for_file(&blocks, &root, &asset_path);
        std::fs::write(&asset_path, next).expect("write asset next");

        let asset_text = std::fs::read_to_string(&asset_path).expect("read asset");
        let page_text = std::fs::read_to_string(root.join("Page.md")).expect("read page");
        assert!(asset_text.contains("新内容"));
        assert!(page_text.contains("正文"));
        assert!(!page_text.contains("新内容"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_resource_write_accepts_tiptap_document_payload() {
        let request = LocalResourceWriteRequest {
            root_uri: "file:///tmp/mnote".into(),
            path: "Page.assets/note.md".into(),
            content_format: Some("tiptapDocument".into()),
            tiptap_document: Some(serde_json::json!({
                "type": "doc",
                "content": [
                    {
                        "type": "heading",
                        "attrs": { "level": 2 },
                        "content": [{ "type": "text", "text": "资源标题" }]
                    },
                    {
                        "type": "paragraph",
                        "content": [{ "type": "text", "text": "资源正文" }]
                    }
                ]
            })),
            ..Default::default()
        };

        let blocks = local_resource_write_editor_blocks(&request);
        let markdown = editor_blocks_to_markdown_for_file(
            &blocks,
            std::path::Path::new("/tmp"),
            std::path::Path::new("/tmp/note.md"),
        );

        assert!(markdown.contains("## 资源标题"));
        assert!(markdown.contains("资源正文"));
    }

    #[tokio::test]
    async fn local_resource_read_preserves_uploaded_markdown_attachment_links() {
        let root = temp_root("mnote-local-resource-read-uploaded-md-links");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create page dir");
        std::fs::write(root.join("Page").join("Page.md"), "# Page\n").expect("write page");
        let root_uri = format!("file://{}", root.display());
        let document_id = "local-md:Page~2FPage.md";
        let resource = write_local_markdown_asset(
            &root_uri,
            document_id,
            "attachment",
            LocalUploadFile {
                name: "resource.md".to_string(),
                content_type: "text/markdown".to_string(),
                bytes: b"# Resource\n".to_vec(),
            },
        )
        .expect("upload resource markdown");
        let first = write_local_markdown_asset(
            &root_uri,
            document_id,
            "attachment",
            LocalUploadFile {
                name: "notes.md".to_string(),
                content_type: "text/markdown".to_string(),
                bytes: b"# Notes one\n".to_vec(),
            },
        )
        .expect("upload first linked markdown");
        let second = write_local_markdown_asset(
            &root_uri,
            document_id,
            "attachment",
            LocalUploadFile {
                name: "notes.md".to_string(),
                content_type: "text/markdown".to_string(),
                bytes: b"# Notes two\n".to_vec(),
            },
        )
        .expect("upload second linked markdown");
        assert_eq!(resource["sourcePath"], "resource.md");
        assert_eq!(first["sourcePath"], "notes.md");
        assert_eq!(second["sourcePath"], "notes-1.md");
        std::fs::write(
            root.join("Page").join("resource.md"),
            "# Resource\n\n[notes.md](notes.md)\n\n[notes-1.md](notes-1.md)\n",
        )
        .expect("write resource links");

        let state = test_state();
        state
            .control_plane()
            .upsert_user(UpsertUserInput {
                id: Some("resource_reader".into()),
                email: Some("resource-reader@example.com".into()),
                username: "resource_reader".into(),
                display_name: "resource_reader".into(),
                role: None,
                password_hash: None,
            })
            .expect("upsert resource reader");
        state
            .control_plane()
            .grant_directory_access(DirectoryGrantInput {
                user_id: "resource_reader".into(),
                workspace_id: None,
                root_uri: root_uri.clone(),
                root_path: root
                    .canonicalize()
                    .expect("canonical root")
                    .display()
                    .to_string(),
                permission: "read".into(),
                recursive: true,
                capabilities: vec![],
                source: "test".into(),
                created_by: None,
            })
            .expect("grant read");
        let context = request_context("resource_reader", "user");
        let (_, _, payload) = read_local_resource(
            State(state),
            Extension(context),
            Query(LocalResourceReadQuery {
                root_uri: root_uri.clone(),
                path: "Page/resource.md".into(),
            }),
        )
        .await
        .expect("read resource markdown");

        let media_paths = payload["result"]["content"]
            .as_array()
            .expect("content blocks")
            .iter()
            .filter(|block| block["type"].as_str() == Some("media"))
            .map(|block| {
                block["props"]["sourcePath"]
                    .as_str()
                    .unwrap_or_default()
                    .to_string()
            })
            .collect::<Vec<_>>();
        assert_eq!(media_paths, vec!["notes.md", "notes-1.md"]);

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_delete_folder_moves_directory_to_trash() {
        let root = temp_root("mnote-local-delete-folder");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Folder").join("Nested")).expect("create folder");
        std::fs::write(root.join("Folder").join("Nested").join("note.txt"), "note")
            .expect("write nested file");
        let root_uri = format!("file://{}", root.display());

        let folder_id = format!("local-dir:{}", encode_local_id_segment("Folder"));
        let result = execute_local_tree_command(&root_uri, "delete", &folder_id, None, None)
            .expect("delete folder");

        assert!(
            !root.join("Folder").exists(),
            "Folder should move away from root"
        );
        let trash_path = result["trashPath"].as_str().expect("trashPath");
        assert!(
            root.join(trash_path)
                .join("Nested")
                .join("note.txt")
                .is_file(),
            "folder contents should exist in trash"
        );
        assert_eq!(result["resourceKind"].as_str(), Some("local_directory"));
        assert_eq!(
            result["trashEntryId"].as_str(),
            Some("local-dir-trash:Folder")
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_delete_page_bundle_directory_uses_page_trash() {
        let root = temp_root("mnote-local-delete-page-bundle-dir");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create page bundle");
        std::fs::write(root.join("Page").join("Page.md"), "# Page\n").expect("write markdown");
        std::fs::write(root.join("Page").join("asset.txt"), "asset").expect("write asset");
        let root_uri = format!("file://{}", root.display());
        let folder_id = format!("local-dir:{}", encode_local_id_segment("Page"));
        let page_id = local_markdown_path_page_id("Page/Page.md");

        let result = execute_local_tree_command(&root_uri, "delete", &folder_id, None, None)
            .expect("delete page bundle");

        assert!(
            !root.join("Page").exists(),
            "页面 bundle 目录应整体移入垃圾箱"
        );
        assert_eq!(result["id"].as_str(), Some(page_id.as_str()));
        assert_eq!(result["documentId"].as_str(), Some(page_id.as_str()));
        assert_eq!(result["resourceKind"].as_str(), Some("markdown_bundle"));
        assert_ne!(
            result["resourceKind"].as_str(),
            Some("local_directory"),
            "页面 bundle 目录不能作为普通资源目录进入垃圾箱"
        );
        let trash_path = result["trashPath"].as_str().expect("trashPath");
        assert!(root.join(trash_path).join("Page.md").is_file());
        assert!(root.join(trash_path).join("asset.txt").is_file());

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_delete_local_file_id_uses_trash_index() {
        let root = temp_root("mnote-local-delete-local-file-id");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("docs")).expect("create docs");
        std::fs::write(root.join("docs").join("photo.png"), b"png").expect("write asset");
        let root_uri = format!("file://{}", root.display());

        let result = execute_local_tree_command(
            &root_uri,
            "delete",
            "local-file:docs/photo.png",
            None,
            None,
        )
        .expect("delete local-file asset");

        assert_eq!(result["resourceKind"].as_str(), Some("local_file"));
        assert_eq!(
            result["trashEntryId"].as_str(),
            Some("local-file:docs/photo.png")
        );
        assert!(!root.join("docs").join("photo.png").exists());
        assert!(root.join(".mnote").join("trash").join("photo.png").exists());
        let trash_index = std::fs::read_to_string(root.join(".mnote").join("trash-index.json"))
            .expect("trash index");
        assert!(trash_index.contains("local-file:docs/photo.png"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_delete_chinese_named_asset_in_subdir_uses_trash_index() {
        let root = temp_root("mnote-local-delete-chinese-asset");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create Page dir");
        std::fs::write(root.join("Page").join("资源.ext"), b"chinese asset content")
            .expect("write asset");
        let root_uri = format!("file://{}", root.display());
        let asset_id = "local-file:Page/资源.ext";

        let result = execute_local_tree_command(&root_uri, "delete", asset_id, None, None)
            .expect("delete chinese-named asset");
        assert_eq!(result["resourceKind"].as_str(), Some("local_file"));
        assert_eq!(result["trashEntryId"].as_str(), Some(asset_id));
        assert!(!root.join("Page").join("资源.ext").exists());
        assert!(root.join(".mnote").join("trash").join("资源.ext").exists());
        let trash_index = std::fs::read_to_string(root.join(".mnote").join("trash-index.json"))
            .expect("trash index");
        assert!(
            trash_index.contains(asset_id),
            "trash index should contain {asset_id}: {trash_index}"
        );

        let restored = execute_local_tree_command(&root_uri, "restore", asset_id, None, None)
            .expect("restore chinese-named asset");
        assert_eq!(restored["documentId"].as_str(), Some(asset_id));
        assert!(root.join("Page").join("资源.ext").is_file());
        assert!(!root.join(".mnote").join("trash").join("资源.ext").exists());

        execute_local_tree_command(&root_uri, "delete", asset_id, None, None)
            .expect("re-delete chinese-named asset");
        let purged = execute_local_tree_command(&root_uri, "purge", asset_id, None, None)
            .expect("purge chinese-named asset");
        assert_eq!(purged["ok"].as_bool(), Some(true));
        assert!(!root.join(".mnote").join("trash").join("资源.ext").exists());
        let trash_index_after_purge =
            std::fs::read_to_string(root.join(".mnote").join("trash-index.json"))
                .expect("trash index after purge");
        assert!(
            !trash_index_after_purge.contains(asset_id),
            "trash index should not contain purged {asset_id}: {trash_index_after_purge}"
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_move_with_sort_order_persists_file_order() {
        let root = temp_root("mnote-local-move-sort-order");
        init_workspace(&root);
        std::fs::write(root.join("alpha.md"), "# Alpha\n").expect("write alpha");
        std::fs::write(root.join("beta.md"), "# Beta\n").expect("write beta");
        std::fs::write(root.join("source.md"), "# Source\n").expect("write source");
        let root_uri = format!("file://{}", root.display());
        let doc_id = "local-md:source.md";

        let result =
            execute_local_tree_command_with_sort(&root_uri, "move", doc_id, None, None, Some(0))
                .expect("move with sort_order should succeed");
        assert!(result.get("_unsupportedFields").is_none());
        assert!(root.join("source.md").is_file());
        let order_index =
            std::fs::read_to_string(root.join(".mnote").join("file-order.json")).expect("order");
        assert!(
            order_index.contains("\"source.md\""),
            "file-order 应记录排序后的 source.md: {order_index}"
        );
        let snapshot =
            load_local_folder_file_tree_snapshot(&root_uri).expect("file tree snapshot after move");
        let items = snapshot.projection["items"]
            .as_array()
            .expect("projection items");
        let root_rows = items
            .iter()
            .filter(|item| item["parentNodeId"].is_null())
            .map(|item| {
                item["resourceMeta"]["extra"]["source"]["relativePath"]
                    .as_str()
                    .unwrap_or("")
            })
            .collect::<Vec<_>>();
        assert_eq!(root_rows.first().copied(), Some("source.md"));

        let result_without_sort = execute_local_tree_command(&root_uri, "move", doc_id, None, None)
            .expect("move without sort_order");
        assert!(result_without_sort.get("_unsupportedFields").is_none());

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_move_returns_affected_parents() {
        let root = temp_root("mnote-local-move-affected-parents");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("docs")).expect("create docs");
        std::fs::write(root.join("source.md"), "# Source\n").expect("write source");
        let root_uri = format!("file://{}", root.display());

        let result = execute_local_tree_command(
            &root_uri,
            "move",
            "local-md:source.md",
            Some("local-dir:docs"),
            None,
        )
        .expect("move should succeed");
        let affected = result["affectedParents"]
            .as_array()
            .expect("affected parents");
        assert!(
            affected
                .iter()
                .any(|parent| parent["relativePath"].as_str() == Some("")
                    && parent["reason"].as_str() == Some("old-parent")),
            "affectedParents 应包含旧父级 root: {affected:?}"
        );
        assert!(
            affected
                .iter()
                .any(|parent| parent["relativePath"].as_str() == Some("docs")
                    && parent["reason"].as_str() == Some("new-parent")),
            "affectedParents 应包含新父级 docs: {affected:?}"
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_file_tree_marks_page_bundle_folder_with_document_id() {
        let root = temp_root("mnote-local-page-bundle-folder-document-id");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create page bundle");
        std::fs::write(root.join("Page").join("Page.md"), "# Page\n").expect("write page");
        std::fs::write(root.join("Page").join("notes.txt"), "note").expect("write child");
        let root_uri = format!("file://{}", root.display());

        let snapshot = load_local_folder_file_tree_snapshot(&root_uri).expect("file tree");
        let items = snapshot
            .projection
            .get("items")
            .and_then(Value::as_array)
            .expect("items");
        let page_folder = items
            .iter()
            .find(|item| item["title"].as_str() == Some("Page"))
            .expect("page bundle folder row");

        assert_eq!(page_folder["rowKind"].as_str(), Some("folder"));
        assert_eq!(
            page_folder["documentId"].as_str(),
            Some("local-md:Page~2FPage.md")
        );
        assert_eq!(
            page_folder["resourceMeta"]["documentId"].as_str(),
            Some("local-md:Page~2FPage.md")
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_move_raw_file_into_folder() {
        let root = temp_root("mnote-local-move-raw-file");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("docs")).expect("create docs");
        std::fs::write(root.join("report.pdf"), "pdf").expect("write report");
        let root_uri = format!("file://{}", root.display());

        let result = execute_local_tree_command(
            &root_uri,
            "move",
            "local:asset:report.pdf",
            Some("local:folder:docs"),
            None,
        )
        .expect("move raw file");

        assert!(root.join("docs").join("report.pdf").is_file());
        assert!(!root.join("report.pdf").exists());
        assert_eq!(result["resourceKind"].as_str(), Some("local_file"));
        assert_eq!(result["relativePath"].as_str(), Some("docs/report.pdf"));
        assert_eq!(result["previousRelativePath"].as_str(), Some("report.pdf"));
        assert_eq!(
            result["documentId"].as_str(),
            Some("local:asset:docs/report.pdf")
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_rename_returns_operation_contract() {
        let root = temp_root("mnote-local-rename-operation-contract");
        init_workspace(&root);
        std::fs::write(root.join("Old.md"), "# Old\n").expect("write old");
        let root_uri = format!("file://{}", root.display());

        let result =
            execute_local_tree_command(&root_uri, "rename", "local-md:Old.md", None, Some("New"))
                .expect("rename should succeed");

        assert_eq!(result["schema"], "mnote.local_file_operation_result.v1");
        assert_eq!(result["action"], "rename");
        assert_eq!(result["rootUri"], root_uri);
        assert!(
            result["operationId"]
                .as_str()
                .is_some_and(|value| value.starts_with("local-op-rename-")),
            "operationId 应稳定标记 action: {result:?}"
        );
        assert_eq!(result["resource"]["relativePath"], "New.md");
        assert_eq!(result["resource"]["documentId"], "local-md:New.md");
        assert_eq!(result["resource"]["objectIdentity"]["objectKind"], "page");
        assert_eq!(result["previousResource"]["relativePath"], "Old.md");
        assert_eq!(result["revealTarget"]["relativePath"], "New.md");
        assert_eq!(result["revealTarget"]["rowId"], "local:markdown:New.md");
        assert_eq!(result["selectTarget"]["rowId"], "local:markdown:New.md");

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_delete_returns_parent_focus_contract() {
        let root = temp_root("mnote-local-delete-operation-contract");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("docs")).expect("create docs");
        std::fs::write(root.join("docs/remove.md"), "# Remove\n").expect("write remove");
        let root_uri = format!("file://{}", root.display());

        let result = execute_local_tree_command(
            &root_uri,
            "delete",
            "local-md:docs~2Fremove.md",
            None,
            None,
        )
        .expect("delete should succeed");

        assert_eq!(result["schema"], "mnote.local_file_operation_result.v1");
        let affected = result["affectedParents"]
            .as_array()
            .expect("affected parents");
        assert!(
            affected
                .iter()
                .any(|parent| parent["relativePath"].as_str() == Some("docs")
                    && parent["reason"].as_str() == Some("old-parent")),
            "delete affectedParents 应包含旧父级 docs: {affected:?}"
        );
        assert_eq!(result["previousResource"]["relativePath"], "docs/remove.md");
        assert_eq!(result["selectTarget"]["relativePath"], "docs");
        assert_eq!(result["selectTarget"]["rowId"], "local:folder:docs");

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_save_refreshes_search_index_after_write() {
        let root = temp_root("mnote-local-save-refresh-search-index");
        init_workspace(&root);
        std::fs::write(root.join("README.md"), "# Before\nold-token\n").expect("write md");
        let root_uri = format!("file://{}", root.display());

        crate::routes::local_search_index::refresh_local_search_index(
            &root,
            &root_uri,
            "local-ws-search-index-test",
        )
        .expect("initial index");
        save_local_markdown_page(
            &root_uri,
            "local-md:README.md",
            None,
            &serde_json::json!([
                {
                    "id": "heading_1",
                    "type": "heading",
                    "props": { "level": 1 },
                    "content": [{ "type": "text", "text": "After" }]
                },
                {
                    "id": "p_1",
                    "type": "paragraph",
                    "content": [{ "type": "text", "text": "new-token" }]
                }
            ]),
        )
        .expect("save markdown");

        let index =
            std::fs::read_to_string(root.join(".mnote").join("index").join("search-index.json"))
                .expect("index");
        assert!(index.contains("new-token"), "{index}");
        assert!(!index.contains("old-token"), "{index}");

        let _ = std::fs::remove_dir_all(&root);
    }

    #[tokio::test]
    async fn local_file_open_rejects_root_escape() {
        let root = temp_root("mnote-local-file-open-root-escape");
        std::fs::write(root.join("README.txt"), "hello").expect("write file");
        init_workspace(&root);
        let root_uri = format!("file://{}", root.display());
        let mut headers = HeaderMap::new();
        headers.insert("x-mnote-actor-id", "user_test".parse().unwrap());
        headers.insert("x-mnote-actor-type", "user".parse().unwrap());
        let context = RequestContext::from_http_parts(
            &Method::GET,
            &"/api/local-folder/files/open".parse().expect("uri"),
            &headers,
        );
        let query = LocalFileOpenQuery {
            root_uri,
            path: "../escape.txt".into(),
            download: None,
        };

        let error = open_local_file(State(test_state()), Extension(context), Query(query))
            .await
            .expect_err("should reject root escape");
        assert_eq!(error.status(), StatusCode::BAD_REQUEST);
        assert!(error.message().contains("root"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[tokio::test]
    async fn local_file_open_allows_read_grant() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-local-file-open-read-grant-root");
        let policy_root = temp_root("mnote-local-file-open-read-grant-config");
        let policy_file = policy_root.join("access-policy.json");
        let root_uri = format!("file://{}", root.display());
        std::fs::write(root.join("README.txt"), "hello").expect("write file");
        std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
        std::fs::write(
            &policy_file,
            serde_json::json!({
                "grants": [
                    {
                        "userId": "user_reader",
                        "rootUri": root_uri,
                        "permission": "read",
                        "recursive": true
                    }
                ]
            })
            .to_string(),
        )
        .expect("write policy");

        let mut headers = HeaderMap::new();
        headers.insert("x-mnote-actor-id", "user_reader".parse().unwrap());
        headers.insert("x-mnote-actor-type", "user".parse().unwrap());
        let context = RequestContext::from_http_parts(
            &Method::GET,
            &"/api/local-folder/files/open".parse().expect("uri"),
            &headers,
        );

        let (_, _, bytes) = open_local_file(
            State(test_state()),
            Extension(context),
            Query(LocalFileOpenQuery {
                root_uri,
                path: "README.txt".into(),
                download: None,
            }),
        )
        .await
        .expect("read grant can open local file");
        assert_eq!(bytes, b"hello");

        std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
        let _ = std::fs::remove_dir_all(&root);
        let _ = std::fs::remove_dir_all(&policy_root);
    }

    #[tokio::test]
    async fn local_file_open_allows_sqlite_directory_read_grant_without_json_policy() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-local-file-open-sqlite-read-grant-root");
        let policy_root = temp_root("mnote-local-file-open-sqlite-read-grant-config");
        let policy_file = policy_root.join("missing-access-policy.json");
        let state = test_state();
        let root_uri = format!("file://{}", root.display());
        std::fs::write(root.join("README.txt"), "hello sqlite").expect("write file");
        std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
        state
            .control_plane()
            .upsert_user(UpsertUserInput {
                id: Some("sqlite_reader".into()),
                email: Some("sqlite-reader@example.com".into()),
                username: "sqlite_reader".into(),
                display_name: "sqlite_reader".into(),
                role: None,
                password_hash: None,
            })
            .expect("upsert sqlite reader");
        state
            .control_plane()
            .grant_directory_access(DirectoryGrantInput {
                user_id: "sqlite_reader".into(),
                workspace_id: None,
                root_uri: root_uri.clone(),
                root_path: root
                    .canonicalize()
                    .expect("canonical root")
                    .display()
                    .to_string(),
                permission: "read".into(),
                recursive: true,
                capabilities: vec![],
                source: "test".into(),
                created_by: None,
            })
            .expect("grant sqlite read");

        let context = request_context("sqlite_reader", "user");
        let (_, _, bytes) = open_local_file(
            State(state),
            Extension(context),
            Query(LocalFileOpenQuery {
                root_uri,
                path: "README.txt".into(),
                download: None,
            }),
        )
        .await
        .expect("sqlite read grant can open local file");
        assert_eq!(bytes, b"hello sqlite");
        assert!(!policy_file.exists(), "SQLite grant 不应写旧 JSON policy");

        std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
        let _ = std::fs::remove_dir_all(&root);
        let _ = std::fs::remove_dir_all(&policy_root);
    }

    #[tokio::test]
    async fn local_file_open_allows_legacy_sqlite_directory_grant_with_path_root_uri() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-local-file-open-legacy-sqlite-grant-root");
        let policy_root = temp_root("mnote-local-file-open-legacy-sqlite-grant-config");
        let policy_file = policy_root.join("missing-access-policy.json");
        let state = test_state();
        let canonical_root = root.canonicalize().expect("canonical root");
        let canonical_root_path = canonical_root.display().to_string();
        let root_uri = format!("file://{canonical_root_path}");
        std::fs::write(root.join("README.txt"), "hello legacy sqlite").expect("write file");
        std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
        state
            .control_plane()
            .upsert_user(UpsertUserInput {
                id: Some("legacy_sqlite_reader".into()),
                email: Some("legacy-sqlite-reader@example.com".into()),
                username: "legacy_sqlite_reader".into(),
                display_name: "legacy_sqlite_reader".into(),
                role: None,
                password_hash: None,
            })
            .expect("upsert sqlite reader");
        state
            .control_plane()
            .grant_directory_access(DirectoryGrantInput {
                user_id: "legacy_sqlite_reader".into(),
                workspace_id: None,
                root_uri: canonical_root_path.clone(),
                root_path: canonical_root_path,
                permission: "read".into(),
                recursive: true,
                capabilities: vec![],
                source: "legacy-test".into(),
                created_by: None,
            })
            .expect("grant legacy sqlite read");

        let context = request_context("legacy_sqlite_reader", "user");
        let (_, _, bytes) = open_local_file(
            State(state),
            Extension(context),
            Query(LocalFileOpenQuery {
                root_uri,
                path: "README.txt".into(),
                download: None,
            }),
        )
        .await
        .expect("legacy sqlite read grant can open local file");
        assert_eq!(bytes, b"hello legacy sqlite");
        assert!(!policy_file.exists(), "SQLite grant 不应写旧 JSON policy");

        std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
        let _ = std::fs::remove_dir_all(&root);
        let _ = std::fs::remove_dir_all(&policy_root);
    }

    #[tokio::test]
    async fn local_resource_write_allows_sqlite_directory_write_grant_without_json_policy() {
        let _guard = env_lock().lock().expect("env lock");
        let root = temp_root("mnote-local-resource-write-sqlite-grant-root");
        let policy_root = temp_root("mnote-local-resource-write-sqlite-grant-config");
        let policy_file = policy_root.join("missing-access-policy.json");
        let state = test_state();
        let root_uri = format!("file://{}", root.display());
        std::fs::create_dir_all(root.join(".mnote")).expect("create metadata");
        std::fs::write(root.join("README.md"), "old").expect("seed file");
        std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
        state
            .control_plane()
            .upsert_user(UpsertUserInput {
                id: Some("sqlite_writer".into()),
                email: Some("sqlite-writer@example.com".into()),
                username: "sqlite_writer".into(),
                display_name: "sqlite_writer".into(),
                role: None,
                password_hash: None,
            })
            .expect("upsert sqlite writer");
        state
            .control_plane()
            .grant_directory_access(DirectoryGrantInput {
                user_id: "sqlite_writer".into(),
                workspace_id: None,
                root_uri: root_uri.clone(),
                root_path: root
                    .canonicalize()
                    .expect("canonical root")
                    .display()
                    .to_string(),
                permission: "write".into(),
                recursive: true,
                capabilities: vec![],
                source: "test".into(),
                created_by: None,
            })
            .expect("grant sqlite write");

        let context = request_context("sqlite_writer", "user");
        let (_, _, payload) = write_local_resource(
            State(state),
            Extension(context),
            Json(LocalResourceWriteRequest {
                root_uri: root_uri.clone(),
                path: "README.md".into(),
                expected_file_version: None,
                content_format: Some("editorBlocks".into()),
                content: serde_json::json!([
                    {
                        "type": "paragraph",
                        "content": [{"type": "text", "text": "new content"}]
                    }
                ]),
                tiptap_document: None,
            }),
        )
        .await
        .expect("sqlite write grant can write local resource");
        assert_eq!(payload["ok"], true);
        let written = std::fs::read_to_string(root.join("README.md")).expect("read file");
        assert!(written.contains("new content"));
        assert!(!policy_file.exists(), "SQLite grant 不应写旧 JSON policy");

        std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
        let _ = std::fs::remove_dir_all(&root);
        let _ = std::fs::remove_dir_all(&policy_root);
    }

    #[tokio::test]
    async fn local_file_open_sets_image_and_markdown_content_type() {
        let root = temp_root("mnote-local-file-open-content-type");
        init_workspace(&root);
        std::fs::write(root.join("photo.jpg"), b"jpg").expect("write image");
        std::fs::write(root.join("note.md"), "# Note\n").expect("write markdown");
        let root_uri = format!("file://{}", root.display());
        let mut headers = HeaderMap::new();
        headers.insert("x-mnote-actor-id", "user_test".parse().unwrap());
        headers.insert("x-mnote-actor-type", "user".parse().unwrap());
        let context = RequestContext::from_http_parts(
            &Method::GET,
            &"/api/local-folder/files/open".parse().expect("uri"),
            &headers,
        );

        let (_, image_headers, _) = open_local_file(
            State(test_state()),
            Extension(context.clone()),
            Query(LocalFileOpenQuery {
                root_uri: root_uri.clone(),
                path: "photo.jpg".into(),
                download: None,
            }),
        )
        .await
        .expect("open image");
        assert_eq!(
            image_headers
                .get(axum::http::header::CONTENT_TYPE)
                .and_then(|v| v.to_str().ok()),
            Some("image/jpeg")
        );

        let (_, markdown_headers, _) = open_local_file(
            State(test_state()),
            Extension(context),
            Query(LocalFileOpenQuery {
                root_uri,
                path: "note.md".into(),
                download: None,
            }),
        )
        .await
        .expect("open markdown");
        assert_eq!(
            markdown_headers
                .get(axum::http::header::CONTENT_TYPE)
                .and_then(|v| v.to_str().ok()),
            Some("text/markdown; charset=utf-8")
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[tokio::test]
    async fn local_file_open_sets_pdf_and_office_content_type() {
        let root = temp_root("mnote-local-file-open-office-content-type");
        init_workspace(&root);
        std::fs::write(root.join("spec.pdf"), b"pdf").expect("write pdf");
        std::fs::write(root.join("report.docx"), b"docx").expect("write docx");
        let root_uri = format!("file://{}", root.display());
        let mut headers = HeaderMap::new();
        headers.insert("x-mnote-actor-id", "user_test".parse().unwrap());
        headers.insert("x-mnote-actor-type", "user".parse().unwrap());
        let context = RequestContext::from_http_parts(
            &Method::GET,
            &"/api/local-folder/files/open".parse().expect("uri"),
            &headers,
        );

        let (_, pdf_headers, _) = open_local_file(
            State(test_state()),
            Extension(context.clone()),
            Query(LocalFileOpenQuery {
                root_uri: root_uri.clone(),
                path: "spec.pdf".into(),
                download: None,
            }),
        )
        .await
        .expect("open pdf");
        assert_eq!(
            pdf_headers
                .get(axum::http::header::CONTENT_TYPE)
                .and_then(|v| v.to_str().ok()),
            Some("application/pdf")
        );

        let (_, office_headers, _) = open_local_file(
            State(test_state()),
            Extension(context),
            Query(LocalFileOpenQuery {
                root_uri,
                path: "report.docx".into(),
                download: None,
            }),
        )
        .await
        .expect("open docx");
        assert_eq!(
            office_headers
                .get(axum::http::header::CONTENT_TYPE)
                .and_then(|v| v.to_str().ok()),
            Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document")
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[tokio::test]
    async fn local_file_open_download_sets_content_disposition_filename() {
        let root = temp_root("mnote-local-file-open-download-filename");
        init_workspace(&root);
        std::fs::write(root.join("报告 2026.docx"), b"docx").expect("write docx");
        let root_uri = format!("file://{}", root.display());
        let mut headers = HeaderMap::new();
        headers.insert("x-mnote-actor-id", "user_test".parse().unwrap());
        headers.insert("x-mnote-actor-type", "user".parse().unwrap());
        let context = RequestContext::from_http_parts(
            &Method::GET,
            &"/api/local-folder/files/open".parse().expect("uri"),
            &headers,
        );

        let (_, response_headers, _) = open_local_file(
            State(test_state()),
            Extension(context),
            Query(LocalFileOpenQuery {
                root_uri,
                path: "报告 2026.docx".into(),
                download: Some(true),
            }),
        )
        .await
        .expect("download local file");

        let disposition = response_headers
            .get(axum::http::header::CONTENT_DISPOSITION)
            .and_then(|value| value.to_str().ok())
            .expect("content disposition");
        assert!(disposition.starts_with("attachment;"));
        assert!(disposition.contains("filename=\"__ 2026.docx\""));
        assert!(
            disposition.contains("filename*=UTF-8''%E6%8A%A5%E5%91%8A%202026.docx"),
            "{disposition}"
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[tokio::test]
    async fn local_folder_open_downloads_directory_as_tar() {
        let root = temp_root("mnote-local-folder-open-download-directory");
        init_workspace(&root);
        let reports = root.join("Reports");
        std::fs::create_dir_all(&reports).expect("create reports");
        std::fs::write(reports.join("summary.txt"), b"summary").expect("write summary");
        let root_uri = format!("file://{}", root.display());
        let mut headers = HeaderMap::new();
        headers.insert("x-mnote-actor-id", "user_test".parse().unwrap());
        headers.insert("x-mnote-actor-type", "user".parse().unwrap());
        let context = RequestContext::from_http_parts(
            &Method::GET,
            &"/api/local-folder/files/open".parse().expect("uri"),
            &headers,
        );

        let (_, response_headers, bytes) = open_local_file(
            State(test_state()),
            Extension(context),
            Query(LocalFileOpenQuery {
                root_uri,
                path: "Reports".into(),
                download: Some(true),
            }),
        )
        .await
        .expect("download local directory");

        assert_eq!(
            response_headers
                .get(axum::http::header::CONTENT_TYPE)
                .and_then(|value| value.to_str().ok()),
            Some("application/x-tar")
        );
        let disposition = response_headers
            .get(axum::http::header::CONTENT_DISPOSITION)
            .and_then(|value| value.to_str().ok())
            .expect("content disposition");
        assert!(disposition.contains("filename=\"Reports.tar\""));
        assert!(bytes.windows(5).any(|window| window == b"ustar"));

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_file_tree_classifies_mindmap_and_office_assets() {
        let root = temp_root("mnote-local-filetree-resource-kinds");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("Page")).expect("create page dir");
        std::fs::write(root.join("Page").join("Page.md"), "").expect("write md");
        std::fs::write(root.join("Page").join("思维导图123456.json"), "{}").expect("write mindmap");
        std::fs::write(root.join("Page").join("report.docx"), b"docx").expect("write docx");
        let root_uri = format!("file://{}", root.display());

        let snapshot =
            load_local_folder_file_tree_children_snapshot(&root_uri, "Page").expect("file tree");
        let items = snapshot.projection["items"].as_array().expect("items");
        let mindmap = items
            .iter()
            .find(|item| item["title"].as_str() == Some("思维导图123456.json"))
            .expect("mindmap row");
        assert_eq!(mindmap["rowKind"].as_str(), Some("asset"));
        assert_eq!(mindmap["iconHint"].as_str(), Some("mindmap"));
        assert_eq!(
            mindmap["resourceMeta"]["workspacePath"]["objectIdentity"]["objectKind"].as_str(),
            Some("mindmap")
        );

        let office = items
            .iter()
            .find(|item| item["title"].as_str() == Some("report.docx"))
            .expect("office row");
        assert_eq!(office["rowKind"].as_str(), Some("asset"));
        assert_eq!(office["iconHint"].as_str(), Some("office"));
        assert_eq!(
            office["resourceMeta"]["workspacePath"]["objectIdentity"]["objectKind"].as_str(),
            Some("only_office")
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    // ── Gap A2 BufferStore 运行时接入集成测试 ────────────────────────

    #[test]
    fn document_buffer_mark_saved_after_page_body_write() {
        let root = temp_root("mnote-buf-save-after-write");
        std::fs::write(root.join("README.md"), "# Old\n").expect("write md");
        let root_uri = format!("file://{}", root.display());

        let store = crate::document_buffer_store::BufferStore::new();
        let ws_path = crate::document_buffer_store::build_local_folder_workspace_path(
            &local_workspace_id(&root),
            &root_uri,
            "README.md",
            "local-md:README.md",
        );
        // 模拟编辑器打开文档：先初始化 buffer
        store.get_or_create(&ws_path);

        // 经过 write_local_markdown_page_body 保存入口
        write_local_markdown_page_body(
            &core_protocol::PageBodyWriteRequest {
                document_id: "local-md:README.md".into(),
                workspace_id: String::new(),
                source_kind: core_protocol::WorkspaceSourceKind::LocalFolder,
                root_uri: root_uri.clone(),
                expected_file_version: None,
                base_content_hash: None,
                content_format: "editorBlocks".into(),
                content: serde_json::json!([
                    {"type":"heading","props":{"level":1},"content":[{"type":"text","text":"Written"}]}
                ]),
                editor_source: Some("unit-test".into()),
            },
            Some(&store),
        )
        .expect("write_local_markdown_page_body");

        // 验证 buffer store 已被消费
        let buf = store
            .get_by_path(&ws_path)
            .expect("buffer should exist after write");
        assert_eq!(
            buf.dirty_state,
            core_protocol::DocBufferDirtyState::Clean,
            "保存后 buffer 应为 Clean"
        );
        assert!(buf.file_version.is_some(), "file_version 应被更新");
        assert!(buf.last_saved_at.is_some(), "last_saved_at 应被更新");

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn document_buffer_marks_external_modified_from_local_folder_event() {
        let store = crate::document_buffer_store::BufferStore::new();
        let ws_path = crate::document_buffer_store::build_local_folder_workspace_path(
            "ws_test",
            "file:///tmp/test-root",
            "doc.md",
            "local-md:doc.md",
        );
        // 模拟编辑器打开文档：Clean 状态 buffer
        store.get_or_create(&ws_path);

        // 模拟 watcher 事件调用
        let result = store.mark_external_modified(&ws_path, Some("external-editor".into()));
        assert!(result.is_some(), "外部修改应返回 buffer");

        let buf = result.unwrap();
        assert_eq!(
            buf.dirty_state,
            core_protocol::DocBufferDirtyState::ExternalModified,
            "Clean buffer 受外部修改后应变为 ExternalModified"
        );
        assert_eq!(buf.external_actor.as_deref(), Some("external-editor"));
    }

    #[test]
    fn document_buffer_marks_stale_from_local_folder_event_when_dirty() {
        let store = crate::document_buffer_store::BufferStore::new();
        let ws_path = crate::document_buffer_store::build_local_folder_workspace_path(
            "ws_test",
            "file:///tmp/test-root",
            "doc.md",
            "local-md:doc.md",
        );
        // 模拟编辑器打开并且有未保存修改：Dirty 状态 buffer
        let buf = store.get_or_create(&ws_path);
        let mut dirty_buf = buf;
        dirty_buf.mark_dirty("sha256:dirty_content".into());
        let key = crate::document_buffer_store::BufferKey::from_workspace_path(&ws_path);
        store.update(&key, dirty_buf);

        // 模拟 watcher 事件调用
        let result = store.mark_external_modified(&ws_path, Some("vim".into()));
        assert!(result.is_some(), "外部修改应返回 buffer");

        let buf = result.unwrap();
        assert_eq!(
            buf.dirty_state,
            core_protocol::DocBufferDirtyState::Stale,
            "Dirty buffer 受外部修改后应变为 Stale"
        );
        assert_eq!(buf.external_actor.as_deref(), Some("vim"));
    }

    #[test]
    fn local_tree_command_create_page_creates_timestamped_nested_bundle() {
        let root = temp_root("mnote-local-tree-create-page-timestamp");
        init_workspace(&root);
        let root_uri = format!("file://{}", root.display());

        let result = execute_local_tree_command(&root_uri, "create", "", None, Some("新页面"))
            .expect("create page with default title");

        let relative_path = result["relativePath"].as_str().expect("relativePath");
        let document_id = result["documentId"].as_str().expect("documentId");

        // 应形如“新页面HHMMSS/新页面HHMMSS.md”。
        assert!(
            relative_path.starts_with("新页面"),
            "relativePath should start with 新页面, got: {relative_path}"
        );
        assert!(
            relative_path.ends_with(".md"),
            "relativePath should end with .md, got: {relative_path}"
        );
        assert!(
            relative_path.len() > "新页面.md".len(),
            "expected timestamp suffix"
        );

        let (dir, file) = relative_path.split_once('/').expect("nested bundle path");
        assert_eq!(file, format!("{dir}.md"));
        assert!(
            root.join(dir).is_dir(),
            "bundle directory should exist: {}",
            dir
        );

        // Markdown 文件应已创建。
        assert!(
            root.join(relative_path).is_file(),
            "markdown file should exist: {relative_path}"
        );

        // documentId 继续使用路径型身份。
        assert!(
            document_id.starts_with("local-md:"),
            "documentId should be path-based"
        );
        assert_eq!(result["action"], "create");
        assert_eq!(result["sourceKind"], "local_folder");
        assert_eq!(result["parentRelativePath"], "");
        assert!(
            result["affectedParents"]
                .as_array()
                .expect("affected parents")
                .iter()
                .any(|parent| parent["relativePath"].as_str() == Some("")),
            "create page affectedParents 应包含承载新页面包的父级 root: {}",
            result["affectedParents"]
        );

        // 页面标题来自文件名，默认正文不再写第二个标题真相。
        let content = std::fs::read_to_string(root.join(relative_path)).expect("read markdown");
        assert!(
            content.is_empty(),
            "new page body should be empty: {content}"
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_tree_command_create_folder_uses_timestamped_default_name() {
        let root = temp_root("mnote-local-tree-create-folder-timestamp");
        init_workspace(&root);
        let root_uri = format!("file://{}", root.display());

        let result =
            execute_local_tree_command(&root_uri, "create_folder", "", None, Some("新建文件夹"))
                .expect("create folder with default title");

        let relative_path = result["relativePath"].as_str().expect("relativePath");
        assert!(
            relative_path.starts_with("新建文件夹"),
            "relativePath should start with 新建文件夹, got: {relative_path}"
        );
        assert!(
            relative_path.len() > "新建文件夹".len(),
            "expected timestamp suffix"
        );
        assert!(
            root.join(relative_path).is_dir(),
            "default folder should create directory"
        );
        assert!(
            !root
                .join(relative_path)
                .join(format!("{relative_path}.md"))
                .exists(),
            "default folder creation must not create page markdown"
        );
        assert_eq!(result["parentRelativePath"], "");
        assert!(
            result["affectedParents"]
                .as_array()
                .expect("affected parents")
                .iter()
                .any(|parent| parent["relativePath"].as_str() == Some("")),
            "create folder affectedParents 应包含承载新文件夹的父级 root: {}",
            result["affectedParents"]
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_page_tree_projects_nested_bundle_as_page_node() {
        let root = temp_root("mnote-local-page-tree-nested-bundle");
        init_workspace(&root);
        let root_uri = format!("file://{}", root.display());

        // 构造 Root/Root.md + Root/Child/Child.md。
        std::fs::create_dir_all(root.join("Root")).expect("create Root dir");
        std::fs::write(root.join("Root").join("Root.md"), "# Body Root\n").expect("write Root.md");
        std::fs::create_dir_all(root.join("Root").join("Child")).expect("create Child dir");
        std::fs::write(
            root.join("Root").join("Child").join("Child.md"),
            "# Body Child\n",
        )
        .expect("write Child.md");

        // 读取页面树。
        let snapshot = load_local_folder_page_tree_snapshot(&root_uri).expect("load page tree");
        let items = snapshot.projection["items"].as_array().expect("items");

        // 找到 Root 页面节点和 Child 页面节点。标题来自文件名，不来自正文 H1。
        let root_node = items
            .iter()
            .find(|item| {
                item["title"].as_str() == Some("Root")
                    && item["resourceMeta"]["resourceKind"].as_str() == Some("document")
            })
            .expect("Root page node");

        let child_node = items
            .iter()
            .find(|item| {
                item["title"].as_str() == Some("Child")
                    && item["resourceMeta"]["resourceKind"].as_str() == Some("document")
            })
            .expect("Child page node");

        // Child 的父节点应是 Root 的 documentId。
        let root_doc_id = root_node["resourceMeta"]["documentId"]
            .as_str()
            .or_else(|| root_node["nodeId"].as_str())
            .expect("root documentId");
        assert_eq!(child_node["parentNodeId"].as_str(), Some(root_doc_id));
        assert_eq!(root_doc_id, "local-md:Root~2FRoot.md");

        // Root bundle 目录不应额外生成 page-group 行。
        let page_group_for_root = items.iter().find(|item| {
            item["rowId"]
                .as_str()
                .map_or(false, |id| id.contains("page-group:Root"))
        });
        assert!(
            page_group_for_root.is_none(),
            "should not have page-group for Root"
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_file_tree_keeps_nested_bundle_filesystem_details() {
        let root = temp_root("mnote-local-file-tree-nested-bundle");
        init_workspace(&root);
        let root_uri = format!("file://{}", root.display());

        // 构造 Root/Root.md + Root/Child/Child.md。
        std::fs::create_dir_all(root.join("Root")).expect("create Root dir");
        std::fs::write(root.join("Root").join("Root.md"), "# Root\n").expect("write Root.md");
        std::fs::create_dir_all(root.join("Root").join("Child")).expect("create Child dir");
        std::fs::write(
            root.join("Root").join("Child").join("Child.md"),
            "# Child\n",
        )
        .expect("write Child.md");

        // 读取文件树。
        let snapshot = load_local_folder_file_tree_snapshot_with_reveal(
            &root_uri,
            Some("local-md:Root~2FChild~2FChild.md"),
        )
        .expect("load file tree");
        let items = snapshot.projection["items"].as_array().expect("items");

        // 文件树应保留 Root 文件夹和其中的 Root.md 真实条目。
        let root_md = items.iter().find(|item| {
            item["title"].as_str() == Some("Root.md")
                && item["resourceMeta"]["resourceKind"].as_str() == Some("markdown")
        });
        assert!(
            root_md.is_some(),
            "file tree should have Root.md as markdown row"
        );

        let root_folder = items.iter().find(|item| {
            item["title"].as_str() == Some("Root")
                && item["resourceMeta"]["resourceKind"].as_str() == Some("folder")
        });
        assert!(
            root_folder.is_some(),
            "file tree should have Root as folder row"
        );

        // 同名页面包 reveal 时只需要让 Child 目录可见，不展开包内部 Child.md。
        let child_folder = items.iter().find(|item| {
            item["title"].as_str() == Some("Child")
                && item["resourceMeta"]["resourceKind"].as_str() == Some("folder")
        });
        assert!(
            child_folder.is_some(),
            "file tree should have Child folder under Root"
        );
        let child_md = items.iter().find(|item| {
            item["title"].as_str() == Some("Child.md")
                && item["resourceMeta"]["resourceKind"].as_str() == Some("markdown")
        });
        assert!(
            child_md.is_none(),
            "file tree should not auto-expand same-name bundle markdown"
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_markdown_asset_upload_copies_next_to_markdown_with_relative_path() {
        let root = temp_root("mnote-local-markdown-asset-upload");
        init_workspace(&root);
        std::fs::create_dir_all(root.join("docs")).expect("create docs");
        std::fs::create_dir_all(root.join("docs").join("README")).expect("create page dir");
        std::fs::write(
            root.join("docs").join("README").join("README.md"),
            "# Assets\n",
        )
        .expect("write md");
        std::fs::create_dir_all(root.join("docs").join("README")).expect("create page dir");
        std::fs::write(root.join("docs").join("README").join("photo.png"), b"old")
            .expect("write existing asset");
        let root_uri = format!("file://{}", root.display());

        let asset = write_local_markdown_asset(
            &root_uri,
            "local-md:docs~2FREADME~2FREADME.md",
            "image",
            LocalUploadFile {
                name: "photo.png".to_string(),
                content_type: "image/png".to_string(),
                bytes: b"new-image".to_vec(),
            },
        )
        .expect("upload asset");

        assert_eq!(asset["sourcePath"], "photo-1.png");
        assert_eq!(asset["file_url"], "photo-1.png");
        assert_eq!(asset["asset_type"], "image");
        assert_eq!(asset["sourceKind"], "local_folder");
        assert_eq!(asset["uploadIntent"], "editor.markdown.attach");
        assert_eq!(asset["rootRelativePath"], "docs/README/photo-1.png");
        assert_eq!(asset["markdownRelativePath"], "photo-1.png");
        assert_eq!(
            asset["ownerDocumentId"],
            "local-md:docs~2FREADME~2FREADME.md"
        );
        assert_eq!(
            std::fs::read(root.join("docs").join("README").join("photo-1.png"))
                .expect("read copied asset"),
            b"new-image"
        );
        let markdown_asset = write_local_markdown_asset(
            &root_uri,
            "local-md:docs~2FREADME~2FREADME.md",
            "attachment",
            LocalUploadFile {
                name: "notes.md".to_string(),
                content_type: "text/markdown".to_string(),
                bytes: b"# Uploaded notes\n".to_vec(),
            },
        )
        .expect("upload markdown asset");
        assert_eq!(markdown_asset["sourcePath"], "notes.md");
        assert_eq!(markdown_asset["uploadIntent"], "editor.markdown.attach");
        assert_eq!(markdown_asset["rootRelativePath"], "docs/README/notes.md");
        assert_eq!(markdown_asset["markdownRelativePath"], "notes.md");
        assert_eq!(
            markdown_asset["ownerDocumentId"],
            "local-md:docs~2FREADME~2FREADME.md"
        );
        let uploaded_asset_index =
            std::fs::read_to_string(root.join(".mnote").join("uploaded-assets.json"))
                .expect("uploaded asset index");
        assert!(uploaded_asset_index.contains("docs/README/notes.md"));

        let snapshot = load_local_folder_file_tree_children_snapshot(&root_uri, "docs/README")
            .expect("file tree");
        let items = snapshot.projection["items"].as_array().expect("items");
        let notes_row = items
            .iter()
            .find(|item| item["title"].as_str() == Some("notes.md"))
            .expect("uploaded markdown asset row");
        assert_eq!(notes_row["rowKind"].as_str(), Some("asset"));
        assert_eq!(notes_row["iconHint"].as_str(), Some("markdown"));
        assert_eq!(
            notes_row["resourceMeta"]["assetId"].as_str(),
            Some("local-file:docs/README/notes.md")
        );
        let page_tree = load_local_folder_page_tree_snapshot(&root_uri).expect("page tree");
        let page_items = page_tree.projection["items"]
            .as_array()
            .expect("page items");
        let page_notes_row = page_items
            .iter()
            .find(|item| item["title"].as_str() == Some("notes.md"))
            .expect("uploaded markdown asset page row");
        assert_eq!(page_notes_row["rowKind"].as_str(), Some("asset"));
        assert_eq!(page_notes_row["iconHint"].as_str(), Some("markdown"));
        assert_eq!(
            page_notes_row["resourceMeta"]["assetId"].as_str(),
            Some("local-file:docs/README/notes.md")
        );

        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn local_rename_markdown_page_renames_nested_bundle() {
        let root = temp_root("mnote-local-rename-nested-bundle");
        init_workspace(&root);
        let root_uri = format!("file://{}", root.display());

        // 构造 Old/Old.md + Old/Child.md。
        std::fs::create_dir_all(root.join("Old")).expect("create Old dir");
        std::fs::write(root.join("Old").join("Old.md"), "# Old\n").expect("write Old.md");
        std::fs::write(root.join("Old").join("Child.md"), "# Child\n").expect("write Child.md");

        // 将 Old 重命名为 New。
        let old_document_id = "local-md:Old~2FOld.md";
        let result =
            execute_local_tree_command(&root_uri, "rename", old_document_id, None, Some("New"))
                .expect("rename page");

        // New/New.md 应存在。
        assert!(
            root.join("New").join("New.md").is_file(),
            "New/New.md should exist"
        );
        // New/Child.md 应存在。
        assert!(
            root.join("New").join("Child.md").is_file(),
            "New/Child.md should exist"
        );
        // Old/ 不应再存在。
        assert!(
            !root.join("Old").exists(),
            "Old/ directory should not exist"
        );

        assert_eq!(result["action"], "rename");
        assert_eq!(result["sourceKind"], "local_folder");
        assert_eq!(result["documentId"].as_str(), Some("local-md:New~2FNew.md"));

        let _ = std::fs::remove_dir_all(&root);
    }
}
