use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::gateway::default_workspace_name_for_context;
use crate::routes::web_shell::{
    build_page_aggregate_snapshot, load_file_tree_html, load_sidebar_tree_html,
    load_workspace_shell_projection, render_local_file_tree_html, render_local_sidebar_tree_html,
};
use crate::ssr::pages::mindmap::MindmapPage;
use crate::workspace_shell::render_workspace_shell_sidebar_html;
use axum::extract::{Extension, Path, Query, State};
use axum::http::{HeaderMap, HeaderName, HeaderValue};
use axum::response::{Html, IntoResponse, Response};
use serde::Deserialize;
use serde_json::json;

const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
const HEADER_MNOTE_WEB_SHELL: &str = "x-mnote-web-shell";

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

pub async fn mindmap_object_shell(
    State(state): State<AppState>,
    Extension(context): Extension<RequestContext>,
    Path((doc_id, mindmap_id)): Path<(String, String)>,
    Query(query): Query<MindmapShellQuery>,
) -> Result<Response, WebError> {
    let default_workspace_name = default_workspace_name_for_context(&state, &context);
    let source_kind = query.source_kind.as_deref();
    let root_uri = query.root_uri.as_deref();
    let aggregate = build_page_aggregate_snapshot(
        &state,
        &context,
        &doc_id,
        query.workspace_id.as_deref(),
        source_kind,
        root_uri,
    )
    .await
    .ok();
    let workspace_id = aggregate
        .as_ref()
        .map(|value| value.identity.workspace_id.clone())
        .filter(|value| !value.trim().is_empty());
    let title = aggregate
        .as_ref()
        .map(|value| value.head.title.trim().to_string())
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| "思维导图".to_string());
    let is_local_folder = source_kind.map(str::trim) == Some("local_folder");
    let (workspace_name, sidebar_tree_html, workspace_sidebar_html) = if is_local_folder {
        let root_uri = root_uri.unwrap_or_default();
        let sidebar_tree_html =
            render_local_sidebar_tree_html(root_uri, Some(&doc_id)).unwrap_or_default();
        let file_tree_html =
            render_local_file_tree_html(root_uri, Some(&doc_id), None).unwrap_or_default();
        let workspace_projection = load_workspace_shell_projection(
            Some(&state),
            state.config(),
            &context,
            workspace_id.as_deref().unwrap_or("local-folder"),
            Some(&doc_id),
            &default_workspace_name,
        )
        .await;
        let workspace_name = workspace_projection.workspace_name.clone();
        let workspace_sidebar_html = render_workspace_shell_sidebar_html(
            &workspace_projection,
            Some(sidebar_tree_html.as_str()),
            Some(file_tree_html.as_str()),
            None,
            None,
        );
        (
            Some(workspace_name),
            Some(sidebar_tree_html),
            Some(workspace_sidebar_html),
        )
    } else if let Some(workspace_id) = workspace_id.as_deref() {
        let workspace_projection = load_workspace_shell_projection(
            Some(&state),
            state.config(),
            &context,
            workspace_id,
            Some(&doc_id),
            &default_workspace_name,
        )
        .await;
        let sidebar_tree_html =
            load_sidebar_tree_html(state.config(), &context, workspace_id, Some(&doc_id))
                .await
                .unwrap_or_default();
        let active_filetree_row_id = format!("asset:{mindmap_id}");
        let file_tree_html = load_file_tree_html(
            state.config(),
            &context,
            workspace_id,
            Some(&doc_id),
            Some(active_filetree_row_id.as_str()),
        )
        .await
        .unwrap_or_default();
        let workspace_name = workspace_projection.workspace_name.clone();
        let workspace_sidebar_html = render_workspace_shell_sidebar_html(
            &workspace_projection,
            Some(sidebar_tree_html.as_str()),
            Some(file_tree_html.as_str()),
            None,
            None,
        );
        (
            Some(workspace_name),
            Some(sidebar_tree_html),
            Some(workspace_sidebar_html),
        )
    } else {
        (None, None, None)
    };
    let editor_bootstrap = json!({
        "documentId": format!("__mindmap_object__:{doc_id}:{mindmap_id}"),
        "workspaceId": workspace_id
            .as_ref()
            .map(|value| format!("__mindmap_object__:{value}")),
        "title": title.clone(),
        "content": {
            "type": "doc",
            "content": [
                {
                    "type": "paragraph",
                    "attrs": {
                        "mindmapId": mindmap_id,
                        "mnoteBlockType": "mindmap",
                        "projectionVersion": 1,
                        "rootNodeId": "root",
                        "mnoteMindmapData": serde_json::Value::Null
                    }
                }
            ]
        },
        "readOnly": false,
        "editable": true,
        "standaloneObject": {
            "kind": "mindmap",
            "documentId": doc_id,
            "mindmapId": mindmap_id
        },
        "sourceKind": source_kind.unwrap_or("local_folder"),
        "rootUri": root_uri.unwrap_or(""),
        "revision": serde_json::Value::Null,
        "conflictDetectionKey": serde_json::Value::Null,
        "pageOptions": {
            "pageWidth": "full",
            "smallText": false,
            "showHeadingNumbers": false,
            "fontFamily": "sans"
        }
    });
    let contract = json!({
        "schema": "mnote.mindmap_shell.v1",
        "owner": "mnote-web",
        "shell": "mindmap",
        "documentId": doc_id,
        "mindmapId": mindmap_id,
        "sourceKind": source_kind.unwrap_or("local_folder"),
        "rootUri": root_uri.unwrap_or(""),
        "projection": {
            "schema": "mnote.mindmap.simple_mind_map_scene.v1",
            "runtime": "simple-mind-map",
            "source": "compat-blob",
            "owner": "rust-kernel",
            "queryName": "mindmap.simple_mind_map_scene.get"
        },
        "island": {
            "kind": "leptos_mindmap_adapter",
            "mountId": "mnote-mindmap-island",
            "runtimeRole": "simple_mind_map_adapter",
            "commandName": "mindmap.command.apply"
        },
        "requestId": context.trace.request_id,
        "traceId": context.trace.trace_id
    });
    let editor_bootstrap_json =
        serde_json::to_string(&editor_bootstrap).unwrap_or_else(|_| "null".to_string());
    let contract_json = serde_json::to_string(&contract).unwrap_or_else(|_| "null".to_string());
    let body_content = crate::ssr::render_view(leptos::view! {
        <MindmapPage
            document_id={doc_id.clone()}
            mindmap_id={mindmap_id.clone()}
            title={title.clone()}
            sidebar_tree_html={sidebar_tree_html.unwrap_or_default()}
            workspace_name={workspace_name.unwrap_or_default()}
            workspace_sidebar_html={workspace_sidebar_html.unwrap_or_default()}
        />
    });
    let html = format!(
        r#"<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <title>{}</title>
    <style>{}</style>
  </head>
  <body data-mnote-web-owner="mnote-web" data-mnote-shell="mindmap" data-document-id="{}" data-mindmap-id="{}" data-mnote-source-kind="{}" data-mnote-root-uri="{}">
    {}
    <script id="__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__" type="application/json">{}</script>
    <script id="__MNOTE_MINDMAP_SHELL__" type="application/json">{}</script>
    {}
  </body>
</html>"#,
        escape_html(&title),
        crate::ssr::MNOTE_CSS,
        escape_html(&doc_id),
        escape_html(&mindmap_id),
        escape_html(source_kind.unwrap_or("local_folder")),
        escape_html(root_uri.unwrap_or("")),
        body_content,
        escape_script_json(&editor_bootstrap_json),
        escape_script_json(&contract_json),
        render_mindmap_standalone_bootstrap_script(),
    );
    let mut response = Html(html).into_response();
    stamp_shell_headers(response.headers_mut(), "mindmap");
    Ok(response)
}

