use base64::Engine;
use bridge_runtime::{
    execute_runtime_query, RuntimeActorWire, RuntimeBridgeContextWire, RuntimeInput,
    RuntimeQueryEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire, RuntimeToolInvocationWire,
};
use clap::ValueEnum;
use core_protocol::{
    default_tool_registry, query::Pagination, tool_effect_label, ActorPayload, CommandEnvelope,
    CreateDocumentPage, DeleteDocumentPage, GetMindmap, GetPageContent, InvocationKind,
    ListSidebarDataset, MindmapOp, MindmapTreeNode, MoveDocumentPage, PatchPageBlock, PutMindmap,
    QueryEnvelope, RestoreDocumentPage, SavePageContent, SearchBlocks, SearchDocuments,
    SourcePayload, TargetRef, ToolExecutionMode, ToolInvocation, UpdatePageTitle,
};
use mnote_editor_core::{
    apply_ai_pipeline, export_markdown, import_markdown, BlockType, DocumentBlock,
    EditorAiScenario, EditorCommand, EditorInputKind, EditorPipelineRequest, EditorSession,
    VisibilitySnapshot,
};
use reqwest::blocking::Client;
use serde::Serialize;
use serde_json::{json, Value};
use std::env;
use storage_convex_bridge::{build_query_request, build_write_request, BridgeContext};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CliContext {
    pub actor_id: String,
    pub actor_type: String,
    pub session_id: Option<String>,
    pub reason: Option<String>,
    pub idempotency_key: Option<String>,
    pub validate_only: bool,
    pub dry_run: bool,
}

impl Default for CliContext {
    fn default() -> Self {
        Self {
            actor_id: "cli_user".into(),
            actor_type: "human".into(),
            session_id: None,
            reason: None,
            idempotency_key: None,
            validate_only: false,
            dry_run: false,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CliError {
    pub code: &'static str,
    pub message: String,
}

impl CliError {
    pub fn validation(message: impl Into<String>) -> Self {
        Self {
            code: "VALIDATION_ERROR",
            message: message.into(),
        }
    }

    pub fn transport(message: impl Into<String>) -> Self {
        Self {
            code: "TRANSPORT_ERROR",
            message: message.into(),
        }
    }
}

pub type CliResult<T> = Result<T, CliError>;

#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CliJsonOutput {
    pub ok: bool,
    pub entrypoint: &'static str,
    pub domain: String,
    pub action: String,
    pub context: CliOutputContext,
    pub operation: CliOperationOutput,
}

#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CliOutputContext {
    pub request_id: String,
    pub trace_id: String,
    pub actor_id: String,
    pub actor_type: String,
    pub session_id: Option<String>,
    pub workspace_id: Option<String>,
    pub idempotency_key: Option<String>,
    pub validate_only: bool,
    pub dry_run: bool,
}

#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum CliOperationOutput {
    Command {
        name: String,
        command_id: String,
        normalized_input: Value,
        transport: CliTransportPlan,
        #[serde(skip_serializing_if = "Option::is_none")]
        execution: Option<Value>,
    },
    Query {
        name: String,
        normalized_input: Value,
        transport: CliTransportPlan,
        #[serde(skip_serializing_if = "Option::is_none")]
        execution: Option<Value>,
    },
    Tool {
        tool_name: String,
        invocation_kind: String,
        execution_mode: String,
        toolset_id: String,
        effect: String,
        requires_confirmation: bool,
        normalized_input: Value,
        #[serde(skip_serializing_if = "Option::is_none")]
        execution: Option<Value>,
    },
}

#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CliTransportPlan {
    pub kind: String,
    pub function_name: String,
    pub payload_json: String,
    pub args_json: Value,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorCliOutput {
    pub ok: bool,
    pub entrypoint: &'static str,
    pub operation: &'static str,
    pub block_count: usize,
    pub output_markdown: String,
    pub visible_block_ids: Vec<String>,
    pub outline: Vec<EditorOutlineItem>,
    pub audit: Vec<EditorAuditItem>,
    pub change_report: EditorChangeReportOutput,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub undo_applied: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub redo_applied: Option<bool>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorOutlineItem {
    pub block_id: String,
    pub level: u8,
    pub title: String,
    pub depth: usize,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAuditItem {
    pub command: String,
    pub target_block_id: Option<String>,
    pub before: Option<EditorBlockSnapshot>,
    pub after: Option<EditorBlockSnapshot>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditorBlockSnapshot {
    pub block_id: String,
    pub block_type: String,
    pub text: String,
    pub parent_id: Option<String>,
    pub indent: u16,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct EditorChangeReportOutput {
    #[serde(default)]
    pub created_blocks: Vec<String>,
    #[serde(default)]
    pub updated_blocks: Vec<String>,
    #[serde(default)]
    pub moved_blocks: Vec<String>,
    #[serde(default)]
    pub removed_blocks: Vec<String>,
    #[serde(default)]
    pub notes: Vec<String>,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum EditorInputKindArg {
    PlainText,
    Markdown,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum EditorAiScenarioArg {
    MeetingNotesToTodos,
    LongParagraphToTitle,
    PageReorder,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageCreateArgs<'a> {
    pub page_id: &'a str,
    pub workspace_id: &'a str,
    pub parent_id: Option<&'a str>,
    pub title: &'a str,
    pub access_scope: &'a str,
    pub content_json: &'a str,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageMoveArgs<'a> {
    pub page_id: &'a str,
    pub workspace_id: Option<&'a str>,
    pub parent_id: Option<&'a str>,
    pub sort_order: i64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageDeleteArgs<'a> {
    pub page_id: &'a str,
    pub workspace_id: Option<&'a str>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageRestoreArgs<'a> {
    pub page_id: &'a str,
    pub workspace_id: Option<&'a str>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockMoveArgs<'a> {
    pub source_document_id: &'a str,
    pub block_id: &'a str,
    pub target_document_id: &'a str,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockEmbedArgs<'a> {
    pub source_document_id: &'a str,
    pub block_id: &'a str,
    pub target_document_id: &'a str,
    pub target_block_id: Option<&'a str>,
}

pub fn supported_command_surface() -> Vec<&'static str> {
    let mut surface = vec![
        "page get",
        "page title",
        "page save",
        "page create",
        "page move",
        "page delete",
        "page restore",
        "block insert",
        "block patch",
        "block move",
        "block embed",
        "mindmap get",
        "mindmap put",
        "mindmap op",
        "search documents",
        "search blocks",
        "sidebar dataset",
        "editor markdown-roundtrip",
        "editor session-demo",
        "tool run",
    ];
    for tool_name in default_tool_registry().tool_names() {
        surface.push(Box::leak(
            format!("tool run --tool-name {tool_name}").into_boxed_str(),
        ));
    }
    surface
}

pub fn supported_json_contracts() -> Vec<&'static str> {
    let mut contracts = vec![
        "page.get",
        "page.title",
        "page.save",
        "page.create",
        "page.move",
        "page.delete",
        "page.restore",
        "block.insert",
        "block.patch",
        "block.move",
        "block.embed",
        "mindmap.get",
        "mindmap.put",
        "mindmap.op",
        "search.documents",
        "search.blocks",
        "sidebar.dataset",
        "editor.markdown_roundtrip",
        "editor.session_demo",
        "tool.run",
    ];
    for tool_name in default_tool_registry().tool_names() {
        contracts.push(Box::leak(format!("tool.{tool_name}").into_boxed_str()));
    }
    contracts
}

pub fn editor_markdown_roundtrip(markdown: &str) -> CliResult<EditorCliOutput> {
    let document = import_markdown(markdown).map_err(map_editor_error)?;
    let projection = VisibilitySnapshot::derive(&document);
    Ok(build_editor_output(
        "markdown_roundtrip",
        document.blocks().len(),
        export_markdown(&document),
        &projection,
        Vec::new(),
        EditorChangeReportOutput::default(),
        None,
        None,
    ))
}

pub fn editor_ai_pipeline(
    kind: EditorInputKind,
    scenario: EditorAiScenario,
    input: &str,
) -> CliResult<EditorCliOutput> {
    let result = apply_ai_pipeline(EditorPipelineRequest {
        kind,
        scenario,
        input: input.to_string(),
    })
    .map_err(map_editor_error)?;
    let projection = VisibilitySnapshot::derive(&result.document);
    Ok(build_editor_output(
        "ai_pipeline",
        result.document.blocks().len(),
        result.markdown,
        &projection,
        result
            .audit
            .into_iter()
            .map(|item| EditorAuditItem {
                command: item.command,
                target_block_id: item.target_block_id,
                before: item.before.map(|snapshot| EditorBlockSnapshot {
                    block_id: snapshot.block_id,
                    block_type: snapshot.block_type,
                    text: snapshot.text,
                    parent_id: snapshot.parent_id,
                    indent: snapshot.indent,
                }),
                after: item.after.map(|snapshot| EditorBlockSnapshot {
                    block_id: snapshot.block_id,
                    block_type: snapshot.block_type,
                    text: snapshot.text,
                    parent_id: snapshot.parent_id,
                    indent: snapshot.indent,
                }),
            })
            .collect(),
        EditorChangeReportOutput {
            created_blocks: result.change_report.created_blocks,
            updated_blocks: result.change_report.updated_blocks,
            moved_blocks: result.change_report.moved_blocks,
            removed_blocks: result.change_report.removed_blocks,
            notes: result.change_report.notes,
        },
        None,
        None,
    ))
}

pub fn editor_session_demo(markdown: &str) -> CliResult<EditorCliOutput> {
    let document = import_markdown(markdown).map_err(map_editor_error)?;
    let bootstrap = if document.blocks().is_empty() {
        mnote_editor_core::DocumentModel::new(vec![DocumentBlock::new(
            "demo_root",
            BlockType::Paragraph,
        )
        .with_text("demo root")])
    } else {
        document
    };

    let mut session = EditorSession::new(bootstrap);
    let primary_block_id = session
        .document()
        .blocks()
        .first()
        .map(|block| block.id.clone())
        .ok_or_else(|| CliError::validation("session demo 缺少可编辑块"))?;
    let original_text = session
        .document()
        .block(&primary_block_id)
        .map(|block| block.content.text.clone())
        .unwrap_or_default();
    let next_text = if original_text.trim().is_empty() {
        "demo edited".to_string()
    } else {
        format!("{original_text} [edited]")
    };

    session
        .apply_command(EditorCommand::ReplaceBlock {
            block_id: primary_block_id.clone(),
            text: next_text,
        })
        .map_err(map_editor_error)?;
    session
        .apply_command(EditorCommand::InsertBlockAfter {
            after_block_id: Some(primary_block_id),
            block: DocumentBlock::new("cli_demo_block", BlockType::Paragraph)
                .with_text("CLI demo block"),
        })
        .map_err(map_editor_error)?;

    let undo_applied = session.undo();
    let redo_applied = session.redo();
    let projection = VisibilitySnapshot::derive(session.document());
    Ok(build_editor_output(
        "session_demo",
        session.document().blocks().len(),
        export_markdown(session.document()),
        &projection,
        Vec::new(),
        EditorChangeReportOutput::default(),
        Some(undo_applied),
        Some(redo_applied),
    ))
}

pub fn render_editor_plain_output(output: &EditorCliOutput) -> String {
    format!(
        "entrypoint={} operation={} block_count={} visible_blocks={} audit_count={} updated_blocks={} undo_applied={} redo_applied={}",
        output.entrypoint,
        output.operation,
        output.block_count,
        output.visible_block_ids.join(","),
        output.audit.len(),
        output.change_report.updated_blocks.join(","),
        output.undo_applied.unwrap_or(false),
        output.redo_applied.unwrap_or(false),
    )
}

fn build_editor_output(
    operation: &'static str,
    block_count: usize,
    output_markdown: String,
    projection: &VisibilitySnapshot,
    audit: Vec<EditorAuditItem>,
    change_report: EditorChangeReportOutput,
    undo_applied: Option<bool>,
    redo_applied: Option<bool>,
) -> EditorCliOutput {
    EditorCliOutput {
        ok: true,
        entrypoint: "mnote-cli",
        operation,
        block_count,
        output_markdown,
        visible_block_ids: projection.visible_block_ids.clone(),
        outline: projection
            .outline
            .iter()
            .map(|entry| EditorOutlineItem {
                block_id: entry.block_id.clone(),
                level: entry.level,
                title: entry.title.clone(),
                depth: entry.depth,
            })
            .collect(),
        audit,
        change_report,
        undo_applied,
        redo_applied,
    }
}

pub fn render_plain_output(output: &CliJsonOutput) -> String {
    match &output.operation {
        CliOperationOutput::Command {
            name,
            command_id,
            transport,
            execution,
            ..
        } => format!(
            "domain={} action={} kind=command name={} command_id={} function={} executed={} request_id={} trace_id={}",
            output.domain,
            output.action,
            name,
            command_id,
            transport.function_name,
            execution.is_some(),
            output.context.request_id,
            output.context.trace_id,
        ),
        CliOperationOutput::Query {
            name,
            transport,
            execution,
            ..
        } => format!(
            "domain={} action={} kind=query name={} function={} executed={} request_id={} trace_id={}",
            output.domain,
            output.action,
            name,
            transport.function_name,
            execution.is_some(),
            output.context.request_id,
            output.context.trace_id,
        ),
        CliOperationOutput::Tool {
            tool_name,
            invocation_kind,
            execution_mode,
            toolset_id,
            effect,
            requires_confirmation,
            execution,
            ..
        } => format!(
            "domain={} action={} kind=tool tool={} invocation_kind={} execution_mode={} toolset={} effect={} requires_confirmation={} executed={} request_id={} trace_id={}",
            output.domain,
            output.action,
            tool_name,
            invocation_kind,
            execution_mode,
            toolset_id,
            effect,
            requires_confirmation,
            execution.is_some(),
            output.context.request_id,
            output.context.trace_id,
        ),
    }
}

pub fn plan_page_get(
    ctx: &CliContext,
    page_id: &str,
    workspace_id: Option<&str>,
) -> CliResult<CliJsonOutput> {
    let bridge = build_bridge_context(ctx, workspace_id, "page", "get", page_id);
    let query = QueryEnvelope {
        name: "documents.content.get".into(),
        payload: GetPageContent {
            page_id: page_id.into(),
            workspace_id: workspace_id.map(|value| value.into()),
        },
    };
    let request = build_query_request(&bridge, &query).map_err(map_bridge_error)?;
    Ok(build_query_output(
        "page",
        "get",
        &bridge,
        query.name,
        json!({
            "pageId": page_id,
            "workspaceId": workspace_id,
        }),
        CliTransportPlan {
            kind: "convex_query".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "id": page_id,
            }),
        },
    ))
}

pub fn plan_page_title(
    ctx: &CliContext,
    page_id: &str,
    workspace_id: Option<&str>,
    title: &str,
) -> CliResult<CliJsonOutput> {
    let bridge = build_bridge_context(ctx, workspace_id, "page", "title", page_id);
    let command = CommandEnvelope {
        name: "documents.title.update".into(),
        command_id: build_command_id("page", "title", page_id),
        idempotency_key: bridge.idempotency_key.clone(),
        actor: build_actor_payload(ctx),
        source: build_source_payload(),
        target: Some(TargetRef {
            workspace_id: workspace_id.map(|value| value.into()),
            page_id: Some(page_id.into()),
            block_id: None,
        }),
        payload: UpdatePageTitle {
            page_id: page_id.into(),
            title: title.into(),
        },
        reason: ctx.reason.clone(),
        refs: vec!["phase2-cli".into()],
        dry_run: ctx.dry_run,
        validate_only: ctx.validate_only,
    };
    let request = build_write_request(&bridge, &command).map_err(map_bridge_error)?;
    Ok(build_command_output(
        "page",
        "title",
        &bridge,
        command.name,
        command.command_id,
        json!({
            "pageId": page_id,
            "workspaceId": workspace_id,
            "title": title,
        }),
        CliTransportPlan {
            kind: "convex_mutation".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "id": page_id,
                "title": title,
            }),
        },
    ))
}

pub fn plan_page_save(
    ctx: &CliContext,
    page_id: &str,
    workspace_id: Option<&str>,
    revision: Option<u64>,
    content_json: &str,
    conflict_detection_key: Option<&str>,
) -> CliResult<CliJsonOutput> {
    let content_value = parse_json_value(content_json, "content_json")?;
    let bridge = build_bridge_context(ctx, workspace_id, "page", "save", page_id);
    let command = CommandEnvelope {
        name: "documents.save".into(),
        command_id: build_command_id("page", "save", page_id),
        idempotency_key: bridge.idempotency_key.clone(),
        actor: build_actor_payload(ctx),
        source: build_source_payload(),
        target: Some(TargetRef {
            workspace_id: workspace_id.map(|value| value.into()),
            page_id: Some(page_id.into()),
            block_id: None,
        }),
        payload: SavePageContent {
            page_id: page_id.into(),
            workspace_id: workspace_id.map(|value| value.into()),
            revision,
            content_json: content_json.into(),
            conflict_detection_key: conflict_detection_key.map(|value| value.into()),
        },
        reason: ctx.reason.clone(),
        refs: vec!["phase2-cli".into()],
        dry_run: ctx.dry_run,
        validate_only: ctx.validate_only,
    };
    let request = build_write_request(&bridge, &command).map_err(map_bridge_error)?;
    Ok(build_command_output(
        "page",
        "save",
        &bridge,
        command.name,
        command.command_id,
        json!({
            "pageId": page_id,
            "workspaceId": workspace_id,
            "revision": revision,
            "content": content_value,
            "conflictDetectionKey": conflict_detection_key,
        }),
        CliTransportPlan {
            kind: "convex_mutation".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "id": page_id,
                "content": content_value,
                "expectedRevision": revision,
                "conflictDetectionKey": conflict_detection_key,
            }),
        },
    ))
}

pub fn plan_page_create(ctx: &CliContext, args: &PageCreateArgs<'_>) -> CliResult<CliJsonOutput> {
    let content_value = parse_json_value(args.content_json, "content_json")?;
    let bridge = build_bridge_context(ctx, Some(args.workspace_id), "page", "create", args.page_id);
    let command = CommandEnvelope {
        name: "documents.create".into(),
        command_id: build_command_id("page", "create", args.page_id),
        idempotency_key: bridge.idempotency_key.clone(),
        actor: build_actor_payload(ctx),
        source: build_source_payload(),
        target: Some(TargetRef {
            workspace_id: Some(args.workspace_id.into()),
            page_id: Some(args.page_id.into()),
            block_id: None,
        }),
        payload: CreateDocumentPage {
            page_id: args.page_id.into(),
            parent_page_id: args.parent_id.map(|value| value.into()),
            title: args.title.into(),
            workspace_seed_id: args.workspace_id.into(),
        },
        reason: ctx.reason.clone(),
        refs: vec!["phase8-cli".into(), "task-048".into()],
        dry_run: ctx.dry_run,
        validate_only: ctx.validate_only,
    };
    let request = build_write_request(&bridge, &command).map_err(map_bridge_error)?;
    Ok(build_command_output(
        "page",
        "create",
        &bridge,
        command.name,
        command.command_id,
        json!({
            "pageId": args.page_id,
            "workspaceId": args.workspace_id,
            "parentId": args.parent_id,
            "title": args.title,
            "accessScope": args.access_scope,
            "content": content_value,
        }),
        CliTransportPlan {
            kind: "convex_mutation".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "id": args.page_id,
                "workspaceId": args.workspace_id,
                "parentId": args.parent_id,
                "title": args.title,
                "accessScope": args.access_scope,
                "content": content_value,
            }),
        },
    ))
}

