use crate::app::AppConfig;
use crate::context::RequestContext;
use crate::error::WebError;
use base64::Engine;
use bridge_runtime::{
    build_runtime_command_artifact_plan, RuntimeBridgeContextWire, RuntimeCommandArtifactPlan,
    RuntimeCommandEnvelopeWire, RuntimeCommandExecutionPlan, RuntimeQueryExecutionPlan,
};
use serde_json::{json, Value};
use std::fs;
use std::sync::OnceLock;
use std::time::Duration;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};

const HEADER_REQUEST_ID: &str = "x-request-id";
const HEADER_TRACE_ID: &str = "x-trace-id";
const HEADER_WORKSPACE_ID: &str = "x-mnote-workspace-id";
const HEADER_SOURCE_CHANNEL: &str = "x-mnote-source-channel";
const HEADER_SOURCE_CLIENT: &str = "x-mnote-source-client";
const HEADER_IDEMPOTENCY_KEY: &str = "x-idempotency-key";
const COOKIE_MNOTE_WEB_CONVEX_TOKEN: &str = "mnote_web_convex_token";
const COOKIE_CONVEX_AUTH_JWT: &str = "__convexAuthJWT";
static CONVEX_HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();

fn convex_http_client(context: &RequestContext) -> Result<&'static reqwest::Client, WebError> {
    if let Some(client) = CONVEX_HTTP_CLIENT.get() {
        return Ok(client);
    }

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(20))
        .pool_max_idle_per_host(16)
        .pool_idle_timeout(Duration::from_secs(30))
        .build()
        .map_err(|error| {
            WebError::internal(format!("Convex HTTP 客户端创建失败: {error}"))
                .with_context(context)
                .with_header("x-error-phase", "client_build")
                .with_header("x-upstream-service", "convex")
        })?;
    let _ = CONVEX_HTTP_CLIENT.set(client);
    CONVEX_HTTP_CLIENT.get().ok_or_else(|| {
        WebError::internal("Convex HTTP 客户端初始化失败")
            .with_context(context)
            .with_header("x-error-phase", "client_build")
            .with_header("x-upstream-service", "convex")
    })
}

fn read_env_or_dotenv(key: &str) -> Option<String> {
    if let Ok(value) = std::env::var(key) {
        let trimmed = value.trim().trim_matches('"').to_string();
        if !trimmed.is_empty() {
            return Some(trimmed);
        }
    }

    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../..")
        .join(".env.all");
    let content = fs::read_to_string(root).ok()?;
    for line in content.lines() {
        let line = line.trim_end_matches('\r');
        if line.starts_with('#') || line.trim().is_empty() {
            continue;
        }
        let Some((k, v)) = line.split_once('=') else {
            continue;
        };
        if k.trim() != key {
            continue;
        }
        let trimmed = v.trim().trim_matches('"').to_string();
        if !trimmed.is_empty() {
            return Some(trimmed);
        }
    }
    None
}

fn build_authorization(config: &AppConfig, context: &RequestContext) -> Result<String, WebError> {
    if let Some(authorization) = context
        .auth
        .authorization
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        return Ok(authorization.to_string());
    }

    if let Some(convex_token) = extract_cookie_value(context, COOKIE_CONVEX_AUTH_JWT) {
        return Ok(format!("Bearer {convex_token}"));
    }

    if let Some(convex_token) = extract_cookie_value(context, COOKIE_MNOTE_WEB_CONVEX_TOKEN) {
        return Ok(format!("Bearer {convex_token}"));
    }

    let admin_key = config
        .convex_admin_key
        .clone()
        .or_else(|| read_env_or_dotenv("CONVEX_SELF_HOSTED_ADMIN_KEY"))
        .ok_or_else(|| {
            WebError::service_unavailable_code(
                "convex_config_missing",
                "缺少 CONVEX_SELF_HOSTED_ADMIN_KEY，且请求未携带 Authorization",
            )
            .with_context(context)
            .with_header("x-error-phase", "convex_auth")
            .with_header("x-upstream-service", "convex")
        })?;

    let dev_user_id = config.dev_user_id.trim();
    if dev_user_id.is_empty() {
        return Ok(format!("Convex {admin_key}"));
    }

    // 说明：Rust Web 直接调用 Convex HTTP API 时没有 Next/Convex Auth cookie。
    // 自托管开发态用 admin auth 携带 acting identity，让 @convex-dev/auth 的
    // getAuthUserId(ctx) 能得到 DEV_USER_ID，从而和 Next 开发态免登录语义一致。
    let identity = json!({
        "subject": format!("{}|mnote-web-dev-session", dev_user_id),
        "issuer": "mnote-web-dev",
        "name": config.dev_user_name,
        "email": config.dev_user_email,
    });
    let identity_encoded =
        base64::engine::general_purpose::STANDARD.encode(identity.to_string().as_bytes());
    Ok(format!("Convex {admin_key}:{identity_encoded}"))
}

