//! SQLite-backed control-plane store.

use std::collections::BTreeMap;
use std::sync::Mutex;

use rusqlite::{params, Connection, OptionalExtension};

use crate::error::ControlPlaneError;
use crate::migrations;
use crate::model::{
    password_hash_v1, share_token_hash_v1, AiAgentProfileAccessRecord, AiAgentProfileGrantRecord,
    AiAgentProfileRecord, AiExternalConversationBindingRecord, AiPolicyRecord,
    AiRuntimeEventRecord, AiRuntimeJournalEventRecord, AiRuntimeRunRecord,
    AppendAiRuntimeEventInput, AppendAuditInput, AuditLogRecord, AuthSessionRecord,
    AuthenticatePasswordInput, CreatePasswordIdentityInput, CreateSessionInput,
    CreateShareLinkInput, CreatedShareLink, DirectoryGrantInput, DirectoryGrantLookup,
    DirectoryGrantRecord, NavigationRecentRecord, OutboxEventInput, OutboxEventRecord,
    ResolvedAccess, ResolvedAuthSession, ShareLinkRecord, SidebarShortcutRecord, SyncStateRecord,
    UpsertAiExternalConversationBindingInput, UpsertAiPolicyInput, UpsertAiRuntimeRunInput,
    UpsertNavigationRecentInput, UpsertSidebarShortcutInput, UpsertSyncStateInput, UpsertUserInput,
    UpsertUserUiPreferenceInput, UserRecord, UserUiPreferenceRecord, WorkspaceRecord,
};
use crate::store::ControlPlaneStore;

pub struct SqliteControlPlaneStore {
    conn: Mutex<Connection>,
}

impl SqliteControlPlaneStore {
    pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, ControlPlaneError> {
        let conn = Connection::open(path)?;
        configure_connection(&conn)?;
        migrations::run_migrations(&conn)?;
        Ok(Self {
            conn: Mutex::new(conn),
        })
    }

    pub fn in_memory() -> Result<Self, ControlPlaneError> {
        let conn = Connection::open_in_memory()?;
        configure_connection(&conn)?;
        migrations::run_migrations(&conn)?;
        Ok(Self {
            conn: Mutex::new(conn),
        })
    }
}

fn configure_connection(conn: &Connection) -> Result<(), ControlPlaneError> {
    conn.execute_batch(
        "PRAGMA journal_mode = WAL;
         PRAGMA synchronous = NORMAL;
         PRAGMA foreign_keys = ON;
         PRAGMA busy_timeout = 5000;",
    )?;
    Ok(())
}

fn now_text() -> String {
    chrono::Utc::now().to_rfc3339()
}

fn new_id(prefix: &str) -> String {
    format!("{prefix}_{}", uuid::Uuid::new_v4().simple())
}

fn capabilities_json(capabilities: &[String]) -> Result<String, ControlPlaneError> {
    serde_json::to_string(capabilities).map_err(ControlPlaneError::from)
}

fn file_uri_to_legacy_path(value: &str) -> String {
    value
        .trim()
        .strip_prefix("file://")
        .unwrap_or("")
        .to_string()
}

fn row_to_user(row: &rusqlite::Row<'_>) -> rusqlite::Result<UserRecord> {
    Ok(UserRecord {
        id: row.get(0)?,
        email: row.get(1)?,
        username: row.get(2)?,
        display_name: row.get(3)?,
        role: row.get(4)?,
        status: row.get(5)?,
        created_at: row.get(6)?,
        updated_at: row.get(7)?,
        revision: row.get(8)?,
    })
}

fn row_to_auth_session(row: &rusqlite::Row<'_>) -> rusqlite::Result<AuthSessionRecord> {
    Ok(AuthSessionRecord {
        id: row.get(0)?,
        user_id: row.get(1)?,
        token_hash: row.get(2)?,
        user_agent: row.get(3)?,
        ip_hash: row.get(4)?,
        created_at: row.get(5)?,
        expires_at: row.get(6)?,
        revoked_at: row.get(7)?,
        last_seen_at: row.get(8)?,
    })
}

fn row_to_workspace(row: &rusqlite::Row<'_>) -> rusqlite::Result<WorkspaceRecord> {
    Ok(WorkspaceRecord {
        id: row.get(0)?,
        owner_user_id: row.get(1)?,
        name: row.get(2)?,
        kind: row.get(3)?,
        root_uri: row.get(4)?,
        root_path: row.get(5)?,
        source_kind: row.get(6)?,
        status: row.get(7)?,
        created_at: row.get(8)?,
        updated_at: row.get(9)?,
        revision: row.get(10)?,
    })
}

fn row_to_grant(row: &rusqlite::Row<'_>) -> rusqlite::Result<DirectoryGrantRecord> {
    Ok(DirectoryGrantRecord {
        id: row.get(0)?,
        user_id: row.get(1)?,
        workspace_id: row.get(2)?,
        root_uri: row.get(3)?,
        root_path: row.get(4)?,
        permission: row.get(5)?,
        recursive: row.get::<_, i64>(6)? != 0,
        capabilities_json: row.get(7)?,
        source: row.get(8)?,
        status: row.get(9)?,
        created_by: row.get(10)?,
        created_at: row.get(11)?,
        updated_at: row.get(12)?,
        revision: row.get(13)?,
    })
}

fn row_to_outbox(row: &rusqlite::Row<'_>) -> rusqlite::Result<OutboxEventRecord> {
    Ok(OutboxEventRecord {
        id: row.get(0)?,
        topic: row.get(1)?,
        event_type: row.get(2)?,
        payload_json: row.get(3)?,
        created_at: row.get(4)?,
        delivered_at: row.get(5)?,
        attempts: row.get(6)?,
    })
}

fn row_to_share_link(row: &rusqlite::Row<'_>) -> rusqlite::Result<ShareLinkRecord> {
    Ok(ShareLinkRecord {
        id: row.get(0)?,
        workspace_id: row.get(1)?,
        resource_kind: row.get(2)?,
        resource_id: row.get(3)?,
        token_hash: row.get(4)?,
        permission: row.get(5)?,
        created_by: row.get(6)?,
        expires_at: row.get(7)?,
        revoked_at: row.get(8)?,
        created_at: row.get(9)?,
        updated_at: row.get(10)?,
        revision: row.get(11)?,
    })
}

fn row_to_audit_log(row: &rusqlite::Row<'_>) -> rusqlite::Result<AuditLogRecord> {
    Ok(AuditLogRecord {
        id: row.get(0)?,
        actor_user_id: row.get(1)?,
        action: row.get(2)?,
        target_kind: row.get(3)?,
        target_id: row.get(4)?,
        metadata_json: row.get(5)?,
        created_at: row.get(6)?,
    })
}

fn row_to_sidebar_shortcut(row: &rusqlite::Row<'_>) -> rusqlite::Result<SidebarShortcutRecord> {
    Ok(SidebarShortcutRecord {
        id: row.get(0)?,
        user_id: row.get(1)?,
        workspace_id: row.get(2)?,
        root_uri: row.get(3)?,
        kind: row.get(4)?,
        source_kind: row.get(5)?,
        target_id: row.get(6)?,
        relative_path: row.get(7)?,
        document_id: row.get(8)?,
        title: row.get(9)?,
        icon: row.get(10)?,
        sort_order: row.get(11)?,
        status: row.get(12)?,
        metadata_json: row.get(13)?,
        created_at: row.get(14)?,
        updated_at: row.get(15)?,
        revision: row.get(16)?,
    })
}

fn empty_string_to_option(value: String) -> Option<String> {
    if value.trim().is_empty() {
        None
    } else {
        Some(value)
    }
}

fn option_to_stored_text(value: Option<String>) -> String {
    value
        .map(|item| item.trim().to_string())
        .filter(|item| !item.is_empty())
        .unwrap_or_default()
}

fn sanitize_profile_component(value: &str) -> String {
    let mut sanitized = value
        .trim()
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() {
                ch.to_ascii_lowercase()
            } else {
                '-'
            }
        })
        .collect::<String>();
    while sanitized.contains("--") {
        sanitized = sanitized.replace("--", "-");
    }
    sanitized = sanitized.trim_matches('-').to_string();
    if sanitized.is_empty() {
        "user".to_string()
    } else {
        sanitized
    }
}

fn row_to_ai_agent_profile(row: &rusqlite::Row<'_>) -> rusqlite::Result<AiAgentProfileRecord> {
    let owner_user_id: String = row.get(3)?;
    Ok(AiAgentProfileRecord {
        id: row.get(0)?,
        agent_id: row.get(1)?,
        profile_kind: row.get(2)?,
        owner_user_id: empty_string_to_option(owner_user_id),
        base_profile_name: row.get(4)?,
        isolated_profile_name: row.get(5)?,
        display_name: row.get(6)?,
        status: row.get(7)?,
        created_at: row.get(8)?,
        updated_at: row.get(9)?,
        revision: row.get(10)?,
    })
}

fn row_to_ai_agent_profile_access(
    row: &rusqlite::Row<'_>,
) -> rusqlite::Result<AiAgentProfileAccessRecord> {
    Ok(AiAgentProfileAccessRecord {
        profile: row_to_ai_agent_profile(row)?,
        grant: AiAgentProfileGrantRecord {
            id: row.get(11)?,
            profile_id: row.get(12)?,
            user_id: empty_string_to_option(row.get(13)?),
            role: row.get(14)?,
            can_run: row.get::<_, i64>(15)? != 0,
            can_manage_skills: row.get::<_, i64>(16)? != 0,
            can_manage_config: row.get::<_, i64>(17)? != 0,
            created_at: row.get(18)?,
            updated_at: row.get(19)?,
            revision: row.get(20)?,
        },
    })
}

fn row_to_user_ui_preference(row: &rusqlite::Row<'_>) -> rusqlite::Result<UserUiPreferenceRecord> {
    Ok(UserUiPreferenceRecord {
        id: row.get(0)?,
        user_id: row.get(1)?,
        workspace_id: empty_string_to_option(row.get(2)?),
        source_kind: empty_string_to_option(row.get(3)?),
        scope_kind: row.get(4)?,
        scope_id: row.get(5)?,
        key: row.get(6)?,
        value_json: row.get(7)?,
        status: row.get(8)?,
        created_at: row.get(9)?,
        updated_at: row.get(10)?,
        revision: row.get(11)?,
    })
}

fn row_to_navigation_recent(row: &rusqlite::Row<'_>) -> rusqlite::Result<NavigationRecentRecord> {
    Ok(NavigationRecentRecord {
        id: row.get(0)?,
        user_id: row.get(1)?,
        workspace_id: empty_string_to_option(row.get(2)?),
        kind: row.get(3)?,
        source_kind: row.get(4)?,
        root_uri: row.get(5)?,
        target_key: row.get(6)?,
        relative_path: row.get(7)?,
        document_id: row.get(8)?,
        title: row.get(9)?,
        status: row.get(10)?,
        metadata_json: row.get(11)?,
        visited_at: row.get(12)?,
        created_at: row.get(13)?,
        updated_at: row.get(14)?,
        revision: row.get(15)?,
    })
}

fn navigation_recent_target_key(
    kind: &str,
    root_uri: &str,
    relative_path: Option<&str>,
    document_id: Option<&str>,
) -> Result<String, ControlPlaneError> {
    match kind {
        "folder" => Ok(format!(
            "folder:{}:{}",
            root_uri.trim(),
            relative_path.map(str::trim).unwrap_or_default()
        )),
        "page" => {
            let document_id = document_id
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .ok_or_else(|| {
                    ControlPlaneError::InvalidInput(
                        "navigation recent page 需要 document_id".to_string(),
                    )
                })?;
            Ok(format!("page:{}:{document_id}", root_uri.trim()))
        }
        _ => Err(ControlPlaneError::InvalidInput(
            "navigation recent kind 只能是 folder 或 page".to_string(),
        )),
    }
}

fn validate_ai_external_conversation_status(status: &str) -> Result<(), ControlPlaneError> {
    match status {
        "active"
        | "local_deleted"
        | "remote_deleted"
        | "remote_delete_failed"
        | "remote_missing" => Ok(()),
        _ => Err(ControlPlaneError::InvalidInput(
            "external conversation status 不合法".to_string(),
        )),
    }
}

fn row_to_ai_policy(row: &rusqlite::Row<'_>) -> rusqlite::Result<AiPolicyRecord> {
    Ok(AiPolicyRecord {
        id: row.get(0)?,
        user_id: row.get(1)?,
        workspace_id: row.get(2)?,
        allowed_roots_json: row.get(3)?,
        model_policy_json: row.get(4)?,
        quota_json: row.get(5)?,
        created_at: row.get(6)?,
        updated_at: row.get(7)?,
        revision: row.get(8)?,
    })
}

fn row_to_sync_state(row: &rusqlite::Row<'_>) -> rusqlite::Result<SyncStateRecord> {
    Ok(SyncStateRecord {
        id: row.get(0)?,
        workspace_id: row.get(1)?,
        remote_kind: row.get(2)?,
        remote_id: row.get(3)?,
        cursor: row.get(4)?,
        last_synced_at: row.get(5)?,
        status: row.get(6)?,
        error_json: row.get(7)?,
        updated_at: row.get(8)?,
        revision: row.get(9)?,
    })
}

fn row_to_ai_runtime_run(row: &rusqlite::Row<'_>) -> rusqlite::Result<AiRuntimeRunRecord> {
    Ok(AiRuntimeRunRecord {
        id: row.get(0)?,
        user_id: row.get(1)?,
        workspace_id: row.get(2)?,
        document_id: row.get(3)?,
        session_id: row.get(4)?,
        run_id: row.get(5)?,
        title: row.get(6)?,
        profile: row.get(7)?,
        acp_runtime: row.get(8)?,
        trace_id: row.get(9)?,
        status: row.get(10)?,
        runtime_json: row.get(11)?,
        payload_json: row.get(12)?,
        deleted_at: row.get(13)?,
        created_at: row.get(14)?,
        updated_at: row.get(15)?,
        revision: row.get(16)?,
    })
}

fn row_to_ai_runtime_event(row: &rusqlite::Row<'_>) -> rusqlite::Result<AiRuntimeEventRecord> {
    Ok(AiRuntimeEventRecord {
        id: row.get(0)?,
        user_id: row.get(1)?,
        workspace_id: row.get(2)?,
        document_id: row.get(3)?,
        session_id: row.get(4)?,
        run_id: row.get(5)?,
        profile: row.get(6)?,
        acp_runtime: row.get(7)?,
        event_type: row.get(8)?,
        payload_json: row.get(9)?,
        created_at: row.get(10)?,
    })
}

fn row_to_ai_runtime_journal_event(
    row: &rusqlite::Row<'_>,
) -> rusqlite::Result<AiRuntimeJournalEventRecord> {
    Ok(AiRuntimeJournalEventRecord {
        seq: row.get(0)?,
        event: AiRuntimeEventRecord {
            id: row.get(1)?,
            user_id: row.get(2)?,
            workspace_id: row.get(3)?,
            document_id: row.get(4)?,
            session_id: row.get(5)?,
            run_id: row.get(6)?,
            profile: row.get(7)?,
            acp_runtime: row.get(8)?,
            event_type: row.get(9)?,
            payload_json: row.get(10)?,
            created_at: row.get(11)?,
        },
    })
}

fn row_to_ai_external_conversation_binding(
    row: &rusqlite::Row<'_>,
) -> rusqlite::Result<AiExternalConversationBindingRecord> {
    Ok(AiExternalConversationBindingRecord {
        id: row.get(0)?,
        user_id: row.get(1)?,
        workspace_id: row.get(2)?,
        mnote_session_id: row.get(3)?,
        acp_session_id: row.get(4)?,
        agent_id: row.get(5)?,
        profile: row.get(6)?,
        provider: row.get(7)?,
        remote_conversation_id: row.get(8)?,
        remote_url: row.get(9)?,
        status: row.get(10)?,
        metadata_json: row.get(11)?,
        created_at: row.get(12)?,
        updated_at: row.get(13)?,
        deleted_at: row.get(14)?,
        revision: row.get(15)?,
    })
}

fn derive_ai_runtime_title(payload_json: &str, fallback: &str) -> String {
    let title = serde_json::from_str::<serde_json::Value>(payload_json)
        .ok()
        .and_then(|payload| {
            payload
                .get("title")
                .or_else(|| payload.get("message"))
                .and_then(serde_json::Value::as_str)
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(ToOwned::to_owned)
        })
        .unwrap_or_else(|| fallback.to_string());
    title.chars().take(80).collect()
}

