"""Application factory.

One Python process serves the API and the pre-built SPA (react.md). Node exists
only in the Docker build stage.
"""

from __future__ import annotations

import logging
from pathlib import Path

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

from app.config import get_settings
from app.routers import catalog, exports, media, meta, tickets, transcription

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
logger = logging.getLogger("coachpapa")

STATIC_DIR = Path(__file__).resolve().parent.parent / "static"

settings = get_settings()

app = FastAPI(
    title="coachpapa",
    version=settings.version,
    description="Capture-first maintenance ticketing.",
)

app.include_router(meta.router)
app.include_router(catalog.router)
app.include_router(tickets.router)
app.include_router(media.router)
app.include_router(exports.router)
app.include_router(transcription.router)


@app.exception_handler(Exception)
async def unhandled_error(request: Request, exc: Exception) -> JSONResponse:
    """Structured errors, and nothing leaked into them (coding.md)."""
    logger.exception("unhandled error on %s %s", request.method, request.url.path)
    return JSONResponse(
        status_code=500,
        content={
            "error_code": "INTERNAL_ERROR",
            "summary": "Something went wrong handling that request.",
            "details": None,
        },
    )


if STATIC_DIR.is_dir():
    app.mount(
        "/assets",
        StaticFiles(directory=STATIC_DIR / "assets"),
        name="assets",
    )

    @app.get("/{full_path:path}", include_in_schema=False)
    def spa(full_path: str) -> FileResponse:
        """Serve the SPA shell for any non-API path (client-side routing).

        Static files here are the built frontend only. Uploaded media is NOT
        served from this mount - it goes through the authorized /api/media
        route (agents.md).
        """
        if full_path:
            root = STATIC_DIR.resolve()
            candidate = (root / full_path).resolve()
            # Containment check: never serve anything outside the build output,
            # whatever the request path claims. Starlette already normalises
            # '..', so this is defence in depth rather than the only guard.
            if candidate.is_relative_to(root) and candidate.is_file():
                return FileResponse(candidate)
        return FileResponse(STATIC_DIR / "index.html")
