"""FastAPI application entrypoint."""
import logging
from pathlib import Path

from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from sqlalchemy import text
from sqlalchemy.exc import IntegrityError
from starlette.exceptions import HTTPException
from starlette.staticfiles import StaticFiles

from app.api.deps import client_ip
from app.api.v1.router import api_router
from app.core.config import settings
from app.core.ratelimit import SlidingWindowLimiter
from app.core.time import now_local
from app.db.session import engine

logging.basicConfig(
    level=logging.DEBUG if settings.DEBUG else logging.INFO,
    format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
)
logger = logging.getLogger("isp")

app = FastAPI(
    title=settings.APP_NAME,
    version="0.1.0",
    docs_url="/docs",
    openapi_url="/openapi.json",
    description="Self-hosted ISP management system — applications, equipment, complaints.",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.exception_handler(RequestValidationError)
async def validation_handler(_: Request, exc: RequestValidationError) -> JSONResponse:
    """Flatten pydantic errors into a form the UI can show next to fields."""
    errors = [
        {"field": ".".join(str(p) for p in err["loc"][1:]) or "body", "message": err["msg"]}
        for err in exc.errors()
    ]
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content={"detail": "Validation failed", "errors": errors},
    )


@app.exception_handler(IntegrityError)
async def integrity_handler(_: Request, exc: IntegrityError) -> JSONResponse:
    """A DB constraint fired — surface a clean 409 instead of a 500."""
    logger.warning("IntegrityError: %s", exc.orig)
    return JSONResponse(
        status_code=status.HTTP_409_CONFLICT,
        content={"detail": "The operation conflicts with existing data"},
    )


@app.get("/health", tags=["system"])
def health() -> dict[str, object]:
    """Liveness + DB connectivity, for Compose healthchecks and nginx."""
    db_ok = True
    try:
        with engine.connect() as conn:
            conn.execute(text("SELECT 1"))
    except Exception as exc:  # pragma: no cover
        logger.error("Health check DB failure: %s", exc)
        db_ok = False

    return {
        "status": "ok" if db_ok else "degraded",
        "database": "up" if db_ok else "down",
        "server_time": now_local().isoformat(),
        "timezone": settings.TIMEZONE,
        "environment": settings.ENV,
    }


app.include_router(api_router, prefix=settings.API_V1_PREFIX)


# --- Single-process deployments (cPanel / Passenger) ---------------------------
# With nginx absent, the app owns the headers and the auth rate limits itself.


@app.middleware("http")
async def security_headers(request: Request, call_next):
    response = await call_next(request)
    response.headers.setdefault("X-Content-Type-Options", "nosniff")
    response.headers.setdefault("X-Frame-Options", "SAMEORIGIN")
    response.headers.setdefault("Referrer-Policy", "same-origin")
    return response


_auth_limiter = SlidingWindowLimiter(
    limit=settings.AUTH_RATE_LIMIT_PER_MINUTE,
    burst=settings.AUTH_RATE_LIMIT_BURST,
)
_AUTH_PATHS = {
    f"{settings.API_V1_PREFIX}/auth/login",
    f"{settings.API_V1_PREFIX}/auth/refresh",
}


@app.middleware("http")
async def auth_rate_limit(request: Request, call_next):
    # Production only: under Docker the edge proxy owns this and there is no edge
    # in local dev, where tests and hot-reload would otherwise trip it constantly.
    if (
        settings.ENV == "production"
        and request.method == "POST"
        and request.scope.get("path") in _AUTH_PATHS
    ):
        ip = client_ip(request)
        # Without a usable client address every caller would share one bucket.
        if ip is not None and not _auth_limiter.allow(ip):
            return JSONResponse(
                status_code=status.HTTP_429_TOO_MANY_REQUESTS,
                content={"detail": "Too many attempts. Please wait a minute and try again."},
            )
    return await call_next(request)


class SPAStaticFiles(StaticFiles):
    """Serve the built SPA; unmatched page routes fall back to index.html.

    API paths and requests for missing asset files keep their real 404s so the
    client never receives HTML where it expects JSON or JavaScript.
    """

    async def get_response(self, path: str, scope):
        try:
            return await super().get_response(path, scope)
        except HTTPException as exc:
            if exc.status_code != 404:
                raise
            last_segment = path.rsplit("/", 1)[-1]
            if path.startswith("api/") or "." in last_segment:
                raise
            return await super().get_response("index.html", scope)


if settings.STATIC_DIR:
    _static_dir = Path(settings.STATIC_DIR).resolve()
    if _static_dir.is_dir():
        app.mount("/", SPAStaticFiles(directory=_static_dir, html=True, check_dir=True), name="spa")
