use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::command_support::execute_runtime_command_via_convex_with_artifacts;
use crate::routes::query_support::{
    execute_runtime_query_via_convex, fetch_documents_meta_via_convex, fetch_query_data_via_convex,
    resolve_effective_workspace_id,
};
use axum::extract::{Extension, Path, Query, State};
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
use axum::Json;
use bridge_runtime::{
    apply_mindmap_kernel_commands_to_value, RuntimeActorWire, RuntimeCommandEnvelopeWire,
    RuntimeQueryEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire,
};
use serde::Deserialize;
use serde_json::{json, Value};

const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
const HEADER_MINDMAP_TRANSPORT: &str = "x-mnote-mindmap-transport";

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MindmapQueryParams {
    pub view: Option<String>,
    pub query_name: Option<String>,
    pub root_node_id: Option<String>,
    pub workspace_id: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MindmapCommandRequest {
    pub command_name: Option<String>,
    #[serde(default)]
    pub commands: Vec<Value>,
    pub projection_revision: Option<u64>,
    pub workspace_id: Option<String>,
    pub data: Option<Value>,
    pub create_only: Option<bool>,
}

fn default_mindmap_data() -> Value {
    json!({
        "data": {"text": "中心主题"},
        "children": [],
    })
}

fn response_headers() -> HeaderMap {
    let mut headers = HeaderMap::new();
    if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_WEB_OWNER.as_bytes()) {
        headers.insert(name, HeaderValue::from_static("mnote-web"));
    }
    if let Ok(name) = HeaderName::from_lowercase(HEADER_MINDMAP_TRANSPORT.as_bytes()) {
        headers.insert(name, HeaderValue::from_static("mindmap-api"));
    }
    headers
}

fn resolve_query_name(params: &MindmapQueryParams) -> &'static str {
    if params.query_name.as_deref() == Some("mindmap.simple_mind_map_scene.get")
        || params.view.as_deref() == Some("simple_mind_map_scene")
    {
        return "mindmap.simple_mind_map_scene.get";
    }
    if params.query_name.as_deref() == Some("mindmap.kernel_projection.get")
        || params.view.as_deref() == Some("kernel_projection")
    {
        return "mindmap.kernel_projection.get";
    }
    "mindmap.projection.get"
}

fn read_workspace_id_from_meta(meta: &Value) -> Option<String> {
    meta.get("workspace_id")
        .or_else(|| meta.get("workspaceId"))
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
}

async fn resolve_mindmap_workspace_id(
    state: &AppState,
    context: &RequestContext,
    explicit_workspace_id: Option<&str>,
    document_id: &str,
) -> Result<Option<String>, WebError> {
    let effective_workspace_id =
        resolve_effective_workspace_id(context, explicit_workspace_id, false)?;
    if effective_workspace_id.is_some() {
        return Ok(effective_workspace_id);
    }

    // 思维导图 runtime 的历史请求体不一定带 workspaceId；
    // 这里从页面 meta 反查，确保后续 command artifacts 能进入正确 workspace 的实时流。
    let meta = fetch_documents_meta_via_convex(state.config(), context, None, document_id).await?;
    Ok(read_workspace_id_from_meta(&meta))
}

fn execution_artifacts_json(execution: &crate::transport::convex::ConvexCommandExecution) -> Value {
    execution
        .artifacts
        .as_ref()
        .and_then(|artifacts| serde_json::to_value(artifacts).ok())
        .unwrap_or(Value::Null)
}

pub async fn get_mindmap(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Path((document_id, mindmap_id)): Path<(String, String)>,
    Query(params): Query<MindmapQueryParams>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
    let document_id = document_id.trim();
    let mindmap_id = mindmap_id.trim();
    if document_id.is_empty() || mindmap_id.is_empty() {
        return Err(WebError::bad_request_code(
            "mindmap_id_required",
            "缺少有效 documentId 或 mindmapId",
        )
        .with_context(&context));
    }
    let effective_workspace_id =
        resolve_effective_workspace_id(&context, params.workspace_id.as_deref(), false)?;
    let query_name = resolve_query_name(&params);
    let result = execute_runtime_query_via_convex(
        state.config(),
        &context,
        effective_workspace_id.as_deref(),
        RuntimeQueryEnvelopeWire {
            name: query_name.into(),
            payload: json!({
                "documentId": document_id,
                "mindmapId": mindmap_id,
                "rootNodeId": params.root_node_id,
                "workspaceId": effective_workspace_id.clone(),
            }),
        },
    )
    .await?;

    Ok((StatusCode::OK, response_headers(), Json(result)))
}