pub fn plan_page_move(ctx: &CliContext, args: &PageMoveArgs<'_>) -> CliResult<CliJsonOutput> {
    let bridge = build_bridge_context(ctx, args.workspace_id, "page", "move", args.page_id);
    let command = CommandEnvelope {
        name: "documents.move".into(),
        command_id: build_command_id("page", "move", args.page_id),
        idempotency_key: bridge.idempotency_key.clone(),
        actor: build_actor_payload(ctx),
        source: build_source_payload(),
        target: Some(TargetRef {
            workspace_id: args.workspace_id.map(|value| value.into()),
            page_id: Some(args.page_id.into()),
            block_id: None,
        }),
        payload: MoveDocumentPage {
            page_id: args.page_id.into(),
            parent_page_id: args.parent_id.map(|value| value.into()),
            sort_order: args.sort_order,
        },
        reason: ctx.reason.clone(),
        refs: vec!["phase8-cli".into(), "task-048".into()],
        dry_run: ctx.dry_run,
        validate_only: ctx.validate_only,
    };
    let request = build_write_request(&bridge, &command).map_err(map_bridge_error)?;
    Ok(build_command_output(
        "page",
        "move",
        &bridge,
        command.name,
        command.command_id,
        json!({
            "pageId": args.page_id,
            "workspaceId": args.workspace_id,
            "parentId": args.parent_id,
            "sortOrder": args.sort_order,
        }),
        CliTransportPlan {
            kind: "convex_mutation".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "id": args.page_id,
                "parentId": args.parent_id,
                "sortOrder": args.sort_order,
            }),
        },
    ))
}

pub fn plan_page_delete(ctx: &CliContext, args: &PageDeleteArgs<'_>) -> CliResult<CliJsonOutput> {
    let bridge = build_bridge_context(ctx, args.workspace_id, "page", "delete", args.page_id);
    let command = CommandEnvelope {
        name: "documents.delete".into(),
        command_id: build_command_id("page", "delete", args.page_id),
        idempotency_key: bridge.idempotency_key.clone(),
        actor: build_actor_payload(ctx),
        source: build_source_payload(),
        target: Some(TargetRef {
            workspace_id: args.workspace_id.map(|value| value.into()),
            page_id: Some(args.page_id.into()),
            block_id: None,
        }),
        payload: DeleteDocumentPage {
            page_id: args.page_id.into(),
        },
        reason: ctx.reason.clone(),
        refs: vec!["phase8-cli".into(), "task-048".into()],
        dry_run: ctx.dry_run,
        validate_only: ctx.validate_only,
    };
    let request = build_write_request(&bridge, &command).map_err(map_bridge_error)?;
    Ok(build_command_output(
        "page",
        "delete",
        &bridge,
        command.name,
        command.command_id,
        json!({
            "pageId": args.page_id,
            "workspaceId": args.workspace_id,
        }),
        CliTransportPlan {
            kind: "convex_mutation".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "id": args.page_id,
            }),
        },
    ))
}

pub fn plan_page_restore(ctx: &CliContext, args: &PageRestoreArgs<'_>) -> CliResult<CliJsonOutput> {
    let bridge = build_bridge_context(ctx, args.workspace_id, "page", "restore", args.page_id);
    let command = CommandEnvelope {
        name: "documents.restore".into(),
        command_id: build_command_id("page", "restore", args.page_id),
        idempotency_key: bridge.idempotency_key.clone(),
        actor: build_actor_payload(ctx),
        source: build_source_payload(),
        target: Some(TargetRef {
            workspace_id: args.workspace_id.map(|value| value.into()),
            page_id: Some(args.page_id.into()),
            block_id: None,
        }),
        payload: RestoreDocumentPage {
            page_id: args.page_id.into(),
        },
        reason: ctx.reason.clone(),
        refs: vec!["phase8-cli".into(), "task-048".into()],
        dry_run: ctx.dry_run,
        validate_only: ctx.validate_only,
    };
    let request = build_write_request(&bridge, &command).map_err(map_bridge_error)?;
    Ok(build_command_output(
        "page",
        "restore",
        &bridge,
        command.name,
        command.command_id,
        json!({
            "pageId": args.page_id,
            "workspaceId": args.workspace_id,
        }),
        CliTransportPlan {
            kind: "convex_mutation".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "id": args.page_id,
            }),
        },
    ))
}