fn extract_cookie_value(context: &RequestContext, name: &str) -> Option<String> {
    context
        .auth
        .cookie_header
        .as_deref()
        .and_then(|cookie_header| {
            cookie_header.split(';').find_map(|segment| {
                let (key, value) = segment.trim().split_once('=')?;
                if key.trim() != name {
                    return None;
                }
                let trimmed = value.trim();
                if trimmed.is_empty() {
                    return None;
                }
                Some(trimmed.to_string())
            })
        })
}

fn convex_url(config: &AppConfig, context: &RequestContext) -> Result<String, WebError> {
    config
        .convex_url
        .clone()
        .or_else(|| read_env_or_dotenv("CONVEX_SELF_HOSTED_URL"))
        .or_else(|| read_env_or_dotenv("NEXT_PUBLIC_CONVEX_URL"))
        .map(|value| value.trim().trim_end_matches('/').to_string())
        .filter(|value| !value.is_empty())
        .ok_or_else(|| {
            WebError::service_unavailable_code(
                "convex_config_missing",
                "缺少 CONVEX_SELF_HOSTED_URL / NEXT_PUBLIC_CONVEX_URL",
            )
            .with_context(context)
            .with_header("x-error-phase", "convex_url")
            .with_header("x-upstream-service", "convex")
        })
}

fn load_query_fixture(
    config: &AppConfig,
    context: &RequestContext,
    plan: &RuntimeQueryExecutionPlan,
) -> Result<Option<Value>, WebError> {
    if !config.allow_dev_fixtures {
        return Ok(None);
    }

    let Some(raw) = config.query_fixtures_json.as_deref() else {
        return Ok(None);
    };
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return Ok(None);
    }

    let fixtures: Value = serde_json::from_str(trimmed).map_err(|error| {
        WebError::internal(format!("MNOTE_WEB_QUERY_FIXTURES_JSON 非法: {error}"))
            .with_context(context)
            .with_header("x-error-phase", "fixture_parse")
    })?;

    Ok(fixtures
        .as_object()
        .and_then(|map| map.get(plan.function_name.as_str()))
        .cloned())
}

fn load_mutation_fixture(
    config: &AppConfig,
    context: &RequestContext,
    plan: &RuntimeCommandExecutionPlan,
) -> Result<Option<Value>, WebError> {
    load_mutation_fixture_by_name(config, context, plan.function_name.as_str())
}

fn load_mutation_fixture_by_name(
    config: &AppConfig,
    context: &RequestContext,
    function_name: &str,
) -> Result<Option<Value>, WebError> {
    if !config.allow_dev_fixtures {
        return Ok(None);
    }

    let Some(raw) = config.mutation_fixtures_json.as_deref() else {
        return Ok(None);
    };
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return Ok(None);
    }

    let fixtures: Value = serde_json::from_str(trimmed).map_err(|error| {
        WebError::internal(format!("MNOTE_WEB_MUTATION_FIXTURES_JSON 非法: {error}"))
            .with_context(context)
            .with_header("x-error-phase", "fixture_parse")
    })?;

    Ok(fixtures
        .as_object()
        .and_then(|map| map.get(function_name))
        .cloned())
}

