use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::query_support::resolve_effective_workspace_id;
use crate::routes::snapshot_support::{
    execute_kernel_query, load_projection_snapshot, load_sidebar_dataset, subtree_query,
    ProjectionSnapshotSpec,
};
use axum::extract::{Extension, Query, State};
use axum::http::StatusCode;
use axum::Json;
use bridge_runtime::RuntimeQueryEnvelopeWire;
use core_protocol::{KernelGraphDirection, KernelProjectionKind};
use serde::Deserialize;
use serde_json::{json, Value};

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

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

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

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KernelGraphQuery {
    pub workspace_id: Option<String>,
    pub start_node_id: String,
    pub max_depth: Option<u32>,
}

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

async fn project_projection(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Query(query): Query<KernelProjectionQuery>,
    projection: KernelProjectionKind,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let effective_workspace_id =
        resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), true)?
            .expect("workspace_required 已确保存在");
    let snapshot = load_projection_snapshot(
        state.config(),
        &context,
        &ProjectionSnapshotSpec {
            workspace_id: &effective_workspace_id,
            root_node_id: query.root_node_id.as_deref(),
            depth: query.depth,
            projection,
        },
    )
    .await?;

    Ok(ok_response(&context, snapshot.projection))
}

pub async fn project_tree_sidebar(
    state: State<AppState>,
    context: Extension<RequestContext>,
    query: Query<KernelProjectionQuery>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    project_projection(state, context, query, KernelProjectionKind::SidebarTree).await
}

pub async fn project_tree_page(
    state: State<AppState>,
    context: Extension<RequestContext>,
    query: Query<KernelProjectionQuery>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    project_projection(state, context, query, KernelProjectionKind::PageTree).await
}

pub async fn project_tree_file(
    state: State<AppState>,
    context: Extension<RequestContext>,
    query: Query<KernelProjectionQuery>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    project_projection(state, context, query, KernelProjectionKind::FileTree).await
}

pub async fn project_sidebar(
    state: State<AppState>,
    context: Extension<RequestContext>,
    query: Query<KernelProjectionQuery>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    project_tree_sidebar(state, context, query).await
}

pub async fn subtree(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Query(query): Query<KernelSubtreeQuery>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let effective_workspace_id =
        resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), true)?
            .expect("workspace_required 已确保存在");
    let dataset = load_sidebar_dataset(state.config(), &context, &effective_workspace_id).await?;
    let result = execute_kernel_query(
        &context,
        &effective_workspace_id,
        subtree_query(&effective_workspace_id, &query.root_node_id, query.depth),
        dataset,
    )?;
    Ok(ok_response(&context, result))
}

pub async fn edges(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Query(query): Query<KernelEdgesQuery>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let effective_workspace_id =
        resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), true)?
            .expect("workspace_required 已确保存在");
    let dataset = load_sidebar_dataset(state.config(), &context, &effective_workspace_id).await?;
    let result = execute_kernel_query(
        &context,
        &effective_workspace_id,
        RuntimeQueryEnvelopeWire {
            name: "kernel.edges.list".into(),
            payload: json!({
                "workspaceId": effective_workspace_id,
                "nodeId": query.node_id,
                "direction": KernelGraphDirection::Both,
            }),
        },
        dataset,
    )?;
    Ok(ok_response(&context, result))
}

pub async fn graph(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Query(query): Query<KernelGraphQuery>,
) -> Result<(StatusCode, Json<Value>), WebError> {
    let effective_workspace_id =
        resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), true)?
            .expect("workspace_required 已确保存在");
    let dataset = load_sidebar_dataset(state.config(), &context, &effective_workspace_id).await?;
    let result = execute_kernel_query(
        &context,
        &effective_workspace_id,
        RuntimeQueryEnvelopeWire {
            name: "kernel.graph.traverse".into(),
            payload: json!({
                "workspaceId": effective_workspace_id,
                "startNodeId": query.start_node_id,
                "maxDepth": query.max_depth.unwrap_or(2),
                "edgeTypes": [],
            }),
        },
        dataset,
    )?;
    Ok(ok_response(&context, result))
}