pub fn plan_block_insert(
    ctx: &CliContext,
    workspace_id: &str,
    page_id: &str,
    content: &str,
    block_type: &str,
    parent_block_id: Option<&str>,
    prev_block_id: Option<&str>,
) -> CliResult<CliJsonOutput> {
    let bridge = build_bridge_context(ctx, Some(workspace_id), "block", "insert", page_id);
    let command = CommandEnvelope {
        name: "insert_block".into(),
        command_id: build_command_id("block", "insert", page_id),
        idempotency_key: bridge.idempotency_key.clone(),
        actor: build_actor_payload(ctx),
        source: build_source_payload(),
        target: Some(TargetRef {
            workspace_id: Some(workspace_id.into()),
            page_id: Some(page_id.into()),
            block_id: None,
        }),
        payload: core_protocol::InsertBlock {
            page_id: page_id.into(),
            block_type: block_type.into(),
            content: content.into(),
            parent_block_id: parent_block_id.map(|value| value.into()),
            prev_block_id: prev_block_id.map(|value| value.into()),
        },
        reason: ctx.reason.clone(),
        refs: vec!["phase2-cli".into()],
        dry_run: ctx.dry_run,
        validate_only: ctx.validate_only,
    };
    let request = build_write_request(&bridge, &command).map_err(map_bridge_error)?;
    Ok(build_command_output(
        "block",
        "insert",
        &bridge,
        command.name,
        command.command_id,
        json!({
            "workspaceId": workspace_id,
            "pageId": page_id,
            "content": content,
            "blockType": block_type,
            "parentBlockId": parent_block_id,
            "prevBlockId": prev_block_id,
        }),
        CliTransportPlan {
            kind: "convex_mutation".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "pageId": page_id,
                "content": content,
                "blockType": block_type,
                "parentBlockId": parent_block_id,
                "prevBlockId": prev_block_id,
            }),
        },
    ))
}

pub fn plan_block_patch(
    ctx: &CliContext,
    workspace_id: Option<&str>,
    page_id: &str,
    block_id: &str,
    revision: Option<u64>,
    snapshot_json: &str,
    conflict_detection_key: Option<&str>,
) -> CliResult<CliJsonOutput> {
    let snapshot_value = parse_json_value(snapshot_json, "snapshot_json")?;
    let bridge = build_bridge_context(ctx, workspace_id, "block", "patch", block_id);
    let command = CommandEnvelope {
        name: "blocks.patch".into(),
        command_id: build_command_id("block", "patch", block_id),
        idempotency_key: bridge.idempotency_key.clone(),
        actor: build_actor_payload(ctx),
        source: build_source_payload(),
        target: Some(TargetRef {
            workspace_id: workspace_id.map(|value| value.into()),
            page_id: Some(page_id.into()),
            block_id: Some(block_id.into()),
        }),
        payload: PatchPageBlock {
            page_id: page_id.into(),
            block_id: block_id.into(),
            workspace_id: workspace_id.map(|value| value.into()),
            revision,
            block_snapshot_json: snapshot_json.into(),
            conflict_detection_key: conflict_detection_key.map(|value| value.into()),
        },
        reason: ctx.reason.clone(),
        refs: vec!["phase2-cli".into()],
        dry_run: ctx.dry_run,
        validate_only: ctx.validate_only,
    };
    let request = build_write_request(&bridge, &command).map_err(map_bridge_error)?;
    Ok(build_command_output(
        "block",
        "patch",
        &bridge,
        command.name,
        command.command_id,
        json!({
            "workspaceId": workspace_id,
            "pageId": page_id,
            "blockId": block_id,
            "revision": revision,
            "snapshot": snapshot_value,
            "conflictDetectionKey": conflict_detection_key,
        }),
        CliTransportPlan {
            kind: "convex_mutation".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "pageId": page_id,
                "blockId": block_id,
                "snapshot": snapshot_value,
                "expectedRevision": revision,
                "conflictDetectionKey": conflict_detection_key,
            }),
        },
    ))
}

pub fn plan_block_move(ctx: &CliContext, args: &BlockMoveArgs<'_>) -> CliResult<CliJsonOutput> {
    let bridge = build_bridge_context(ctx, None, "block", "move", args.block_id);
    let command = CommandEnvelope {
        name: "blocks.move".into(),
        command_id: build_command_id("block", "move", args.block_id),
        idempotency_key: bridge.idempotency_key.clone(),
        actor: build_actor_payload(ctx),
        source: build_source_payload(),
        target: Some(TargetRef {
            workspace_id: None,
            page_id: Some(args.target_document_id.into()),
            block_id: Some(args.block_id.into()),
        }),
        payload: core_protocol::MoveBlock {
            block_id: args.block_id.into(),
            new_parent_block_id: None,
            new_page_id: Some(args.target_document_id.into()),
            prev_block_id: None,
        },
        reason: ctx.reason.clone(),
        refs: vec!["phase8-cli".into(), "task-048".into()],
        dry_run: ctx.dry_run,
        validate_only: ctx.validate_only,
    };
    let request = build_write_request(&bridge, &command).map_err(map_bridge_error)?;
    Ok(build_command_output(
        "block",
        "move",
        &bridge,
        command.name,
        command.command_id,
        json!({
            "sourceDocumentId": args.source_document_id,
            "blockId": args.block_id,
            "targetDocumentId": args.target_document_id,
        }),
        CliTransportPlan {
            kind: "convex_mutation".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "id": args.block_id,
                "sourceDocumentId": args.source_document_id,
                "targetDocumentId": args.target_document_id,
            }),
        },
    ))
}

pub fn plan_block_embed(ctx: &CliContext, args: &BlockEmbedArgs<'_>) -> CliResult<CliJsonOutput> {
    let bridge = build_bridge_context(ctx, None, "block", "embed", args.block_id);
    let command = CommandEnvelope {
        name: "blocks.embed".into(),
        command_id: build_command_id("block", "embed", args.block_id),
        idempotency_key: bridge.idempotency_key.clone(),
        actor: build_actor_payload(ctx),
        source: build_source_payload(),
        target: Some(TargetRef {
            workspace_id: None,
            page_id: Some(args.target_document_id.into()),
            block_id: Some(args.block_id.into()),
        }),
        payload: core_protocol::EmbedBlock {
            source_document_id: args.source_document_id.into(),
            source_block_id: args.block_id.into(),
            target_document_id: args.target_document_id.into(),
            target_block_id: args.target_block_id.map(|value| value.into()),
        },
        reason: ctx.reason.clone(),
        refs: vec!["phase8-cli".into(), "task-048".into()],
        dry_run: ctx.dry_run,
        validate_only: ctx.validate_only,
    };
    let request = build_write_request(&bridge, &command).map_err(map_bridge_error)?;
    Ok(build_command_output(
        "block",
        "embed",
        &bridge,
        command.name,
        command.command_id,
        json!({
            "sourceDocumentId": args.source_document_id,
            "blockId": args.block_id,
            "targetDocumentId": args.target_document_id,
            "targetBlockId": args.target_block_id,
        }),
        CliTransportPlan {
            kind: "convex_mutation".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "sourceDocumentId": args.source_document_id,
                "blockId": args.block_id,
                "targetDocumentId": args.target_document_id,
                "targetBlockId": args.target_block_id,
            }),
        },
    ))
}

pub fn plan_search_documents(
    ctx: &CliContext,
    query: &str,
    workspace_id: Option<&str>,
    limit: u32,
    cursor: Option<&str>,
) -> CliResult<CliJsonOutput> {
    let workspace_id =
        workspace_id.ok_or_else(|| CliError::validation("search documents 缺少 --workspace-id"))?;
    let bridge = build_bridge_context(ctx, Some(workspace_id), "search", "documents", query);
    let envelope = QueryEnvelope {
        name: "search.documents".into(),
        payload: SearchDocuments {
            query: query.into(),
            workspace_id: workspace_id.into(),
            page_id: None,
            pagination: Pagination {
                limit,
                cursor: cursor.map(|value| value.into()),
            },
            title_only: false,
            exact: false,
            include_ocr: false,
            time_range: "any".into(),
            time_field: "updated".into(),
            custom_range_from: None,
            custom_range_to: None,
        },
    };
    let request = build_query_request(&bridge, &envelope).map_err(map_bridge_error)?;
    Ok(build_query_output(
        "search",
        "documents",
        &bridge,
        envelope.name,
        json!({
            "query": query,
            "workspaceId": workspace_id,
            "pagination": {
                "limit": limit,
                "cursor": cursor,
            },
            "filters": {
                "titleOnly": false,
                "exact": false,
                "includeOcr": false,
                "timeRange": "any",
                "timeField": "updated",
            },
        }),
        CliTransportPlan {
            kind: "convex_query".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "query": query,
                "workspaceId": workspace_id,
                "pageId": Value::Null,
                "limit": limit,
                "cursor": cursor,
                "titleOnly": false,
                "exact": false,
                "includeOcr": false,
                "timeRange": "any",
                "timeField": "updated",
                "customRangeFrom": Value::Null,
                "customRangeTo": Value::Null,
            }),
        },
    ))
}

pub fn plan_search_blocks(
    ctx: &CliContext,
    query: &str,
    page_id: Option<&str>,
    limit: u32,
    cursor: Option<&str>,
) -> CliResult<CliJsonOutput> {
    let bridge = build_bridge_context(ctx, None, "search", "blocks", query);
    let envelope = QueryEnvelope {
        name: "search_blocks".into(),
        payload: SearchBlocks {
            query: query.into(),
            page_id: page_id.map(|value| value.into()),
            pagination: Pagination {
                limit,
                cursor: cursor.map(|value| value.into()),
            },
        },
    };
    let request = build_query_request(&bridge, &envelope).map_err(map_bridge_error)?;
    Ok(build_query_output(
        "search",
        "blocks",
        &bridge,
        envelope.name,
        json!({
            "query": query,
            "pageId": page_id,
            "pagination": {
                "limit": limit,
                "cursor": cursor,
            },
        }),
        CliTransportPlan {
            kind: "convex_query".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "query": query,
                "pageId": page_id,
                "limit": limit,
                "cursor": cursor,
            }),
        },
    ))
}

pub fn plan_sidebar_dataset(ctx: &CliContext, workspace_id: &str) -> CliResult<CliJsonOutput> {
    let bridge = build_bridge_context(ctx, Some(workspace_id), "sidebar", "dataset", workspace_id);
    let envelope = QueryEnvelope {
        name: "sidebar.dataset.list".into(),
        payload: ListSidebarDataset {
            workspace_id: workspace_id.into(),
        },
    };
    let request = build_query_request(&bridge, &envelope).map_err(map_bridge_error)?;
    Ok(build_query_output(
        "sidebar",
        "dataset",
        &bridge,
        envelope.name,
        json!({
            "workspaceId": workspace_id,
        }),
        CliTransportPlan {
            kind: "convex_query".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "workspaceId": workspace_id,
            }),
        },
    ))
}

pub fn plan_mindmap_get(
    ctx: &CliContext,
    workspace_id: Option<&str>,
    document_id: &str,
    mindmap_id: &str,
) -> CliResult<CliJsonOutput> {
    let bridge = build_bridge_context(
        ctx,
        workspace_id,
        "mindmap",
        "get",
        &format!("{document_id}_{mindmap_id}"),
    );
    let envelope = QueryEnvelope {
        name: "mindmaps.get".into(),
        payload: GetMindmap {
            document_id: document_id.into(),
            mindmap_id: mindmap_id.into(),
            workspace_id: workspace_id.map(|value| value.into()),
        },
    };
    let request = build_query_request(&bridge, &envelope).map_err(map_bridge_error)?;
    Ok(build_query_output(
        "mindmap",
        "get",
        &bridge,
        envelope.name,
        json!({
            "workspaceId": workspace_id,
            "documentId": document_id,
            "mindmapId": mindmap_id,
        }),
        CliTransportPlan {
            kind: "convex_query".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "docId": document_id,
                "mindmapId": mindmap_id,
            }),
        },
    ))
}