pub async fn execute_convex_query_plan(
    config: &AppConfig,
    context: &RequestContext,
    plan: &RuntimeQueryExecutionPlan,
) -> Result<Value, WebError> {
    if plan.function_name.trim().is_empty() || plan.function_name.ends_with(":unknown") {
        return Err(WebError::bad_request_code(
            "transport_query_unsupported",
            format!("mnote-web transport 暂不支持 query: {}", plan.function_name),
        )
        .with_context(context)
        .with_header("x-error-phase", "plan_validation"));
    }

    if let Some(fixture) = load_query_fixture(config, context, plan)? {
        return Ok(fixture);
    }

    let payload = json!({
        "path": plan.function_name,
        "format": "convex_encoded_json",
        "args": plan.args_json,
    });

    let client = convex_http_client(context)?;

    let mut request = client
        .post(format!("{}/api/query", convex_url(config, context)?))
        .header("Authorization", build_authorization(config, context)?)
        .header("Content-Type", "application/json")
        .header("Convex-Client", "mnote-web")
        .header(HEADER_REQUEST_ID, &context.trace.request_id)
        .header(HEADER_TRACE_ID, &context.trace.trace_id)
        .header(HEADER_SOURCE_CHANNEL, context.source.channel.as_str())
        .header(HEADER_SOURCE_CLIENT, context.source.client.as_str())
        .json(&payload);

    if let Some(workspace_id) = plan
        .workspace_id
        .as_deref()
        .or(context.workspace.workspace_id.as_deref())
    {
        request = request.header(HEADER_WORKSPACE_ID, workspace_id);
    }

    let response = request.send().await.map_err(|error| {
        let base = if error.is_timeout() {
            WebError::gateway_timeout_code("convex_timeout", format!("Convex query 超时: {error}"))
        } else {
            WebError::service_unavailable_code(
                "convex_unavailable",
                format!("Convex query 请求失败: {error}"),
            )
        };
        base.with_context(context)
            .with_header("x-error-phase", "query_send")
            .with_header("x-upstream-service", "convex")
    })?;

    let status = response.status();
    let body: Value = response.json().await.map_err(|error| {
        WebError::bad_gateway_code(
            "convex_bad_response",
            format!("Convex 响应解析失败: {error}"),
        )
        .with_context(context)
        .with_header("x-error-phase", "query_decode")
        .with_header("x-upstream-service", "convex")
        .with_header("x-upstream-status", status.as_u16().to_string())
    })?;
    if !status.is_success() {
        let message = body
            .get("errorMessage")
            .and_then(Value::as_str)
            .unwrap_or("Convex query 失败");
        return Err(
            WebError::bad_gateway_code("convex_upstream_error", message.to_string())
                .with_context(context)
                .with_header("x-error-phase", "query_status")
                .with_header("x-upstream-service", "convex")
                .with_header("x-upstream-status", status.as_u16().to_string()),
        );
    }

    match body.get("status").and_then(Value::as_str) {
        Some("success") => Ok(body.get("value").cloned().unwrap_or(Value::Null)),
        Some("error") => Err(WebError::bad_gateway_code(
            "convex_upstream_error",
            body.get("errorMessage")
                .and_then(Value::as_str)
                .unwrap_or("Convex 返回 error")
                .to_string(),
        )
        .with_context(context)
        .with_header("x-error-phase", "query_payload")
        .with_header("x-upstream-service", "convex")
        .with_header("x-upstream-status", status.as_u16().to_string())),
        _ => Err(WebError::bad_gateway_code(
            "convex_bad_response",
            format!("未知 Convex 响应: {body}"),
        )
        .with_context(context)
        .with_header("x-error-phase", "query_payload")
        .with_header("x-upstream-service", "convex")
        .with_header("x-upstream-status", status.as_u16().to_string())),
    }
}

pub async fn execute_sidebar_dataset_query(
    config: &AppConfig,
    context: &RequestContext,
    plan: &RuntimeQueryExecutionPlan,
) -> Result<Value, WebError> {
    execute_convex_query_plan(config, context, plan).await
}

pub async fn execute_convex_query_by_name(
    config: &AppConfig,
    context: &RequestContext,
    function_name: &str,
    args: Value,
    workspace_id: Option<&str>,
    error_phase: &'static str,
) -> Result<Value, WebError> {
    let plan = RuntimeQueryExecutionPlan {
        query_name: function_name.to_string(),
        function_name: function_name.to_string(),
        workspace_id: workspace_id.map(ToOwned::to_owned),
        request_id: context.trace.request_id.clone(),
        trace_id: context.trace.trace_id.clone(),
        actor_id: context.auth.actor_id.clone(),
        payload_json: args.to_string(),
        args_json: args,
    };
    execute_convex_query_plan(config, context, &plan)
        .await
        .map_err(|error| error.with_header("x-error-phase", error_phase))
}