#[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::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(),
            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#"{"sidebar:datasetList":{"documents":[{"id":"page_root","workspace_id":"ws_demo","title":"工作区首页","parent_id":null,"sort_order":0,"is_starred":true,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"page_child","workspace_id":"ws_demo","title":"子页面","parent_id":"page_root","sort_order":1,"is_starred":false,"is_template":false,"created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"media_assets":[{"id":"asset_file_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"封面.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_child_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"节点图片.png","mime_type":"image/png","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_pdf_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"说明书.pdf","mime_type":"application/pdf","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"},{"id":"asset_book_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"file","file_name":"小说.epub","mime_type":"application/epub+zip","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_assets":[{"id":"mind_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"mindmap","file_name":"头脑风暴.json","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"table_assets":[{"id":"table_1","workspace_id":"ws_demo","document_id":"page_root","asset_type":"luckysheet","file_name":"预算.luckysheet","mime_type":"application/json","created_at":"2026-04-16T00:00:00Z","updated_at":"2026-04-16T00:00:00Z"}],"mindmap_asset_children":{"mind_1":["asset_child_1"]}},"bridgeLogs:listWorkspaceOverview":{"workspace_id":"ws_demo","command_logs":[{"command_id":"cmd_1","request_id":"req_1","status":"applied","created_at":"2026-04-16T00:00:00Z"}],"domain_events":[{"command_id":"cmd_1","status":"published","created_at":"2026-04-16T00:00:00Z"}],"next_cursor":null,"has_more":false,"filters":{"command_status":null,"event_status":null,"target_page_id":null,"target_block_id":null,"aggregate_type":null,"aggregate_id":null},"generated_at":"2026-04-16T00:00:00Z"}}"#.into()),
            mutation_fixtures_json: None,
            dev_user_id: "dev-user".into(),
            dev_user_name: "开发用户".into(),
            dev_user_email: "dev@mnote.local".into(),
        }))
    }

    #[tokio::test]
    async fn kernel_projection_route_returns_sidebar_projection() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/api/kernel/projections/sidebar?workspaceId=ws_demo&rootNodeId=page_root")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[tokio::test]
    async fn tree_projection_contract_route_keeps_shared_fields() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/api/kernel/projections/sidebar?workspaceId=ws_demo&rootNodeId=page_root")
                    .body(Body::empty())
                    .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");
        let first = &payload["result"]["items"][0];
        assert_eq!(first["projectionKind"], "sidebar_tree");
        assert_eq!(first["resourceMeta"]["resourceKind"], "document");
        assert_eq!(first["iconHint"], "page");
    }

    #[tokio::test]
    async fn tree_projection_routes_return_sidebar_page_and_file_projection_kinds() {
        let cases = [
            (
                "/api/tree/projections/sidebar?workspaceId=ws_demo&rootNodeId=page_root",
                "sidebar_tree",
            ),
            (
                "/api/tree/projections/page?workspaceId=ws_demo&rootNodeId=page_root",
                "page_tree",
            ),
            (
                "/api/tree/projections/file?workspaceId=ws_demo&rootNodeId=page_root",
                "file_tree",
            ),
        ];

        for (uri, expected_projection_kind) in cases {
            let response = app()
                .oneshot(
                    Request::builder()
                        .uri(uri)
                        .body(Body::empty())
                        .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["result"]["projection"], expected_projection_kind);
            assert_eq!(
                payload["result"]["items"][0]["projectionKind"],
                expected_projection_kind
            );
        }
    }

    #[tokio::test]
    async fn tree_projection_routes_keep_ok_response_shape() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/api/tree/projections/page?workspaceId=ws_demo&rootNodeId=page_root")
                    .body(Body::empty())
                    .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["ok"], true);
        assert_eq!(payload["result"]["projection"], "page_tree");
        assert_eq!(payload["result"]["rootNodeId"], "page_root");
    }

    #[tokio::test]
    async fn file_tree_projection_includes_index_and_asset_rows() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/api/tree/projections/file?workspaceId=ws_demo&rootNodeId=page_root")
                    .body(Body::empty())
                    .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");
        let items = payload["result"]["items"].as_array().expect("items");
        let row_kinds = items
            .iter()
            .filter_map(|item| item["rowKind"].as_str())
            .collect::<Vec<_>>();
        let resource_kinds = items
            .iter()
            .filter_map(|item| item["resourceMeta"]["resourceKind"].as_str())
            .collect::<Vec<_>>();

        assert!(row_kinds.contains(&"document"));
        assert!(row_kinds.contains(&"index"));
        assert!(row_kinds.contains(&"asset"));
        assert!(row_kinds.contains(&"asset_folder"));
        assert!(resource_kinds.contains(&"document"));
        assert!(resource_kinds.contains(&"index"));
        assert!(resource_kinds.contains(&"asset"));
        assert!(resource_kinds.contains(&"mindmap"));
        assert!(resource_kinds.contains(&"table"));
        assert_eq!(items[0]["nodeId"], "page_root");
        assert_eq!(items[1]["nodeId"], "index:page_root");
    }

    #[tokio::test]
    async fn file_tree_projection_resource_variants_keep_icon_hint_and_capabilities() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/api/tree/projections/file?workspaceId=ws_demo&rootNodeId=page_root")
                    .body(Body::empty())
                    .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");
        let item_by_row_id = payload["result"]["items"]
            .as_array()
            .expect("items")
            .iter()
            .filter_map(|item| item["rowId"].as_str().map(|row_id| (row_id, item)))
            .collect::<std::collections::BTreeMap<_, _>>();

        assert_eq!(item_by_row_id["asset:asset_pdf_1"]["iconHint"], "pdf");
        assert_eq!(item_by_row_id["asset:asset_book_1"]["iconHint"], "book");
        assert_eq!(item_by_row_id["asset:table_1"]["iconHint"], "table");
        assert_eq!(item_by_row_id["asset-folder:mind_1"]["iconHint"], "mindmap");
        assert_eq!(
            item_by_row_id["asset-folder:mind_1"]["resourceMeta"]["resourceKind"],
            "mindmap"
        );
        assert!(item_by_row_id["asset-folder:mind_1"]["capabilities"]
            .as_array()
            .expect("capabilities")
            .iter()
            .any(|value| value == "expand"));
    }
}