pub fn plan_mindmap_put(
    ctx: &CliContext,
    workspace_id: Option<&str>,
    document_id: &str,
    mindmap_id: &str,
    data_json: &str,
    create_only: bool,
) -> CliResult<CliJsonOutput> {
    let data = parse_json_value(data_json, "data_json")?;
    serde_json::from_value::<MindmapTreeNode>(data.clone())
        .map_err(|error| CliError::validation(format!("data_json 不是合法导图树: {error}")))?;
    let bridge = build_bridge_context(
        ctx,
        workspace_id,
        "mindmap",
        "put",
        &format!("{document_id}_{mindmap_id}"),
    );
    let command = CommandEnvelope {
        name: "mindmaps.put".into(),
        command_id: build_command_id("mindmap", "put", &format!("{document_id}_{mindmap_id}")),
        idempotency_key: bridge.idempotency_key.clone(),
        actor: build_actor_payload(ctx),
        source: build_source_payload(),
        target: Some(TargetRef {
            workspace_id: workspace_id.map(|value| value.into()),
            page_id: Some(document_id.into()),
            block_id: Some(mindmap_id.into()),
        }),
        payload: PutMindmap {
            document_id: document_id.into(),
            mindmap_id: mindmap_id.into(),
            data_json: serde_json::to_string(&data).map_err(|error| {
                CliError::validation(format!("mindmap data_json 序列化失败: {error}"))
            })?,
            create_only,
        },
        reason: ctx.reason.clone(),
        refs: vec!["phase6-cli".into(), "task-032".into()],
        dry_run: ctx.dry_run,
        validate_only: ctx.validate_only,
    };
    let request = build_write_request(&bridge, &command).map_err(map_bridge_error)?;
    Ok(build_command_output(
        "mindmap",
        "put",
        &bridge,
        command.name,
        command.command_id,
        json!({
            "workspaceId": workspace_id,
            "documentId": document_id,
            "mindmapId": mindmap_id,
            "createOnly": create_only,
            "data": data,
        }),
        CliTransportPlan {
            kind: "convex_mutation".into(),
            function_name: request.function_name,
            payload_json: request.payload_json,
            args_json: json!({
                "docId": document_id,
                "mindmapId": mindmap_id,
                "data": data,
                "createOnly": create_only,
            }),
        },
    ))
}

pub fn plan_mindmap_op(
    ctx: &CliContext,
    workspace_id: Option<&str>,
    document_id: &str,
    mindmap_id: &str,
    ops_json: &str,
) -> CliResult<CliJsonOutput> {
    let ops_value = parse_json_value(ops_json, "ops_json")?;
    let ops: Vec<MindmapOp> = serde_json::from_value(ops_value.clone())
        .map_err(|error| CliError::validation(format!("ops_json 不是合法 MindmapOp[]: {error}")))?;
    if ops.is_empty() {
        return Err(CliError::validation("ops_json 不能为空数组"));
    }
    let bridge = build_bridge_context(
        ctx,
        workspace_id,
        "mindmap",
        "op",
        &format!("{document_id}_{mindmap_id}"),
    );
    let spec = default_tool_registry()
        .tool("mindmap_apply_ops")
        .ok_or_else(|| CliError::validation("未注册 tool: mindmap_apply_ops"))?;
    Ok(CliJsonOutput {
        ok: true,
        entrypoint: "mnote-cli",
        domain: "mindmap".into(),
        action: "op".into(),
        context: build_output_context(&bridge, ctx),
        operation: CliOperationOutput::Tool {
            tool_name: "mindmap_apply_ops".into(),
            invocation_kind: invocation_kind_label(&InvocationKind::Command).into(),
            execution_mode: core_protocol::tool_mode_label(&ToolExecutionMode::Result).into(),
            toolset_id: spec.toolset_id.into(),
            effect: tool_effect_label(&spec.effect).into(),
            requires_confirmation: spec.requires_confirmation,
            normalized_input: json!({
                "toolName": "mindmap_apply_ops",
                "workspaceId": workspace_id,
                "documentId": document_id,
                "mindmapId": mindmap_id,
                "ops": ops_value,
                "reason": ctx.reason,
                "validateOnly": bridge.validate_only,
                "dryRun": bridge.dry_run,
            }),
            execution: None,
        },
    })
}

pub fn plan_tool_run(
    ctx: &CliContext,
    tool_name: &str,
    invocation_kind: InvocationKind,
    execution_mode: ToolExecutionMode,
    args_json: &str,
) -> CliResult<CliJsonOutput> {
    let args_value = parse_json_value(args_json, "args_json")?;
    let bridge = build_bridge_context(ctx, None, "tool", "run", tool_name);
    let spec = default_tool_registry()
        .tool(tool_name)
        .ok_or_else(|| CliError::validation(format!("未知 tool: {tool_name}")))?;
    if spec.invocation_kind != invocation_kind {
        return Err(CliError::validation(format!(
            "tool {tool_name} 仅支持 {}，当前传入 {}",
            invocation_kind_label(&spec.invocation_kind),
            invocation_kind_label(&invocation_kind),
        )));
    }
    let invocation = ToolInvocation {
        tool: tool_name.into(),
        kind: invocation_kind,
        mode: execution_mode,
        args_json: args_json.into(),
    };
    let operation = CliOperationOutput::Tool {
        tool_name: invocation.tool,
        invocation_kind: invocation_kind_label(&invocation.kind).into(),
        execution_mode: core_protocol::tool_mode_label(&invocation.mode).into(),
        toolset_id: spec.toolset_id.into(),
        effect: tool_effect_label(&spec.effect).into(),
        requires_confirmation: spec.requires_confirmation,
        normalized_input: json!({
            "toolName": tool_name,
            "invocationKind": invocation_kind_label(&invocation.kind),
            "executionMode": core_protocol::tool_mode_label(&invocation.mode),
            "toolsetId": spec.toolset_id,
            "effect": tool_effect_label(&spec.effect),
            "requiresConfirmation": spec.requires_confirmation,
            "validateOnly": bridge.validate_only,
            "dryRun": bridge.dry_run,
            "args": args_value,
        }),
        execution: None,
    };
    Ok(CliJsonOutput {
        ok: true,
        entrypoint: "mnote-cli",
        domain: "tool".into(),
        action: "run".into(),
        context: build_output_context(&bridge, ctx),
        operation,
    })
}

pub fn execute_output(output: &CliJsonOutput, ctx: &CliContext) -> CliResult<CliJsonOutput> {
    let mut next = output.clone();
    match &mut next.operation {
        CliOperationOutput::Query {
            name,
            transport,
            execution,
            ..
        } => {
            *execution = Some(execute_query(name, transport, &next.context, ctx)?);
        }
        CliOperationOutput::Command {
            name,
            normalized_input,
            transport,
            execution,
            ..
        } => {
            *execution = Some(execute_command(
                name,
                normalized_input,
                transport,
                &next.context,
                ctx,
            )?);
        }
        CliOperationOutput::Tool {
            tool_name,
            invocation_kind,
            execution_mode,
            normalized_input,
            execution,
            ..
        } => {
            *execution = Some(execute_tool(
                tool_name,
                invocation_kind,
                execution_mode,
                normalized_input,
                &next.context,
                ctx,
            )?);
        }
    }
    Ok(next)
}

fn execute_query(
    name: &str,
    transport: &CliTransportPlan,
    context: &CliOutputContext,
    cli_ctx: &CliContext,
) -> CliResult<Value> {
    match name {
        "documents.content.get"
        | "documents.meta.get"
        | "mindmaps.get"
        | "sidebar.dataset.list" => execute_convex_query(transport, cli_ctx),
        "search.documents" => execute_search_documents(transport, context, cli_ctx),
        other => Err(CliError::validation(format!("暂不支持执行 query: {other}"))),
    }
}

fn execute_command(
    name: &str,
    normalized_input: &Value,
    transport: &CliTransportPlan,
    context: &CliOutputContext,
    cli_ctx: &CliContext,
) -> CliResult<Value> {
    match name {
        "documents.create"
        | "documents.move"
        | "documents.delete"
        | "documents.restore"
        | "documents.title.update"
        | "documents.save"
        | "mindmaps.put" => execute_convex_mutation(transport, cli_ctx),
        "insert_block" => execute_block_insert(normalized_input, context, cli_ctx),
        "blocks.patch" => execute_block_patch(normalized_input, transport, cli_ctx),
        "blocks.move" => execute_block_move(normalized_input, cli_ctx),
        "blocks.embed" => execute_block_embed(normalized_input, cli_ctx),
        other => Err(CliError::validation(format!(
            "暂不支持执行 command: {other}"
        ))),
    }
}

fn execute_tool(
    tool_name: &str,
    invocation_kind: &str,
    execution_mode: &str,
    normalized_input: &Value,
    context: &CliOutputContext,
    cli_ctx: &CliContext,
) -> CliResult<Value> {
    if execution_mode != "result" {
        return Err(CliError::validation(
            "tool --execute 目前只支持 mode=result",
        ));
    }
    let args = normalized_input.get("args").cloned().unwrap_or(Value::Null);
    let runtime_args = merge_tool_args(tool_name, normalized_input, &args, context);
    let target = build_tool_target(tool_name, normalized_input, context)?;
    let runtime_context = build_runtime_context(context, cli_ctx);
    let data = load_tool_data(tool_name, &runtime_args, target.as_ref(), cli_ctx)?;
    execute_runtime_query(RuntimeInput::Tool {
        context: runtime_context,
        tool: RuntimeToolInvocationWire {
            tool: tool_name.into(),
            kind: invocation_kind.into(),
            mode: Some("result".into()),
            args_json: runtime_args,
            target,
            reason: cli_ctx.reason.clone(),
            refs: vec!["mnote-cli-execute".into()],
        },
        data: Some(data),
    })
    .map_err(|error| CliError::transport(error.message))
}

fn merge_tool_args(
    tool_name: &str,
    normalized_input: &Value,
    args: &Value,
    context: &CliOutputContext,
) -> Value {
    let mut merged = args.as_object().cloned().unwrap_or_default();
    if let Some(workspace_id) = context.workspace_id.as_ref() {
        merged
            .entry("workspaceId")
            .or_insert_with(|| json!(workspace_id));
    }
    if let Some(document_id) = normalized_input.get("documentId").cloned() {
        merged.entry("documentId").or_insert(document_id);
    }
    if let Some(mindmap_id) = normalized_input.get("mindmapId").cloned() {
        merged.entry("mindmapId").or_insert(mindmap_id);
    }
    if let Some(ops) = normalized_input.get("ops").cloned() {
        merged.entry("ops").or_insert(ops);
    }
    if tool_name == "mindmap_apply_ops" {
        if let Some(page_id) = normalized_input.get("documentId").cloned() {
            merged.entry("pageId").or_insert(page_id);
        }
    }
    Value::Object(merged)
}

