"""Scout — one Python process serving the API and the pre-built SPA.

Node exists only in the image build; nothing here needs it at runtime.
"""

import logging
import os
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI, Response
from fastapi.responses import FileResponse, PlainTextResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware

from app.config import DEV_SESSION_SECRET, get_settings
from app.db import SessionLocal
from app.middleware import AuthMiddleware
from app.routers import (
    accounts,
    auth,
    auth_admin,
    levels_admin,
    materials,
    meta,
    options,
    projects,
    results,
    review,
    view_as,
)
from app.services import accounts as accounts_service
from app.services import levels as levels_service
from app.version import VERSION

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

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


@asynccontextmanager
async def lifespan(_app: FastAPI):
    settings = get_settings()
    if settings.is_production and settings.scout_session_secret == DEV_SESSION_SECRET:
        raise RuntimeError(
            "SCOUT_SESSION_SECRET is still the development default. Set a real value "
            "in .app.env before running in production."
        )
    # The accounts kit must be wired before anything asks a permission question.
    levels_service.init_accounts_kit()

    if settings.initial_admins:
        with SessionLocal() as db:
            seeded = accounts_service.seed_initial_admins(db, settings.initial_admins)
        if seeded:
            log.info("seeded bootstrap admin level for: %s", ", ".join(seeded))
    if not settings.has_bw_client:
        log.warning("BW Auth is not configured — sign-in is unavailable until it is.")
    else:
        # Publish the project catalog and everyone's access for the central
        # console. Display-only, best-effort, and never fatal.
        levels_service.sync_all()
    yield


app = FastAPI(
    title="Scout",
    version=VERSION,
    description="Website design-direction scouting for client projects.",
    lifespan=lifespan,
    docs_url=None,
    redoc_url=None,
)

# Middleware order matters: the LAST one added runs FIRST. SessionMiddleware must
# run before AuthMiddleware, because AuthMiddleware reads request.session.
app.add_middleware(AuthMiddleware)
app.add_middleware(
    SessionMiddleware,
    secret_key=get_settings().scout_session_secret,
    session_cookie="scout_session",
    same_site="lax",   # blocks cross-site POSTs from carrying the session
    https_only=True,   # the app is only ever reached over TLS through Caddy
    max_age=14 * 24 * 3600,
)

app.include_router(meta.router)
app.include_router(projects.router)
app.include_router(accounts.router)
app.include_router(auth.router)
app.include_router(options.router)
app.include_router(review.router)
app.include_router(results.router)
app.include_router(materials.router)
app.include_router(view_as.router)
app.include_router(levels_admin.router)
app.include_router(auth_admin.router)


@app.get("/healthz", include_in_schema=False)
def healthz() -> PlainTextResponse:
    return PlainTextResponse("ok")


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


# response_model=None: the return type is a union of Response subclasses, which
# FastAPI would otherwise try to turn into a Pydantic response model and reject.
# HEAD is accepted alongside GET so link checkers and health probes don't see 405.
@app.api_route("/{path:path}", methods=["GET", "HEAD"], include_in_schema=False, response_model=None)
def spa(path: str) -> Response:
    """Serve the SPA shell for any route the API did not claim, so a deep link or
    a refresh lands on the right screen. The shell carries no data — it renders
    the sign-in screen until /api/me says otherwise."""
    static_file = (STATIC_DIR / path).resolve() if path else None
    if static_file and static_file.is_file() and STATIC_DIR in static_file.parents:
        return FileResponse(static_file)
    if INDEX_HTML.is_file():
        return FileResponse(INDEX_HTML)
    return PlainTextResponse(
        "Scout frontend is not built. Run `npm ci && npm run build` in main/frontend.",
        status_code=503,
    )


if __name__ == "__main__":  # pragma: no cover - local convenience only
    import uvicorn

    uvicorn.run(app, host="127.0.0.1", port=int(os.environ.get("PORT", "3000")))