fn prune_navigation_recent_for_kind(
    conn: &Connection,
    user_id: &str,
    kind: &str,
) -> Result<(), ControlPlaneError> {
    let keep = match kind {
        "folder" => 10_i64,
        "page" => 20_i64,
        _ => 20_i64,
    };
    conn.execute(
        "UPDATE user_navigation_recent
         SET status = 'removed', updated_at = ?1, revision = revision + 1
         WHERE user_id = ?2
           AND kind = ?3
           AND status = 'active'
           AND id NOT IN (
             SELECT id FROM user_navigation_recent
             WHERE user_id = ?2 AND kind = ?3 AND status = 'active'
             ORDER BY visited_at DESC, updated_at DESC
             LIMIT ?4
           )",
        params![now_text(), user_id, kind, keep],
    )?;
    Ok(())
}

fn list_ai_agent_profile_access_rows(
    conn: &Connection,
    user_id: &str,
) -> Result<Vec<AiAgentProfileAccessRecord>, ControlPlaneError> {
    let mut stmt = conn.prepare(
        "SELECT
           p.id, p.agent_id, p.profile_kind, p.owner_user_id, p.base_profile_name,
           p.isolated_profile_name, p.display_name, p.status, p.created_at, p.updated_at,
           p.revision,
           g.id, g.profile_id, g.user_id, g.role, g.can_run, g.can_manage_skills,
           g.can_manage_config, g.created_at, g.updated_at, g.revision
         FROM ai_agent_profiles p
         JOIN ai_agent_profile_grants g ON g.profile_id = p.id
         WHERE p.agent_id = 'hermes'
           AND p.status = 'active'
           AND g.can_run = 1
           AND (
             (p.profile_kind = 'personal' AND p.owner_user_id = ?1 AND g.user_id = ?1)
             OR (p.profile_kind = 'shared' AND (g.user_id = '' OR g.user_id = ?1))
           )
         ORDER BY
           CASE p.profile_kind WHEN 'personal' THEN 0 ELSE 1 END,
           p.display_name ASC,
           g.can_manage_skills DESC,
           g.can_manage_config DESC",
    )?;
    let rows = stmt
        .query_map(params![user_id], row_to_ai_agent_profile_access)?
        .collect::<Result<Vec<_>, _>>()
        .map_err(ControlPlaneError::from)?;
    let mut by_profile = BTreeMap::<String, AiAgentProfileAccessRecord>::new();
    for row in rows {
        by_profile
            .entry(row.profile.id.clone())
            .and_modify(|existing| {
                if row.grant.can_manage_skills && !existing.grant.can_manage_skills {
                    *existing = row.clone();
                }
            })
            .or_insert(row);
    }
    Ok(by_profile.into_values().collect())
}