fn execute_search_documents(
    transport: &CliTransportPlan,
    context: &CliOutputContext,
    cli_ctx: &CliContext,
) -> CliResult<Value> {
    let workspace_id = transport
        .args_json
        .get("workspaceId")
        .and_then(Value::as_str)
        .ok_or_else(|| CliError::validation("search documents 缺少 workspaceId"))?;
    let documents = execute_convex_query_raw(
        "documents:listSearchDataByWorkspace",
        json!({ "workspaceId": workspace_id }),
        cli_ctx,
    )?;
    let mindmaps = execute_convex_query_raw(
        "mindmaps:listByWorkspace",
        json!({ "workspaceId": workspace_id, "includeDeleted": false }),
        cli_ctx,
    )
    .unwrap_or_else(|_| Value::Array(vec![]));
    let tables = execute_convex_query_raw(
        "tables:listByWorkspaceForSearch",
        json!({
            "userId": context.actor_id,
            "workspaceId": workspace_id,
            "includeArchived": false,
            "limit": 3000,
        }),
        cli_ctx,
    )
    .unwrap_or_else(|_| Value::Array(vec![]));
    let table_rows = execute_convex_query_raw(
        "tables:listRowsByWorkspaceForSearch",
        json!({
            "userId": context.actor_id,
            "workspaceId": workspace_id,
            "limit": 8000,
        }),
        cli_ctx,
    )
    .unwrap_or_else(|_| Value::Array(vec![]));
    let assets = execute_convex_query_raw(
        "mediaAssets:listSearchDataByWorkspace",
        json!({
            "userId": context.actor_id,
            "workspaceId": workspace_id,
            "includeDeleted": false,
            "limit": 5000,
        }),
        cli_ctx,
    )
    .unwrap_or_else(|_| Value::Array(vec![]));
    let dataset = json!({
        "documents": documents.as_array().cloned().unwrap_or_default().into_iter().map(|item| json!({
            "id": item.get("id").cloned().unwrap_or(Value::Null),
            "workspaceId": item.get("workspace_id").cloned().unwrap_or(json!(workspace_id)),
            "title": item.get("title").cloned().unwrap_or(Value::Null),
            "rawText": item.get("raw_text").cloned().unwrap_or(Value::Null),
            "createdAt": item.get("created_at").cloned().unwrap_or(Value::Null),
            "updatedAt": item.get("updated_at").cloned().unwrap_or(Value::Null),
        })).collect::<Vec<Value>>(),
        "mindmaps": mindmaps.as_array().cloned().unwrap_or_default().into_iter().map(|item| json!({
            "documentId": item.get("document_id").cloned().unwrap_or(Value::Null),
            "data": item.get("data").cloned().unwrap_or(Value::Null),
        })).collect::<Vec<Value>>(),
        "tables": tables.as_array().cloned().unwrap_or_default().into_iter().map(|item| json!({
            "id": item.get("id").cloned().unwrap_or(Value::Null),
            "documentId": item.get("document_id").cloned().unwrap_or(Value::Null),
            "title": item.get("title").cloned().unwrap_or(Value::Null),
        })).collect::<Vec<Value>>(),
        "tableRows": table_rows.as_array().cloned().unwrap_or_default().into_iter().map(|item| json!({
            "tableId": item.get("table_id").cloned().unwrap_or(Value::Null),
            "documentId": item.get("document_id").cloned().unwrap_or(Value::Null),
            "rowHash": item.get("row_hash").cloned().unwrap_or(Value::Null),
        })).collect::<Vec<Value>>(),
        "assets": assets.as_array().cloned().unwrap_or_default().into_iter().map(|item| json!({
            "id": item.get("id").cloned().unwrap_or(Value::Null),
            "documentId": item.get("document_id").cloned().unwrap_or(Value::Null),
            "assetType": item.get("asset_type").cloned().unwrap_or(Value::Null),
            "fileName": item.get("file_name").cloned().unwrap_or(Value::Null),
            "mimeType": item.get("mime_type").cloned().unwrap_or(Value::Null),
            "ocrText": item.get("ocr_text").cloned().unwrap_or(Value::Null),
            "ocrStatus": item.get("ocr_status").cloned().unwrap_or(Value::Null),
        })).collect::<Vec<Value>>(),
    });
    execute_runtime_query(RuntimeInput::Query {
        context: build_runtime_context(context, cli_ctx),
        query: RuntimeQueryEnvelopeWire {
            name: "search.documents".into(),
            payload: transport.args_json.clone(),
        },
        data: Some(dataset),
    })
    .map_err(|error| CliError::transport(error.message))
}

fn execute_block_insert(
    normalized_input: &Value,
    context: &CliOutputContext,
    cli_ctx: &CliContext,
) -> CliResult<Value> {
    let page_id = required_str(normalized_input, "pageId")?;
    let workspace_id = required_str(normalized_input, "workspaceId")?;
    let content = required_str(normalized_input, "content")?;
    let block_type = normalized_input
        .get("blockType")
        .and_then(Value::as_str)
        .unwrap_or("paragraph");
    let before_block_id = None::<String>;
    let after_block_id = normalized_input
        .get("prevBlockId")
        .and_then(Value::as_str)
        .map(str::to_string);
    let data = execute_convex_query_raw("documents:getContent", json!({ "id": page_id }), cli_ctx)?;
    let block_specs = json!([{
        "type": block_type,
        "text": content,
    }]);
    let runtime_result = execute_runtime_query(RuntimeInput::Tool {
        context: build_runtime_context(context, cli_ctx),
        tool: RuntimeToolInvocationWire {
            tool: "doc_insert_blocks".into(),
            kind: "command".into(),
            mode: Some("result".into()),
            args_json: json!({
                "pageId": page_id,
                "workspaceId": workspace_id,
                "afterBlockId": after_block_id,
                "beforeBlockId": before_block_id,
                "blocks": block_specs,
            }),
            target: Some(RuntimeTargetWire {
                workspace_id: Some(workspace_id.to_string()),
                page_id: Some(page_id.to_string()),
                block_id: None,
            }),
            reason: cli_ctx.reason.clone(),
            refs: vec!["mnote-cli-execute".into()],
        },
        data: Some(data.clone()),
    })
    .map_err(|error| CliError::transport(error.message))?;
    let next_content = write_blocks_back(
        &data,
        runtime_result
            .get("data")
            .cloned()
            .unwrap_or(Value::Array(vec![])),
    );
    let save_result = execute_convex_mutation_raw(
        "documents:updateContent",
        json!({
            "id": page_id,
            "content": next_content,
            "expectedRevision": data.get("revision").cloned().unwrap_or(Value::Null),
            "conflictDetectionKey": data.get("conflict_detection_key").cloned().unwrap_or(Value::Null),
        }),
        cli_ctx,
    )?;
    Ok(json!({
        "ok": true,
        "pageId": page_id,
        "inserted": runtime_result.get("inserted").cloned().unwrap_or(Value::Array(vec![])),
        "revision": save_result.get("revision").cloned().unwrap_or(Value::Null),
        "conflictDetectionKey": save_result.get("conflict_detection_key").cloned().unwrap_or(Value::Null),
        "data": runtime_result.get("data").cloned().unwrap_or(Value::Null),
    }))
}

fn execute_block_patch(
    normalized_input: &Value,
    transport: &CliTransportPlan,
    cli_ctx: &CliContext,
) -> CliResult<Value> {
    let page_id = required_str(normalized_input, "pageId")?;
    let block_id = required_str(normalized_input, "blockId")?;
    let snapshot = normalized_input
        .get("snapshot")
        .cloned()
        .ok_or_else(|| CliError::validation("block patch 缺少 snapshot"))?;
    let current =
        execute_convex_query_raw("documents:getContent", json!({ "id": page_id }), cli_ctx)?;
    let next_blocks = replace_block_subtree(extract_blocks(&current), block_id, &snapshot)?;
    let args = json!({
        "id": page_id,
        "content": write_blocks_back(&current, Value::Array(next_blocks)),
        "expectedRevision": transport.args_json.get("expectedRevision").cloned().unwrap_or(Value::Null),
        "conflictDetectionKey": transport.args_json.get("conflictDetectionKey").cloned().unwrap_or(Value::Null),
    });
    execute_convex_mutation_raw("documents:updateContent", args, cli_ctx)
}

fn execute_block_move(normalized_input: &Value, cli_ctx: &CliContext) -> CliResult<Value> {
    let source_document_id = required_str(normalized_input, "sourceDocumentId")?;
    let target_document_id = required_str(normalized_input, "targetDocumentId")?;
    let block_id = required_str(normalized_input, "blockId")?;

    let source = execute_convex_query_raw(
        "documents:getContent",
        json!({ "id": source_document_id }),
        cli_ctx,
    )?;
    let target = execute_convex_query_raw(
        "documents:getContent",
        json!({ "id": target_document_id }),
        cli_ctx,
    )?;
    let source_blocks = extract_blocks(&source);
    let target_blocks = extract_blocks(&target);
    let (removed, next_source_blocks) = remove_block_subtree(source_blocks, block_id)?;
    let mut next_target_blocks = target_blocks;
    next_target_blocks.push(removed);
    let next_source_content = write_blocks_back(&source, Value::Array(next_source_blocks));
    let next_target_content = write_blocks_back(&target, Value::Array(next_target_blocks));
    let source_save = execute_convex_mutation_raw(
        "documents:updateContent",
        json!({
            "id": source_document_id,
            "content": next_source_content,
            "expectedRevision": source.get("revision").cloned().unwrap_or(Value::Null),
            "conflictDetectionKey": source.get("conflict_detection_key").cloned().unwrap_or(Value::Null),
        }),
        cli_ctx,
    )?;
    let target_save = execute_convex_mutation_raw(
        "documents:updateContent",
        json!({
            "id": target_document_id,
            "content": next_target_content,
            "expectedRevision": target.get("revision").cloned().unwrap_or(Value::Null),
            "conflictDetectionKey": target.get("conflict_detection_key").cloned().unwrap_or(Value::Null),
        }),
        cli_ctx,
    )?;
    Ok(json!({
        "ok": true,
        "sourceRevision": source_save.get("revision").cloned().unwrap_or(Value::Null),
        "targetRevision": target_save.get("revision").cloned().unwrap_or(Value::Null),
    }))
}

fn execute_block_embed(normalized_input: &Value, cli_ctx: &CliContext) -> CliResult<Value> {
    let source_document_id = required_str(normalized_input, "sourceDocumentId")?;
    let target_document_id = required_str(normalized_input, "targetDocumentId")?;
    let block_id = required_str(normalized_input, "blockId")?;
    let target_block_id = normalized_input
        .get("targetBlockId")
        .and_then(Value::as_str);

    let source_meta = execute_convex_query_raw(
        "documents:getMeta",
        json!({ "id": source_document_id }),
        cli_ctx,
    )?;
    let target_content = execute_convex_query_raw(
        "documents:getContent",
        json!({ "id": target_document_id }),
        cli_ctx,
    )?;
    let target_meta = execute_convex_query_raw(
        "documents:getMeta",
        json!({ "id": target_document_id }),
        cli_ctx,
    )?;
    let mut target_blocks = extract_blocks(&target_content);
    let reference_block = json!({
        "id": format!("cli_embed_{}_{}", target_document_id, block_id),
        "type": "blockReference",
        "props": {
            "sourceDocumentId": source_document_id,
            "targetBlockId": block_id,
            "display": "embed",
        },
        "content": [],
        "children": [],
    });
    let anchor_id = target_block_id.map(str::to_string).or_else(|| {
        target_meta
            .get("embed_default_block_id")
            .and_then(Value::as_str)
            .map(str::to_string)
    });
    if let Some(anchor_id) = anchor_id {
        if let Some(index) = find_block_index(&target_blocks, &anchor_id) {
            target_blocks.insert(index + 1, reference_block.clone());
        } else {
            target_blocks.push(reference_block.clone());
        }
    } else {
        target_blocks.push(reference_block.clone());
    }
    let saved = execute_convex_mutation_raw(
        "documents:updateContent",
        json!({
            "id": target_document_id,
            "content": write_blocks_back(&target_content, Value::Array(target_blocks)),
            "expectedRevision": target_content.get("revision").cloned().unwrap_or(Value::Null),
            "conflictDetectionKey": target_content.get("conflict_detection_key").cloned().unwrap_or(Value::Null),
        }),
        cli_ctx,
    )?;
    Ok(json!({
        "ok": true,
        "sourceTitle": source_meta.get("title").cloned().unwrap_or(Value::Null),
        "referenceBlockId": reference_block.get("id").cloned().unwrap_or(Value::Null),
        "revision": saved.get("revision").cloned().unwrap_or(Value::Null),
    }))
}