fn convex_command_args_for_plan(plan: &RuntimeCommandExecutionPlan) -> Value {
    let mut args = plan.args_json.clone();
    if matches!(
        plan.command_name.as_str(),
        "tree.node.create"
            | "tree.node.rename"
            | "tree.node.archive"
            | "tree.node.restore"
            | "tree.subtree.move"
            | "documents.create"
            | "documents.title.update"
            | "documents.delete"
            | "documents.restore"
            | "documents.move"
    ) {
        strip_tree_artifact_fields(&mut args);
    }
    if matches!(plan.command_name.as_str(), "mindmaps.put")
        || matches!(plan.function_name.as_str(), "mindmaps:put")
    {
        // Rust plan 保留 tree domain event / stream hint 作为正式契约；
        // Convex mindmaps.put legacy validator 仍只接收真实写入字段。
        strip_tree_artifact_fields(&mut args);
    }
    if matches!(
        plan.command_name.as_str(),
        "documents.save" | "page.body.save"
    ) {
        if let Value::Object(map) = &mut args {
            // 当前自托管 Convex 的 documents:updateContent 仍是 legacy validator。
            // Rust plan 保留正式事件契约，但发送给 legacy mutation 时只传它实际接受的字段。
            map.remove("editorDocument");
            map.remove("tiptapDocument");
        }
        strip_tree_artifact_fields(&mut args);
    }
    if matches!(
        plan.command_name.as_str(),
        "documents.options.update" | "page.layout.updateOptions"
    ) {
        // documents:updateOptions 仍只接收页面设置字段；
        // tree stream hint 留在 Rust artifact plan 中持久化。
        strip_tree_artifact_fields(&mut args);
    }
    if plan.command_name == "mindmap.command.apply" {
        if let Value::Object(map) = &mut args {
            // Convex mindmaps:applyCommand 仍只接收 blob substrate 写入所需字段；
            // rootNodeId/projectionRevision/canonicalCommand 属于 Rust command envelope 语义。
            map.remove("rootNodeId");
            map.remove("projectionRevision");
            map.remove("canonicalCommand");
            if let Some(document_id) = map.remove("documentId") {
                map.insert("docId".into(), document_id);
            }
        }
    }
    args
}

fn strip_tree_artifact_fields(args: &mut Value) {
    if let Value::Object(map) = args {
        map.remove("streamDeltaHint");
        map.remove("domainEventHint");
        map.remove("domainEventPlan");
        map.remove("domainEventPlans");
    }
}

pub async fn execute_convex_command_plan(
    config: &AppConfig,
    context: &RequestContext,
    plan: &RuntimeCommandExecutionPlan,
) -> Result<Value, WebError> {
    if plan.function_name.trim().is_empty() || plan.function_name.ends_with(":unknown") {
        return Err(WebError::bad_request_code(
            "transport_command_unsupported",
            format!(
                "mnote-web transport 暂不支持 command: {}",
                plan.function_name
            ),
        )
        .with_context(context)
        .with_header("x-error-phase", "plan_validation"));
    }

    if let Some(fixture) = load_mutation_fixture(config, context, plan)? {
        return Ok(fixture);
    }

    let payload = json!({
        "path": plan.function_name,
        "format": "convex_encoded_json",
        "args": [convex_command_args_for_plan(plan)],
    });

    let client = convex_http_client(context)?;

    let mut request = client
        .post(format!("{}/api/mutation", convex_url(config, context)?))
        .header("Authorization", build_authorization(config, context)?)
        .header("Content-Type", "application/json")
        .header("Convex-Client", "mnote-web")
        .header(HEADER_REQUEST_ID, &context.trace.request_id)
        .header(HEADER_TRACE_ID, &context.trace.trace_id)
        .header(HEADER_SOURCE_CHANNEL, context.source.channel.as_str())
        .header(HEADER_SOURCE_CLIENT, context.source.client.as_str())
        .json(&payload);

    if let Some(workspace_id) = plan
        .workspace_id
        .as_deref()
        .or(context.workspace.workspace_id.as_deref())
    {
        request = request.header(HEADER_WORKSPACE_ID, workspace_id);
    }
    if let Some(idempotency_key) = plan
        .idempotency_key
        .as_deref()
        .or(context.source.idempotency_key.as_deref())
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        request = request.header(HEADER_IDEMPOTENCY_KEY, idempotency_key);
    }

    let response = request.send().await.map_err(|error| {
        let base = if error.is_timeout() {
            WebError::gateway_timeout_code(
                "convex_timeout",
                format!("Convex mutation 超时: {error}"),
            )
        } else {
            WebError::service_unavailable_code(
                "convex_unavailable",
                format!("Convex mutation 请求失败: {error}"),
            )
        };
        base.with_context(context)
            .with_header("x-error-phase", "mutation_send")
            .with_header("x-upstream-service", "convex")
    })?;

    let status = response.status();
    let body: Value = response.json().await.map_err(|error| {
        WebError::bad_gateway_code(
            "convex_bad_response",
            format!("Convex mutation 响应解析失败: {error}"),
        )
        .with_context(context)
        .with_header("x-error-phase", "mutation_decode")
        .with_header("x-upstream-service", "convex")
        .with_header("x-upstream-status", status.as_u16().to_string())
    })?;
    if !status.is_success() {
        let message = body
            .get("errorMessage")
            .and_then(Value::as_str)
            .unwrap_or("Convex mutation 失败");
        return Err(
            WebError::bad_gateway_code("convex_upstream_error", message.to_string())
                .with_context(context)
                .with_header("x-error-phase", "mutation_status")
                .with_header("x-upstream-service", "convex")
                .with_header("x-upstream-status", status.as_u16().to_string()),
        );
    }

    match body.get("status").and_then(Value::as_str) {
        Some("success") => Ok(body.get("value").cloned().unwrap_or(Value::Null)),
        Some("error") => Err(WebError::bad_gateway_code(
            "convex_upstream_error",
            body.get("errorMessage")
                .and_then(Value::as_str)
                .unwrap_or("Convex 返回 error")
                .to_string(),
        )
        .with_context(context)
        .with_header("x-error-phase", "mutation_payload")
        .with_header("x-upstream-service", "convex")
        .with_header("x-upstream-status", status.as_u16().to_string())),
        _ => Err(WebError::bad_gateway_code(
            "convex_bad_response",
            format!("未知 Convex 响应: {body}"),
        )
        .with_context(context)
        .with_header("x-error-phase", "mutation_payload")
        .with_header("x-upstream-service", "convex")
        .with_header("x-upstream-status", status.as_u16().to_string())),
    }
}