impl ControlPlaneStore for SqliteControlPlaneStore {
    fn upsert_user(&self, input: UpsertUserInput) -> Result<UserRecord, ControlPlaneError> {
        if input.username.trim().is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "username 不能为空".to_string(),
            ));
        }
        if input.display_name.trim().is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "display_name 不能为空".to_string(),
            ));
        }

        let conn = self.conn.lock().unwrap();
        let now = now_text();
        let id = input.id.unwrap_or_else(|| new_id("usr"));
        let role = input.role.unwrap_or_else(|| "user".to_string());

        let existing = conn
            .query_row(
                "SELECT id, email, username, display_name, role, status, created_at, updated_at, revision
                 FROM users WHERE id = ?1",
                params![id],
                row_to_user,
            )
            .optional()?;

        if let Some(existing) = existing {
            let revision = existing.revision + 1;
            conn.execute(
                "UPDATE users
                 SET email = ?1, username = ?2, display_name = ?3, role = ?4, updated_at = ?5, revision = ?6
                 WHERE id = ?7",
                params![
                    input.email,
                    input.username,
                    input.display_name,
                    role,
                    now,
                    revision,
                    existing.id
                ],
            )?;
            return Ok(UserRecord {
                id: existing.id,
                email: input.email,
                username: input.username,
                display_name: input.display_name,
                role,
                status: existing.status,
                created_at: existing.created_at,
                updated_at: now,
                revision,
            });
        }

        conn.execute(
            "INSERT INTO users (id, email, username, display_name, role, status, created_at, updated_at, revision)
             VALUES (?1, ?2, ?3, ?4, ?5, 'active', ?6, ?7, 1)",
            params![id, input.email, input.username, input.display_name, role, now, now],
        )?;

        Ok(UserRecord {
            id,
            email: input.email,
            username: input.username,
            display_name: input.display_name,
            role,
            status: "active".to_string(),
            created_at: now.clone(),
            updated_at: now,
            revision: 1,
        })
    }

    fn create_password_identity(
        &self,
        input: CreatePasswordIdentityInput,
    ) -> Result<(), ControlPlaneError> {
        let username = input.username.trim();
        if username.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "username 不能为空".to_string(),
            ));
        }
        if input.password.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "password 不能为空".to_string(),
            ));
        }

        let conn = self.conn.lock().unwrap();
        let now = now_text();
        let user_id = input.user_id;
        let password_hash = password_hash_v1(&input.password);
        conn.execute(
            "INSERT INTO auth_identities (id, user_id, provider, provider_subject, password_hash, password_version, created_at, updated_at)
             VALUES (?1, ?2, 'password_username', ?3, ?4, 1, ?5, ?6)
             ON CONFLICT(provider, provider_subject)
             DO UPDATE SET user_id = excluded.user_id, password_hash = excluded.password_hash, updated_at = excluded.updated_at",
            params![
                new_id("ident"),
                user_id,
                username,
                password_hash,
                now,
                now
            ],
        )?;
        if let Some(email) = input
            .email
            .map(|value| value.trim().to_string())
            .filter(|value| !value.is_empty())
        {
            conn.execute(
                "INSERT INTO auth_identities (id, user_id, provider, provider_subject, password_hash, password_version, created_at, updated_at)
                 VALUES (?1, ?2, 'password_email', ?3, ?4, 1, ?5, ?6)
                 ON CONFLICT(provider, provider_subject)
                 DO UPDATE SET user_id = excluded.user_id, password_hash = excluded.password_hash, updated_at = excluded.updated_at",
                params![
                    new_id("ident"),
                    user_id,
                    email,
                    password_hash_v1(&input.password),
                    now,
                    now
                ],
            )?;
        }
        Ok(())
    }

    fn authenticate_password(
        &self,
        input: AuthenticatePasswordInput,
    ) -> Result<ResolvedAuthSession, ControlPlaneError> {
        let account = input.account.trim();
        if account.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "account 不能为空".to_string(),
            ));
        }

        let conn = self.conn.lock().unwrap();
        let provider = if account.contains('@') {
            "password_email"
        } else {
            "password_username"
        };
        let expected_hash = password_hash_v1(&input.password);
        let user = conn
            .query_row(
                "SELECT u.id, u.email, u.username, u.display_name, u.role, u.status, u.created_at, u.updated_at, u.revision
                 FROM auth_identities i
                 JOIN users u ON u.id = i.user_id
                 WHERE i.provider = ?1
                   AND i.provider_subject = ?2
                   AND i.password_hash = ?3
                   AND u.status = 'active'
                 LIMIT 1",
                params![provider, account, expected_hash],
                row_to_user,
            )
            .optional()?
            .ok_or_else(|| ControlPlaneError::Unauthorized("账号或密码错误".to_string()))?;
        drop(conn);

        let session = self.create_session(CreateSessionInput {
            id: input.session_id,
            user_id: user.id.clone(),
            token_hash: input.token_hash,
            user_agent: input.user_agent,
            ip_hash: input.ip_hash,
            expires_at: input.expires_at,
        })?;
        Ok(ResolvedAuthSession { session, user })
    }

    fn create_session(
        &self,
        input: CreateSessionInput,
    ) -> Result<AuthSessionRecord, ControlPlaneError> {
        let token_hash = input.token_hash.trim();
        if token_hash.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "token_hash 不能为空".to_string(),
            ));
        }

        let conn = self.conn.lock().unwrap();
        let now = now_text();
        let expires_at = input
            .expires_at
            .unwrap_or_else(|| (chrono::Utc::now() + chrono::Duration::days(30)).to_rfc3339());
        let record = AuthSessionRecord {
            id: input.id.unwrap_or_else(|| new_id("sess")),
            user_id: input.user_id,
            token_hash: token_hash.to_string(),
            user_agent: input.user_agent,
            ip_hash: input.ip_hash,
            created_at: now.clone(),
            expires_at,
            revoked_at: None,
            last_seen_at: now.clone(),
        };
        conn.execute(
            "INSERT INTO auth_sessions (id, user_id, token_hash, user_agent, ip_hash, created_at, expires_at, revoked_at, last_seen_at)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL, ?8)",
            params![
                record.id,
                record.user_id,
                record.token_hash,
                record.user_agent,
                record.ip_hash,
                record.created_at,
                record.expires_at,
                record.last_seen_at
            ],
        )?;
        Ok(record)
    }

    fn get_session_by_token_hash(
        &self,
        token_hash: &str,
    ) -> Result<Option<ResolvedAuthSession>, ControlPlaneError> {
        let token_hash = token_hash.trim();
        if token_hash.is_empty() {
            return Ok(None);
        }

        let conn = self.conn.lock().unwrap();
        let now = now_text();
        let resolved = conn
            .query_row(
                "SELECT
                   s.id, s.user_id, s.token_hash, s.user_agent, s.ip_hash, s.created_at, s.expires_at, s.revoked_at, s.last_seen_at,
                   u.id, u.email, u.username, u.display_name, u.role, u.status, u.created_at, u.updated_at, u.revision
                 FROM auth_sessions s
                 JOIN users u ON u.id = s.user_id
                 WHERE s.token_hash = ?1
                   AND s.revoked_at IS NULL
                   AND s.expires_at > ?2
                   AND u.status = 'active'
                 LIMIT 1",
                params![token_hash, now],
                |row| {
                    Ok(ResolvedAuthSession {
                        session: row_to_auth_session(row)?,
                        user: UserRecord {
                            id: row.get(9)?,
                            email: row.get(10)?,
                            username: row.get(11)?,
                            display_name: row.get(12)?,
                            role: row.get(13)?,
                            status: row.get(14)?,
                            created_at: row.get(15)?,
                            updated_at: row.get(16)?,
                            revision: row.get(17)?,
                        },
                    })
                },
            )
            .optional()?;

        if let Some(resolved) = &resolved {
            conn.execute(
                "UPDATE auth_sessions SET last_seen_at = ?1 WHERE id = ?2",
                params![now, resolved.session.id],
            )?;
        }

        Ok(resolved)
    }

    fn revoke_session(&self, session_id: &str) -> Result<(), ControlPlaneError> {
        let session_id = session_id.trim();
        if session_id.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "session_id 不能为空".to_string(),
            ));
        }
        let conn = self.conn.lock().unwrap();
        conn.execute(
            "UPDATE auth_sessions SET revoked_at = ?1 WHERE id = ?2 AND revoked_at IS NULL",
            params![now_text(), session_id],
        )?;
        Ok(())
    }

    fn ensure_default_workspace(
        &self,
        actor_id: &str,
    ) -> Result<WorkspaceRecord, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        let existing = conn
            .query_row(
                "SELECT id, owner_user_id, name, kind, root_uri, root_path, source_kind, status, created_at, updated_at, revision
                 FROM workspaces
                 WHERE owner_user_id = ?1 AND kind = 'personal' AND status = 'active'
                 LIMIT 1",
                params![actor_id],
                row_to_workspace,
            )
            .optional()?;
        if let Some(workspace) = existing {
            return Ok(workspace);
        }

        let user = conn
            .query_row(
                "SELECT id, email, username, display_name, role, status, created_at, updated_at, revision
                 FROM users WHERE id = ?1",
                params![actor_id],
                row_to_user,
            )
            .optional()?
            .ok_or_else(|| ControlPlaneError::NotFound(format!("user not found: {actor_id}")))?;

        let now = now_text();
        let workspace = WorkspaceRecord {
            id: new_id("ws"),
            owner_user_id: user.id.clone(),
            name: format!("{} 的空间", user.display_name),
            kind: "personal".to_string(),
            root_uri: format!("local://users/{}/workspaces/my-space", user.id),
            root_path: format!(
                "/mnt/Data1T/Mnote_data/users/{}/workspaces/my-space",
                user.id
            ),
            source_kind: "local_folder".to_string(),
            status: "active".to_string(),
            created_at: now.clone(),
            updated_at: now.clone(),
            revision: 1,
        };

        conn.execute(
            "INSERT INTO workspaces (id, owner_user_id, name, kind, root_uri, root_path, source_kind, status, created_at, updated_at, revision)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
            params![
                workspace.id,
                workspace.owner_user_id,
                workspace.name,
                workspace.kind,
                workspace.root_uri,
                workspace.root_path,
                workspace.source_kind,
                workspace.status,
                workspace.created_at,
                workspace.updated_at,
                workspace.revision
            ],
        )?;
        conn.execute(
            "INSERT INTO workspace_members (id, workspace_id, user_id, role, status, created_at, updated_at, revision)
             VALUES (?1, ?2, ?3, 'owner', 'active', ?4, ?5, 1)",
            params![new_id("wsm"), workspace.id, user.id, now, now],
        )?;
        conn.execute(
            "INSERT INTO directory_grants (id, user_id, workspace_id, root_uri, root_path, permission, recursive, capabilities_json, source, status, created_by, created_at, updated_at, revision)
             VALUES (?1, ?2, ?3, ?4, ?5, 'write', 1, ?6, 'auto', 'active', ?7, ?8, ?9, 1)",
            params![
                new_id("grant"),
                workspace.owner_user_id,
                workspace.id,
                workspace.root_uri,
                workspace.root_path,
                capabilities_json(&["ai".to_string(), "share".to_string()])?,
                workspace.owner_user_id,
                now,
                now
            ],
        )?;

        Ok(workspace)
    }

    fn grant_directory_access(
        &self,
        input: DirectoryGrantInput,
    ) -> Result<DirectoryGrantRecord, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        let now = now_text();
        let record = DirectoryGrantRecord {
            id: new_id("grant"),
            user_id: input.user_id,
            workspace_id: input.workspace_id,
            root_uri: input.root_uri,
            root_path: input.root_path,
            permission: input.permission,
            recursive: input.recursive,
            capabilities_json: capabilities_json(&input.capabilities)?,
            source: input.source,
            status: "active".to_string(),
            created_by: input.created_by,
            created_at: now.clone(),
            updated_at: now,
            revision: 1,
        };
        conn.execute(
            "INSERT INTO directory_grants (id, user_id, workspace_id, root_uri, root_path, permission, recursive, capabilities_json, source, status, created_by, created_at, updated_at, revision)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
            params![
                record.id,
                record.user_id,
                record.workspace_id,
                record.root_uri,
                record.root_path,
                record.permission,
                if record.recursive { 1 } else { 0 },
                record.capabilities_json,
                record.source,
                record.status,
                record.created_by,
                record.created_at,
                record.updated_at,
                record.revision
            ],
        )?;
        Ok(record)
    }

    fn list_directory_grants(&self) -> Result<Vec<DirectoryGrantRecord>, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "SELECT id, user_id, workspace_id, root_uri, root_path, permission, recursive, capabilities_json, source, status, created_by, created_at, updated_at, revision
             FROM directory_grants
             WHERE status = 'active'
             ORDER BY created_at ASC",
        )?;
        let grants = stmt
            .query_map([], row_to_grant)?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(grants)
    }

    fn list_directory_grants_for_actor(
        &self,
        actor_id: &str,
    ) -> Result<Vec<DirectoryGrantRecord>, ControlPlaneError> {
        let actor_id = actor_id.trim();
        if actor_id.is_empty() {
            return Ok(Vec::new());
        }
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "SELECT id, user_id, workspace_id, root_uri, root_path, permission, recursive, capabilities_json, source, status, created_by, created_at, updated_at, revision
             FROM directory_grants
             WHERE user_id = ?1 AND status = 'active'
             ORDER BY created_at ASC",
        )?;
        let grants = stmt
            .query_map(params![actor_id], row_to_grant)?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(grants)
    }

    fn find_directory_grants(
        &self,
        lookup: DirectoryGrantLookup,
    ) -> Result<Vec<DirectoryGrantRecord>, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        let grant_id = lookup
            .grant_id
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty());
        let user_id = lookup
            .user_id
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty());
        let root_uri = lookup
            .root_uri
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty());
        let status = if lookup.include_revoked {
            "%"
        } else {
            "active"
        };
        let mut stmt = conn.prepare(
            "SELECT id, user_id, workspace_id, root_uri, root_path, permission, recursive, capabilities_json, source, status, created_by, created_at, updated_at, revision
             FROM directory_grants
             WHERE (?1 IS NULL OR id = ?1)
               AND (?2 IS NULL OR user_id = ?2)
               AND (?3 IS NULL OR root_uri = ?3)
               AND status LIKE ?4
             ORDER BY created_at ASC",
        )?;
        let grants = stmt
            .query_map(params![grant_id, user_id, root_uri, status], row_to_grant)?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(grants)
    }

    fn revoke_directory_grant(
        &self,
        grant_id: &str,
        expected_revision: Option<i64>,
    ) -> Result<(), ControlPlaneError> {
        let grant_id = grant_id.trim();
        if grant_id.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "grant_id 不能为空".to_string(),
            ));
        }
        let conn = self.conn.lock().unwrap();
        let existing_revision = conn
            .query_row(
                "SELECT revision FROM directory_grants WHERE id = ?1 AND status = 'active'",
                params![grant_id],
                |row| row.get::<_, i64>(0),
            )
            .optional()?
            .ok_or_else(|| ControlPlaneError::NotFound(format!("grant not found: {grant_id}")))?;
        if let Some(expected_revision) = expected_revision {
            if expected_revision != existing_revision {
                return Err(ControlPlaneError::Conflict(
                    "目录授权 revision 已变化".to_string(),
                ));
            }
        }
        conn.execute(
            "UPDATE directory_grants
             SET status = 'revoked', updated_at = ?1, revision = revision + 1
             WHERE id = ?2 AND status = 'active'",
            params![now_text(), grant_id],
        )?;
        Ok(())
    }

    fn resolve_access(
        &self,
        actor_id: &str,
        root_uri: &str,
    ) -> Result<ResolvedAccess, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        let root_path = file_uri_to_legacy_path(root_uri);
        let mut stmt = conn.prepare(
            "SELECT id, user_id, workspace_id, root_uri, root_path, permission, recursive, capabilities_json, source, status, created_by, created_at, updated_at, revision
             FROM directory_grants
             WHERE user_id = ?1
               AND status = 'active'
               AND (
                 ?2 = root_uri
                 OR (recursive = 1 AND ?2 LIKE root_uri || '/%')
                 OR (recursive = 1 AND root_uri LIKE '%/' AND ?2 LIKE root_uri || '%')
                 OR (?3 != '' AND ?3 = root_path)
                 OR (?3 != '' AND recursive = 1 AND ?3 LIKE root_path || '/%')
               )",
        )?;
        let grants = stmt
            .query_map(params![actor_id, root_uri, root_path], row_to_grant)?
            .collect::<Result<Vec<_>, _>>()?;
        let mut permission = "none".to_string();
        for grant in &grants {
            permission = max_permission(&permission, &grant.permission).to_string();
        }
        Ok(ResolvedAccess {
            user_id: actor_id.to_string(),
            root_uri: root_uri.to_string(),
            permission,
            grant_ids: grants.into_iter().map(|grant| grant.id).collect(),
        })
    }

    fn create_share_link(
        &self,
        input: CreateShareLinkInput,
    ) -> Result<CreatedShareLink, ControlPlaneError> {
        let workspace_id = input.workspace_id.trim().to_string();
        if workspace_id.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "workspace_id 不能为空".to_string(),
            ));
        }
        let resource_kind = input.resource_kind.trim().to_string();
        let resource_id = input.resource_id.trim().to_string();
        let permission = input.permission.trim().to_string();
        if resource_kind.is_empty() || resource_id.is_empty() || permission.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "share_link 资源与权限不能为空".to_string(),
            ));
        }
        let token = input
            .token
            .filter(|value| !value.trim().is_empty())
            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
        let token_hash = share_token_hash_v1(&token);
        let conn = self.conn.lock().unwrap();
        let now = now_text();
        let link = ShareLinkRecord {
            id: input.id.unwrap_or_else(|| new_id("share")),
            workspace_id,
            resource_kind,
            resource_id,
            token_hash: token_hash.clone(),
            permission,
            created_by: input.created_by,
            expires_at: input.expires_at,
            revoked_at: None,
            created_at: now.clone(),
            updated_at: now.clone(),
            revision: 1,
        };
        conn.execute(
            "INSERT INTO share_links (id, workspace_id, resource_kind, resource_id, token_hash, permission, created_by, expires_at, revoked_at, created_at, updated_at, revision)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, NULL, ?9, ?10, 1)",
            params![
                link.id,
                link.workspace_id,
                link.resource_kind,
                link.resource_id,
                link.token_hash,
                link.permission,
                link.created_by,
                link.expires_at,
                link.created_at,
                link.updated_at
            ],
        )?;
        Ok(CreatedShareLink { link, token })
    }

    fn list_share_links(
        &self,
        workspace_id: &str,
    ) -> Result<Vec<ShareLinkRecord>, ControlPlaneError> {
        let workspace_id = workspace_id.trim();
        if workspace_id.is_empty() {
            return Ok(Vec::new());
        }
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "SELECT id, workspace_id, resource_kind, resource_id, token_hash, permission, created_by, expires_at, revoked_at, created_at, updated_at, revision
             FROM share_links
             WHERE workspace_id = ?1
             ORDER BY created_at ASC",
        )?;
        let links = stmt
            .query_map(params![workspace_id], row_to_share_link)?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(links)
    }

    fn resolve_share_link(
        &self,
        token_hash: &str,
    ) -> Result<Option<ShareLinkRecord>, ControlPlaneError> {
        let token_hash = token_hash.trim();
        if token_hash.is_empty() {
            return Ok(None);
        }
        let conn = self.conn.lock().unwrap();
        let link = conn
            .query_row(
                "SELECT id, workspace_id, resource_kind, resource_id, token_hash, permission, created_by, expires_at, revoked_at, created_at, updated_at, revision
                 FROM share_links
                 WHERE token_hash = ?1 AND revoked_at IS NULL
                 LIMIT 1",
                params![token_hash],
                row_to_share_link,
            )
            .optional()?;
        Ok(link)
    }

    fn revoke_share_link(&self, link_id: &str) -> Result<(), ControlPlaneError> {
        let link_id = link_id.trim();
        if link_id.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "link_id 不能为空".to_string(),
            ));
        }
        let conn = self.conn.lock().unwrap();
        let existed = conn.execute(
            "UPDATE share_links
                 SET revoked_at = ?1, updated_at = ?2, revision = revision + 1
                 WHERE id = ?3 AND revoked_at IS NULL",
            params![now_text(), now_text(), link_id],
        )?;
        if existed == 0 {
            return Err(ControlPlaneError::NotFound(format!(
                "share link not found: {link_id}"
            )));
        }
        Ok(())
    }

    fn append_audit(&self, input: AppendAuditInput) -> Result<(), ControlPlaneError> {
        let action = input.action.trim();
        let target_kind = input.target_kind.trim();
        if action.is_empty() || target_kind.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "audit action/target_kind 不能为空".to_string(),
            ));
        }
        let conn = self.conn.lock().unwrap();
        conn.execute(
            "INSERT INTO audit_log (id, actor_user_id, action, target_kind, target_id, metadata_json, created_at)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
            params![
                new_id("audit"),
                input.actor_user_id,
                action,
                target_kind,
                input.target_id,
                input.metadata_json,
                now_text()
            ],
        )?;
        Ok(())
    }

    fn list_audit_log(&self, limit: usize) -> Result<Vec<AuditLogRecord>, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "SELECT id, actor_user_id, action, target_kind, target_id, metadata_json, created_at
             FROM audit_log
             ORDER BY created_at DESC
             LIMIT ?1",
        )?;
        let rows = stmt
            .query_map(params![limit as i64], row_to_audit_log)?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(rows)
    }

    fn append_outbox(
        &self,
        input: OutboxEventInput,
    ) -> Result<OutboxEventRecord, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        let record = OutboxEventRecord {
            id: new_id("evt"),
            topic: input.topic,
            event_type: input.event_type,
            payload_json: input.payload_json,
            created_at: now_text(),
            delivered_at: None,
            attempts: 0,
        };
        conn.execute(
            "INSERT INTO outbox_events (id, topic, event_type, payload_json, created_at, delivered_at, attempts)
             VALUES (?1, ?2, ?3, ?4, ?5, NULL, 0)",
            params![
                record.id,
                record.topic,
                record.event_type,
                record.payload_json,
                record.created_at
            ],
        )?;
        Ok(record)
    }

    fn drain_outbox(&self, limit: usize) -> Result<Vec<OutboxEventRecord>, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "SELECT id, topic, event_type, payload_json, created_at, delivered_at, attempts
             FROM outbox_events
             WHERE delivered_at IS NULL
             ORDER BY created_at ASC
             LIMIT ?1",
        )?;
        let rows = stmt
            .query_map(params![limit as i64], row_to_outbox)?
            .collect::<Result<Vec<_>, _>>()?;
        let delivered_at = now_text();
        for row in &rows {
            conn.execute(
                "UPDATE outbox_events SET delivered_at = ?1, attempts = attempts + 1 WHERE id = ?2",
                params![delivered_at, row.id],
            )?;
        }
        Ok(rows)
    }

    fn mark_outbox_delivered(&self, event_id: &str) -> Result<(), ControlPlaneError> {
        let event_id = event_id.trim();
        if event_id.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "event_id 不能为空".to_string(),
            ));
        }
        let conn = self.conn.lock().unwrap();
        let updated = conn.execute(
            "UPDATE outbox_events
             SET delivered_at = COALESCE(delivered_at, ?1), attempts = attempts + 1
             WHERE id = ?2",
            params![now_text(), event_id],
        )?;
        if updated == 0 {
            return Err(ControlPlaneError::NotFound(format!(
                "outbox event not found: {event_id}"
            )));
        }
        Ok(())
    }

    fn upsert_sidebar_shortcut(
        &self,
        input: UpsertSidebarShortcutInput,
    ) -> Result<SidebarShortcutRecord, ControlPlaneError> {
        let user_id = input.user_id.trim().to_string();
        let workspace_id = input.workspace_id.trim().to_string();
        let root_uri = input
            .root_uri
            .map(|value| value.trim().to_string())
            .filter(|value| !value.is_empty());
        let kind = input.kind.trim().to_string();
        let source_kind = input.source_kind.trim().to_string();
        let target_id = input.target_id.trim().to_string();
        let title = input.title.trim().to_string();
        if user_id.is_empty()
            || workspace_id.is_empty()
            || kind.is_empty()
            || source_kind.is_empty()
            || target_id.is_empty()
            || title.is_empty()
        {
            return Err(ControlPlaneError::InvalidInput(
                "sidebar shortcut user/workspace/kind/target/title 不能为空".to_string(),
            ));
        }
        if kind != "page" && kind != "folder" {
            return Err(ControlPlaneError::InvalidInput(
                "sidebar shortcut kind 只能是 page 或 folder".to_string(),
            ));
        }
        let conn = self.conn.lock().unwrap();
        let now = now_text();
        conn.execute(
            "INSERT INTO sidebar_shortcuts (
               id, user_id, workspace_id, root_uri, kind, source_kind, target_id, relative_path,
               document_id, title, icon, sort_order, status, metadata_json, created_at, updated_at, revision
             )
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, 'active', ?13, ?14, ?15, 1)
             ON CONFLICT(user_id, workspace_id, kind, target_id)
             DO UPDATE SET
               root_uri = excluded.root_uri,
               relative_path = excluded.relative_path,
               document_id = excluded.document_id,
               title = excluded.title,
               icon = excluded.icon,
               sort_order = excluded.sort_order,
               status = 'active',
               metadata_json = excluded.metadata_json,
               updated_at = excluded.updated_at,
               revision = sidebar_shortcuts.revision + 1",
            params![
                input.id.unwrap_or_else(|| new_id("shortcut")),
                user_id,
                workspace_id,
                root_uri,
                kind,
                source_kind,
                target_id,
                input.relative_path,
                input.document_id,
                title,
                input.icon,
                input.sort_order,
                input.metadata_json,
                now,
                now
            ],
        )?;
        let record = conn.query_row(
            "SELECT id, user_id, workspace_id, root_uri, kind, source_kind, target_id, relative_path,
                    document_id, title, icon, sort_order, status, metadata_json, created_at,
                    updated_at, revision
             FROM sidebar_shortcuts
             WHERE user_id = ?1 AND workspace_id = ?2 AND kind = ?3 AND target_id = ?4
             LIMIT 1",
            params![user_id, workspace_id, kind, target_id],
            row_to_sidebar_shortcut,
        )?;
        Ok(record)
    }

    fn list_sidebar_shortcuts(
        &self,
        user_id: &str,
        workspace_id: &str,
    ) -> Result<Vec<SidebarShortcutRecord>, ControlPlaneError> {
        let user_id = user_id.trim();
        let workspace_id = workspace_id.trim();
        if user_id.is_empty() || workspace_id.is_empty() {
            return Ok(Vec::new());
        }
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "SELECT id, user_id, workspace_id, root_uri, kind, source_kind, target_id, relative_path,
                    document_id, title, icon, sort_order, status, metadata_json, created_at,
                    updated_at, revision
             FROM sidebar_shortcuts
             WHERE user_id = ?1 AND workspace_id = ?2 AND status = 'active'
             ORDER BY sort_order ASC, created_at ASC",
        )?;
        let rows = stmt
            .query_map(params![user_id, workspace_id], row_to_sidebar_shortcut)?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(rows)
    }

    fn list_sidebar_shortcuts_with_global_local(
        &self,
        user_id: &str,
        workspace_id: &str,
    ) -> Result<Vec<SidebarShortcutRecord>, ControlPlaneError> {
        let user_id = user_id.trim();
        let workspace_id = workspace_id.trim();
        if user_id.is_empty() || workspace_id.is_empty() {
            return Ok(Vec::new());
        }
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "SELECT id, user_id, workspace_id, root_uri, kind, source_kind, target_id, relative_path,
                    document_id, title, icon, sort_order, status, metadata_json, created_at,
                    updated_at, revision
             FROM sidebar_shortcuts
             WHERE user_id = ?1
               AND status = 'active'
               AND (workspace_id = ?2 OR source_kind = 'local_folder')
             ORDER BY sort_order ASC, created_at ASC",
        )?;
        let rows = stmt
            .query_map(params![user_id, workspace_id], row_to_sidebar_shortcut)?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(rows)
    }

    fn delete_sidebar_shortcut(
        &self,
        user_id: &str,
        shortcut_id: &str,
    ) -> Result<(), ControlPlaneError> {
        let user_id = user_id.trim();
        let shortcut_id = shortcut_id.trim();
        if user_id.is_empty() || shortcut_id.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "sidebar shortcut user_id/shortcut_id 不能为空".to_string(),
            ));
        }
        let conn = self.conn.lock().unwrap();
        let changed = conn.execute(
            "UPDATE sidebar_shortcuts
             SET status = 'removed', updated_at = ?1, revision = revision + 1
             WHERE id = ?2 AND user_id = ?3 AND status = 'active'",
            params![now_text(), shortcut_id, user_id],
        )?;
        if changed == 0 {
            return Err(ControlPlaneError::NotFound(format!(
                "sidebar shortcut not found: {shortcut_id}"
            )));
        }
        Ok(())
    }

    fn upsert_user_ui_preference(
        &self,
        input: UpsertUserUiPreferenceInput,
    ) -> Result<UserUiPreferenceRecord, ControlPlaneError> {
        let user_id = input.user_id.trim().to_string();
        let workspace_id = option_to_stored_text(input.workspace_id);
        let source_kind = option_to_stored_text(input.source_kind);
        let scope_kind = input.scope_kind.trim().to_string();
        let scope_id = input.scope_id.trim().to_string();
        let key = input.key.trim().to_string();
        if user_id.is_empty() || scope_kind.is_empty() || scope_id.is_empty() || key.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "user UI preference user/scope/key 不能为空".to_string(),
            ));
        }
        serde_json::from_str::<serde_json::Value>(&input.value_json)?;
        let conn = self.conn.lock().unwrap();
        let now = now_text();
        conn.execute(
            "INSERT INTO user_ui_preferences (
               id, user_id, workspace_id, source_kind, scope_kind, scope_id, key,
               value_json, status, created_at, updated_at, revision
             )
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 'active', ?9, ?10, 1)
             ON CONFLICT(user_id, workspace_id, source_kind, scope_kind, scope_id, key)
             DO UPDATE SET
               value_json = excluded.value_json,
               status = 'active',
               updated_at = excluded.updated_at,
               revision = user_ui_preferences.revision + 1",
            params![
                input.id.unwrap_or_else(|| new_id("ui_pref")),
                user_id,
                workspace_id,
                source_kind,
                scope_kind,
                scope_id,
                key,
                input.value_json,
                now,
                now
            ],
        )?;
        let record = conn.query_row(
            "SELECT id, user_id, workspace_id, source_kind, scope_kind, scope_id, key,
                    value_json, status, created_at, updated_at, revision
             FROM user_ui_preferences
             WHERE user_id = ?1
               AND workspace_id = ?2
               AND source_kind = ?3
               AND scope_kind = ?4
               AND scope_id = ?5
               AND key = ?6
             LIMIT 1",
            params![
                user_id,
                workspace_id,
                source_kind,
                scope_kind,
                scope_id,
                key
            ],
            row_to_user_ui_preference,
        )?;
        Ok(record)
    }

    fn list_user_ui_preferences(
        &self,
        user_id: &str,
        workspace_id: Option<&str>,
        source_kind: Option<&str>,
    ) -> Result<Vec<UserUiPreferenceRecord>, ControlPlaneError> {
        let user_id = user_id.trim();
        if user_id.is_empty() {
            return Ok(Vec::new());
        }
        let workspace_id = workspace_id.map(str::trim).unwrap_or_default();
        let source_kind = source_kind.map(str::trim).unwrap_or_default();
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "SELECT id, user_id, workspace_id, source_kind, scope_kind, scope_id, key,
                    value_json, status, created_at, updated_at, revision
             FROM user_ui_preferences
             WHERE user_id = ?1
               AND status = 'active'
               AND (workspace_id = '' OR workspace_id = ?2)
               AND (source_kind = '' OR source_kind = ?3)
             ORDER BY created_at ASC",
        )?;
        let rows = stmt
            .query_map(
                params![user_id, workspace_id, source_kind],
                row_to_user_ui_preference,
            )?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(rows)
    }

    fn list_user_ui_preferences_for_scope(
        &self,
        workspace_id: Option<&str>,
        source_kind: Option<&str>,
        scope_kind: &str,
        scope_id: &str,
        key: &str,
    ) -> Result<Vec<UserUiPreferenceRecord>, ControlPlaneError> {
        let workspace_id = workspace_id.map(str::trim).unwrap_or_default();
        let source_kind = source_kind.map(str::trim).unwrap_or_default();
        let scope_kind = scope_kind.trim();
        let scope_id = scope_id.trim();
        let key = key.trim();
        if scope_kind.is_empty() || scope_id.is_empty() || key.is_empty() {
            return Ok(Vec::new());
        }
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "SELECT id, user_id, workspace_id, source_kind, scope_kind, scope_id, key,
                    value_json, status, created_at, updated_at, revision
             FROM user_ui_preferences
             WHERE status = 'active'
               AND workspace_id = ?1
               AND source_kind = ?2
               AND scope_kind = ?3
               AND scope_id = ?4
               AND key = ?5
             ORDER BY updated_at ASC",
        )?;
        let rows = stmt
            .query_map(
                params![workspace_id, source_kind, scope_kind, scope_id, key],
                row_to_user_ui_preference,
            )?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(rows)
    }

    fn ensure_ai_agent_profile_policy(
        &self,
        user_id: &str,
        is_admin: bool,
    ) -> Result<Vec<AiAgentProfileAccessRecord>, ControlPlaneError> {
        let user_id = user_id.trim();
        if user_id.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "ai agent profile user_id 不能为空".to_string(),
            ));
        }
        let conn = self.conn.lock().unwrap();
        let now = now_text();
        conn.execute(
            "INSERT INTO ai_agent_profiles (
               id, agent_id, profile_kind, owner_user_id, base_profile_name,
               isolated_profile_name, display_name, status, created_at, updated_at, revision
             )
             VALUES ('shared_lite', 'hermes', 'shared', '', 'lite', 'lite', 'Lite', 'active', ?1, ?2, 1)
             ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
             DO UPDATE SET status = 'active', updated_at = excluded.updated_at",
            params![now, now],
        )?;
        conn.execute(
            "INSERT INTO ai_agent_profile_grants (
               id, profile_id, user_id, role, can_run, can_manage_skills,
               can_manage_config, created_at, updated_at, revision
             )
             VALUES ('grant_shared_lite_all', 'shared_lite', '', 'user', 1, 0, 0, ?1, ?2, 1)
             ON CONFLICT(profile_id, user_id, role)
             DO UPDATE SET can_run = 1, can_manage_skills = 0, can_manage_config = 0,
               updated_at = excluded.updated_at",
            params![now, now],
        )?;

        // 旧的泛化 OpenClaw 网页问答入口已拆成明确的三类 chat agent。
        conn.execute(
            "UPDATE ai_agent_profiles
             SET status = 'retired', updated_at = ?1
             WHERE id = 'shared_openclaw_webqa'",
            params![now],
        )?;
        let shared_chat_profiles = [
            (
                "shared_deepseek_chat",
                "deepseek-chat",
                "openclaw-deepseek-chat",
                "DeepSeek Chat",
            ),
            (
                "shared_gemini_chat",
                "gemini-chat",
                "openclaw-gemini-chat",
                "Gemini Chat",
            ),
            (
                "shared_doubao_chat",
                "doubao-chat",
                "openclaw-doubao-chat",
                "豆包 Chat",
            ),
            (
                "shared_api_deepseek_flash_chat",
                "api-deepseek-flash-chat",
                "api-deepseek-flash-chat",
                "DeepSeek Flash Chat",
            ),
            (
                "shared_api_deepseek_pro_chat",
                "api-deepseek-pro-chat",
                "api-deepseek-pro-chat",
                "DeepSeek Pro Chat",
            ),
            (
                "shared_api_gpt_chat",
                "api-gpt-chat",
                "api-gpt-chat",
                "GPT Chat",
            ),
            (
                "shared_api_kimi_chat",
                "api-kimi-chat",
                "api-kimi-chat",
                "Kimi Chat",
            ),
            (
                "shared_api_gemini_chat",
                "api-gemini-chat",
                "api-gemini-chat",
                "Gemini API Chat",
            ),
            (
                "shared_api_grok_chat",
                "api-grok-chat",
                "api-grok-chat",
                "Grok API Chat",
            ),
        ];
        for (profile_id, base_profile, isolated_profile, display_name) in shared_chat_profiles {
            conn.execute(
                "INSERT INTO ai_agent_profiles (
                   id, agent_id, profile_kind, owner_user_id, base_profile_name,
                   isolated_profile_name, display_name, status, created_at, updated_at, revision
                 )
                 VALUES (?1, 'hermes', 'shared', '', ?2, ?3, ?4, 'active', ?5, ?6, 1)
                 ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
                 DO UPDATE SET status = 'active', display_name = excluded.display_name,
                   isolated_profile_name = excluded.isolated_profile_name, updated_at = excluded.updated_at",
                params![profile_id, base_profile, isolated_profile, display_name, now, now],
            )?;
            let grant_id = format!("grant_{profile_id}_all");
            conn.execute(
                "INSERT INTO ai_agent_profile_grants (
                   id, profile_id, user_id, role, can_run, can_manage_skills,
                   can_manage_config, created_at, updated_at, revision
                 )
                 VALUES (?1, ?2, '', 'user', 1, 0, 0, ?3, ?4, 1)
                 ON CONFLICT(profile_id, user_id, role)
                 DO UPDATE SET can_run = 1, can_manage_skills = 0, can_manage_config = 0,
                   updated_at = excluded.updated_at",
                params![grant_id, profile_id, now, now],
            )?;
        }

        let user_part = sanitize_profile_component(user_id);
        let personal_profile_id = format!("usr_{user_part}_default");
        let personal_profile_name = format!("mnote-u-{user_part}-default");
        conn.execute(
            "INSERT INTO ai_agent_profiles (
               id, agent_id, profile_kind, owner_user_id, base_profile_name,
               isolated_profile_name, display_name, status, created_at, updated_at, revision
             )
             VALUES (?1, 'hermes', 'personal', ?2, 'default', ?3, '我的 Hermes', 'active', ?4, ?5, 1)
             ON CONFLICT(agent_id, profile_kind, owner_user_id, base_profile_name)
             DO UPDATE SET status = 'active', updated_at = excluded.updated_at",
            params![personal_profile_id, user_id, personal_profile_name, now, now],
        )?;
        let personal_grant_id = format!("grant_{personal_profile_id}_owner");
        conn.execute(
            "INSERT INTO ai_agent_profile_grants (
               id, profile_id, user_id, role, can_run, can_manage_skills,
               can_manage_config, created_at, updated_at, revision
             )
             VALUES (?1, ?2, ?3, 'owner', 1, 1, 1, ?4, ?5, 1)
             ON CONFLICT(profile_id, user_id, role)
             DO UPDATE SET can_run = 1, can_manage_skills = 1, can_manage_config = 1,
               updated_at = excluded.updated_at",
            params![personal_grant_id, personal_profile_id, user_id, now, now],
        )?;

        if is_admin {
            let admin_grant_id = format!("grant_shared_lite_admin_{user_part}");
            conn.execute(
                "INSERT INTO ai_agent_profile_grants (
                   id, profile_id, user_id, role, can_run, can_manage_skills,
                   can_manage_config, created_at, updated_at, revision
                 )
                 VALUES (?1, 'shared_lite', ?2, 'admin', 1, 1, 1, ?3, ?4, 1)
                 ON CONFLICT(profile_id, user_id, role)
                 DO UPDATE SET can_run = 1, can_manage_skills = 1, can_manage_config = 1,
                   updated_at = excluded.updated_at",
                params![admin_grant_id, user_id, now, now],
            )?;
            for (profile_id, _, _, _) in shared_chat_profiles {
                let admin_chat_grant_id = format!("grant_{profile_id}_admin_{user_part}");
                conn.execute(
                    "INSERT INTO ai_agent_profile_grants (
                       id, profile_id, user_id, role, can_run, can_manage_skills,
                       can_manage_config, created_at, updated_at, revision
                     )
                     VALUES (?1, ?2, ?3, 'admin', 1, 0, 0, ?4, ?5, 1)
                     ON CONFLICT(profile_id, user_id, role)
                     DO UPDATE SET can_run = 1, can_manage_skills = 0, can_manage_config = 0,
                       updated_at = excluded.updated_at",
                    params![admin_chat_grant_id, profile_id, user_id, now, now],
                )?;
            }
        }

        list_ai_agent_profile_access_rows(&conn, user_id)
    }

    fn resolve_ai_agent_profile(
        &self,
        user_id: &str,
        is_admin: bool,
        profile_id: &str,
    ) -> Result<Option<AiAgentProfileAccessRecord>, ControlPlaneError> {
        let profile_id = profile_id.trim();
        if profile_id.is_empty() {
            return Ok(None);
        }
        let access = self.ensure_ai_agent_profile_policy(user_id, is_admin)?;
        Ok(access
            .into_iter()
            .find(|item| item.profile.id == profile_id && item.grant.can_run))
    }

    fn upsert_navigation_recent(
        &self,
        input: UpsertNavigationRecentInput,
    ) -> Result<NavigationRecentRecord, ControlPlaneError> {
        let user_id = input.user_id.trim().to_string();
        let workspace_id = option_to_stored_text(input.workspace_id);
        let kind = input.kind.trim().to_string();
        let source_kind = input.source_kind.trim().to_string();
        let root_uri = input.root_uri.trim().to_string();
        let relative_path = input
            .relative_path
            .map(|value| value.trim().trim_matches('/').to_string())
            .filter(|value| !value.is_empty());
        let document_id = input
            .document_id
            .map(|value| value.trim().to_string())
            .filter(|value| !value.is_empty());
        let title = input.title.trim().to_string();
        if user_id.is_empty()
            || kind.is_empty()
            || source_kind.is_empty()
            || root_uri.is_empty()
            || title.is_empty()
        {
            return Err(ControlPlaneError::InvalidInput(
                "navigation recent user/kind/source/root/title 不能为空".to_string(),
            ));
        }
        serde_json::from_str::<serde_json::Value>(&input.metadata_json)?;
        let target_key = navigation_recent_target_key(
            &kind,
            &root_uri,
            relative_path.as_deref(),
            document_id.as_deref(),
        )?;
        let conn = self.conn.lock().unwrap();
        let now = now_text();
        conn.execute(
            "INSERT INTO user_navigation_recent (
               id, user_id, workspace_id, kind, source_kind, root_uri, target_key,
               relative_path, document_id, title, status, metadata_json, visited_at,
               created_at, updated_at, revision
             )
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, 'active', ?11, ?12, ?13, ?14, 1)
             ON CONFLICT(user_id, kind, target_key)
             DO UPDATE SET
               workspace_id = excluded.workspace_id,
               source_kind = excluded.source_kind,
               root_uri = excluded.root_uri,
               relative_path = excluded.relative_path,
               document_id = excluded.document_id,
               title = excluded.title,
               status = 'active',
               metadata_json = excluded.metadata_json,
               visited_at = excluded.visited_at,
               updated_at = excluded.updated_at,
               revision = user_navigation_recent.revision + 1",
            params![
                input.id.unwrap_or_else(|| new_id("nav_recent")),
                user_id,
                workspace_id,
                kind,
                source_kind,
                root_uri,
                target_key,
                relative_path,
                document_id,
                title,
                input.metadata_json,
                now,
                now,
                now,
            ],
        )?;
        prune_navigation_recent_for_kind(&conn, &user_id, &kind)?;
        let record = conn.query_row(
            "SELECT id, user_id, workspace_id, kind, source_kind, root_uri, target_key,
                    relative_path, document_id, title, status, metadata_json, visited_at,
                    created_at, updated_at, revision
             FROM user_navigation_recent
             WHERE user_id = ?1 AND kind = ?2 AND target_key = ?3
             LIMIT 1",
            params![user_id, kind, target_key],
            row_to_navigation_recent,
        )?;
        Ok(record)
    }

    fn list_navigation_recent(
        &self,
        user_id: &str,
        kind: Option<&str>,
        limit: usize,
    ) -> Result<Vec<NavigationRecentRecord>, ControlPlaneError> {
        let user_id = user_id.trim();
        if user_id.is_empty() {
            return Ok(Vec::new());
        }
        let limit = limit.clamp(1, 100) as i64;
        let conn = self.conn.lock().unwrap();
        let rows = if let Some(kind) = kind.map(str::trim).filter(|value| !value.is_empty()) {
            let mut stmt = conn.prepare(
                "SELECT id, user_id, workspace_id, kind, source_kind, root_uri, target_key,
                        relative_path, document_id, title, status, metadata_json, visited_at,
                        created_at, updated_at, revision
                 FROM user_navigation_recent
                 WHERE user_id = ?1 AND kind = ?2 AND status = 'active'
                 ORDER BY visited_at DESC, updated_at DESC
                 LIMIT ?3",
            )?;
            let rows = stmt
                .query_map(params![user_id, kind, limit], row_to_navigation_recent)?
                .collect::<Result<Vec<_>, _>>()?;
            rows
        } else {
            let mut stmt = conn.prepare(
                "SELECT id, user_id, workspace_id, kind, source_kind, root_uri, target_key,
                        relative_path, document_id, title, status, metadata_json, visited_at,
                        created_at, updated_at, revision
                 FROM user_navigation_recent
                 WHERE user_id = ?1 AND status = 'active'
                 ORDER BY visited_at DESC, updated_at DESC
                 LIMIT ?2",
            )?;
            let rows = stmt
                .query_map(params![user_id, limit], row_to_navigation_recent)?
                .collect::<Result<Vec<_>, _>>()?;
            rows
        };
        Ok(rows)
    }

    fn upsert_ai_policy(
        &self,
        input: UpsertAiPolicyInput,
    ) -> Result<AiPolicyRecord, ControlPlaneError> {
        if input.user_id.is_none() && input.workspace_id.is_none() {
            return Err(ControlPlaneError::InvalidInput(
                "ai_policy 必须绑定 user_id 或 workspace_id".to_string(),
            ));
        }
        serde_json::from_str::<serde_json::Value>(&input.allowed_roots_json)?;
        serde_json::from_str::<serde_json::Value>(&input.model_policy_json)?;
        serde_json::from_str::<serde_json::Value>(&input.quota_json)?;

        let conn = self.conn.lock().unwrap();
        let now = now_text();
        let existing = conn
            .query_row(
                "SELECT id, user_id, workspace_id, allowed_roots_json, model_policy_json, quota_json, created_at, updated_at, revision
                 FROM ai_policies
                 WHERE (?1 IS NULL OR user_id = ?1)
                   AND (?2 IS NULL OR workspace_id = ?2)
                 ORDER BY created_at DESC
                 LIMIT 1",
                params![input.user_id, input.workspace_id],
                row_to_ai_policy,
            )
            .optional()?;

        if let Some(existing) = existing {
            let revision = existing.revision + 1;
            conn.execute(
                "UPDATE ai_policies
                 SET allowed_roots_json = ?1, model_policy_json = ?2, quota_json = ?3, updated_at = ?4, revision = ?5
                 WHERE id = ?6",
                params![
                    input.allowed_roots_json,
                    input.model_policy_json,
                    input.quota_json,
                    now,
                    revision,
                    existing.id
                ],
            )?;
            return Ok(AiPolicyRecord {
                id: existing.id,
                user_id: existing.user_id,
                workspace_id: existing.workspace_id,
                allowed_roots_json: input.allowed_roots_json,
                model_policy_json: input.model_policy_json,
                quota_json: input.quota_json,
                created_at: existing.created_at,
                updated_at: now,
                revision,
            });
        }

        let record = AiPolicyRecord {
            id: input.id.unwrap_or_else(|| new_id("aip")),
            user_id: input.user_id,
            workspace_id: input.workspace_id,
            allowed_roots_json: input.allowed_roots_json,
            model_policy_json: input.model_policy_json,
            quota_json: input.quota_json,
            created_at: now.clone(),
            updated_at: now,
            revision: 1,
        };
        conn.execute(
            "INSERT INTO ai_policies (id, user_id, workspace_id, allowed_roots_json, model_policy_json, quota_json, created_at, updated_at, revision)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 1)",
            params![
                record.id,
                record.user_id,
                record.workspace_id,
                record.allowed_roots_json,
                record.model_policy_json,
                record.quota_json,
                record.created_at,
                record.updated_at
            ],
        )?;
        Ok(record)
    }

    fn get_ai_policy(
        &self,
        actor_id: &str,
        workspace_id: Option<&str>,
    ) -> Result<Option<AiPolicyRecord>, ControlPlaneError> {
        let actor_id = actor_id.trim();
        let workspace_id = workspace_id
            .map(str::trim)
            .filter(|value| !value.is_empty());
        if actor_id.is_empty() && workspace_id.is_none() {
            return Ok(None);
        }
        let conn = self.conn.lock().unwrap();
        if let Some(workspace_id) = workspace_id {
            let workspace_policy = conn
                .query_row(
                    "SELECT id, user_id, workspace_id, allowed_roots_json, model_policy_json, quota_json, created_at, updated_at, revision
                     FROM ai_policies
                     WHERE workspace_id = ?1
                     ORDER BY updated_at DESC
                     LIMIT 1",
                    params![workspace_id],
                    row_to_ai_policy,
                )
                .optional()?;
            if workspace_policy.is_some() {
                return Ok(workspace_policy);
            }
        }
        if actor_id.is_empty() {
            return Ok(None);
        }
        conn.query_row(
            "SELECT id, user_id, workspace_id, allowed_roots_json, model_policy_json, quota_json, created_at, updated_at, revision
             FROM ai_policies
             WHERE user_id = ?1 AND workspace_id IS NULL
             ORDER BY updated_at DESC
             LIMIT 1",
            params![actor_id],
            row_to_ai_policy,
        )
        .optional()
        .map_err(ControlPlaneError::from)
    }

    fn upsert_sync_state(
        &self,
        input: UpsertSyncStateInput,
    ) -> Result<SyncStateRecord, ControlPlaneError> {
        let workspace_id = input.workspace_id.trim().to_string();
        let remote_kind = input.remote_kind.trim().to_string();
        let status = input.status.trim().to_string();
        if workspace_id.is_empty() || remote_kind.is_empty() || status.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "sync_state workspace_id/remote_kind/status 不能为空".to_string(),
            ));
        }
        if let Some(error_json) = &input.error_json {
            serde_json::from_str::<serde_json::Value>(error_json)?;
        }

        let conn = self.conn.lock().unwrap();
        let now = now_text();
        let existing = conn
            .query_row(
                "SELECT id, workspace_id, remote_kind, remote_id, cursor, last_synced_at, status, error_json, updated_at, revision
                 FROM sync_state
                 WHERE workspace_id = ?1 AND remote_kind = ?2
                 LIMIT 1",
                params![workspace_id, remote_kind],
                row_to_sync_state,
            )
            .optional()?;
        if let Some(existing) = existing {
            let revision = existing.revision + 1;
            conn.execute(
                "UPDATE sync_state
                 SET remote_id = ?1, cursor = ?2, last_synced_at = ?3, status = ?4, error_json = ?5, updated_at = ?6, revision = ?7
                 WHERE id = ?8",
                params![
                    input.remote_id,
                    input.cursor,
                    input.last_synced_at,
                    status,
                    input.error_json,
                    now,
                    revision,
                    existing.id
                ],
            )?;
            return Ok(SyncStateRecord {
                id: existing.id,
                workspace_id: existing.workspace_id,
                remote_kind: existing.remote_kind,
                remote_id: input.remote_id,
                cursor: input.cursor,
                last_synced_at: input.last_synced_at,
                status,
                error_json: input.error_json,
                updated_at: now,
                revision,
            });
        }

        let record = SyncStateRecord {
            id: input.id.unwrap_or_else(|| new_id("sync")),
            workspace_id,
            remote_kind,
            remote_id: input.remote_id,
            cursor: input.cursor,
            last_synced_at: input.last_synced_at,
            status,
            error_json: input.error_json,
            updated_at: now,
            revision: 1,
        };
        conn.execute(
            "INSERT INTO sync_state (id, workspace_id, remote_kind, remote_id, cursor, last_synced_at, status, error_json, updated_at, revision)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 1)",
            params![
                record.id,
                record.workspace_id,
                record.remote_kind,
                record.remote_id,
                record.cursor,
                record.last_synced_at,
                record.status,
                record.error_json,
                record.updated_at
            ],
        )?;
        Ok(record)
    }

    fn get_sync_state(
        &self,
        workspace_id: &str,
        remote_kind: &str,
    ) -> Result<Option<SyncStateRecord>, ControlPlaneError> {
        let workspace_id = workspace_id.trim();
        let remote_kind = remote_kind.trim();
        if workspace_id.is_empty() || remote_kind.is_empty() {
            return Ok(None);
        }
        let conn = self.conn.lock().unwrap();
        conn.query_row(
            "SELECT id, workspace_id, remote_kind, remote_id, cursor, last_synced_at, status, error_json, updated_at, revision
             FROM sync_state
             WHERE workspace_id = ?1 AND remote_kind = ?2
             LIMIT 1",
            params![workspace_id, remote_kind],
            row_to_sync_state,
        )
        .optional()
        .map_err(ControlPlaneError::from)
    }

    fn upsert_ai_runtime_run(
        &self,
        input: UpsertAiRuntimeRunInput,
    ) -> Result<AiRuntimeRunRecord, ControlPlaneError> {
        if input.user_id.trim().is_empty()
            || input.session_id.trim().is_empty()
            || input.run_id.trim().is_empty()
        {
            return Err(ControlPlaneError::InvalidInput(
                "ai runtime run user_id/session_id/run_id 不能为空".to_string(),
            ));
        }
        serde_json::from_str::<serde_json::Value>(&input.runtime_json)?;
        serde_json::from_str::<serde_json::Value>(&input.payload_json)?;

        let conn = self.conn.lock().unwrap();
        let now = now_text();
        let existing = conn
            .query_row(
                "SELECT id, user_id, workspace_id, document_id, session_id, run_id, title, profile, acp_runtime, trace_id, status, runtime_json, payload_json, deleted_at, created_at, updated_at, revision
                 FROM ai_runtime_runs
                 WHERE run_id = ?1
                 LIMIT 1",
                params![input.run_id],
                row_to_ai_runtime_run,
            )
            .optional()?;

        if let Some(existing) = existing {
            let revision = existing.revision + 1;
            conn.execute(
                "UPDATE ai_runtime_runs
                 SET title = ?1, profile = ?2, acp_runtime = ?3, trace_id = ?4, status = ?5, runtime_json = ?6, payload_json = ?7, deleted_at = NULL, updated_at = ?8, revision = ?9
                 WHERE id = ?10",
                params![
                    input.title,
                    input.profile,
                    input.acp_runtime,
                    input.trace_id,
                    input.status,
                    input.runtime_json,
                    input.payload_json,
                    now,
                    revision,
                    existing.id
                ],
            )?;
            return Ok(AiRuntimeRunRecord {
                id: existing.id,
                user_id: existing.user_id,
                workspace_id: existing.workspace_id,
                document_id: existing.document_id,
                session_id: existing.session_id,
                run_id: existing.run_id,
                title: input.title,
                profile: input.profile,
                acp_runtime: input.acp_runtime,
                trace_id: input.trace_id,
                status: input.status,
                runtime_json: input.runtime_json,
                payload_json: input.payload_json,
                deleted_at: None,
                created_at: existing.created_at,
                updated_at: now,
                revision,
            });
        }

        let record = AiRuntimeRunRecord {
            id: input.id.unwrap_or_else(|| new_id("acr")),
            user_id: input.user_id,
            workspace_id: input.workspace_id,
            document_id: input.document_id,
            session_id: input.session_id,
            run_id: input.run_id,
            title: input.title,
            profile: input.profile,
            acp_runtime: input.acp_runtime,
            trace_id: input.trace_id,
            status: input.status,
            runtime_json: input.runtime_json,
            payload_json: input.payload_json,
            deleted_at: None,
            created_at: now.clone(),
            updated_at: now,
            revision: 1,
        };
        conn.execute(
            "INSERT INTO ai_runtime_runs (id, user_id, workspace_id, document_id, session_id, run_id, title, profile, acp_runtime, trace_id, status, runtime_json, payload_json, deleted_at, created_at, updated_at, revision)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, NULL, ?14, ?15, 1)",
            params![
                record.id,
                record.user_id,
                record.workspace_id,
                record.document_id,
                record.session_id,
                record.run_id,
                record.title,
                record.profile,
                record.acp_runtime,
                record.trace_id,
                record.status,
                record.runtime_json,
                record.payload_json,
                record.created_at,
                record.updated_at
            ],
        )?;
        Ok(record)
    }

    fn list_ai_runtime_runs(
        &self,
        user_id: &str,
        workspace_id: Option<&str>,
        document_id: Option<&str>,
        session_id: Option<&str>,
        limit: usize,
    ) -> Result<Vec<AiRuntimeRunRecord>, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "SELECT id, user_id, workspace_id, document_id, session_id, run_id, title, profile, acp_runtime, trace_id, status, runtime_json, payload_json, deleted_at, created_at, updated_at, revision
             FROM ai_runtime_runs
             WHERE user_id = ?1
               AND (?2 IS NULL OR workspace_id = ?2)
               AND (?3 IS NULL OR document_id = ?3)
               AND (?4 IS NULL OR session_id = ?4)
               AND deleted_at IS NULL
             ORDER BY updated_at DESC
             LIMIT ?5",
        )?;
        let rows = stmt
            .query_map(
                params![user_id, workspace_id, document_id, session_id, limit as i64],
                row_to_ai_runtime_run,
            )?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(rows)
    }

    fn find_ai_runtime_run(
        &self,
        user_id: &str,
        run_id: &str,
    ) -> Result<Option<AiRuntimeRunRecord>, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        conn.query_row(
            "SELECT id, user_id, workspace_id, document_id, session_id, run_id, title, profile, acp_runtime, trace_id, status, runtime_json, payload_json, deleted_at, created_at, updated_at, revision
             FROM ai_runtime_runs
             WHERE user_id = ?1 AND run_id = ?2 AND deleted_at IS NULL
             LIMIT 1",
            params![user_id, run_id],
            row_to_ai_runtime_run,
        )
        .optional()
        .map_err(ControlPlaneError::from)
    }

    fn append_ai_runtime_event(
        &self,
        input: AppendAiRuntimeEventInput,
    ) -> Result<AiRuntimeEventRecord, ControlPlaneError> {
        if input.user_id.trim().is_empty()
            || input.session_id.trim().is_empty()
            || input.run_id.trim().is_empty()
        {
            return Err(ControlPlaneError::InvalidInput(
                "ai runtime event user_id/session_id/run_id 不能为空".to_string(),
            ));
        }
        serde_json::from_str::<serde_json::Value>(&input.payload_json)?;

        let conn = self.conn.lock().unwrap();
        let record = AiRuntimeEventRecord {
            id: input.id.unwrap_or_else(|| new_id("are")),
            user_id: input.user_id,
            workspace_id: input.workspace_id,
            document_id: input.document_id,
            session_id: input.session_id,
            run_id: input.run_id,
            profile: input.profile,
            acp_runtime: input.acp_runtime,
            event_type: input.event_type,
            payload_json: input.payload_json,
            created_at: now_text(),
        };
        conn.execute(
            "INSERT INTO ai_runtime_events (id, user_id, workspace_id, document_id, session_id, run_id, profile, acp_runtime, event_type, payload_json, created_at)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
            params![
                record.id,
                record.user_id,
                record.workspace_id,
                record.document_id,
                record.session_id,
                record.run_id,
                record.profile,
                record.acp_runtime,
                record.event_type,
                record.payload_json,
                record.created_at
            ],
        )?;
        Ok(record)
    }

    fn list_ai_runtime_events(
        &self,
        user_id: &str,
        run_id: &str,
        limit: usize,
    ) -> Result<Vec<AiRuntimeEventRecord>, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "SELECT id, user_id, workspace_id, document_id, session_id, run_id, profile, acp_runtime, event_type, payload_json, created_at
             FROM ai_runtime_events
             WHERE user_id = ?1 AND run_id = ?2
             ORDER BY created_at ASC
             LIMIT ?3",
        )?;
        let rows = stmt
            .query_map(
                params![user_id, run_id, limit as i64],
                row_to_ai_runtime_event,
            )?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(rows)
    }

    fn list_ai_runtime_journal_events(
        &self,
        user_id: &str,
        run_id: &str,
        after_seq: i64,
        limit: usize,
    ) -> Result<Vec<AiRuntimeJournalEventRecord>, ControlPlaneError> {
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare(
            "WITH ordered AS (
                 SELECT
                   ROW_NUMBER() OVER (ORDER BY created_at ASC, id ASC) AS seq,
                   id, user_id, workspace_id, document_id, session_id, run_id, profile,
                   acp_runtime, event_type, payload_json, created_at
                 FROM ai_runtime_events
                 WHERE user_id = ?1 AND run_id = ?2
             )
             SELECT seq, id, user_id, workspace_id, document_id, session_id, run_id, profile,
                    acp_runtime, event_type, payload_json, created_at
             FROM ordered
             WHERE seq > ?3
             ORDER BY seq ASC
             LIMIT ?4",
        )?;
        let rows = stmt
            .query_map(
                params![user_id, run_id, after_seq.max(0), limit.max(1) as i64],
                row_to_ai_runtime_journal_event,
            )?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(rows)
    }

    fn upsert_ai_external_conversation_binding(
        &self,
        input: UpsertAiExternalConversationBindingInput,
    ) -> Result<AiExternalConversationBindingRecord, ControlPlaneError> {
        let user_id = input.user_id.trim();
        let mnote_session_id = input.mnote_session_id.trim();
        let provider = input.provider.trim();
        let remote_conversation_id = input.remote_conversation_id.trim();
        if user_id.is_empty()
            || mnote_session_id.is_empty()
            || provider.is_empty()
            || remote_conversation_id.is_empty()
        {
            return Err(ControlPlaneError::InvalidInput(
                "external conversation binding user_id/session_id/provider/remote_id 不能为空"
                    .to_string(),
            ));
        }
        serde_json::from_str::<serde_json::Value>(&input.metadata_json)?;
        validate_ai_external_conversation_status(&input.status)?;

        let conn = self.conn.lock().unwrap();
        let now = now_text();
        let existing = conn
            .query_row(
                "SELECT id, user_id, workspace_id, mnote_session_id, acp_session_id, agent_id, profile, provider, remote_conversation_id, remote_url, status, metadata_json, created_at, updated_at, deleted_at, revision
                 FROM ai_external_conversation_bindings
                 WHERE user_id = ?1 AND mnote_session_id = ?2 AND provider = ?3
                 LIMIT 1",
                params![user_id, mnote_session_id, provider],
                row_to_ai_external_conversation_binding,
            )
            .optional()?;
        if let Some(existing) = existing {
            let revision = existing.revision + 1;
            let deleted_at = if input.status == "active" {
                None
            } else {
                existing.deleted_at.or_else(|| Some(now.clone()))
            };
            conn.execute(
                "UPDATE ai_external_conversation_bindings
                 SET workspace_id = ?1, acp_session_id = ?2, agent_id = ?3, profile = ?4, remote_conversation_id = ?5, remote_url = ?6, status = ?7, metadata_json = ?8, updated_at = ?9, deleted_at = ?10, revision = ?11
                 WHERE id = ?12",
                params![
                    input.workspace_id,
                    input.acp_session_id,
                    input.agent_id,
                    input.profile,
                    remote_conversation_id,
                    input.remote_url,
                    input.status,
                    input.metadata_json,
                    now,
                    deleted_at,
                    revision,
                    existing.id
                ],
            )?;
            return Ok(AiExternalConversationBindingRecord {
                id: existing.id,
                user_id: existing.user_id,
                workspace_id: input.workspace_id,
                mnote_session_id: existing.mnote_session_id,
                acp_session_id: input.acp_session_id,
                agent_id: input.agent_id,
                profile: input.profile,
                provider: existing.provider,
                remote_conversation_id: remote_conversation_id.to_string(),
                remote_url: input.remote_url,
                status: input.status,
                metadata_json: input.metadata_json,
                created_at: existing.created_at,
                updated_at: now,
                deleted_at,
                revision,
            });
        }

        let deleted_at = if input.status == "active" {
            None
        } else {
            Some(now.clone())
        };
        let record = AiExternalConversationBindingRecord {
            id: input.id.unwrap_or_else(|| new_id("aecb")),
            user_id: user_id.to_string(),
            workspace_id: input.workspace_id,
            mnote_session_id: mnote_session_id.to_string(),
            acp_session_id: input.acp_session_id,
            agent_id: input.agent_id,
            profile: input.profile,
            provider: provider.to_string(),
            remote_conversation_id: remote_conversation_id.to_string(),
            remote_url: input.remote_url,
            status: input.status,
            metadata_json: input.metadata_json,
            created_at: now.clone(),
            updated_at: now,
            deleted_at,
            revision: 1,
        };
        conn.execute(
            "INSERT INTO ai_external_conversation_bindings (id, user_id, workspace_id, mnote_session_id, acp_session_id, agent_id, profile, provider, remote_conversation_id, remote_url, status, metadata_json, created_at, updated_at, deleted_at, revision)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, 1)",
            params![
                record.id,
                record.user_id,
                record.workspace_id,
                record.mnote_session_id,
                record.acp_session_id,
                record.agent_id,
                record.profile,
                record.provider,
                record.remote_conversation_id,
                record.remote_url,
                record.status,
                record.metadata_json,
                record.created_at,
                record.updated_at,
                record.deleted_at
            ],
        )?;
        Ok(record)
    }

    fn find_ai_external_conversation_binding(
        &self,
        user_id: &str,
        workspace_id: Option<&str>,
        mnote_session_id: &str,
        provider: &str,
    ) -> Result<Option<AiExternalConversationBindingRecord>, ControlPlaneError> {
        let user_id = user_id.trim();
        let mnote_session_id = mnote_session_id.trim();
        let provider = provider.trim();
        if user_id.is_empty() || mnote_session_id.is_empty() || provider.is_empty() {
            return Ok(None);
        }
        let conn = self.conn.lock().unwrap();
        conn.query_row(
            "SELECT id, user_id, workspace_id, mnote_session_id, acp_session_id, agent_id, profile, provider, remote_conversation_id, remote_url, status, metadata_json, created_at, updated_at, deleted_at, revision
             FROM ai_external_conversation_bindings
             WHERE user_id = ?1
               AND (?2 IS NULL OR workspace_id = ?2)
               AND mnote_session_id = ?3
               AND provider = ?4
             LIMIT 1",
            params![user_id, workspace_id, mnote_session_id, provider],
            row_to_ai_external_conversation_binding,
        )
        .optional()
        .map_err(ControlPlaneError::from)
    }

    fn mark_ai_external_conversation_binding_status(
        &self,
        user_id: &str,
        workspace_id: Option<&str>,
        mnote_session_id: &str,
        provider: &str,
        status: &str,
        metadata_json: Option<&str>,
    ) -> Result<usize, ControlPlaneError> {
        let user_id = user_id.trim();
        let mnote_session_id = mnote_session_id.trim();
        let provider = provider.trim();
        validate_ai_external_conversation_status(status)?;
        let metadata_json = metadata_json.unwrap_or("{}");
        serde_json::from_str::<serde_json::Value>(metadata_json)?;
        if user_id.is_empty() || mnote_session_id.is_empty() || provider.is_empty() {
            return Ok(0);
        }
        let conn = self.conn.lock().unwrap();
        let now = now_text();
        let deleted_at = if status == "active" {
            None
        } else {
            Some(now.clone())
        };
        let changed = conn.execute(
            "UPDATE ai_external_conversation_bindings
             SET status = ?1, metadata_json = ?2, updated_at = ?3, deleted_at = COALESCE(?4, deleted_at), revision = revision + 1
             WHERE user_id = ?5
               AND (?6 IS NULL OR workspace_id = ?6)
               AND mnote_session_id = ?7
               AND provider = ?8",
            params![
                status,
                metadata_json,
                now,
                deleted_at,
                user_id,
                workspace_id,
                mnote_session_id,
                provider
            ],
        )?;
        Ok(changed)
    }

    fn rename_ai_runtime_session(
        &self,
        user_id: &str,
        session_id: &str,
        workspace_id: Option<&str>,
        title: &str,
    ) -> Result<Vec<AiRuntimeRunRecord>, ControlPlaneError> {
        let user_id = user_id.trim();
        let session_id = session_id.trim();
        let title = title.trim();
        if user_id.is_empty() || session_id.is_empty() || title.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "ai runtime session user_id/session_id/title 不能为空".to_string(),
            ));
        }
        let conn = self.conn.lock().unwrap();
        let now = now_text();
        conn.execute(
            "UPDATE ai_runtime_runs
             SET title = ?1, updated_at = ?2, revision = revision + 1
             WHERE user_id = ?3
               AND session_id = ?4
               AND (?5 IS NULL OR workspace_id = ?5)
               AND deleted_at IS NULL",
            params![title, now, user_id, session_id, workspace_id],
        )?;
        let mut stmt = conn.prepare(
            "SELECT id, user_id, workspace_id, document_id, session_id, run_id, title, profile, acp_runtime, trace_id, status, runtime_json, payload_json, deleted_at, created_at, updated_at, revision
             FROM ai_runtime_runs
             WHERE user_id = ?1
               AND session_id = ?2
               AND (?3 IS NULL OR workspace_id = ?3)
               AND deleted_at IS NULL
             ORDER BY updated_at DESC",
        )?;
        let rows = stmt
            .query_map(
                params![user_id, session_id, workspace_id],
                row_to_ai_runtime_run,
            )?
            .collect::<Result<Vec<_>, _>>()
            .map_err(ControlPlaneError::from)?;
        Ok(rows)
    }

    fn auto_title_ai_runtime_session(
        &self,
        user_id: &str,
        session_id: &str,
        workspace_id: Option<&str>,
    ) -> Result<Option<AiRuntimeRunRecord>, ControlPlaneError> {
        let user_id = user_id.trim();
        let session_id = session_id.trim();
        if user_id.is_empty() || session_id.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "ai runtime session user_id/session_id 不能为空".to_string(),
            ));
        }
        let conn = self.conn.lock().unwrap();
        let existing = conn
            .query_row(
                "SELECT id, user_id, workspace_id, document_id, session_id, run_id, title, profile, acp_runtime, trace_id, status, runtime_json, payload_json, deleted_at, created_at, updated_at, revision
                 FROM ai_runtime_runs
                 WHERE user_id = ?1
                   AND session_id = ?2
                   AND (?3 IS NULL OR workspace_id = ?3)
                   AND deleted_at IS NULL
                 ORDER BY updated_at DESC
                 LIMIT 1",
                params![user_id, session_id, workspace_id],
                row_to_ai_runtime_run,
            )
            .optional()?;
        let Some(existing) = existing else {
            return Ok(None);
        };
        let title = existing
            .title
            .clone()
            .filter(|value| !value.trim().is_empty())
            .unwrap_or_else(|| {
                derive_ai_runtime_title(&existing.payload_json, &existing.session_id)
            });
        let now = now_text();
        let revision = existing.revision + 1;
        conn.execute(
            "UPDATE ai_runtime_runs
             SET title = ?1, updated_at = ?2, revision = ?3
             WHERE id = ?4",
            params![title, now, revision, existing.id],
        )?;
        Ok(Some(AiRuntimeRunRecord {
            title: Some(title),
            updated_at: now,
            revision,
            ..existing
        }))
    }

    fn delete_ai_runtime_session(
        &self,
        user_id: &str,
        session_id: &str,
        workspace_id: Option<&str>,
    ) -> Result<usize, ControlPlaneError> {
        let user_id = user_id.trim();
        let session_id = session_id.trim();
        if user_id.is_empty() || session_id.is_empty() {
            return Err(ControlPlaneError::InvalidInput(
                "ai runtime session user_id/session_id 不能为空".to_string(),
            ));
        }
        let conn = self.conn.lock().unwrap();
        let changed = conn.execute(
            "UPDATE ai_runtime_runs
             SET deleted_at = ?1, updated_at = ?1, revision = revision + 1
             WHERE user_id = ?2
               AND session_id = ?3
               AND (?4 IS NULL OR workspace_id = ?4)
               AND deleted_at IS NULL",
            params![now_text(), user_id, session_id, workspace_id],
        )?;
        Ok(changed)
    }
}