fn build_tool_target(
    tool_name: &str,
    normalized_input: &Value,
    context: &CliOutputContext,
) -> CliResult<Option<RuntimeTargetWire>> {
    let args = normalized_input.get("args").cloned().unwrap_or(Value::Null);
    let workspace_id = args
        .get("workspaceId")
        .and_then(Value::as_str)
        .map(str::to_string)
        .or_else(|| context.workspace_id.clone());
    let page_id = args
        .get("documentId")
        .and_then(Value::as_str)
        .map(str::to_string)
        .or_else(|| {
            args.get("pageId")
                .and_then(Value::as_str)
                .map(str::to_string)
        });
    let block_id = args
        .get("mindmapId")
        .and_then(Value::as_str)
        .map(str::to_string)
        .or_else(|| {
            args.get("blockId")
                .and_then(Value::as_str)
                .map(str::to_string)
        });
    if matches!(
        tool_name,
        "search_web" | "image_read" | "slash_run" | "event_replay" | "index_rebuild"
    ) {
        return Ok(None);
    }
    if workspace_id.is_none() && page_id.is_none() && block_id.is_none() {
        return Ok(None);
    }
    Ok(Some(RuntimeTargetWire {
        workspace_id,
        page_id,
        block_id,
    }))
}

fn load_tool_data(
    tool_name: &str,
    args: &Value,
    target: Option<&RuntimeTargetWire>,
    cli_ctx: &CliContext,
) -> CliResult<Value> {
    match tool_name {
        "search_web" | "slash_run" | "event_replay" | "index_rebuild" => {
            Ok(json!({ "source": "cli" }))
        }
        "image_read" => Ok(json!({ "source": "cli", "asset": Value::Null })),
        "doc_get" | "doc_find" | "doc_insert_blocks" | "doc_replace_range" => {
            let page_id = read_doc_target(args, target)?;
            let result = execute_convex_query_raw(
                "documents:getContent",
                json!({ "id": page_id }),
                cli_ctx,
            )?;
            Ok(
                json!({ "source": "convex", "content": result.get("content").cloned().unwrap_or(Value::Null) }),
            )
        }
        "docs_read" => {
            let document_id = args
                .get("documentId")
                .and_then(Value::as_str)
                .ok_or_else(|| CliError::validation("docs_read 缺少 documentId"))?;
            let meta = execute_convex_query_raw(
                "documents:getMeta",
                json!({ "id": document_id }),
                cli_ctx,
            )?;
            let content = execute_convex_query_raw(
                "documents:getContent",
                json!({ "id": document_id }),
                cli_ctx,
            )?;
            Ok(json!({
                "source": "convex",
                "documentId": document_id,
                "title": meta.get("title").cloned().unwrap_or(Value::Null),
                "workspaceId": meta.get("workspace_id").cloned().unwrap_or(Value::Null),
                "parentId": meta.get("parent_id").cloned().unwrap_or(Value::Null),
                "updatedAt": meta.get("updated_at").cloned().unwrap_or(Value::Null),
                "rawText": extract_raw_text(&content),
                "rawTextLength": extract_raw_text(&content).chars().count(),
                "content": content.get("content").cloned().unwrap_or(Value::Null),
            }))
        }
        "docs_search" => {
            let workspace_id = args
                .get("workspaceId")
                .and_then(Value::as_str)
                .ok_or_else(|| CliError::validation("docs_search 缺少 workspaceId"))?;
            let documents = execute_convex_query_raw(
                "documents:listSearchDataByWorkspace",
                json!({ "workspaceId": workspace_id }),
                cli_ctx,
            )?;
            let mindmaps = execute_convex_query_raw(
                "mindmaps:listByWorkspace",
                json!({ "workspaceId": workspace_id, "includeDeleted": false }),
                cli_ctx,
            )
            .unwrap_or_else(|_| Value::Array(vec![]));
            let tables = execute_convex_query_raw(
                "tables:listByWorkspaceForSearch",
                json!({ "userId": cli_ctx.actor_id, "workspaceId": workspace_id, "includeArchived": false, "limit": 3000 }),
                cli_ctx,
            ).unwrap_or_else(|_| Value::Array(vec![]));
            let table_rows = execute_convex_query_raw(
                "tables:listRowsByWorkspaceForSearch",
                json!({ "userId": cli_ctx.actor_id, "workspaceId": workspace_id, "limit": 8000 }),
                cli_ctx,
            )
            .unwrap_or_else(|_| Value::Array(vec![]));
            let assets = execute_convex_query_raw(
                "mediaAssets:listSearchDataByWorkspace",
                json!({ "userId": cli_ctx.actor_id, "workspaceId": workspace_id, "includeDeleted": false, "limit": 5000 }),
                cli_ctx,
            ).unwrap_or_else(|_| Value::Array(vec![]));
            Ok(json!({
                "source": "convex",
                "datasets": [{
                    "workspaceId": workspace_id,
                    "documents": documents.as_array().cloned().unwrap_or_default().into_iter().map(|item| json!({
                        "id": item.get("id").cloned().unwrap_or(Value::Null),
                        "workspaceId": item.get("workspace_id").cloned().unwrap_or(json!(workspace_id)),
                        "title": item.get("title").cloned().unwrap_or(Value::Null),
                        "rawText": item.get("raw_text").cloned().unwrap_or(Value::Null),
                        "createdAt": item.get("created_at").cloned().unwrap_or(Value::Null),
                        "updatedAt": item.get("updated_at").cloned().unwrap_or(Value::Null)
                    })).collect::<Vec<Value>>(),
                    "mindmaps": mindmaps.as_array().cloned().unwrap_or_default().into_iter().map(|item| json!({
                        "documentId": item.get("document_id").cloned().unwrap_or(Value::Null),
                        "data": item.get("data").cloned().unwrap_or(Value::Null)
                    })).collect::<Vec<Value>>(),
                    "tables": tables.as_array().cloned().unwrap_or_default().into_iter().map(|item| json!({
                        "id": item.get("id").cloned().unwrap_or(Value::Null),
                        "documentId": item.get("document_id").cloned().unwrap_or(Value::Null),
                        "title": item.get("title").cloned().unwrap_or(Value::Null)
                    })).collect::<Vec<Value>>(),
                    "tableRows": table_rows.as_array().cloned().unwrap_or_default().into_iter().map(|item| json!({
                        "tableId": item.get("table_id").cloned().unwrap_or(Value::Null),
                        "documentId": item.get("document_id").cloned().unwrap_or(Value::Null),
                        "rowHash": item.get("row_hash").cloned().unwrap_or(Value::Null)
                    })).collect::<Vec<Value>>(),
                    "assets": assets.as_array().cloned().unwrap_or_default().into_iter().map(|item| json!({
                        "id": item.get("id").cloned().unwrap_or(Value::Null),
                        "documentId": item.get("document_id").cloned().unwrap_or(Value::Null),
                        "assetType": item.get("asset_type").cloned().unwrap_or(Value::Null),
                        "fileName": item.get("file_name").cloned().unwrap_or(Value::Null),
                        "mimeType": item.get("mime_type").cloned().unwrap_or(Value::Null),
                        "ocrText": item.get("ocr_text").cloned().unwrap_or(Value::Null),
                        "ocrStatus": item.get("ocr_status").cloned().unwrap_or(Value::Null)
                    })).collect::<Vec<Value>>()
                }]
            }))
        }
        "mindmap_get" | "mindmap_get_subtree" | "mindmap_apply_ops" | "mindmap_put" => {
            let document_id = read_doc_target(args, target)?;
            let mindmap_id = args
                .get("mindmapId")
                .and_then(Value::as_str)
                .or_else(|| target.and_then(|value| value.block_id.as_deref()))
                .ok_or_else(|| CliError::validation("mindmap 工具缺少 mindmapId"))?;
            let result = execute_convex_query_raw(
                "mindmaps:get",
                json!({ "docId": document_id, "mindmapId": mindmap_id }),
                cli_ctx,
            )?;
            Ok(
                json!({ "source": "convex", "data": result.get("data").cloned().unwrap_or(Value::Null), "meta": result.get("meta").cloned().unwrap_or(Value::Null) }),
            )
        }
        other => Err(CliError::validation(format!(
            "暂不支持 tool 数据加载: {other}"
        ))),
    }
}

fn execute_convex_query(transport: &CliTransportPlan, cli_ctx: &CliContext) -> CliResult<Value> {
    execute_convex_query_raw(
        &transport.function_name,
        transport.args_json.clone(),
        cli_ctx,
    )
}

fn execute_convex_mutation(transport: &CliTransportPlan, cli_ctx: &CliContext) -> CliResult<Value> {
    execute_convex_mutation_raw(
        &transport.function_name,
        transport.args_json.clone(),
        cli_ctx,
    )
}

fn execute_convex_query_raw(
    function_name: &str,
    args_json: Value,
    cli_ctx: &CliContext,
) -> CliResult<Value> {
    let env = ConvexCliEnv::load()?;
    let payload = json!({
        "path": function_name,
        "format": "convex_encoded_json",
        "args": args_json,
    });
    let response = env
        .client
        .post(format!("{}/api/query", env.url))
        .header("Authorization", env.authorization)
        .header("Content-Type", "application/json")
        .header("Convex-Client", "mnote-cli")
        .json(&payload)
        .send()
        .map_err(|error| CliError::transport(format!("Convex query 请求失败: {error}")))?;
    parse_convex_response(response, cli_ctx)
}

fn execute_convex_mutation_raw(
    function_name: &str,
    args_json: Value,
    cli_ctx: &CliContext,
) -> CliResult<Value> {
    let env = ConvexCliEnv::load()?;
    let payload = json!({
        "path": function_name,
        "format": "convex_encoded_json",
        "args": [args_json],
    });
    let response = env
        .client
        .post(format!("{}/api/mutation", env.url))
        .header("Authorization", env.authorization)
        .header("Content-Type", "application/json")
        .header("Convex-Client", "mnote-cli")
        .json(&payload)
        .send()
        .map_err(|error| CliError::transport(format!("Convex mutation 请求失败: {error}")))?;
    parse_convex_response(response, cli_ctx)
}

fn parse_convex_response(
    response: reqwest::blocking::Response,
    _cli_ctx: &CliContext,
) -> CliResult<Value> {
    let status = response.status();
    let body: Value = response
        .json()
        .map_err(|error| CliError::transport(format!("Convex 响应解析失败: {error}")))?;
    if !status.is_success() {
        let message = body
            .get("errorMessage")
            .and_then(Value::as_str)
            .map(str::to_string)
            .unwrap_or_else(|| format!("HTTP {}", status.as_u16()));
        return Err(CliError::transport(message));
    }
    match body.get("status").and_then(Value::as_str) {
        Some("success") => Ok(body.get("value").cloned().unwrap_or(Value::Null)),
        Some("error") => Err(CliError::transport(
            body.get("errorMessage")
                .and_then(Value::as_str)
                .unwrap_or("Convex 返回 error")
                .to_string(),
        )),
        _ => Err(CliError::transport(format!("未知 Convex 响应: {body}"))),
    }
}

#[derive(Debug, Clone)]
struct ConvexCliEnv {
    url: String,
    authorization: String,
    client: Client,
}