#[derive(Debug)]
pub struct ConvexCommandExecution {
    pub result: Value,
    pub artifacts: Option<RuntimeCommandArtifactPlan>,
    pub artifact_error: Option<String>,
}

fn now_iso_like() -> String {
    OffsetDateTime::now_utc()
        .format(&Rfc3339)
        .unwrap_or_else(|_| "1970-01-01T00:00:00Z".into())
}

pub async fn execute_convex_mutation_by_name(
    config: &AppConfig,
    context: &RequestContext,
    function_name: &str,
    args: Value,
    workspace_id: Option<&str>,
    idempotency_key: Option<&str>,
    error_phase: &'static str,
) -> Result<Value, WebError> {
    if let Some(fixture) = load_mutation_fixture_by_name(config, context, function_name)? {
        return Ok(fixture);
    }

    let payload = json!({
        "path": function_name,
        "format": "convex_encoded_json",
        "args": [args],
    });

    let client = convex_http_client(context)?;

    let mut request = client
        .post(format!("{}/api/mutation", convex_url(config, context)?))
        .header("Authorization", build_authorization(config, context)?)
        .header("Content-Type", "application/json")
        .header("Convex-Client", "mnote-web")
        .header(HEADER_REQUEST_ID, &context.trace.request_id)
        .header(HEADER_TRACE_ID, &context.trace.trace_id)
        .header(HEADER_SOURCE_CHANNEL, context.source.channel.as_str())
        .header(HEADER_SOURCE_CLIENT, context.source.client.as_str())
        .json(&payload);

    if let Some(workspace_id) = workspace_id
        .or(context.workspace.workspace_id.as_deref())
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        request = request.header(HEADER_WORKSPACE_ID, workspace_id);
    }
    if let Some(idempotency_key) = idempotency_key
        .or(context.source.idempotency_key.as_deref())
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        request = request.header(HEADER_IDEMPOTENCY_KEY, idempotency_key);
    }

    let response = request.send().await.map_err(|error| {
        let base = if error.is_timeout() {
            WebError::gateway_timeout_code(
                "convex_timeout",
                format!("Convex mutation 超时: {error}"),
            )
        } else {
            WebError::service_unavailable_code(
                "convex_unavailable",
                format!("Convex mutation 请求失败: {error}"),
            )
        };
        base.with_context(context)
            .with_header("x-error-phase", error_phase)
            .with_header("x-upstream-service", "convex")
    })?;

    let status = response.status();
    let body: Value = response.json().await.map_err(|error| {
        WebError::bad_gateway_code(
            "convex_bad_response",
            format!("Convex mutation 响应解析失败: {error}"),
        )
        .with_context(context)
        .with_header("x-error-phase", error_phase)
        .with_header("x-upstream-service", "convex")
        .with_header("x-upstream-status", status.as_u16().to_string())
    })?;
    if !status.is_success() {
        let message = body
            .get("errorMessage")
            .and_then(Value::as_str)
            .unwrap_or("Convex mutation 失败");
        return Err(
            WebError::bad_gateway_code("convex_upstream_error", message.to_string())
                .with_context(context)
                .with_header("x-error-phase", error_phase)
                .with_header("x-upstream-service", "convex")
                .with_header("x-upstream-status", status.as_u16().to_string()),
        );
    }

    match body.get("status").and_then(Value::as_str) {
        Some("success") => Ok(body.get("value").cloned().unwrap_or(Value::Null)),
        Some("error") => Err(WebError::bad_gateway_code(
            "convex_upstream_error",
            body.get("errorMessage")
                .and_then(Value::as_str)
                .unwrap_or("Convex 返回 error")
                .to_string(),
        )
        .with_context(context)
        .with_header("x-error-phase", error_phase)
        .with_header("x-upstream-service", "convex")
        .with_header("x-upstream-status", status.as_u16().to_string())),
        _ => Err(WebError::bad_gateway_code(
            "convex_bad_response",
            format!("未知 Convex 响应: {body}"),
        )
        .with_context(context)
        .with_header("x-error-phase", error_phase)
        .with_header("x-upstream-service", "convex")
        .with_header("x-upstream-status", status.as_u16().to_string())),
    }
}

