"""App factory — wiring only. No routes, no business logic, no schema here.

The built SPA is mounted last so /api and the auth flow take precedence;
unknown paths fall back to index.html for client-side routing.
"""

from __future__ import annotations

import logging
import os

from fastapi import FastAPI
from fastapi.responses import FileResponse
from starlette.staticfiles import StaticFiles

from .auth import AuthMiddleware
from .config import get_settings
from .errors import install_handlers
from .routers import account, auth, meta, spotify
from .version import APP_VERSION

# Uvicorn configures only its OWN loggers, leaving the root logger with no
# handler — so this app's log.info() calls fall through to logging.lastResort,
# which is WARNING-level and drops them silently. Every INFO diagnostic in this
# codebase (the startup config report, the Spotify connect trail) was invisible
# until this line existed. Configure the root logger before anything logs.
logging.basicConfig(
    level=logging.INFO,
    format="%(levelname)s [%(name)s] %(message)s",
)

log = logging.getLogger(__name__)

# The Vite build output, copied in by the Docker runtime stage.
DIST_DIR = os.environ.get("FRONTEND_DIST", "/app/static")


def create_app() -> FastAPI:
    app = FastAPI(
        title="dailysplice",
        version=APP_VERSION,
        docs_url=None,  # no public API explorer
        redoc_url=None,
        openapi_url="/api/openapi.json",  # gated by AuthMiddleware
    )
    install_handlers(app)
    app.add_middleware(AuthMiddleware)

    app.include_router(auth.router)  # /login, /auth/*, /logout — public
    app.include_router(meta.router)
    app.include_router(account.router)
    app.include_router(spotify.api_router)  # /api/spotify/* — JSON
    app.include_router(spotify.browser_router)  # /spotify/* — redirects

    # Shape-only secret report: which settings are configured, never a value.
    log.info("dailysplice %s starting; config=%s", APP_VERSION,
             get_settings().secret_status())

    if os.path.isdir(DIST_DIR):
        assets = os.path.join(DIST_DIR, "assets")
        if os.path.isdir(assets):
            app.mount("/assets", StaticFiles(directory=assets), name="assets")

        # HEAD as well as GET: FastAPI (unlike plain Starlette) does not add
        # HEAD to a GET route, so this public page would answer every uptime
        # monitor and link checker with a 405.
        @app.api_route(
            "/{full_path:path}", methods=["GET", "HEAD"], include_in_schema=False
        )
        def spa_fallback(full_path: str):
            """Serve real files at the dist root (favicon, manifest…), and fall
            back to the SPA shell for everything else so client-side routing
            survives a deep link or a refresh.

            The silent SSO probe is NOT fired here. It is client-triggered
            (/api/me -> /auth/probe) so the trigger stays in one place and keeps
            working if a caching layer ever answers this document request.
            """
            candidate = os.path.normpath(os.path.join(DIST_DIR, full_path))
            # normpath collapses ../ — confirm the result is still inside
            # DIST_DIR before serving it, or this is a path traversal.
            if candidate.startswith(DIST_DIR) and os.path.isfile(candidate):
                return FileResponse(candidate)
            return FileResponse(os.path.join(DIST_DIR, "index.html"))

    return app


app = create_app()
