mod bridge;
pub(crate) mod command_support;
mod compat;
mod documents;
mod editor;
mod gateway;
mod health;
mod hermes;
mod hermes_client;
mod hermes_tools;
mod kernel;
mod local_folder_events;
mod local_folder_source;
mod local_markdown_parser;
mod media;
mod mindmap_api;
mod mindmap_shell;
mod onlyoffice;
mod query_support;
mod search;
mod session;
mod snapshot_support;
mod sse;
mod stream_support;
mod tree;
pub(crate) mod web_shell;
mod ws;

use crate::app::AppState;
use axum::routing::{any, get, post, put};
use axum::Router;

pub fn build_router(state: AppState) -> Router {
    let hermes_base_path = state.config().hermes_base_path.clone();
    let enable_debug_shell_routes = state.config().enable_debug_shell_routes;

    let mut router = Router::new()
        .route("/health", get(health::health))
        .route("/", get(gateway::root_entry))
        .route("/favicon.ico", get(gateway::favicon))
        .route("/auth", get(gateway::auth_entry).post(gateway::auth_entry))
        .route("/search", get(search::shell))
        .route(
            "/mindmap/{doc_id}/{mindmap_id}",
            get(mindmap_shell::mindmap_object_shell),
        )
        .route(
            "/api/mindmap/{doc_id}/{mindmap_id}",
            get(mindmap_api::get_mindmap).post(mindmap_api::apply_mindmap_command),
        )
        .route(
            "/documents/{document_id}",
            get(web_shell::document_page_shell),
        )
        .route(
            "/api/page-aggregate/{document_id}",
            get(web_shell::page_aggregate),
        )
        .route(
            "/api/leptos-tiptap-runtime/manifest.json",
            get(web_shell::leptos_tiptap_manifest),
        )
        .route(
            "/api/leptos-tiptap-runtime/{*asset_path}",
            get(web_shell::leptos_tiptap_asset),
        )
        .route(
            "/api/editor/image-placeholder.svg",
            get(web_shell::editor_image_placeholder_asset),
        )
        .route("/api/search/documents", post(search::documents))
        .route("/api/gateway/health", get(gateway::gateway_health))
        .route("/api/runtime/config", get(session::runtime_config))
        .route("/api/auth", post(gateway::auth_api))
        .route("/api/auth/session", get(session::session))
        .route("/api/auth/whoami", get(session::session))
        .route("/api/auth/mnote-web-token", get(session::session))
        .route("/api/auth/session/refresh", post(session::refresh_session))
        .route("/api/ai-agent/run", post(compat::next_ai_agent_run))
        .route("/onlyoffice", get(onlyoffice::page))
        .route("/onlyoffice-server/{*path}", any(onlyoffice::server_proxy))
        .route("/cache/{*path}", any(onlyoffice::cache_proxy))
        .route("/api/onlyoffice/sign", post(onlyoffice::sign))
        .route("/api/onlyoffice/proxy", get(onlyoffice::proxy))
        .route("/api/onlyoffice/callback", post(onlyoffice::callback))
        .route("/api/onlyoffice/forcesave", post(onlyoffice::forcesave))
        .route("/api/media/upload", post(media::upload))
        .route("/api/media/sign", get(media::sign))
        .route(
            "/api/tree/filetree/upload-target-preflight",
            post(media::filetree_upload_target_preflight),
        )
        .route("/api/documents/meta", get(documents::meta))
        .route("/api/documents/content", get(documents::content))
        .route("/api/documents/purge", post(documents::purge))
        .route("/api/documents/title", post(documents::title))
        .route("/api/documents/options", post(documents::options))
        .route("/api/documents/save", post(documents::save))
        .route(
            "/api/documents/runtime/transform",
            post(editor::transform_runtime_snapshot),
        )
        .route(
            "/api/tree/projections/sidebar",
            get(kernel::project_tree_sidebar),
        )
        .route("/api/tree/projections/page", get(kernel::project_tree_page))
        .route("/api/tree/projections/file", get(kernel::project_tree_file))
        .route(
            "/api/kernel/projections/sidebar",
            get(kernel::project_sidebar),
        )
        .route("/api/kernel/subtree", get(kernel::subtree))
        .route("/api/kernel/edges", get(kernel::edges))
        .route("/api/kernel/graph", get(kernel::graph))
        .route("/api/tree/commands", post(tree::tree_command))
        .route(
            "/api/tree/local-folder-watch",
            get(tree::local_folder_watch),
        )
        .route(
            "/api/local-folder/events",
            get(local_folder_events::local_folder_events),
        )
        .route(
            "/api/tree/runtime/reduce",
            post(tree::reduce_tree_shell_runtime),
        )
        .route("/api/bridge/workspace", get(bridge::workspace))
        .route("/api/bridge/request", get(bridge::request))
        .route("/api/bridge/trace", get(bridge::trace))
        .route("/api/tree/events", get(sse::tree_events))
        .route("/api/stream/events", get(sse::events))
        .route("/api/realtime/ws", get(ws::socket))
        .nest(
            &hermes_base_path,
            Router::new()
                .route("/health", get(hermes::health))
                .route("/bridge", post(hermes::bridge_runtime))
                .route(
                    "/client/sessions",
                    get(hermes_client::list_sessions).post(hermes_client::create_session),
                )
                .route(
                    "/client/sessions/{session_id}",
                    get(hermes_client::get_session),
                )
                .route("/client/profiles", get(hermes_client::list_profiles))
                .route(
                    "/client/profiles/active",
                    put(hermes_client::switch_active_profile),
                )
                .route(
                    "/client/profiles/{profile_name}",
                    get(hermes_client::get_profile),
                )
                .route(
                    "/client/profile-memory",
                    get(hermes_client::get_profile_memory)
                        .post(hermes_client::save_profile_memory),
                )
                .route(
                    "/client/skills",
                    get(hermes_client::list_skills),
                )
                .route(
                    "/client/skills/toggle",
                    put(hermes_client::toggle_skill),
                )
                .route("/client/runs", post(hermes_client::create_run))
                .route("/client/events/{run_id}", get(hermes_client::stream_events))
                .route(
                    "/client/runs/{run_id}/abort",
                    post(hermes_client::abort_run),
                )
                .route("/client/models", get(hermes_client::list_models))
                .route("/client/tools", get(hermes_client::list_tools)),
        )
        .nest(
            "/api/hermes/tools",
            Router::new()
                .route("/mnote/manifest", get(hermes_tools::mnote_manifest))
                .route("/mnote/call", post(hermes_tools::mnote_call))
                .route("/mnote/audit", get(hermes_tools::mnote_audit)),
        );

    if enable_debug_shell_routes {
        router = router
            .route("/tree", get(tree::tree_shell))
            .route("/document-debug", get(editor::document_editor_shell));
    }

    router.with_state(state)
}