impl ConvexCliEnv {
    fn load() -> CliResult<Self> {
        let url = read_env_or_dotenv("CONVEX_SELF_HOSTED_URL")?
            .or_else(|| env::var("NEXT_PUBLIC_CONVEX_URL").ok())
            .ok_or_else(|| {
                CliError::validation("缺少 CONVEX_SELF_HOSTED_URL / NEXT_PUBLIC_CONVEX_URL")
            })?;
        let admin_key = read_env_or_dotenv("CONVEX_SELF_HOSTED_ADMIN_KEY")?
            .ok_or_else(|| CliError::validation("缺少 CONVEX_SELF_HOSTED_ADMIN_KEY"))?;
        let dev_user_id = read_env_or_dotenv("DEV_USER_ID")?.unwrap_or_else(|| "dev-user".into());
        let dev_user_name =
            read_env_or_dotenv("DEV_USER_NAME")?.unwrap_or_else(|| "开发用户".into());
        let dev_user_email =
            read_env_or_dotenv("DEV_USER_EMAIL")?.unwrap_or_else(|| "dev@mnote.local".into());
        let identity = json!({
            "subject": dev_user_id,
            "issuer": "https://mnote.local/dev-auth",
            "tokenIdentifier": format!("dev-user|{}", dev_user_id),
            "name": dev_user_name,
            "email": dev_user_email,
        });
        let encoded = base64::engine::general_purpose::STANDARD
            .encode(serde_json::to_string(&identity).map_err(|error| {
                CliError::transport(format!("开发用户身份序列化失败: {error}"))
            })?);
        let authorization = format!("Convex {admin_key}:{encoded}");
        let client = Client::builder()
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .map_err(|error| CliError::transport(format!("HTTP 客户端创建失败: {error}")))?;
        Ok(Self {
            url: url.trim().trim_end_matches('/').to_string(),
            authorization,
            client,
        })
    }
}

fn read_env_or_dotenv(key: &str) -> CliResult<Option<String>> {
    if let Ok(value) = env::var(key) {
        let trimmed = value.trim().to_string();
        if !trimmed.is_empty() {
            return Ok(Some(trimmed));
        }
    }
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../..")
        .join(".env.all");
    let content = std::fs::read_to_string(&root)
        .map_err(|error| CliError::transport(format!("读取 .env.all 失败: {error}")))?;
    for line in content.lines() {
        let line = line.trim_end_matches('\r');
        if line.starts_with('#') || line.trim().is_empty() {
            continue;
        }
        if let Some((k, v)) = line.split_once('=') {
            if k.trim() == key {
                let trimmed = v.trim().trim_matches('"').to_string();
                if !trimmed.is_empty() {
                    return Ok(Some(trimmed));
                }
            }
        }
    }
    Ok(None)
}

fn build_runtime_context(
    context: &CliOutputContext,
    cli_ctx: &CliContext,
) -> RuntimeBridgeContextWire {
    RuntimeBridgeContextWire {
        deployment_id: None,
        project_id: None,
        workspace_id: context.workspace_id.clone(),
        request_id: context.request_id.clone(),
        trace_id: context.trace_id.clone(),
        actor: RuntimeActorWire {
            actor_type: context.actor_type.clone(),
            actor_id: context.actor_id.clone(),
            session_id: context.session_id.clone(),
        },
        source: RuntimeSourceWire {
            channel: "cli".into(),
            client: "mnote-cli".into(),
        },
        tenant_id: None,
        auth_token: None,
        idempotency_key: cli_ctx.idempotency_key.clone(),
        validate_only: cli_ctx.validate_only,
        dry_run: cli_ctx.dry_run,
    }
}

fn extract_blocks(content_result: &Value) -> Vec<Value> {
    if let Some(content) = content_result.get("content") {
        if let Some(array) = content.as_array() {
            return array.clone();
        }
        if let Some(array) = content.get("blocks").and_then(Value::as_array) {
            return array.clone();
        }
    }
    vec![]
}

fn write_blocks_back(content_result: &Value, blocks: Value) -> Value {
    let current = content_result
        .get("content")
        .cloned()
        .unwrap_or(Value::Null);
    if current.is_array() {
        return blocks;
    }
    if let Some(obj) = current.as_object() {
        let mut next = obj.clone();
        next.insert("blocks".into(), blocks);
        return Value::Object(next);
    }
    json!({ "blocks": blocks })
}

fn find_block_index(blocks: &[Value], target_id: &str) -> Option<usize> {
    blocks.iter().position(|block| {
        block
            .get("id")
            .and_then(Value::as_str)
            .map(|value| value == target_id)
            .unwrap_or(false)
    })
}

fn remove_block_subtree(mut blocks: Vec<Value>, block_id: &str) -> CliResult<(Value, Vec<Value>)> {
    if let Some(index) = blocks
        .iter()
        .position(|block| block.get("id").and_then(Value::as_str) == Some(block_id))
    {
        let removed = blocks.remove(index);
        return Ok((removed, blocks));
    }
    for block in &mut blocks {
        if let Some(children) = block.get_mut("children").and_then(Value::as_array_mut) {
            let (removed, next_children) = remove_block_subtree(children.clone(), block_id)?;
            *children = next_children;
            return Ok((removed, blocks));
        }
    }
    Err(CliError::validation(format!("未找到 blockId：{block_id}")))
}

fn replace_block_subtree(
    mut blocks: Vec<Value>,
    block_id: &str,
    next_block: &Value,
) -> CliResult<Vec<Value>> {
    let mut normalized = next_block
        .as_object()
        .cloned()
        .ok_or_else(|| CliError::validation("block patch snapshot 必须是对象"))?;
    normalized.insert("id".into(), Value::String(block_id.to_string()));
    let next_value = Value::Object(normalized);

    if let Some(index) = blocks
        .iter()
        .position(|block| block.get("id").and_then(Value::as_str) == Some(block_id))
    {
        blocks[index] = next_value;
        return Ok(blocks);
    }
    for block in &mut blocks {
        if let Some(children) = block.get_mut("children").and_then(Value::as_array_mut) {
            let next_children = replace_block_subtree(children.clone(), block_id, &next_value)?;
            *children = next_children;
            return Ok(blocks);
        }
    }
    Err(CliError::validation(format!("未找到 blockId：{block_id}")))
}

fn extract_raw_text(content_result: &Value) -> String {
    fn walk(value: &Value, parts: &mut Vec<String>) {
        match value {
            Value::Array(items) => {
                for item in items {
                    walk(item, parts);
                }
            }
            Value::Object(map) => {
                if let Some(text) = map.get("text").and_then(Value::as_str) {
                    if !text.trim().is_empty() {
                        parts.push(text.to_string());
                    }
                }
                if let Some(content) = map.get("content") {
                    walk(content, parts);
                }
                if let Some(children) = map.get("children") {
                    walk(children, parts);
                }
                if let Some(blocks) = map.get("blocks") {
                    walk(blocks, parts);
                }
            }
            _ => {}
        }
    }
    let mut parts = Vec::new();
    if let Some(content) = content_result.get("content") {
        walk(content, &mut parts);
    }
    parts.join("\n")
}

fn read_doc_target(args: &Value, target: Option<&RuntimeTargetWire>) -> CliResult<String> {
    args.get("documentId")
        .and_then(Value::as_str)
        .or_else(|| args.get("pageId").and_then(Value::as_str))
        .map(str::to_string)
        .or_else(|| target.and_then(|value| value.page_id.clone()))
        .ok_or_else(|| CliError::validation("缺少 documentId/pageId"))
}

fn required_str<'a>(value: &'a Value, key: &str) -> CliResult<&'a str> {
    value
        .get(key)
        .and_then(Value::as_str)
        .ok_or_else(|| CliError::validation(format!("缺少 {key}")))
}

fn build_query_output(
    domain: &str,
    action: &str,
    bridge: &BridgeContext,
    name: String,
    normalized_input: Value,
    transport: CliTransportPlan,
) -> CliJsonOutput {
    CliJsonOutput {
        ok: true,
        entrypoint: "mnote-cli",
        domain: domain.into(),
        action: action.into(),
        context: build_output_context(
            bridge,
            &CliContext {
                actor_id: bridge.actor_id.clone(),
                actor_type: bridge.actor_type.clone(),
                session_id: bridge.session_id.clone(),
                reason: None,
                idempotency_key: bridge.idempotency_key.clone(),
                validate_only: bridge.validate_only,
                dry_run: bridge.dry_run,
            },
        ),
        operation: CliOperationOutput::Query {
            name,
            normalized_input,
            transport,
            execution: None,
        },
    }
}

fn build_command_output(
    domain: &str,
    action: &str,
    bridge: &BridgeContext,
    name: String,
    command_id: String,
    normalized_input: Value,
    transport: CliTransportPlan,
) -> CliJsonOutput {
    CliJsonOutput {
        ok: true,
        entrypoint: "mnote-cli",
        domain: domain.into(),
        action: action.into(),
        context: build_output_context(
            bridge,
            &CliContext {
                actor_id: bridge.actor_id.clone(),
                actor_type: bridge.actor_type.clone(),
                session_id: bridge.session_id.clone(),
                reason: None,
                idempotency_key: bridge.idempotency_key.clone(),
                validate_only: bridge.validate_only,
                dry_run: bridge.dry_run,
            },
        ),
        operation: CliOperationOutput::Command {
            name,
            command_id,
            normalized_input,
            transport,
            execution: None,
        },
    }
}

fn build_output_context(bridge: &BridgeContext, ctx: &CliContext) -> CliOutputContext {
    CliOutputContext {
        request_id: bridge.request_id.clone(),
        trace_id: bridge.trace_id.clone(),
        actor_id: bridge.actor_id.clone(),
        actor_type: bridge.actor_type.clone(),
        session_id: bridge.session_id.clone(),
        workspace_id: bridge.workspace_id.clone(),
        idempotency_key: ctx.idempotency_key.clone(),
        validate_only: bridge.validate_only,
        dry_run: bridge.dry_run,
    }
}

fn build_bridge_context(
    ctx: &CliContext,
    workspace_id: Option<&str>,
    domain: &str,
    action: &str,
    target: &str,
) -> BridgeContext {
    let key = sanitize_id(&format!("{domain}_{action}_{target}"));
    BridgeContext {
        deployment_id: None,
        project_id: None,
        workspace_id: workspace_id.map(|value| value.into()),
        request_id: format!("req_cli_{key}"),
        trace_id: format!("trace_cli_{key}"),
        actor_type: ctx.actor_type.clone(),
        actor_id: ctx.actor_id.clone(),
        session_id: ctx.session_id.clone(),
        tenant_id: None,
        auth_token: None,
        source_channel: "cli".into(),
        source_client: "mnote-cli".into(),
        idempotency_key: ctx.idempotency_key.clone(),
        validate_only: ctx.validate_only,
        dry_run: ctx.dry_run,
    }
}

fn build_actor_payload(ctx: &CliContext) -> ActorPayload {
    ActorPayload {
        actor_type: ctx.actor_type.clone(),
        actor_id: ctx.actor_id.clone(),
        session_id: ctx.session_id.clone(),
    }
}

fn build_source_payload() -> SourcePayload {
    SourcePayload {
        channel: "cli".into(),
        client: "mnote-cli".into(),
    }
}

fn build_command_id(domain: &str, action: &str, target: &str) -> String {
    format!(
        "cmd_{}",
        sanitize_id(&format!("{domain}_{action}_{target}"))
    )
}

fn sanitize_id(input: &str) -> String {
    input
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() {
                ch.to_ascii_lowercase()
            } else {
                '_'
            }
        })
        .collect()
}

fn parse_json_value(raw: &str, field: &str) -> CliResult<Value> {
    serde_json::from_str::<Value>(raw)
        .map_err(|error| CliError::validation(format!("{field} 不是合法 JSON: {error}")))
}

fn invocation_kind_label(kind: &InvocationKind) -> &'static str {
    match kind {
        InvocationKind::Command => "command",
        InvocationKind::Query => "query",
        InvocationKind::Job => "job",
    }
}

fn map_bridge_error(error: storage_convex_bridge::BridgeError) -> CliError {
    CliError::validation(error.message)
}