fn max_permission<'a>(left: &'a str, right: &'a str) -> &'a str {
    if permission_rank(right) > permission_rank(left) {
        right
    } else {
        left
    }
}

fn permission_rank(permission: &str) -> u8 {
    match permission {
        "admin" => 3,
        "write" => 2,
        "read" => 1,
        _ => 0,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{
        password_hash_v1, session_token_hash, AppendAiRuntimeEventInput, AuthenticatePasswordInput,
        CreatePasswordIdentityInput, UpsertAiExternalConversationBindingInput, UpsertAiPolicyInput,
        UpsertAiRuntimeRunInput, UpsertNavigationRecentInput, UpsertSidebarShortcutInput,
        UpsertSyncStateInput, UpsertUserUiPreferenceInput,
    };

    fn store() -> SqliteControlPlaneStore {
        SqliteControlPlaneStore::in_memory().expect("in-memory control plane")
    }

    fn create_user(store: &SqliteControlPlaneStore, id: &str) -> UserRecord {
        store
            .upsert_user(UpsertUserInput {
                id: Some(id.to_string()),
                email: Some(format!("{id}@example.com")),
                username: id.to_string(),
                display_name: id.to_string(),
                role: None,
                password_hash: None,
            })
            .expect("upsert user")
    }

    fn create_password_identity(
        store: &SqliteControlPlaneStore,
        username: &str,
        email: &str,
        password: &str,
    ) -> UserRecord {
        let user = create_user(store, username);
        store
            .create_password_identity(CreatePasswordIdentityInput {
                user_id: user.id.clone(),
                email: Some(email.to_string()),
                username: username.to_string(),
                password: password.to_string(),
            })
            .expect("create password identity");
        user
    }

    #[test]
    fn upsert_user_inserts_and_updates_revision() {
        let store = store();
        let user = create_user(&store, "alice");
        assert_eq!(user.revision, 1);

        let updated = store
            .upsert_user(UpsertUserInput {
                id: Some("alice".to_string()),
                email: Some("alice2@example.com".to_string()),
                username: "alice".to_string(),
                display_name: "Alice".to_string(),
                role: Some("admin".to_string()),
                password_hash: None,
            })
            .expect("update user");

        assert_eq!(updated.email.as_deref(), Some("alice2@example.com"));
        assert_eq!(updated.role, "admin");
        assert_eq!(updated.revision, 2);
    }

    #[test]
    fn ensure_default_workspace_creates_owner_grant() {
        let store = store();
        create_user(&store, "shujuan");

        let workspace = store
            .ensure_default_workspace("shujuan")
            .expect("default workspace");
        assert_eq!(workspace.name, "shujuan 的空间");

        let access = store
            .resolve_access("shujuan", &workspace.root_uri)
            .expect("resolve access");
        assert_eq!(access.permission, "write");
        assert_eq!(access.grant_ids.len(), 1);
    }

    #[test]
    fn explicit_write_grant_out_ranks_read_grant() {
        let store = store();
        create_user(&store, "bob");
        store
            .grant_directory_access(DirectoryGrantInput {
                user_id: "bob".to_string(),
                workspace_id: None,
                root_uri: "local://shared".to_string(),
                root_path: "/tmp/shared".to_string(),
                permission: "read".to_string(),
                recursive: true,
                capabilities: vec![],
                source: "admin".to_string(),
                created_by: None,
            })
            .expect("read grant");
        store
            .grant_directory_access(DirectoryGrantInput {
                user_id: "bob".to_string(),
                workspace_id: None,
                root_uri: "local://shared".to_string(),
                root_path: "/tmp/shared".to_string(),
                permission: "write".to_string(),
                recursive: true,
                capabilities: vec!["ai".to_string()],
                source: "admin".to_string(),
                created_by: None,
            })
            .expect("write grant");

        let access = store
            .resolve_access("bob", "local://shared/page.md")
            .expect("resolve access");
        assert_eq!(access.permission, "write");
        assert_eq!(access.grant_ids.len(), 2);
    }

    #[test]
    fn list_and_revoke_directory_grant_updates_resolved_access() {
        let store = store();
        create_user(&store, "reader");
        let grant = store
            .grant_directory_access(DirectoryGrantInput {
                user_id: "reader".to_string(),
                workspace_id: None,
                root_uri: "local://shared".to_string(),
                root_path: "/tmp/shared".to_string(),
                permission: "read".to_string(),
                recursive: true,
                capabilities: vec![],
                source: "admin".to_string(),
                created_by: None,
            })
            .expect("read grant");

        let grants = store
            .list_directory_grants_for_actor("reader")
            .expect("list grants");
        assert_eq!(grants.len(), 1);
        assert_eq!(grants[0].id, grant.id);

        store
            .revoke_directory_grant(&grant.id, None)
            .expect("revoke grant");
        let access = store
            .resolve_access("reader", "local://shared/page.md")
            .expect("resolve revoked access");
        assert_eq!(access.permission, "none");
    }

    #[test]
    fn resolve_access_supports_legacy_directory_grants_with_plain_path_root_uri() {
        let store = store();
        create_user(&store, "legacy_reader");
        store
            .grant_directory_access(DirectoryGrantInput {
                user_id: "legacy_reader".to_string(),
                workspace_id: None,
                root_uri: "/tmp/shared".to_string(),
                root_path: "/tmp/shared".to_string(),
                permission: "read".to_string(),
                recursive: true,
                capabilities: vec![],
                source: "legacy".to_string(),
                created_by: None,
            })
            .expect("legacy grant");

        let access = store
            .resolve_access("legacy_reader", "file:///tmp/shared/page.md")
            .expect("resolve access");
        assert_eq!(access.permission, "read");
        assert_eq!(access.grant_ids.len(), 1);
    }

    #[test]
    fn resolve_access_does_not_match_sibling_uri_prefix() {
        let store = store();
        create_user(&store, "reader");
        store
            .grant_directory_access(DirectoryGrantInput {
                user_id: "reader".to_string(),
                workspace_id: None,
                root_uri: "file:///tmp/mnote-ai-root".to_string(),
                root_path: "/tmp/mnote-ai-root".to_string(),
                permission: "write".to_string(),
                recursive: true,
                capabilities: vec!["ai".to_string()],
                source: "admin".to_string(),
                created_by: None,
            })
            .expect("grant root");

        let inside = store
            .resolve_access("reader", "file:///tmp/mnote-ai-root/page.md")
            .expect("resolve inside");
        assert_eq!(inside.permission, "write");

        let sibling = store
            .resolve_access("reader", "file:///tmp/mnote-ai-root-sibling/page.md")
            .expect("resolve sibling");
        assert_eq!(sibling.permission, "none");
        assert!(sibling.grant_ids.is_empty());
    }

    #[test]
    fn share_link_token_is_hashed_and_revocable() {
        let store = store();
        create_user(&store, "owner");
        create_user(&store, "ws_owner");
        let workspace = store
            .ensure_default_workspace("ws_owner")
            .expect("workspace");
        let created = store
            .create_share_link(CreateShareLinkInput {
                id: None,
                workspace_id: workspace.id,
                resource_kind: "page".to_string(),
                resource_id: "page_1".to_string(),
                token: Some("plain-token".to_string()),
                permission: "read".to_string(),
                created_by: "ws_owner".to_string(),
                expires_at: None,
            })
            .expect("create share link");

        assert_eq!(created.token, "plain-token");
        assert!(created.link.token_hash.starts_with("sha256-v1:"));
        assert_ne!(created.link.token_hash, "plain-token");

        let resolved = store
            .resolve_share_link(&created.link.token_hash)
            .expect("resolve share link")
            .expect("active share link");
        assert_eq!(resolved.id, created.link.id);

        let listed = store
            .list_share_links(&created.link.workspace_id)
            .expect("list links");
        assert_eq!(listed.len(), 1);

        store
            .revoke_share_link(&created.link.id)
            .expect("revoke share");
        assert!(store
            .resolve_share_link(&created.link.token_hash)
            .expect("resolve revoked share")
            .is_none());
    }

    #[test]
    fn audit_log_appends_and_lists_latest_first() {
        let store = store();
        create_user(&store, "owner");
        store
            .append_audit(AppendAuditInput {
                actor_user_id: Some("owner".to_string()),
                action: "control.share.created".to_string(),
                target_kind: "share_link".to_string(),
                target_id: Some("share_1".to_string()),
                metadata_json: "{}".to_string(),
            })
            .expect("append audit");
        store
            .append_audit(AppendAuditInput {
                actor_user_id: Some("owner".to_string()),
                action: "control.share.revoked".to_string(),
                target_kind: "share_link".to_string(),
                target_id: Some("share_1".to_string()),
                metadata_json: "{\"reason\":\"test\"}".to_string(),
            })
            .expect("append audit 2");

        let rows = store.list_audit_log(10).expect("list audit");
        assert_eq!(rows.len(), 2);
        assert_eq!(rows[0].action, "control.share.revoked");
        assert_eq!(rows[1].action, "control.share.created");
    }

    #[test]
    fn drain_outbox_marks_events_delivered() {
        let store = store();
        store
            .append_outbox(OutboxEventInput {
                topic: "control".to_string(),
                event_type: "control.workspace.updated".to_string(),
                payload_json: "{}".to_string(),
            })
            .expect("append event");

        let first = store.drain_outbox(10).expect("first drain");
        assert_eq!(first.len(), 1);

        let second = store.drain_outbox(10).expect("second drain");
        assert!(second.is_empty());
    }

    #[test]
    fn mark_outbox_delivered_marks_one_pending_event() {
        let store = store();
        let first = store
            .append_outbox(OutboxEventInput {
                topic: "control".to_string(),
                event_type: "control.grant.created".to_string(),
                payload_json: "{}".to_string(),
            })
            .expect("first event");
        let second = store
            .append_outbox(OutboxEventInput {
                topic: "control".to_string(),
                event_type: "control.share.created".to_string(),
                payload_json: "{}".to_string(),
            })
            .expect("second event");

        store
            .mark_outbox_delivered(&first.id)
            .expect("mark first delivered");

        let pending = store.drain_outbox(10).expect("drain pending");
        assert_eq!(pending.len(), 1);
        assert_eq!(pending[0].id, second.id);
    }

    #[test]
    fn sidebar_shortcuts_are_user_scoped_and_upserted_by_target() {
        let store = store();
        create_user(&store, "alice");
        create_user(&store, "bob");
        let workspace = store.ensure_default_workspace("alice").expect("workspace");

        let first = store
            .upsert_sidebar_shortcut(UpsertSidebarShortcutInput {
                id: None,
                user_id: "alice".to_string(),
                workspace_id: workspace.id.clone(),
                root_uri: Some("file:///tmp/mnote".to_string()),
                kind: "folder".to_string(),
                source_kind: "local_folder".to_string(),
                target_id: "local-dir:design".to_string(),
                relative_path: Some("design".to_string()),
                document_id: None,
                title: "design".to_string(),
                icon: Some("folder_open".to_string()),
                sort_order: 0,
                metadata_json: "{}".to_string(),
            })
            .expect("insert shortcut");

        let updated = store
            .upsert_sidebar_shortcut(UpsertSidebarShortcutInput {
                id: None,
                user_id: "alice".to_string(),
                workspace_id: workspace.id.clone(),
                root_uri: Some("file:///tmp/mnote".to_string()),
                kind: "folder".to_string(),
                source_kind: "local_folder".to_string(),
                target_id: "local-dir:design".to_string(),
                relative_path: Some("design".to_string()),
                document_id: None,
                title: "Design".to_string(),
                icon: Some("folder_open".to_string()),
                sort_order: 4,
                metadata_json: "{\"scope\":\"filetree\"}".to_string(),
            })
            .expect("upsert shortcut");

        assert_eq!(updated.id, first.id);
        assert_eq!(updated.title, "Design");
        assert_eq!(updated.root_uri.as_deref(), Some("file:///tmp/mnote"));
        assert_eq!(updated.sort_order, 4);
        assert_eq!(updated.revision, first.revision + 1);

        let alice_shortcuts = store
            .list_sidebar_shortcuts("alice", &workspace.id)
            .expect("list alice shortcuts");
        assert_eq!(alice_shortcuts.len(), 1);
        assert_eq!(alice_shortcuts[0].target_id, "local-dir:design");
        let alice_global_local_shortcuts = store
            .list_sidebar_shortcuts_with_global_local("alice", "cloud-workspace")
            .expect("list alice global local shortcuts");
        assert_eq!(alice_global_local_shortcuts.len(), 1);
        assert_eq!(alice_global_local_shortcuts[0].workspace_id, workspace.id);

        let bob_shortcuts = store
            .list_sidebar_shortcuts("bob", &workspace.id)
            .expect("list bob shortcuts");
        assert!(bob_shortcuts.is_empty());

        store
            .delete_sidebar_shortcut("alice", &updated.id)
            .expect("delete shortcut");
        let after_delete = store
            .list_sidebar_shortcuts("alice", &workspace.id)
            .expect("list deleted shortcuts");
        assert!(after_delete.is_empty());
    }

    #[test]
    fn user_ui_preferences_are_scoped_upserted_and_user_isolated() {
        let store = store();
        create_user(&store, "alice");
        create_user(&store, "bob");

        let first = store
            .upsert_user_ui_preference(UpsertUserUiPreferenceInput {
                id: None,
                user_id: "alice".to_string(),
                workspace_id: Some("local:_mnt_Data1T_mnote".to_string()),
                source_kind: Some("local_folder".to_string()),
                scope_kind: "source_family".to_string(),
                scope_id: "external_local_folder".to_string(),
                key: "hideTitleHeader".to_string(),
                value_json: "false".to_string(),
            })
            .expect("insert preference");

        let updated = store
            .upsert_user_ui_preference(UpsertUserUiPreferenceInput {
                id: None,
                user_id: "alice".to_string(),
                workspace_id: Some("local:_mnt_Data1T_mnote".to_string()),
                source_kind: Some("local_folder".to_string()),
                scope_kind: "source_family".to_string(),
                scope_id: "external_local_folder".to_string(),
                key: "hideTitleHeader".to_string(),
                value_json: "true".to_string(),
            })
            .expect("upsert preference");

        assert_eq!(updated.id, first.id);
        assert_eq!(updated.value_json, "true");
        assert_eq!(updated.revision, first.revision + 1);

        let alice_preferences = store
            .list_user_ui_preferences(
                "alice",
                Some("local:_mnt_Data1T_mnote"),
                Some("local_folder"),
            )
            .expect("list alice preferences");
        assert_eq!(alice_preferences.len(), 1);
        assert_eq!(alice_preferences[0].scope_kind, "source_family");
        assert_eq!(alice_preferences[0].scope_id, "external_local_folder");

        let bob_preferences = store
            .list_user_ui_preferences("bob", Some("local:_mnt_Data1T_mnote"), Some("local_folder"))
            .expect("list bob preferences");
        assert!(bob_preferences.is_empty());

        let sidebar_scope = "filetree:root-hash:design-hash";
        let sidebar_value = serde_json::json!({
            "schemaVersion": 1,
            "treeKind": "filetree",
            "rootUri": "file:///mnt/Data1T/mnote",
            "scope": "design",
            "expandedRelativePaths": ["design/05-editor-mainline"],
            "expandedIds": [],
            "selectedId": "",
            "focusedId": "",
            "activeId": "",
            "scrollTop": 0
        })
        .to_string();
        let sidebar_first = store
            .upsert_user_ui_preference(UpsertUserUiPreferenceInput {
                id: None,
                user_id: "alice".to_string(),
                workspace_id: Some("local:_mnt_Data1T_mnote".to_string()),
                source_kind: Some("local_folder".to_string()),
                scope_kind: "sidebar_tree".to_string(),
                scope_id: sidebar_scope.to_string(),
                key: "sidebarTreeViewState.v1".to_string(),
                value_json: sidebar_value,
            })
            .expect("insert sidebar tree view state");
        let sidebar_updated = store
            .upsert_user_ui_preference(UpsertUserUiPreferenceInput {
                id: None,
                user_id: "alice".to_string(),
                workspace_id: Some("local:_mnt_Data1T_mnote".to_string()),
                source_kind: Some("local_folder".to_string()),
                scope_kind: "sidebar_tree".to_string(),
                scope_id: sidebar_scope.to_string(),
                key: "sidebarTreeViewState.v1".to_string(),
                value_json: serde_json::json!({
                    "schemaVersion": 1,
                    "treeKind": "filetree",
                    "rootUri": "file:///mnt/Data1T/mnote",
                    "scope": "design",
                    "expandedRelativePaths": ["design/05-editor-mainline", "design/05-editor-mainline/process"],
                    "expandedIds": [],
                    "selectedId": "",
                    "focusedId": "",
                    "activeId": "",
                    "scrollTop": 0
                })
                .to_string(),
            })
            .expect("upsert sidebar tree view state");
        assert_eq!(sidebar_updated.id, sidebar_first.id);
        assert_eq!(sidebar_updated.revision, sidebar_first.revision + 1);

        let alice_sidebar_preferences = store
            .list_user_ui_preferences(
                "alice",
                Some("local:_mnt_Data1T_mnote"),
                Some("local_folder"),
            )
            .expect("list alice sidebar preferences");
        assert!(alice_sidebar_preferences.iter().any(|preference| {
            preference.scope_kind == "sidebar_tree"
                && preference.scope_id == sidebar_scope
                && preference.key == "sidebarTreeViewState.v1"
        }));
        let bob_sidebar_preferences = store
            .list_user_ui_preferences("bob", Some("local:_mnt_Data1T_mnote"), Some("local_folder"))
            .expect("list bob sidebar preferences");
        assert!(bob_sidebar_preferences.is_empty());
    }

    #[test]
    fn ai_agent_profile_policy_provisions_personal_and_shared_boundaries() {
        let store = store();
        create_user(&store, "alice");
        create_user(&store, "bob");
        store
            .upsert_user(UpsertUserInput {
                id: Some("admin".to_string()),
                email: Some("admin@example.com".to_string()),
                username: "admin".to_string(),
                display_name: "admin".to_string(),
                role: Some("admin".to_string()),
                password_hash: None,
            })
            .expect("admin user");

        let alice_profiles = store
            .ensure_ai_agent_profile_policy("alice", false)
            .expect("alice profiles");
        assert_eq!(alice_profiles.len(), 11);
        let alice_personal = alice_profiles
            .iter()
            .find(|item| item.profile.profile_kind == "personal")
            .expect("alice personal profile");
        assert_eq!(
            alice_personal.profile.owner_user_id.as_deref(),
            Some("alice")
        );
        assert!(alice_personal.grant.can_manage_skills);
        let alice_shared = alice_profiles
            .iter()
            .find(|item| item.profile.id == "shared_lite")
            .expect("alice shared profile");
        assert!(alice_shared.grant.can_run);
        assert!(!alice_shared.grant.can_manage_skills);
        for (profile_id, isolated_profile, display_name) in [
            (
                "shared_deepseek_chat",
                "openclaw-deepseek-chat",
                "DeepSeek Chat",
            ),
            ("shared_gemini_chat", "openclaw-gemini-chat", "Gemini Chat"),
            ("shared_doubao_chat", "openclaw-doubao-chat", "豆包 Chat"),
            (
                "shared_api_deepseek_flash_chat",
                "api-deepseek-flash-chat",
                "DeepSeek Flash Chat",
            ),
            (
                "shared_api_deepseek_pro_chat",
                "api-deepseek-pro-chat",
                "DeepSeek Pro Chat",
            ),
            ("shared_api_gpt_chat", "api-gpt-chat", "GPT Chat"),
            ("shared_api_kimi_chat", "api-kimi-chat", "Kimi Chat"),
            (
                "shared_api_gemini_chat",
                "api-gemini-chat",
                "Gemini API Chat",
            ),
            ("shared_api_grok_chat", "api-grok-chat", "Grok API Chat"),
        ] {
            let alice_chat = alice_profiles
                .iter()
                .find(|item| item.profile.id == profile_id)
                .expect("alice shared chat profile");
            assert_eq!(alice_chat.profile.isolated_profile_name, isolated_profile);
            assert_eq!(alice_chat.profile.display_name, display_name);
            assert!(alice_chat.grant.can_run);
            assert!(!alice_chat.grant.can_manage_skills);
            assert!(!alice_chat.grant.can_manage_config);
        }

        let bob_profiles = store
            .ensure_ai_agent_profile_policy("bob", false)
            .expect("bob profiles");
        let bob_personal = bob_profiles
            .iter()
            .find(|item| item.profile.profile_kind == "personal")
            .expect("bob personal profile");
        assert_ne!(bob_personal.profile.id, alice_personal.profile.id);
        assert!(store
            .resolve_ai_agent_profile("bob", false, &alice_personal.profile.id)
            .expect("resolve cross user")
            .is_none());

        let admin_shared = store
            .resolve_ai_agent_profile("admin", true, "shared_lite")
            .expect("admin shared")
            .expect("admin can see shared");
        assert!(admin_shared.grant.can_manage_skills);
        assert!(admin_shared.grant.can_manage_config);
        for profile_id in [
            "shared_deepseek_chat",
            "shared_gemini_chat",
            "shared_doubao_chat",
            "shared_api_deepseek_flash_chat",
            "shared_api_deepseek_pro_chat",
            "shared_api_gpt_chat",
            "shared_api_kimi_chat",
            "shared_api_gemini_chat",
            "shared_api_grok_chat",
        ] {
            let admin_chat = store
                .resolve_ai_agent_profile("admin", true, profile_id)
                .expect("admin chat")
                .expect("admin can see shared chat profile");
            assert!(admin_chat.grant.can_run);
            assert!(!admin_chat.grant.can_manage_skills);
            assert!(!admin_chat.grant.can_manage_config);
        }
    }

    #[test]
    fn navigation_recent_is_user_scoped_upserted_and_limited_by_kind() {
        let store = store();
        create_user(&store, "alice");
        create_user(&store, "bob");

        let first = store
            .upsert_navigation_recent(UpsertNavigationRecentInput {
                id: None,
                user_id: "alice".to_string(),
                workspace_id: Some("local:_tmp_mnote".to_string()),
                kind: "folder".to_string(),
                source_kind: "local_folder".to_string(),
                root_uri: "file:///tmp/mnote".to_string(),
                relative_path: Some("design".to_string()),
                document_id: None,
                title: "design".to_string(),
                metadata_json: "{}".to_string(),
            })
            .expect("insert folder recent");
        let updated = store
            .upsert_navigation_recent(UpsertNavigationRecentInput {
                id: None,
                user_id: "alice".to_string(),
                workspace_id: Some("local:_tmp_mnote".to_string()),
                kind: "folder".to_string(),
                source_kind: "local_folder".to_string(),
                root_uri: "file:///tmp/mnote".to_string(),
                relative_path: Some("design".to_string()),
                document_id: None,
                title: "Design".to_string(),
                metadata_json: "{\"from\":\"test\"}".to_string(),
            })
            .expect("upsert folder recent");
        assert_eq!(updated.id, first.id);
        assert_eq!(updated.title, "Design");
        assert_eq!(updated.revision, first.revision + 1);

        store
            .upsert_navigation_recent(UpsertNavigationRecentInput {
                id: None,
                user_id: "alice".to_string(),
                workspace_id: Some("local:_tmp_mnote".to_string()),
                kind: "page".to_string(),
                source_kind: "local_folder".to_string(),
                root_uri: "file:///tmp/mnote".to_string(),
                relative_path: Some("design/Plan.md".to_string()),
                document_id: Some("local-md:design~2FPlan.md".to_string()),
                title: "Plan".to_string(),
                metadata_json: "{}".to_string(),
            })
            .expect("insert page recent");
        for index in 0..12 {
            store
                .upsert_navigation_recent(UpsertNavigationRecentInput {
                    id: None,
                    user_id: "alice".to_string(),
                    workspace_id: Some("local:_tmp_mnote".to_string()),
                    kind: "folder".to_string(),
                    source_kind: "local_folder".to_string(),
                    root_uri: "file:///tmp/mnote".to_string(),
                    relative_path: Some(format!("folder-{index}")),
                    document_id: None,
                    title: format!("folder-{index}"),
                    metadata_json: "{}".to_string(),
                })
                .expect("insert bounded folder recent");
        }

        let alice_folders = store
            .list_navigation_recent("alice", Some("folder"), 50)
            .expect("list alice folders");
        assert_eq!(alice_folders.len(), 10);
        assert_eq!(alice_folders[0].kind, "folder");
        assert_eq!(alice_folders[0].title, "folder-11");
        assert!(alice_folders.iter().all(|record| record.user_id == "alice"));
        assert!(!alice_folders.iter().any(|record| record.title == "Design"));

        let alice_pages = store
            .list_navigation_recent("alice", Some("page"), 20)
            .expect("list alice pages");
        assert_eq!(alice_pages.len(), 1);
        assert_eq!(
            alice_pages[0].document_id.as_deref(),
            Some("local-md:design~2FPlan.md")
        );

        let bob_recent = store
            .list_navigation_recent("bob", None, 20)
            .expect("list bob recent");
        assert!(bob_recent.is_empty());
    }

    #[test]
    fn ai_policy_upsert_returns_workspace_policy_before_user_policy() {
        let store = store();
        create_user(&store, "ai_user");
        let workspace = store
            .ensure_default_workspace("ai_user")
            .expect("default workspace");

        let user_policy = store
            .upsert_ai_policy(UpsertAiPolicyInput {
                id: None,
                user_id: Some("ai_user".to_string()),
                workspace_id: None,
                allowed_roots_json: "[\"file:///user-root\"]".to_string(),
                model_policy_json: "{\"default\":\"local\"}".to_string(),
                quota_json: "{}".to_string(),
            })
            .expect("user policy");
        assert_eq!(user_policy.revision, 1);

        let workspace_policy = store
            .upsert_ai_policy(UpsertAiPolicyInput {
                id: None,
                user_id: Some("ai_user".to_string()),
                workspace_id: Some(workspace.id.clone()),
                allowed_roots_json: "[\"file:///workspace-root\"]".to_string(),
                model_policy_json: "{\"default\":\"workspace\"}".to_string(),
                quota_json: "{}".to_string(),
            })
            .expect("workspace policy");

        let resolved = store
            .get_ai_policy("ai_user", Some(&workspace.id))
            .expect("get workspace policy")
            .expect("workspace policy exists");
        assert_eq!(resolved.id, workspace_policy.id);
        assert!(resolved.allowed_roots_json.contains("workspace-root"));

        let user_only = store
            .get_ai_policy("ai_user", None)
            .expect("get user policy")
            .expect("user policy exists");
        assert_eq!(user_only.id, user_policy.id);
    }

    #[test]
    fn sync_state_upsert_tracks_cursor_status_and_revision() {
        let store = store();
        create_user(&store, "sync_user");
        let workspace = store
            .ensure_default_workspace("sync_user")
            .expect("default workspace");

        let first = store
            .upsert_sync_state(UpsertSyncStateInput {
                id: None,
                workspace_id: workspace.id.clone(),
                remote_kind: "local_worker".to_string(),
                remote_id: Some("worker-1".to_string()),
                cursor: Some("cursor-1".to_string()),
                last_synced_at: None,
                status: "idle".to_string(),
                error_json: None,
            })
            .expect("first sync state");
        assert_eq!(first.revision, 1);

        let second = store
            .upsert_sync_state(UpsertSyncStateInput {
                id: None,
                workspace_id: workspace.id.clone(),
                remote_kind: "local_worker".to_string(),
                remote_id: Some("worker-1".to_string()),
                cursor: Some("cursor-2".to_string()),
                last_synced_at: Some("2026-05-22T00:00:00Z".to_string()),
                status: "error".to_string(),
                error_json: Some("{\"code\":\"test\"}".to_string()),
            })
            .expect("second sync state");

        assert_eq!(second.id, first.id);
        assert_eq!(second.revision, 2);
        assert_eq!(second.cursor.as_deref(), Some("cursor-2"));

        let loaded = store
            .get_sync_state(&workspace.id, "local_worker")
            .expect("get sync state")
            .expect("sync state exists");
        assert_eq!(loaded.status, "error");
        assert_eq!(loaded.error_json.as_deref(), Some("{\"code\":\"test\"}"));
    }

    #[test]
    fn ai_runtime_run_upsert_lists_updates_and_events() {
        let store = store();
        create_user(&store, "ai_runtime_user");

        let first = store
            .upsert_ai_runtime_run(UpsertAiRuntimeRunInput {
                id: None,
                user_id: "ai_runtime_user".to_string(),
                workspace_id: Some("ws_1".to_string()),
                document_id: Some("doc_1".to_string()),
                session_id: "sess_1".to_string(),
                run_id: "run_1".to_string(),
                title: Some("初始标题".to_string()),
                profile: "reasonix".to_string(),
                acp_runtime: "reasonix".to_string(),
                trace_id: Some("trace_1".to_string()),
                status: "running".to_string(),
                runtime_json: "{\"status\":\"running\"}".to_string(),
                payload_json: "{\"message\":\"读取当前页面\"}".to_string(),
            })
            .expect("insert runtime run");
        assert_eq!(first.revision, 1);

        let updated = store
            .upsert_ai_runtime_run(UpsertAiRuntimeRunInput {
                id: None,
                user_id: "ai_runtime_user".to_string(),
                workspace_id: Some("ws_1".to_string()),
                document_id: Some("doc_1".to_string()),
                session_id: "sess_1".to_string(),
                run_id: "run_1".to_string(),
                title: Some("更新标题".to_string()),
                profile: "reasonix".to_string(),
                acp_runtime: "reasonix".to_string(),
                trace_id: Some("trace_2".to_string()),
                status: "completed".to_string(),
                runtime_json: "{\"status\":\"completed\"}".to_string(),
                payload_json: "{\"message\":\"完成\"}".to_string(),
            })
            .expect("update runtime run");
        assert_eq!(updated.id, first.id);
        assert_eq!(updated.revision, 2);

        let runs = store
            .list_ai_runtime_runs(
                "ai_runtime_user",
                Some("ws_1"),
                Some("doc_1"),
                Some("sess_1"),
                10,
            )
            .expect("list runtime runs");
        assert_eq!(runs.len(), 1);
        assert_eq!(runs[0].title.as_deref(), Some("更新标题"));

        let event = store
            .append_ai_runtime_event(AppendAiRuntimeEventInput {
                id: None,
                user_id: "ai_runtime_user".to_string(),
                workspace_id: Some("ws_1".to_string()),
                document_id: Some("doc_1".to_string()),
                session_id: "sess_1".to_string(),
                run_id: "run_1".to_string(),
                profile: "reasonix".to_string(),
                acp_runtime: "reasonix".to_string(),
                event_type: "message.delta".to_string(),
                payload_json: "{\"text\":\"hello\"}".to_string(),
            })
            .expect("append runtime event");
        assert_eq!(event.event_type, "message.delta");
        store
            .append_ai_runtime_event(AppendAiRuntimeEventInput {
                id: None,
                user_id: "ai_runtime_user".to_string(),
                workspace_id: Some("ws_1".to_string()),
                document_id: Some("doc_1".to_string()),
                session_id: "sess_1".to_string(),
                run_id: "run_1".to_string(),
                profile: "reasonix".to_string(),
                acp_runtime: "reasonix".to_string(),
                event_type: "run.completed".to_string(),
                payload_json: "{\"status\":\"completed\"}".to_string(),
            })
            .expect("append second runtime event");

        let events = store
            .list_ai_runtime_events("ai_runtime_user", "run_1", 10)
            .expect("list runtime events");
        assert_eq!(events.len(), 2);
        assert_eq!(events[0].payload_json, "{\"text\":\"hello\"}");

        let found = store
            .find_ai_runtime_run("ai_runtime_user", "run_1")
            .expect("find runtime run")
            .expect("runtime run exists");
        assert_eq!(found.status, "completed");

        let journal_events = store
            .list_ai_runtime_journal_events("ai_runtime_user", "run_1", 1, 10)
            .expect("list runtime journal events");
        assert_eq!(journal_events.len(), 1);
        assert_eq!(journal_events[0].seq, 2);
        assert_eq!(journal_events[0].event.event_type, "run.completed");
    }

    #[test]
    fn ai_runtime_session_management_renames_auto_titles_and_soft_deletes() {
        let store = store();
        create_user(&store, "ai_runtime_user");
        store
            .upsert_ai_runtime_run(UpsertAiRuntimeRunInput {
                id: None,
                user_id: "ai_runtime_user".to_string(),
                workspace_id: Some("ws_1".to_string()),
                document_id: Some("doc_1".to_string()),
                session_id: "sess_1".to_string(),
                run_id: "run_1".to_string(),
                title: None,
                profile: "reasonix".to_string(),
                acp_runtime: "reasonix".to_string(),
                trace_id: None,
                status: "completed".to_string(),
                runtime_json: "{\"status\":\"completed\"}".to_string(),
                payload_json: "{\"message\":\"请总结这篇文档\"}".to_string(),
            })
            .expect("insert runtime run");

        let titled = store
            .auto_title_ai_runtime_session("ai_runtime_user", "sess_1", Some("ws_1"))
            .expect("auto title")
            .expect("titled run");
        assert_eq!(titled.title.as_deref(), Some("请总结这篇文档"));

        let renamed = store
            .rename_ai_runtime_session("ai_runtime_user", "sess_1", Some("ws_1"), "人工标题")
            .expect("rename session");
        assert_eq!(renamed.len(), 1);
        assert_eq!(renamed[0].title.as_deref(), Some("人工标题"));

        let deleted = store
            .delete_ai_runtime_session("ai_runtime_user", "sess_1", Some("ws_1"))
            .expect("delete session");
        assert_eq!(deleted, 1);

        let runs = store
            .list_ai_runtime_runs("ai_runtime_user", Some("ws_1"), None, Some("sess_1"), 10)
            .expect("list after delete");
        assert!(runs.is_empty());
    }

    #[test]
    fn ai_external_conversation_binding_is_user_scoped_and_statused() {
        let store = store();
        create_user(&store, "doubao_user_a");
        create_user(&store, "doubao_user_b");

        let binding = store
            .upsert_ai_external_conversation_binding(UpsertAiExternalConversationBindingInput {
                id: None,
                user_id: "doubao_user_a".to_string(),
                workspace_id: Some("ws_1".to_string()),
                mnote_session_id: "chatonly_session_1".to_string(),
                acp_session_id: Some("acp_session_1".to_string()),
                agent_id: "chat_only".to_string(),
                profile: "openclaw-doubao-chat".to_string(),
                provider: "doubao-web".to_string(),
                remote_conversation_id: "38428454119180290".to_string(),
                remote_url: Some("https://www.doubao.com/chat/38428454119180290".to_string()),
                status: "active".to_string(),
                metadata_json: "{\"source\":\"provider.conversation.bound\"}".to_string(),
            })
            .expect("upsert external conversation binding");

        assert_eq!(binding.status, "active");
        assert_eq!(binding.remote_conversation_id, "38428454119180290");

        let found = store
            .find_ai_external_conversation_binding(
                "doubao_user_a",
                Some("ws_1"),
                "chatonly_session_1",
                "doubao-web",
            )
            .expect("find binding")
            .expect("binding exists");
        assert_eq!(found.id, binding.id);
        assert_eq!(found.acp_session_id.as_deref(), Some("acp_session_1"));

        let other_user = store
            .find_ai_external_conversation_binding(
                "doubao_user_b",
                Some("ws_1"),
                "chatonly_session_1",
                "doubao-web",
            )
            .expect("find other user binding");
        assert!(other_user.is_none());

        let changed = store
            .mark_ai_external_conversation_binding_status(
                "doubao_user_a",
                Some("ws_1"),
                "chatonly_session_1",
                "doubao-web",
                "local_deleted",
                Some("{\"reason\":\"mnote_session_deleted\"}"),
            )
            .expect("mark local deleted");
        assert_eq!(changed, 1);

        let deleted = store
            .find_ai_external_conversation_binding(
                "doubao_user_a",
                Some("ws_1"),
                "chatonly_session_1",
                "doubao-web",
            )
            .expect("find deleted binding")
            .expect("binding remains auditable");
        assert_eq!(deleted.status, "local_deleted");
        assert!(deleted.deleted_at.is_some());
    }

    #[test]
    fn session_lookup_resolves_active_user_by_token_hash() {
        let store = store();
        create_user(&store, "shujuan");

        let session = store
            .create_session(CreateSessionInput {
                id: None,
                user_id: "shujuan".to_string(),
                token_hash: session_token_hash("raw-session-token"),
                user_agent: Some("mnote-test".to_string()),
                ip_hash: None,
                expires_at: None,
            })
            .expect("create session");

        let resolved = store
            .get_session_by_token_hash(&session.token_hash)
            .expect("lookup session")
            .expect("active session");

        assert_eq!(resolved.session.user_id, "shujuan");
        assert_eq!(resolved.user.id, "shujuan");
        assert_eq!(resolved.user.email.as_deref(), Some("shujuan@example.com"));
    }

    #[test]
    fn revoked_session_is_not_resolved() {
        let store = store();
        create_user(&store, "shujuan");

        let session = store
            .create_session(CreateSessionInput {
                id: None,
                user_id: "shujuan".to_string(),
                token_hash: session_token_hash("revoked-token"),
                user_agent: None,
                ip_hash: None,
                expires_at: None,
            })
            .expect("create session");
        store.revoke_session(&session.id).expect("revoke session");

        let resolved = store
            .get_session_by_token_hash(&session.token_hash)
            .expect("lookup session");
        assert!(resolved.is_none());
    }

    #[test]
    fn password_hash_v1_is_sha256_hex_placeholder() {
        assert!(password_hash_v1("secret").starts_with("sha256-v1:"));
    }

    #[test]
    fn authenticate_password_accepts_email_login_and_creates_session() {
        let store = store();
        let test_password = ["correct", "horse"].join(" ");
        create_password_identity(&store, "alice", "alice@example.com", &test_password);

        let resolved = store
            .authenticate_password(AuthenticatePasswordInput {
                account: "alice@example.com".to_string(),
                password: test_password,
                session_id: None,
                token_hash: session_token_hash("alice-email-session"),
                user_agent: Some("mnote-test".to_string()),
                ip_hash: None,
                expires_at: None,
            })
            .expect("authenticate by email");

        assert_eq!(resolved.user.username, "alice");
        assert_eq!(
            resolved.session.token_hash,
            session_token_hash("alice-email-session")
        );

        let lookup = store
            .get_session_by_token_hash(&resolved.session.token_hash)
            .expect("lookup created session")
            .expect("created session");
        assert_eq!(lookup.user.id, resolved.user.id);
    }

    #[test]
    fn authenticate_password_accepts_username_login() {
        let store = store();
        let test_password = ["sword", "fish"].join("");
        create_password_identity(&store, "bob", "bob@example.com", &test_password);

        let resolved = store
            .authenticate_password(AuthenticatePasswordInput {
                account: "bob".to_string(),
                password: test_password,
                session_id: None,
                token_hash: session_token_hash("bob-username-session"),
                user_agent: None,
                ip_hash: None,
                expires_at: None,
            })
            .expect("authenticate by username");

        assert_eq!(resolved.user.email.as_deref(), Some("bob@example.com"));
        assert_eq!(
            resolved.session.token_hash,
            session_token_hash("bob-username-session")
        );
    }

    #[test]
    fn authenticate_password_rejects_wrong_password() {
        let store = store();
        let right_password = ["right", "password"].join("-");
        let wrong_password = ["wrong", "password"].join("-");
        create_password_identity(&store, "chris", "chris@example.com", &right_password);

        let err = store
            .authenticate_password(AuthenticatePasswordInput {
                account: "chris@example.com".to_string(),
                password: wrong_password,
                session_id: None,
                token_hash: session_token_hash("chris-failed-session"),
                user_agent: None,
                ip_hash: None,
                expires_at: None,
            })
            .expect_err("wrong password should fail");

        assert!(matches!(err, ControlPlaneError::Unauthorized(_)));
        assert!(store
            .get_session_by_token_hash(&session_token_hash("chris-failed-session"))
            .expect("lookup failed session")
            .is_none());
    }
}