fn command_log_artifact_args(artifact: &bridge_runtime::RuntimeCommandLogArtifactPlan) -> Value {
    json!({
        "workspaceId": artifact.workspace_id,
        "id": artifact.id,
        "requestId": artifact.request_id,
        "traceId": artifact.trace_id,
        "commandId": artifact.command_id,
        "commandName": artifact.command_name,
        "actorId": artifact.actor_id,
        "actorType": artifact.actor_type,
        "sourceChannel": artifact.source_channel,
        "sourceClient": artifact.source_client,
        "status": artifact.status,
        "targetPageId": artifact.target_page_id,
        "targetBlockId": artifact.target_block_id,
        "payload": artifact.payload,
        "payloadSummary": artifact.payload_summary,
        "refs": artifact.refs,
        "idempotencyKey": artifact.idempotency_key,
        "error": artifact.error,
        "createdAt": artifact.created_at,
        "finishedAt": artifact.finished_at,
    })
}

fn domain_event_artifact_args(artifact: &bridge_runtime::RuntimeDomainEventArtifactPlan) -> Value {
    json!({
        "workspaceId": artifact.workspace_id,
        "id": artifact.id,
        "requestId": artifact.request_id,
        "traceId": artifact.trace_id,
        "commandId": artifact.command_id,
        "commandLogId": artifact.command_log_id,
        "eventType": artifact.event_type,
        "aggregateType": artifact.aggregate_type,
        "aggregateId": artifact.aggregate_id,
        "eventVersion": artifact.event_version,
        "status": artifact.status,
        "actorType": artifact.actor_type,
        "payload": artifact.payload,
        "createdAt": artifact.created_at,
    })
}

pub async fn persist_runtime_command_artifacts(
    config: &AppConfig,
    context: &RequestContext,
    artifacts: &RuntimeCommandArtifactPlan,
) -> Result<(), WebError> {
    execute_convex_mutation_by_name(
        config,
        context,
        "bridgeLogs:recordCommandLog",
        command_log_artifact_args(&artifacts.command_log),
        Some(artifacts.command_log.workspace_id.as_str()),
        artifacts.command_log.idempotency_key.as_deref(),
        "artifact_command_log",
    )
    .await?;

    if let Some(domain_event) = artifacts.domain_event.as_ref() {
        execute_convex_mutation_by_name(
            config,
            context,
            "bridgeLogs:recordDomainEvent",
            domain_event_artifact_args(domain_event),
            Some(domain_event.workspace_id.as_str()),
            artifacts.command_log.idempotency_key.as_deref(),
            "artifact_domain_event",
        )
        .await?;
    }

    Ok(())
}

pub async fn execute_convex_command_plan_with_artifacts(
    config: &AppConfig,
    context: &RequestContext,
    runtime_context: &RuntimeBridgeContextWire,
    command: &RuntimeCommandEnvelopeWire,
    plan: &RuntimeCommandExecutionPlan,
) -> Result<ConvexCommandExecution, WebError> {
    let result = execute_convex_command_plan(config, context, plan).await?;
    let artifacts = build_runtime_command_artifact_plan(
        runtime_context,
        command,
        plan,
        &result,
        &now_iso_like(),
    );
    if let Some(artifacts) = artifacts.as_ref() {
        if let Err(error) = persist_runtime_command_artifacts(config, context, artifacts).await {
            let message = error.message().to_string();
            tracing::warn!(
                error = %message,
                command_id = %command.command_id,
                "Rust command artifact 持久化失败，主 mutation 结果继续返回"
            );
            return Ok(ConvexCommandExecution {
                result,
                artifacts: Some(artifacts.clone()),
                artifact_error: Some(message),
            });
        }
    }
    Ok(ConvexCommandExecution {
        result,
        artifacts,
        artifact_error: None,
    })
}

#[cfg(test)]
mod tests {
    use super::{build_authorization, convex_command_args_for_plan};
    use crate::app::AppConfig;
    use crate::context::RequestContext;
    use axum::http::{HeaderMap, HeaderValue, Method, Uri};
    use base64::Engine;
    use bridge_runtime::RuntimeCommandExecutionPlan;
    use serde_json::json;

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