fn map_editor_error(error: mnote_editor_core::CoreError) -> CliError {
    CliError::validation(error.to_string())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn supported_surface_covers_phase2_minimum_domains() {
        let commands = supported_command_surface();
        assert!(commands.contains(&"page get"));
        assert!(commands.contains(&"page create"));
        assert!(commands.contains(&"page move"));
        assert!(commands.contains(&"block patch"));
        assert!(commands.contains(&"block move"));
        assert!(commands.contains(&"mindmap get"));
        assert!(commands.contains(&"search documents"));
        assert!(commands.contains(&"sidebar dataset"));
        assert!(commands.contains(&"editor markdown-roundtrip"));
        assert!(commands.contains(&"editor session-demo"));
        assert!(commands.contains(&"tool run"));
    }

    #[test]
    fn editor_markdown_roundtrip_returns_outline_and_markdown() {
        let output =
            editor_markdown_roundtrip("# 标题\n普通段落").expect("roundtrip should succeed");

        assert_eq!(output.operation, "markdown_roundtrip");
        assert_eq!(output.block_count, 2);
        assert!(output.output_markdown.contains("# 标题"));
        assert_eq!(
            output.visible_block_ids,
            vec!["imported_1".to_string(), "imported_2".to_string()]
        );
        assert_eq!(output.outline.len(), 1);
        assert_eq!(output.outline[0].title, "标题");
    }

    #[test]
    fn editor_session_demo_applies_insert_and_redo() {
        let output = editor_session_demo("原始段落").expect("session demo should succeed");

        assert_eq!(output.operation, "session_demo");
        assert_eq!(output.block_count, 2);
        assert_eq!(output.undo_applied, Some(true));
        assert_eq!(output.redo_applied, Some(true));
        assert!(output.output_markdown.contains("原始段落 [edited]"));
        assert!(output.output_markdown.contains("CLI demo block"));
    }

    #[test]
    fn editor_ai_pipeline_exposes_audit_and_change_report() {
        let output = editor_ai_pipeline(
            EditorInputKind::PlainText,
            EditorAiScenario::MeetingNotesToTodos,
            "待办：整理会议纪要\n普通段落",
        )
        .expect("ai pipeline should succeed");

        assert_eq!(output.operation, "ai_pipeline");
        assert_eq!(output.audit.len(), 1);
        assert_eq!(
            output.change_report.updated_blocks,
            vec!["imported_1".to_string()]
        );
        assert!(output.output_markdown.contains("待办：整理会议纪要"));
    }

    #[test]
    fn editor_ai_pipeline_reorders_pages() {
        let output = editor_ai_pipeline(
            EditorInputKind::PlainText,
            EditorAiScenario::PageReorder,
            "第一页\n\n第二页",
        )
        .expect("ai pipeline should succeed");

        assert_eq!(output.output_markdown.lines().next(), Some("第二页"));
        assert_eq!(
            output.change_report.moved_blocks,
            vec!["imported_1".to_string()]
        );
    }

    #[test]
    fn ai_regression_meeting_notes_to_todos() {
        let output = editor_ai_pipeline(
            EditorInputKind::PlainText,
            EditorAiScenario::MeetingNotesToTodos,
            "待办：整理会议纪要\n普通段落",
        )
        .expect("ai regression meeting notes should succeed");

        assert!(output.output_markdown.contains("- [ ] 待办：整理会议纪要"));
        assert_eq!(
            output.change_report.updated_blocks,
            vec!["imported_1".to_string()]
        );
    }

    #[test]
    fn ai_regression_long_paragraph_to_title() {
        let output = editor_ai_pipeline(
            EditorInputKind::PlainText,
            EditorAiScenario::LongParagraphToTitle,
            "这是一个很长的段落，用来验证 AI 回归样例会把首块提炼成标题，并保留后续结构化内容。",
        )
        .expect("ai regression long paragraph should succeed");

        assert!(output.output_markdown.starts_with("# 这是一个很长的段落"));
        assert_eq!(
            output.change_report.updated_blocks,
            vec!["imported_1".to_string()]
        );
    }

    #[test]
    fn ai_regression_page_reorder() {
        let output = editor_ai_pipeline(
            EditorInputKind::PlainText,
            EditorAiScenario::PageReorder,
            "第一页\n\n第二页",
        )
        .expect("ai regression reorder should succeed");

        assert_eq!(output.output_markdown.lines().next(), Some("第二页"));
        assert_eq!(
            output.change_report.moved_blocks,
            vec!["imported_1".to_string()]
        );
    }

    #[test]
    fn page_save_json_contract_uses_documents_save() {
        let output = plan_page_save(
            &CliContext::default(),
            "page_1",
            Some("ws_1"),
            Some(7),
            r#"[{"id":"block_1"}]"#,
            Some("conflict_1"),
        )
        .expect("page save plan should build");

        match output.operation {
            CliOperationOutput::Command {
                name,
                command_id,
                transport,
                ..
            } => {
                assert_eq!(name, "documents.save");
                assert_eq!(command_id, "cmd_page_save_page_1");
                assert_eq!(transport.function_name, "documents:updateContent");
                assert_eq!(
                    transport.args_json,
                    json!({
                        "id": "page_1",
                        "content": [{"id": "block_1"}],
                        "expectedRevision": 7,
                        "conflictDetectionKey": "conflict_1",
                    })
                );
            }
            _ => panic!("expected command output"),
        }
    }

    #[test]
    fn page_create_json_contract_uses_documents_create() {
        let output = plan_page_create(
            &CliContext::default(),
            &PageCreateArgs {
                page_id: "page_1",
                workspace_id: "ws_1",
                parent_id: Some("parent_1"),
                title: "新页面",
                access_scope: "private",
                content_json: r#"[{"id":"block_1"}]"#,
            },
        )
        .expect("page create plan should build");

        match output.operation {
            CliOperationOutput::Command {
                name, transport, ..
            } => {
                assert_eq!(name, "documents.create");
                assert_eq!(
                    transport.function_name,
                    "documents:createWithParentReference"
                );
                assert_eq!(transport.args_json["workspaceId"], json!("ws_1"));
                assert_eq!(transport.args_json["parentId"], json!("parent_1"));
            }
            _ => panic!("expected command output"),
        }
    }

    #[test]
    fn page_move_json_contract_uses_documents_move() {
        let output = plan_page_move(
            &CliContext::default(),
            &PageMoveArgs {
                page_id: "page_1",
                workspace_id: Some("ws_1"),
                parent_id: Some("parent_2"),
                sort_order: 3,
            },
        )
        .expect("page move plan should build");

        match output.operation {
            CliOperationOutput::Command {
                name, transport, ..
            } => {
                assert_eq!(name, "documents.move");
                assert_eq!(transport.function_name, "documents:move");
                assert_eq!(transport.args_json["sortOrder"], json!(3));
            }
            _ => panic!("expected command output"),
        }
    }

    #[test]
    fn sidebar_dataset_json_contract_uses_single_dataset_query() {
        let output = plan_sidebar_dataset(&CliContext::default(), "ws_1")
            .expect("sidebar dataset plan should build");

        match output.operation {
            CliOperationOutput::Query {
                name, transport, ..
            } => {
                assert_eq!(name, "sidebar.dataset.list");
                assert_eq!(transport.function_name, "sidebar:datasetList");
                assert_eq!(transport.args_json, json!({ "workspaceId": "ws_1" }));
            }
            _ => panic!("expected query output"),
        }
    }

    #[test]
    fn tool_run_keeps_kind_and_args_json() {
        let output = plan_tool_run(
            &CliContext::default(),
            "doc_get",
            InvocationKind::Query,
            ToolExecutionMode::Plan,
            r#"{"pageId":"page_1"}"#,
        )
        .expect("tool plan should build");

        match output.operation {
            CliOperationOutput::Tool {
                tool_name,
                invocation_kind,
                execution_mode,
                toolset_id,
                effect,
                requires_confirmation,
                normalized_input,
                ..
            } => {
                assert_eq!(tool_name, "doc_get");
                assert_eq!(invocation_kind, "query");
                assert_eq!(execution_mode, "plan");
                assert_eq!(toolset_id, "toolset.doc_read");
                assert_eq!(effect, "read");
                assert!(!requires_confirmation);
                assert_eq!(
                    normalized_input,
                    json!({
                        "toolName": "doc_get",
                        "invocationKind": "query",
                        "executionMode": "plan",
                        "toolsetId": "toolset.doc_read",
                        "effect": "read",
                        "requiresConfirmation": false,
                        "validateOnly": false,
                        "dryRun": false,
                        "args": {
                            "pageId": "page_1",
                        },
                    })
                );
            }
            _ => panic!("expected tool output"),
        }
    }

    #[test]
    fn tool_run_rejects_unknown_tool() {
        let error = plan_tool_run(
            &CliContext::default(),
            "doc_unknown",
            InvocationKind::Query,
            ToolExecutionMode::Plan,
            r#"{}"#,
        )
        .expect_err("unknown tool should fail");

        assert_eq!(error.code, "VALIDATION_ERROR");
        assert!(error.message.contains("未知 tool"));
    }

    #[test]
    fn mindmap_get_json_contract_uses_mindmaps_get() {
        let output = plan_mindmap_get(&CliContext::default(), Some("ws_1"), "page_1", "mind_1")
            .expect("mindmap get plan should build");

        match output.operation {
            CliOperationOutput::Query {
                name, transport, ..
            } => {
                assert_eq!(name, "mindmaps.get");
                assert_eq!(transport.function_name, "mindmaps:get");
                assert_eq!(
                    transport.args_json,
                    json!({
                        "docId": "page_1",
                        "mindmapId": "mind_1",
                    })
                );
            }
            _ => panic!("expected query output"),
        }
    }

    #[test]
    fn mindmap_put_json_contract_uses_mindmaps_put() {
        let output = plan_mindmap_put(
            &CliContext::default(),
            Some("ws_1"),
            "page_1",
            "mind_1",
            r#"{"data":{"text":"中心主题"},"children":[]}"#,
            true,
        )
        .expect("mindmap put plan should build");

        match output.operation {
            CliOperationOutput::Command {
                name, transport, ..
            } => {
                assert_eq!(name, "mindmaps.put");
                assert_eq!(transport.function_name, "mindmaps:put");
                assert_eq!(
                    transport.args_json,
                    json!({
                        "docId": "page_1",
                        "mindmapId": "mind_1",
                        "data": {
                            "data": {
                                "text": "中心主题"
                            },
                            "children": []
                        },
                        "createOnly": true,
                    })
                );
            }
            _ => panic!("expected command output"),
        }
    }

    #[test]
    fn replace_block_subtree_preserves_other_blocks() {
        let next_blocks = replace_block_subtree(
            vec![
                json!({
                    "id": "block_1",
                    "type": "paragraph",
                    "content": [{"type": "text", "text": "old"}],
                    "children": [],
                }),
                json!({
                    "id": "block_2",
                    "type": "paragraph",
                    "content": [{"type": "text", "text": "keep"}],
                    "children": [],
                }),
            ],
            "block_1",
            &json!({
                "type": "paragraph",
                "content": [{"type": "text", "text": "new"}],
                "children": [],
            }),
        )
        .expect("block replacement should succeed");

        assert_eq!(
            next_blocks,
            vec![
                json!({
                    "id": "block_1",
                    "type": "paragraph",
                    "content": [{"type": "text", "text": "new"}],
                    "children": [],
                }),
                json!({
                    "id": "block_2",
                    "type": "paragraph",
                    "content": [{"type": "text", "text": "keep"}],
                    "children": [],
                }),
            ]
        );
    }

    #[test]
    fn read_env_or_dotenv_trims_crlf_values() {
        let got = read_env_or_dotenv("CONVEX_SELF_HOSTED_URL").expect("read env should work");
        assert_eq!(got.as_deref(), Some("http://127.0.0.1:3210"));
    }
}