pub async fn apply_mindmap_command(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Path((document_id, mindmap_id)): Path<(String, String)>,
    Json(body): Json<MindmapCommandRequest>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
    let document_id = document_id.trim();
    let mindmap_id = mindmap_id.trim();
    if document_id.is_empty() || mindmap_id.is_empty() {
        return Err(WebError::bad_request_code(
            "mindmap_id_required",
            "缺少有效 documentId 或 mindmapId",
        )
        .with_context(&context));
    }
    let command_name = body.command_name.as_deref();
    if !matches!(
        command_name,
        None | Some("mindmaps.put") | Some("mindmap.command.apply")
    ) {
        return Err(WebError::bad_request_code(
            "mindmap_command_required",
            "仅支持 mindmaps.put 或 mindmap.command.apply",
        )
        .with_context(&context));
    }
    let effective_workspace_id =
        resolve_mindmap_workspace_id(&state, &context, body.workspace_id.as_deref(), document_id)
            .await?;

    if command_name != Some("mindmap.command.apply") {
        let command = RuntimeCommandEnvelopeWire {
            name: "mindmaps.put".into(),
            command_id: format!("mindmap_put_{}", context.trace.request_id),
            idempotency_key: context.source.idempotency_key.clone(),
            actor: RuntimeActorWire {
                actor_type: context.auth.actor_type.clone(),
                actor_id: context.auth.actor_id.clone(),
                session_id: context.auth.session_id.clone(),
            },
            source: RuntimeSourceWire {
                channel: context.source.channel.clone(),
                client: context.source.client.clone(),
                source_kind: None,
                root_uri: None,
                workspace_id: None,
                capabilities: Vec::new(),
            },
            target: Some(RuntimeTargetWire {
                workspace_id: effective_workspace_id.clone(),
                page_id: Some(document_id.to_string()),
                block_id: Some(mindmap_id.to_string()),
            }),
            payload: json!({
                "documentId": document_id,
                "mindmapId": mindmap_id,
                "workspaceId": effective_workspace_id,
                "data": body.data.unwrap_or_else(default_mindmap_data),
                "createOnly": body.create_only.unwrap_or(false),
            }),
            preflight_data: None,
            reason: Some("mnote-web mindmap put via kernel projection".into()),
            refs: vec!["task168-mindmap-put-validator-smoke".into()],
            dry_run: false,
            validate_only: false,
        };

        let execution = execute_runtime_command_via_convex_with_artifacts(
            state.config(),
            &context,
            effective_workspace_id.as_deref(),
            command,
        )
        .await?;

        return Ok((
            StatusCode::OK,
            response_headers(),
            Json(json!({
                "ok": true,
                "commandName": "mindmaps.put",
                "result": execution.result,
                "artifacts": execution_artifacts_json(&execution),
                "artifactError": execution.artifact_error,
            })),
        ));
    }

    let current = fetch_query_data_via_convex(
        state.config(),
        &context,
        effective_workspace_id.as_deref(),
        RuntimeQueryEnvelopeWire {
            name: "mindmaps.get".into(),
            payload: json!({
                "documentId": document_id,
                "mindmapId": mindmap_id,
                "workspaceId": effective_workspace_id,
            }),
        },
    )
    .await?;
    let applied = apply_mindmap_kernel_commands_to_value(&current, &body.commands)
        .map_err(|error| WebError::bad_request(error.message).with_context(&context))?;
    if !applied.errors.is_empty() {
        return Err(WebError::bad_request_code(
            "mindmap_command_failed",
            applied.errors.join("; "),
        )
        .with_context(&context)
        .with_header("x-error-phase", "mindmap_command_apply"));
    }

    let command = RuntimeCommandEnvelopeWire {
        name: "mindmaps.put".into(),
        command_id: format!("mindmap_put_{}", context.trace.request_id),
        idempotency_key: context.source.idempotency_key.clone(),
        actor: RuntimeActorWire {
            actor_type: context.auth.actor_type.clone(),
            actor_id: context.auth.actor_id.clone(),
            session_id: context.auth.session_id.clone(),
        },
        source: RuntimeSourceWire {
            channel: context.source.channel.clone(),
            client: context.source.client.clone(),
            source_kind: None,
            root_uri: None,
            workspace_id: None,
            capabilities: Vec::new(),
        },
        target: Some(RuntimeTargetWire {
            workspace_id: effective_workspace_id.clone(),
            page_id: Some(document_id.to_string()),
            block_id: Some(mindmap_id.to_string()),
        }),
        payload: json!({
            "documentId": document_id,
            "mindmapId": mindmap_id,
            "workspaceId": effective_workspace_id,
            "data": applied.data,
            "createOnly": false,
        }),
        preflight_data: None,
        reason: Some("mnote-web mindmap command apply via kernel projection".into()),
        refs: vec!["task166-mindmap-phase6-block-smoke".into()],
        dry_run: false,
        validate_only: false,
    };

    let execution = execute_runtime_command_via_convex_with_artifacts(
        state.config(),
        &context,
        effective_workspace_id.as_deref(),
        command,
    )
    .await?;

    Ok((
        StatusCode::OK,
        response_headers(),
        Json(json!({
            "ok": true,
            "commandName": "mindmap.command.apply",
            "applied": applied.applied,
            "errors": applied.errors,
            "projectionRevision": body.projection_revision,
            "result": execution.result,
            "artifacts": execution_artifacts_json(&execution),
            "artifactError": execution.artifact_error,
        })),
    ))
}