    fn request_context(headers: HeaderMap) -> RequestContext {
        RequestContext::from_http_parts(
            &Method::GET,
            &"/api/compat/next/sidebar?workspaceId=ws_demo"
                .parse::<Uri>()
                .expect("uri"),
            &headers,
        )
    }

    #[test]
    fn convex_command_args_strips_editor_runtime_fields_for_legacy_document_save() {
        let plan = RuntimeCommandExecutionPlan {
            command_name: "documents.save".into(),
            command_id: "cmd_1".into(),
            function_name: "documents:updateContent".into(),
            workspace_id: Some("ws_1".into()),
            request_id: "req_1".into(),
            trace_id: "trace_1".into(),
            actor_id: "actor_1".into(),
            idempotency_key: None,
            source: json!({}),
            payload_json: "{}".into(),
            args_json: json!({
                "id": "doc_1",
                "content": [],
                "expectedRevision": 0,
                "conflictDetectionKey": "doc_1:0",
                "editorDocument": {"rootBlockIds": []},
                "tiptapDocument": {"type": "doc", "content": []},
                "streamDeltaHint": {"family": "tree"},
                "domainEventHint": {"eventType": "page.body.saved"},
                "domainEventPlan": {"eventType": "page.body.saved"},
                "domainEventPlans": [{"eventType": "page.body.saved"}],
            }),
        };

        let args = convex_command_args_for_plan(&plan);

        assert_eq!(
            args,
            json!({
                "id": "doc_1",
                "content": [],
                "expectedRevision": 0,
                "conflictDetectionKey": "doc_1:0",
            })
        );
    }

    #[test]
    fn convex_command_args_strips_mindmap_put_bridge_artifacts_for_legacy_mutation() {
        let plan = RuntimeCommandExecutionPlan {
            command_name: "mindmaps.put".into(),
            command_id: "cmd_mindmap_put_1".into(),
            function_name: "mindmaps:put".into(),
            workspace_id: Some("ws_1".into()),
            request_id: "req_1".into(),
            trace_id: "trace_1".into(),
            actor_id: "actor_1".into(),
            idempotency_key: None,
            source: json!({}),
            payload_json: "{}".into(),
            args_json: json!({
                "docId": "doc_1",
                "mindmapId": "mind_1",
                "data": {"data": {"text": "KMIND"}, "children": []},
                "createOnly": false,
                "streamDeltaHint": {"family": "tree"},
                "domainEventHint": {"eventType": "tree.resource.mindmap.put"},
                "domainEventPlan": {"eventType": "tree.resource.mindmap.put"},
                "domainEventPlans": [{"eventType": "tree.resource.mindmap.put"}],
            }),
        };

        let args = convex_command_args_for_plan(&plan);

        assert_eq!(
            args,
            json!({
                "docId": "doc_1",
                "mindmapId": "mind_1",
                "data": {"data": {"text": "KMIND"}, "children": []},
                "createOnly": false,
            })
        );
    }

    #[test]
    fn convex_command_args_strips_page_options_artifacts_for_legacy_mutation() {
        let plan = RuntimeCommandExecutionPlan {
            command_name: "page.layout.updateOptions".into(),
            command_id: "cmd_options_1".into(),
            function_name: "documents:updateOptions".into(),
            workspace_id: Some("ws_1".into()),
            request_id: "req_1".into(),
            trace_id: "trace_1".into(),
            actor_id: "actor_1".into(),
            idempotency_key: None,
            source: json!({}),
            payload_json: "{}".into(),
            args_json: json!({
                "id": "doc_1",
                "options": {
                    "showToc": false,
                    "layoutDensity": "compact",
                },
                "streamDeltaHint": {"family": "tree"},
                "domainEventHint": {"eventType": "page.layout.options_updated"},
                "domainEventPlan": {"eventType": "page.layout.options_updated"},
                "domainEventPlans": [{"eventType": "page.layout.options_updated"}],
            }),
        };

        let args = convex_command_args_for_plan(&plan);

        assert_eq!(
            args,
            json!({
                "id": "doc_1",
                "options": {
                    "showToc": false,
                    "layoutDensity": "compact",
                },
            })
        );
    }

