"""Volleyboard — the one Python process (react.md).

FastAPI serves the JSON API and the pre-built Vite SPA (StaticFiles)
behind Caddy on 172.17.0.1:3131. Node exists only in the Docker build
stage. create_app() is a factory (mirrors /srv/apps/with) so tests can
build an app per-case — which is what lets the production boot guard be
TESTED, not just believed.
"""
from __future__ import annotations

import asyncio
import logging
import os
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from starlette.responses import FileResponse, JSONResponse

from . import __version__, events
from .auth import AuthMiddleware
from .routers import attachments as attachments_router
from .routers import auth as auth_router
from .routers import autopass as autopass_router
from .routers import board as board_router
from .routers import conversation as conversation_router
from .routers import events as events_router
from .routers import me as me_router
from .routers import meta as meta_router
from .routers import notifications as notifications_router
from .routers import rally as rally_router
from .routers import view_as as view_as_router
from .services.auth import config as auth_config

log = logging.getLogger("volleyboard.app")


def create_app() -> FastAPI:
    # The AUTH_DEV_USER stub refuses production (mirrors with's T-AZ-043):
    # die loudly at boot rather than serve with an identity backdoor.
    auth_config.enforce_dev_stub_policy()
    if auth_config.dev_user():
        log.warning(
            "AUTH_DEV_USER=%r — dev identity stub ACTIVE (APP_ENV=%s). "
            "Every request resolves to this identity. Local testing only.",
            auth_config.dev_user(),
            auth_config.app_env(),
        )

    @asynccontextmanager
    async def lifespan(app: FastAPI):
        # The SSE bus publishes from sync routes via this captured loop.
        events.set_loop(asyncio.get_running_loop())
        yield

    app = FastAPI(title="Volleyboard", version=__version__, lifespan=lifespan)

    # Default-deny auth wraps everything (added last = outermost).
    app.add_middleware(AuthMiddleware)

    app.include_router(auth_router.router)
    app.include_router(me_router.router)
    app.include_router(view_as_router.router)
    app.include_router(events_router.router)
    app.include_router(rally_router.router)
    app.include_router(meta_router.router)
    app.include_router(board_router.router)
    app.include_router(conversation_router.router)
    app.include_router(attachments_router.router)
    app.include_router(notifications_router.router)
    app.include_router(autopass_router.router)

    @app.get("/health")
    def health() -> dict:
        return {"status": "ok"}

    @app.get("/api/version")
    def version() -> dict:
        # The visible version confirms a deploy landed (coding.md).
        return {"version": __version__}

    # -----------------------------------------------------------------
    # Static SPA. In the built image the Vite output lives at /app/static;
    # in local dev it may not exist yet, so mounting is guarded. The
    # /assets + /sounds prefixes are public (middleware allowlist); the
    # shell itself is behind the session gate — an unauthenticated page
    # hit 302s into /auth/login before routing ever reaches the catch-all.
    # -----------------------------------------------------------------
    static_dir = os.environ.get("STATIC_DIR", "/app/static")

    if os.path.isdir(static_dir):
        assets = os.path.join(static_dir, "assets")
        if os.path.isdir(assets):
            app.mount("/assets", StaticFiles(directory=assets), name="assets")
        sounds = os.path.join(static_dir, "sounds")
        if os.path.isdir(sounds):
            app.mount("/sounds", StaticFiles(directory=sounds), name="sounds")

        @app.get("/{full_path:path}", include_in_schema=False)
        def spa(full_path: str):
            # SPA client-side routing: serve index.html for any non-API path.
            index = os.path.join(static_dir, "index.html")
            if os.path.isfile(index):
                return FileResponse(index)
            return JSONResponse({"error_code": "SPA_NOT_BUILT"}, status_code=404)

    return app


app = create_app()
