"""Application FastAPI : routes, en-têtes de sécurité, contrôle d'origine, gestion d'erreurs sans fuite."""
from __future__ import annotations

import logging
import secrets
from pathlib import Path

from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import FileResponse, JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException

from .api import admin, auth, bilans, documents, imports, stats
from .api.deps import UNSAFE_METHODS, ApiError
from .core.config import get_config

log = logging.getLogger("edcf")
FONTS = Path(__file__).resolve().parent / "assets" / "fonts"
MAX_BODY = 30 * 1024 * 1024


def create_app() -> FastAPI:
    cfg = get_config()
    dev = cfg.env == "development"
    app = FastAPI(
        title="EDCF 52 — Bilan d'activité", version="1.0.0",
        docs_url="/api/docs" if dev else None, redoc_url=None, openapi_url="/api/openapi.json" if dev else None,
    )

    @app.middleware("http")
    async def security_middleware(request: Request, call_next):  # type: ignore[no-untyped-def]
        if request.method in UNSAFE_METHODS and request.url.path.startswith("/api/"):
            origin = request.headers.get("origin") or ""
            referer = request.headers.get("referer") or ""
            allowed = cfg.public_origin
            if origin:
                if origin.rstrip("/") != allowed:
                    return JSONResponse({"code": "bad_origin", "message": "Origine de la requête refusée."}, 403)
            elif not referer.startswith(allowed + "/"):
                return JSONResponse({"code": "bad_origin", "message": "Origine de la requête refusée."}, 403)
            length = request.headers.get("content-length")
            if length and length.isdigit() and int(length) > MAX_BODY:
                return JSONResponse({"code": "too_large", "message": "Requête trop volumineuse."}, 413)
        response = await call_next(request)
        h = response.headers
        h.setdefault("X-Content-Type-Options", "nosniff")
        h.setdefault("Referrer-Policy", "no-referrer")
        h.setdefault("X-Frame-Options", "SAMEORIGIN")
        h.setdefault("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()")
        h.setdefault("Cross-Origin-Opener-Policy", "same-origin")
        h.setdefault("Cross-Origin-Resource-Policy", "same-origin")
        h.setdefault("X-Robots-Tag", "noindex, nofollow")
        h.setdefault("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
        h.setdefault("Cache-Control", "no-store")
        if cfg.cookie_secure:
            h.setdefault("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        return response

    @app.exception_handler(ApiError)
    async def api_error(_: Request, exc: ApiError) -> JSONResponse:
        return JSONResponse({"code": exc.code, "message": exc.message, **exc.extra}, exc.status)

    @app.exception_handler(RequestValidationError)
    async def validation_error(_: Request, exc: RequestValidationError) -> JSONResponse:
        fields = [".".join(str(p) for p in e.get("loc", [])[1:]) for e in exc.errors()][:10]
        return JSONResponse({"code": "invalid_request", "message": "Données invalides.", "fields": fields}, 422)

    @app.exception_handler(StarletteHTTPException)
    async def http_error(_: Request, exc: StarletteHTTPException) -> JSONResponse:
        message = {404: "Ressource introuvable.", 405: "Méthode non autorisée."}.get(exc.status_code, "Erreur.")
        return JSONResponse({"code": f"http_{exc.status_code}", "message": message}, exc.status_code)

    @app.exception_handler(Exception)
    async def unexpected(request: Request, exc: Exception) -> JSONResponse:
        ref = secrets.token_hex(4)
        log.exception("Erreur interne [%s] %s %s", ref, request.method, request.url.path)
        return JSONResponse({"code": "internal", "message": f"Erreur interne (référence {ref}).",
                             **({"detail": repr(exc)} if dev else {})}, 500)

    @app.get("/api/health")
    def health() -> dict[str, bool]:
        return {"ok": True}

    @app.get("/api/assets/fonts/{name}")
    def font(name: str) -> FileResponse:
        path = (FONTS / name).resolve()
        if path.parent != FONTS or path.suffix != ".ttf" or not path.is_file():
            raise ApiError(404, "Police introuvable.", "not_found")
        return FileResponse(path, media_type="font/ttf", headers={"Cache-Control": "public, max-age=2592000"})

    for module in (auth, bilans, documents, admin, stats, imports):
        app.include_router(module.router)
    return app


app = create_app()