#[cfg(test)]
mod tests {
    use crate::app::{build_app, AppConfig, AppState};
    use axum::body::{to_bytes, Body};
    use axum::http::{Request, StatusCode};
    use serde_json::{json, Value};
    use tower::util::ServiceExt;

    fn app() -> axum::Router {
        build_app(AppState::new(AppConfig {
            service_name: "mnote-web".into(),
            service_version: "0.1.0".into(),
            bind_addr: "127.0.0.1:0".into(),
            public_bind_addr: "127.0.0.1:3000".into(),
            legacy_next_base_url: 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: None,
            convex_admin_key: None,
            allow_dev_fixtures: true,
            query_fixtures_json: Some(
                r#"{"documents:getMeta":{"id":"doc_1","workspace_id":"ws_demo","title":"页面"},"mindmaps:get":{"data":{"data":{"text":"KMIND","uid":"root"},"children":[]},"revision":1}}"#
                    .into(),
            ),
            mutation_fixtures_json: Some(
                r#"{"mindmaps:put":{"ok":true,"document_id":"doc_1","mindmap_id":"mind_1","updated_at":"2026-05-12T00:00:00Z"},"bridgeLogs:recordCommandLog":{"ok":true,"id":"clog_fixture"},"bridgeLogs:recordDomainEvent":{"ok":true,"id":"evt_fixture"}}"#
                    .into(),
            ),
            dev_user_id: "dev-user".into(),
            dev_user_name: "开发用户".into(),
            dev_user_email: "dev@mnote.local".into(),
        }))
    }

    #[tokio::test]
    async fn mindmap_put_derives_workspace_and_returns_tree_artifacts() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/mindmap/doc_1/mind_1")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        json!({
                            "data": {
                                "data": {"text": "KMIND", "uid": "root"},
                                "children": []
                            },
                            "createOnly": true
                        })
                        .to_string(),
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");

        assert_eq!(payload["artifacts"]["commandLog"]["workspaceId"], "ws_demo");
        assert_eq!(payload["artifacts"]["commandLog"]["targetPageId"], "doc_1");
        assert_eq!(
            payload["artifacts"]["commandLog"]["targetBlockId"],
            "mind_1"
        );
        assert_eq!(
            payload["artifacts"]["domainEvent"]["eventType"],
            "tree.resource.mindmap.put"
        );
        assert_eq!(
            payload["artifacts"]["domainEvent"]["payload"]["streamDelta"],
            json!({
                "op": "resync_required",
                "reason": "mindmap.put",
                "documentId": "doc_1",
                "blockId": "mind_1"
            })
        );
    }

    #[tokio::test]
    async fn mindmap_command_apply_returns_object_artifacts_without_tree_resync() {
        let response = app()
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/api/mindmap/doc_1/mind_1")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        json!({
                            "commandName": "mindmap.command.apply",
                            "commands": [
                                {
                                    "type": "updateText",
                                    "mindmapId": "mind_1",
                                    "nodeId": "root",
                                    "text": "KMIND 已更新"
                                }
                            ],
                            "projectionRevision": 1
                        })
                        .to_string(),
                    ))
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let payload: Value = serde_json::from_slice(&body).expect("json");

        assert_eq!(payload["commandName"], "mindmap.command.apply");
        assert_eq!(
            payload["artifacts"]["domainEvent"]["eventType"],
            "mindmap.content.updated"
        );
        assert_eq!(
            payload["artifacts"]["domainEvent"]["payload"]["streamDelta"],
            json!({
                "op": "noop"
            })
        );
    }
}