#[cfg(test)]
mod tests {
    use super::build_router;
    use crate::app::{AppConfig, AppState};
    use axum::body::Body;
    use axum::http::{Request, StatusCode};
    use axum::Router;
    use tower::ServiceExt;

    fn app(enable_debug_shell_routes: bool) -> Router {
        build_router(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: false,
            enable_debug_shell_routes,
            hermes_base_path: "/api/hermes".into(),
            compat_next_base_path: "/api/compat/next".into(),
            convex_url: None,
            convex_admin_key: None,
            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(),
        }))
    }

    #[tokio::test]
    async fn debug_shell_routes_are_not_mounted_by_default() {
        for path in ["/tree", "/document-debug"] {
            let response = app(false)
                .oneshot(
                    Request::builder()
                        .uri(path)
                        .body(Body::empty())
                        .expect("request"),
                )
                .await
                .expect("response");
            assert_eq!(
                response.status(),
                StatusCode::NOT_FOUND,
                "{path} 默认不应暴露"
            );
        }
    }

    #[tokio::test]
    async fn debug_shell_routes_are_only_available_when_explicitly_enabled() {
        let tree_response = app(true)
            .oneshot(
                Request::builder()
                    .uri("/tree")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");
        assert_ne!(
            tree_response.status(),
            StatusCode::NOT_FOUND,
            "/tree 显式启用 debug gate 后不应继续返回 404",
        );

        let document_debug_response = app(true)
            .oneshot(
                Request::builder()
                    .uri("/document-debug")
                    .body(Body::empty())
                    .expect("request"),
            )
            .await
            .expect("response");
        assert_ne!(
            document_debug_response.status(),
            StatusCode::NOT_FOUND,
            "/document-debug 显式启用 debug gate 后不应继续返回 404",
        );
    }

    #[tokio::test]
    async fn onlyoffice_and_media_transport_routes_are_explicitly_mounted() {
        for (method, path) in [
            ("GET", "/onlyoffice"),
            ("POST", "/api/onlyoffice/sign"),
            ("GET", "/api/onlyoffice/proxy?u=x"),
            ("POST", "/api/onlyoffice/callback"),
            ("POST", "/api/onlyoffice/forcesave"),
            ("POST", "/api/media/upload"),
            ("GET", "/api/media/sign?assetId=x"),
            ("POST", "/api/tree/filetree/upload-target-preflight"),
            (
                "GET",
                "/onlyoffice-server/web-apps/apps/api/documents/api.js",
            ),
        ] {
            let response = app(false)
                .oneshot(
                    Request::builder()
                        .method(method)
                        .uri(path)
                        .body(Body::empty())
                        .expect("request"),
                )
                .await
                .expect("response");
            assert_ne!(
                response.status(),
                StatusCode::NOT_FOUND,
                "{method} {path} 应显式挂到 Rust 3000 单入口边界",
            );
        }
    }
}
