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

Node exists only in the image build; nothing here needs it at runtime. The whole
account/level/View-As surface comes from the BW-Auth kit: `bw_admin_api.build_router`
mounts the admin API + the /api/me capability contract, and the app supplies only
identity, policy, and its own product routes.
"""

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 import (
    accounts, bw_auth, bw_client, bw_config, bw_view_as, identity, view_as_policy,
)
from app.bw_admin_api import build_router
from app import bw_accounts
from app.config import DEV_SESSION_SECRET, get_settings
from app.middleware import AuthMiddleware
from app.routers import app_routes, auth, meta
from app.version import VERSION

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

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


@asynccontextmanager
async def lifespan(_app: FastAPI):
    settings = get_settings()
    settings.data_dir.mkdir(parents=True, exist_ok=True)
    if settings.is_production and settings.session_secret == DEV_SESSION_SECRET:
        raise RuntimeError(
            "APP_SESSION_SECRET is still the development default. Set a real value "
            "in .app.env before running in production.")
    accounts.init_accounts_kit()   # wire storage + seed levels before any permission question
    if not settings.has_bw_client:
        log.warning("BW Auth is not configured — sign-in is unavailable until it is.")
    elif bw_config.HUB_REPORTING:
        try:
            bw_accounts.sync_reports(bw_auth)   # backfill central visibility, best-effort
        except Exception as exc:  # noqa: BLE001
            log.warning("initial sync_reports failed: %s", exc)
    yield


app = FastAPI(title=bw_config.APP_TITLE, version=VERSION, lifespan=lifespan,
              docs_url=None, redoc_url=None)

# Middleware order matters: the LAST added runs FIRST. SessionMiddleware must run
# before AuthMiddleware, which reads request.session.
app.add_middleware(AuthMiddleware)
app.add_middleware(
    SessionMiddleware,
    secret_key=get_settings().session_secret,
    session_cookie=get_settings().session_cookie,
    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=30 * 24 * 3600,
)

# The standard BW-Auth admin API + the /api/me capability contract, all from the
# kit. The app injects only identity, policy hooks, and (optionally) View-As.
_view_as_on = bw_config.VIEW_AS
app.include_router(build_router(
    bwa=bw_accounts,
    optional_user=identity.optional_user,
    real_user=identity.real_user,
    bw_auth=bw_auth,
    bw_view_as=(bw_view_as if _view_as_on else None),
    instance_obj=(accounts.instance_obj if bw_config.HAS_INSTANCES else None),
    session_of=((lambda req: req.session) if _view_as_on else None),
    view_as_policy=(view_as_policy.can_view_as if _view_as_on else None),
    rank_of=view_as_policy.rank_of,
    target_valid=view_as_policy.target_valid,
    start_hook=(view_as_policy.start if _view_as_on else None),
    stop_hook=(view_as_policy.stop if _view_as_on else None),
    account_url=bw_client.account_url(),
    prefix="/api/bw",
    audit=accounts.audit,
))

app.include_router(meta.router)
app.include_router(auth.router)
app.include_router(app_routes.router)


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


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


# response_model=None: the return is a union of Response subclasses. HEAD 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/bw/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(
        "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")))