    #[test]
    fn convex_command_args_strips_tree_archive_artifacts_for_legacy_mutation() {
        let plan = RuntimeCommandExecutionPlan {
            command_name: "tree.node.archive".into(),
            command_id: "cmd_archive_1".into(),
            function_name: "documents:softDelete".into(),
            workspace_id: Some("ws_1".into()),
            request_id: "req_1".into(),
            trace_id: "trace_1".into(),
            actor_id: "actor_1".into(),
            idempotency_key: None,
            source: json!({}),
            payload_json: "{}".into(),
            args_json: json!({
                "id": "doc_1",
                "streamDeltaHint": {"family": "tree", "kind": "remove_document"},
                "domainEventHint": {"eventType": "tree.node.archived"},
                "domainEventPlan": {"eventType": "tree.node.archived"},
                "domainEventPlans": [{"eventType": "tree.node.archived"}],
            }),
        };

        let args = convex_command_args_for_plan(&plan);

        assert_eq!(
            args,
            json!({
                "id": "doc_1",
            })
        );
    }

    #[test]
    fn convex_command_args_adapts_mindmap_command_apply_for_legacy_mutation() {
        let plan = RuntimeCommandExecutionPlan {
            command_name: "mindmap.command.apply".into(),
            command_id: "cmd_mindmap_1".into(),
            function_name: "mindmaps:applyCommand".into(),
            workspace_id: Some("ws_1".into()),
            request_id: "req_1".into(),
            trace_id: "trace_1".into(),
            actor_id: "actor_1".into(),
            idempotency_key: None,
            source: json!({}),
            payload_json: "{}".into(),
            args_json: json!({
                "documentId": "doc_1",
                "mindmapId": "mind_1",
                "rootNodeId": "root",
                "commands": [
                    {"type": "renameNode", "mapId": "mind_1", "nodeId": "root", "title": "KMIND 已编辑"}
                ],
                "projectionRevision": 3,
                "canonicalCommand": "mindmap.command.apply",
            }),
        };

        let args = convex_command_args_for_plan(&plan);

        assert_eq!(
            args,
            json!({
                "docId": "doc_1",
                "mindmapId": "mind_1",
                "commands": [
                    {"type": "renameNode", "mapId": "mind_1", "nodeId": "root", "title": "KMIND 已编辑"}
                ],
            })
        );
    }

    #[test]
    fn build_authorization_prefers_forwarded_authorization() {
        let mut headers = HeaderMap::new();
        headers.insert(
            "authorization",
            HeaderValue::from_static("Bearer real-token"),
        );

        let authorization =
            build_authorization(&config(), &request_context(headers)).expect("authorization");

        assert_eq!(authorization, "Bearer real-token");
    }

    #[test]
    fn build_authorization_falls_back_to_dev_admin_identity() {
        let authorization = build_authorization(&config(), &request_context(HeaderMap::new()))
            .expect("authorization");

        assert!(authorization.starts_with("Convex admin-demo:"));
        let encoded = authorization
            .trim_start_matches("Convex admin-demo:")
            .trim();
        let decoded = base64::engine::general_purpose::STANDARD
            .decode(encoded)
            .expect("identity base64");
        let identity: serde_json::Value = serde_json::from_slice(&decoded).expect("identity json");
        assert_eq!(identity["subject"], "dev-user|mnote-web-dev-session");
        assert_eq!(identity["issuer"], "mnote-web-dev");
        assert_eq!(identity["email"], "dev@mnote.local");
    }

    #[test]
    fn build_authorization_reads_convex_token_from_cookie() {
        let mut headers = HeaderMap::new();
        headers.insert(
            "cookie",
            HeaderValue::from_static(
                "foo=bar; mnote_web_convex_token=token-from-cookie; theme=light",
            ),
        );

        let authorization =
            build_authorization(&config(), &request_context(headers)).expect("authorization");

        assert_eq!(authorization, "Bearer token-from-cookie");
    }

    #[test]
    fn build_authorization_prefers_convex_auth_jwt_over_legacy_handoff_cookie() {
        let mut headers = HeaderMap::new();
        headers.insert(
            "cookie",
            HeaderValue::from_static(
                "mnote_web_convex_token=legacy-token; __convexAuthJWT=jwt-from-convex-auth",
            ),
        );

        let authorization =
            build_authorization(&config(), &request_context(headers)).expect("authorization");

        assert_eq!(authorization, "Bearer jwt-from-convex-auth");
    }

    #[test]
    fn build_authorization_reads_convex_auth_jwt_cookie() {
        let mut headers = HeaderMap::new();
        headers.insert(
            "cookie",
            HeaderValue::from_static(
                "foo=bar; __convexAuthJWT=jwt-from-convex-auth; __convexAuthRefreshToken=refresh",
            ),
        );

        let authorization =
            build_authorization(&config(), &request_context(headers)).expect("authorization");

        assert_eq!(authorization, "Bearer jwt-from-convex-auth");
    }
}
