"""<<project_name>> MCP 服务装配 (transport wiring).

═══════════════════════════════════════════════════════════════════════════
  这个文件负责把 MCP 工具暴露成两种 HTTP 传输：
    - streamable-HTTP  挂载在 settings.mcp.mcp_path (默认 /mcp)
    - SSE              挂载在 settings.mcp.sse_path  (默认 /sse，可关闭)
  共享的 `mcp` 单例来自 app/mcp_instance.py（工具文件也从那里 import 并用装饰器
  注册）；本文件只负责"传输装配"的后端差异 (fastmcp vs official)。
  This file wires the MCP tools onto two HTTP transports (streamable-HTTP + SSE).
  The shared `mcp` singleton comes from app/mcp_instance.py (tool files import it
  from there too and register via the decorator); this file owns only the
  TRANSPORT-level backend differences.

★ 普通用户无需修改本文件 ★ —— 写工具请去 app/tools/ 下新建 .py 文件。
You normally never edit this file — add tools under app/tools/ instead.
═══════════════════════════════════════════════════════════════════════════
"""

import contextlib

from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route

from app.logging import get_logger
from app.mcp_instance import mcp
from app.settings import settings
from app.tools import register_tools

logger = get_logger(__name__)

# 在构建传输 app 之前，import 所有工具模块 —— 它们的 `@mcp.tool` 装饰器在被 import
# 时把工具注册到共享的 `mcp` 单例上。注册必须发生在构建 ASGI app 之前。
# Import all tool modules BEFORE building the transport apps: their `@mcp.tool`
# decorators register against the shared `mcp` singleton at import time.
register_tools()

<%if mcp_backend_is_official%>
# ── 后端：official (modelcontextprotocol/python-sdk v1) ──────────────────────
#    实例在 app/mcp_instance.py 用 json_response/stateless_http 构造 (v1 ctor)。
#
# 路径拥有一次：让每个传输子应用在其工厂里就拥有完整路径 (/mcp、/sse)，
# 随后把子应用的子路由"上提"到父应用，而不是用 Mount 前缀包一层。
# Path is owned ONCE — each transport sub-app owns its FULL path (/mcp, /sse) in
# its own factory; we then SPLICE the sub-app routes up into the parent rather
# than wrapping them under a Mount prefix.
mcp.settings.streamable_http_path = settings.mcp.mcp_path
mcp.settings.sse_path = settings.mcp.sse_path

streamable_app = mcp.streamable_http_app()
sse_app = mcp.sse_app()


@contextlib.asynccontextmanager
async def lifespan(app: Starlette):
    """进入 streamable 会话管理器的生命周期 (official v1)."""
    async with contextlib.AsyncExitStack() as stack:
        await stack.enter_async_context(mcp.session_manager.run())
        yield
<%-else%>
# ── 后端：fastmcp (默认 / default) ───────────────────────────────────────────
#    fastmcp 的构造器不接收 json_response/stateless_http (已移除)；
#    stateless_http 在 http_app() 工厂上传入。
#
# 路径拥有一次：让每个传输子应用在其工厂里就拥有完整路径 (/mcp、/sse)，
# 随后把子应用的子路由"上提"到父应用，而不是用 Mount 前缀包一层。
# Path is owned ONCE — each transport sub-app owns its FULL path (/mcp, /sse) in
# its own factory; we then SPLICE the sub-app routes up into the parent rather
# than wrapping them under a Mount prefix.
streamable_app = mcp.http_app(path=settings.mcp.mcp_path, stateless_http=settings.mcp.stateless)
sse_app = mcp.http_app(path=settings.mcp.sse_path, transport="sse")


@contextlib.asynccontextmanager
async def lifespan(app: Starlette):
    """组合两个子应用的生命周期 (fastmcp)."""
    async with contextlib.AsyncExitStack() as stack:
        await stack.enter_async_context(streamable_app.lifespan(app))
        if settings.mcp.sse_enabled:
            await stack.enter_async_context(sse_app.lifespan(app))
        yield
<%-endif%>


async def healthz(request: Request) -> JSONResponse:
    """健康检查端点 (liveness probe) —— GET /healthz."""
    return JSONResponse({"status": "ok"})


def _build_routes() -> list:
    """组装父应用路由：把各传输子应用的子路由直接上提，规避 Mount 前缀的 307 重定向.

    为什么不用 Mount(prefix, app)：Starlette 的 Mount("/mcp", ...) 内部正则是
    ``^/mcp/(?P<path>.*)$``，不匹配裸 ``/mcp``（无尾斜杠），于是对 ``POST /mcp``
    回 307 重定向到 ``/mcp/``——噪声大且部分客户端不喜欢 POST 重定向。改为把子应用
    的路由（它们本就以完整路径 /mcp、/sse 注册）直接拼进父应用的 routes，路径仍只
    被拥有一次，且裸 ``/mcp`` / ``/sse`` 直接命中处理器（200），不再产生 307。

    Why not Mount(prefix, app): Starlette's Mount("/mcp", ...) compiles to
    ``^/mcp/(?P<path>.*)$``, which does NOT match the bare ``/mcp`` (no trailing
    slash), so it 307-redirects ``POST /mcp`` to ``/mcp/`` — noisy, and some clients
    dislike POST redirects. Instead we splice the sub-apps' OWN routes (already
    registered at the full paths /mcp, /sse) straight into the parent's route list:
    the path is still owned exactly once, and bare ``/mcp`` / ``/sse`` reach the
    handler directly (200) with no 307.
    """
    routes: list = [Route("/healthz", healthz)]
    routes.extend(streamable_app.routes)
    if settings.mcp.sse_enabled:
        routes.extend(sse_app.routes)
    return routes

<%if auth_enabled%>
def _collect_middleware() -> list:
    # 子应用把 bearer 认证中间件挂在 app 级 middleware 上；_build_routes 只拼了 routes，
    # 必须把认证中间件也复制到父应用，否则 RequireAuthMiddleware 读不到 scope['user'] 会一律 401。
    # Re-attach the sub-apps' app-level auth middleware onto the parent
    # (routes were spliced, middleware was not).
    mw: list = []
    if settings.auth.enabled:
        seen = set()
        subs = (streamable_app, sse_app) if settings.mcp.sse_enabled else (streamable_app,)
        for sub in subs:
            for m in getattr(sub, "user_middleware", []):
                key = getattr(m, "cls", m)
                if key not in seen:
                    seen.add(key)
                    mw.append(m)
    return mw


# 父 Starlette 应用：main.py 以 "app.server:asgi_app" 运行它。
# Parent Starlette app — main.py serves it as "app.server:asgi_app".
_middleware = _collect_middleware() or None
asgi_app = Starlette(routes=_build_routes(), middleware=_middleware, lifespan=lifespan)
<%- else %>
# 父 Starlette 应用：main.py 以 "app.server:asgi_app" 运行它。
# Parent Starlette app — main.py serves it as "app.server:asgi_app".
asgi_app = Starlette(routes=_build_routes(), lifespan=lifespan)
<%- endif %>