fn stamp_shell_headers(headers: &mut HeaderMap, shell: &'static str) {
    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_MNOTE_WEB_SHELL.as_bytes()) {
        headers.insert(name, HeaderValue::from_static(shell));
    }
}

fn escape_html(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

fn escape_script_json(value: &str) -> String {
    value.replace("</script", "<\\/script")
}

fn render_mindmap_standalone_bootstrap_script() -> String {
    r#"<script type="module">
(() => {
  const DEV_HOT_BUSTER = "__MNOTE_DEV_HOT_BUSTER__";
  const BOOTSTRAP_ID = '__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__';
  const MOUNT_ID = 'mnote-mindmap-island';

  const withDevHot = (path) => {
    const url = new URL(path, window.location.origin);
    if (DEV_HOT_BUSTER) url.searchParams.set('devHot', DEV_HOT_BUSTER);
    return url.toString();
  };

  const parseJsonScript = (id) => {
    const node = document.getElementById(id);
    if (!node) return null;
    try {
      return JSON.parse(node.textContent || 'null');
    } catch (error) {
      console.warn(`mnote mindmap bootstrap JSON 解析失败: ${id}`, error);
      return null;
    }
  };

  const loadRuntime = async () => {
    if (window.__mnoteLeptosTiptapRuntimePromise) {
      return window.__mnoteLeptosTiptapRuntimePromise;
    }
    window.__mnoteLeptosTiptapRuntimePromise = (async () => {
      const manifestResponse = await fetch(withDevHot('/api/leptos-tiptap-runtime/manifest.json'));
      if (!manifestResponse.ok) throw new Error(`manifest_failed_${manifestResponse.status}`);
      const manifest = await manifestResponse.json();
      if (!manifest.entryAssetPath) throw new Error('island manifest 缺少 entryAssetPath');
      const entryUrl = withDevHot(`/api/leptos-tiptap-runtime/${manifest.entryAssetPath}`);
      const wasmUrl = manifest.wasmAssetPath ? withDevHot(`/api/leptos-tiptap-runtime/${manifest.wasmAssetPath}`) : undefined;
      const runtime = await import(entryUrl);
      if (typeof runtime.default !== 'function' || typeof runtime.mount !== 'function' || typeof runtime.unmount !== 'function') {
        throw new Error('island runtime 导出不完整');
      }
      await runtime.default(wasmUrl);
      if (typeof runtime.mount_mindmap_shell === 'function' && typeof runtime.unmount_mindmap_shell === 'function') {
        window.__MNOTE_MINDMAP_RUST_SHELL__ = {
          mount: runtime.mount_mindmap_shell,
          unmount: runtime.unmount_mindmap_shell,
        };
      }
      return runtime;
    })();
    return window.__mnoteLeptosTiptapRuntimePromise;
  };

  const bootstrap = parseJsonScript(BOOTSTRAP_ID);
  const mountTarget = document.getElementById(MOUNT_ID);
  if (!bootstrap || !(mountTarget instanceof HTMLElement)) return;

  let mountId = null;
  let runtimeModule = null;

  const mountStandaloneMindmap = async () => {
    runtimeModule = await loadRuntime();
    mountId = runtimeModule.mount(mountTarget, bootstrap);
    mountTarget.setAttribute('data-runtime-mount-id', String(mountId));
  };

  void mountStandaloneMindmap().catch((error) => {
    console.error('mnote standalone mindmap mount failed', error);
    mountTarget.setAttribute('data-runtime-editor-status', 'error');
    mountTarget.setAttribute('data-runtime-editor-error', error instanceof Error ? error.message : 'unknown');
  });

  window.addEventListener('beforeunload', () => {
    if (mountId != null && runtimeModule && typeof runtimeModule.unmount === 'function') {
      try {
        runtimeModule.unmount(mountId);
      } catch (_error) {}
    }
  }, { once: true });
})();
</script>"#
    .replace(
        "__MNOTE_DEV_HOT_BUSTER__",
        crate::routes::dev_hot::dev_hot_cache_buster().unwrap_or(""),
    )
}

#[cfg(test)]
mod tests {
    use crate::app::{build_app, AppConfig, AppState};
    use axum::body::{to_bytes, Body};
    use axum::http::{Request, StatusCode};
    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,
            enable_editor_actor: true,
            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(
                serde_json::json!({
                    "sidebar:datasetList": {
                        "active_workspace_id": "ws_demo",
                        "documents": [
                            {
                                "id": "doc_1",
                                "workspace_id": "ws_demo",
                                "title": "页面",
                                "parent_id": null,
                                "sort_order": 0
                            }
                        ],
                        "media_assets": [],
                        "mindmap_assets": [
                            {
                                "id": "mind_1",
                                "workspace_id": "ws_demo",
                                "document_id": "doc_1",
                                "asset_type": "mindmap",
                                "file_name": "思维导图.json",
                                "mime_type": "application/json"
                            }
                        ],
                        "table_assets": [],
                        "trashed_documents": [],
                        "trashed_media_assets": [],
                        "trashed_mindmap_assets": [],
                        "trashed_table_assets": [],
                        "mindmap_docs": [],
                        "mindmap_asset_children": {}
                    },
                    "documents:getMeta": {
                        "id": "doc_1",
                        "workspace_id": "ws_demo",
                        "title": "页面"
                    },
                    "documents:getContent": {
                        "content": [],
                        "revision": 1,
                        "conflict_detection_key": "doc_1:1",
                        "pageSubtree": {"rootNodeId": "doc_1", "outline": []}
                    }
                })
                .to_string(),
            ),
            mutation_fixtures_json: None,
            dev_user_id: "dev-user".into(),
            dev_user_name: "开发用户".into(),
            dev_user_email: "dev@mnote.local".into(),
        }))
    }

    fn app_with_mindmap_fixture() -> 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,
            enable_editor_actor: true,
            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(
                serde_json::json!({
                    "mindmaps:get": {
                        "ok": true,
                        "source": "compat-blob",
                        "revision": 7,
                        "data": {
                            "data": {"uid": "root", "text": "KMIND"},
                            "children": [
                                {"data": {"uid": "topic", "text": "二级节点"}, "children": []}
                            ]
                        },
                        "meta": {
                            "document_id": "doc_1",
                            "mindmap_id": "mind_1"
                        }
                    }
                })
                .to_string(),
            ),
            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 mindmap_shell_returns_rust_object_shell_contract() {
        let response = app()
            .oneshot(
                Request::builder()
                    .uri("/mindmap/doc_1/mind_1")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");

        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(
            response
                .headers()
                .get("x-mnote-web-owner")
                .and_then(|value| value.to_str().ok()),
            Some("mnote-web")
        );
        assert_eq!(
            response
                .headers()
                .get("x-mnote-web-shell")
                .and_then(|value| value.to_str().ok()),
            Some("mindmap")
        );
        let body = to_bytes(response.into_body(), usize::MAX)
            .await
            .expect("body");
        let html = String::from_utf8(body.to_vec()).expect("utf8");
        assert!(html.contains("mnote.mindmap_shell.v1"));
        assert!(html.contains("data-mnote-object-editor=\"mindmap\""));
        assert!(html.contains("data-mnote-object-identity=\"resource:mindmap:doc_1:mind_1\""));
        assert!(html.contains("data-leptos-mindmap-island=\"standalone\""));
        assert!(html.contains("mindmap.simple_mind_map_scene.get"));
        assert!(html.contains("mindmap.command.apply"));
        assert!(html.contains("__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__"));
        assert!(html.contains("mnoteBlockType"));
        assert!(html.contains("__mindmap_object__:doc_1:mind_1"));
        assert!(html.contains("\"standaloneObject\""));
        assert!(html.contains("\"documentId\":\"doc_1\""));
        assert!(html.contains("\"mindmapId\":\"mind_1\""));
        assert!(html.contains("data-row-id=\"asset:mind_1\""));
        assert!(html.contains("data-selected=\"true\""));
        assert!(!html.contains("data-row-id=\"doc:doc_1\" data-row-kind=\"document\" data-node-id=\"doc_1\" data-document-id=\"doc_1\" data-doc-id=\"doc_1\" data-asset-id=\"\" data-object-identity=\"{&quot;objectKind&quot;:&quot;page&quot;,&quot;documentId&quot;:&quot;doc_1&quot;,&quot;blockId&quot;:null,&quot;assetId&quot;:null}\" data-shell-mode=\"filetree\" data-selected=\"true\""));
        assert!(html.contains("runtimeModule.mount(mountTarget, bootstrap)"));
        assert!(html.contains("/api/leptos-tiptap-runtime/manifest.json"));
        assert!(!html.contains("react_mindmap_runtime"));
        assert!(!html.contains("next-app-router"));
    }

    #[test]
    fn mindmap_standalone_bootstrap_propagates_dev_hot_to_island_runtime() {
        let _guard = crate::test_support::hermes_env_lock()
            .lock()
            .expect("env lock");
        std::env::set_var("MNOTE_WEB_DEV_HOT_RELOAD", "1");
        let script = super::render_mindmap_standalone_bootstrap_script();
        std::env::remove_var("MNOTE_WEB_DEV_HOT_RELOAD");

        assert!(script.contains("const DEV_HOT_BUSTER = \""));
        assert!(script.contains("fetch(withDevHot('/api/leptos-tiptap-runtime/manifest.json'))"));
        assert!(
            script.contains("withDevHot(`/api/leptos-tiptap-runtime/${manifest.entryAssetPath}`)")
        );
        assert!(
            script.contains("withDevHot(`/api/leptos-tiptap-runtime/${manifest.wasmAssetPath}`)")
        );
    }

    #[tokio::test]
    async fn mindmap_api_returns_same_adapter_contract_for_standalone_and_block() {
        let response = app_with_mindmap_fixture()
            .oneshot(
                Request::builder()
                    .uri("/api/mindmap/doc_1/mind_1?view=simple_mind_map_scene&queryName=mindmap.simple_mind_map_scene.get")
                    .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: serde_json::Value =
            serde_json::from_slice(&body).expect("adapter projection json");
        assert_eq!(
            payload["schema"],
            serde_json::json!("mnote.mindmap.simple_mind_map_scene.v1")
        );
        assert_eq!(payload["runtime"], serde_json::json!("simple-mind-map"));
        assert_eq!(payload["root"]["data"]["uid"], serde_json::json!("root"));
        assert_eq!(payload["root"]["data"]["text"], serde_json::json!("KMIND"));
        assert_eq!(payload["kernelRevision"], serde_json::json!(7));
        assert_eq!(
            payload["compatPayload"]["source"],
            serde_json::json!("compat-blob")
        );
    }
}
