"""GenDesign FastAPI application entrypoint."""

import logging
import os
import re
from collections.abc import AsyncIterator, Awaitable, Callable
from contextlib import asynccontextmanager

import sentry_sdk
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, Response
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.httpx import HttpxIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration

from app.api.v1 import (
    admin_cadastre,
    admin_etl,
    admin_jobs,
    admin_leads,
    admin_scrape,
    admin_weight_profiles,
    analytics,
    chat,
    concepts,
    custom_pois,
    insights,
    landing,
    locations,
    market,
    me,
    own_projects,
    parcels,
    photos,
    pilot,
    ping,
    trade_in,
    users,
)
from app.core.audit_middleware import audit_log_middleware
from app.core.auth import get_role
from app.core.config import settings
from app.observability.sentry_scrub import scrub_sensitive_query

logger = logging.getLogger(__name__)

# App-level логи в stdout контейнера (#1926 observability). По умолчанию у app.*
# логгеров нет StreamHandler → их INFO теряется (в `docker logs` видны только
# uvicorn-логгеры), поэтому полезные строки (напр. "OSRM road-distance applied",
# RBAC, analyze) невидимы при отладке на VPS. Вешаем один StreamHandler на корневой
# "app" логгер на уровне APP_LOG_LEVEL (default INFO). propagate оставляем True →
# Sentry/GlitchTip LoggingIntegration (хендлер на root) продолжает получать
# breadcrumbs/events. Двойного stdout нет: у root своего stdout-хендлера нет.
# Идемпотентно (маркер _gd_app_stream — guard против дубля при reload/повторном
# импорте). Только API-процесс (main.py); worker (celery_app.py) НЕ трогаем —
# там loader'ы чатятся per-sync, INFO бы зашумил.
_app_logger = logging.getLogger("app")
_app_logger.setLevel(getattr(logging, os.getenv("APP_LOG_LEVEL", "INFO").upper(), logging.INFO))
if not any(getattr(_h, "_gd_app_stream", False) for _h in _app_logger.handlers):
    _stream = logging.StreamHandler()
    _stream.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s"))
    _stream._gd_app_stream = True  # type: ignore[attr-defined]  # idempotency marker
    _app_logger.addHandler(_stream)

# Инициализируем SDK до создания FastAPI app, чтобы все инструменты
# (middleware, маршруты) видели активный client с самого старта процесса.
# GlitchTip не поддерживает profiling — profiles_sample_rate=0.0.
if settings.glitchtip_dsn:
    sentry_sdk.init(
        dsn=settings.glitchtip_dsn,
        environment=settings.environment,
        release=os.getenv("GIT_SHA") or os.getenv("SENTRY_RELEASE") or "unknown",
        traces_sample_rate=settings.glitchtip_traces_sample_rate,
        profiles_sample_rate=0.0,
        send_default_pii=False,
        before_send=scrub_sensitive_query,
        before_send_transaction=scrub_sensitive_query,
        integrations=[
            StarletteIntegration(),
            FastApiIntegration(),
            CeleryIntegration(monitor_beat_tasks=True),
            SqlalchemyIntegration(),
            HttpxIntegration(),
            LoggingIntegration(level=logging.INFO, event_level=logging.ERROR),
        ],
    )
    logger.info(
        "GlitchTip SDK initialised (env=%s, traces=%.2f)",
        settings.environment,
        settings.glitchtip_traces_sample_rate,
    )


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    yield


app = FastAPI(title="GenDesign API", version="0.1.0", lifespan=lifespan)

# §19 audit log (#962, EPIC18): аудит sensitive actions (analyze/forecast/export).
# Регистрируем ПЕРВЫМ → Starlette применяет HTTP-middleware в LIFO-порядке
# (add_middleware insert(0); build_middleware_stack wraps reversed), поэтому
# rbac_guard (зарегистрирован НИЖЕ) оказывается ВНЕШНИМ слоем, а audit — внутри:
# request flow = rbac_guard → audit → router. Аудитим ТОЛЬКО запросы, которые
# rbac_guard уже пропустил (его 401/403 short-circuit'ы не доходят до audit).
# Best-effort: сбой записи не ломает запрос (см. app/core/audit_middleware.py).
app.middleware("http")(audit_log_middleware)

# RBAC: defense-in-depth поверх Caddy basic_auth + X-Authenticated-User
# (см. app/core/auth.py + auth/roles.yaml). Правила:
#   1) Любой non-public path требует X-Authenticated-User — иначе 401
#      (Caddy пробрасывает заголовок только для авторизованных юзеров; нет
#      заголовка = локальный curl мимо Caddy = доступ запрещён).
#   2) Юзер должен быть в roles.yaml — иначе 403 («неизвестный юзер ничего
#      не видит»). Это закрывает кейс если Caddy basic_auth пропустил кого-то
#      из снятой записи, или прокси-подмена заголовка.
#   3) /api/v1/admin/* — только role=admin, иначе 403.
# Public paths без auth (/health, /docs, /openapi.json) пропускаем без проверки —
# X-Authenticated-User там просто не приходит из Caddy.
_ADMIN_API_RE = re.compile(r"^/api/v1/admin/")
_PUBLIC_PATHS = frozenset({"/health", "/api/v1/ping", "/docs", "/redoc", "/openapi.json"})


@app.middleware("http")
async def rbac_guard(
    request: Request,
    call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
    # Test-mode bypass: pytest бьёт по app мимо Caddy → нет X-Authenticated-User.
    # СТРОГО gated на settings.testing (default False) — прод RBAC не затронут.
    # RBAC-логика покрыта отдельно в tests/test_rbac.py (своя копия middleware).
    if settings.testing:
        return await call_next(request)

    path = request.url.path
    if path in _PUBLIC_PATHS:
        return await call_next(request)

    username = request.headers.get("X-Authenticated-User")
    if not username:
        # Любой non-public path без auth-header → 401. Локальный curl мимо Caddy
        # или прокси-фронт без header_up. 401 точнее чем 403 — "сначала
        # аутентифицируйся".
        return JSONResponse(
            status_code=401,
            content={"detail": "no authenticated user (Caddy basic_auth required)"},
        )

    try:
        role = get_role(username)
    except KeyError:
        # Юзер в Caddy basic_auth, но не в roles.yaml → 403 на ВСЁ.
        # Decided 2026-05-25: «человек без ролей вообще ничего не видит».
        logger.warning("RBAC: unknown user %r tried %s", username, path)
        return JSONResponse(
            status_code=403,
            content={"detail": "user not in roles config"},
        )

    if _ADMIN_API_RE.match(path) and role != "admin":
        logger.info("RBAC: blocked %s (role=%s) from %s", username, role, path)
        return JSONResponse(
            status_code=403,
            content={"detail": "admin only"},
        )
    return await call_next(request)


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

app.include_router(concepts.router, prefix="/api/v1/concepts", tags=["concepts"])
app.include_router(chat.router, prefix="/api/v1/chat", tags=["chat"])
app.include_router(parcels.router, prefix="/api/v1/parcels", tags=["parcels"])
app.include_router(analytics.router, prefix="/api/v1/analytics", tags=["analytics"])
app.include_router(market.router, prefix="/api/v1/market", tags=["market"])
app.include_router(admin_scrape.router, prefix="/api/v1/admin/scrape", tags=["admin"])
app.include_router(admin_jobs.router, prefix="/api/v1/admin/jobs", tags=["admin"])
app.include_router(admin_leads.router, prefix="/api/v1/admin/leads", tags=["admin"])
app.include_router(
    admin_weight_profiles.router,
    prefix="/api/v1/admin/site-finder/weight-profiles",
    tags=["admin", "site-finder"],
)
app.include_router(photos.router, prefix="/api/v1/photos", tags=["photos"])
app.include_router(custom_pois.router, prefix="/api/v1/custom-pois", tags=["custom-pois"])
app.include_router(locations.router, prefix="/api/v1/locations", tags=["locations"])
app.include_router(insights.router, prefix="/api/v1/insights", tags=["insights"])
app.include_router(own_projects.router, prefix="/api/v1/own-projects", tags=["own-projects"])
app.include_router(
    admin_cadastre.router,
    prefix="/api/v1/admin/cadastre",
    tags=["admin", "cadastre"],
)
app.include_router(
    admin_etl.router,
    prefix="/api/v1/admin/etl",
    tags=["admin", "etl"],
)
app.include_router(trade_in.router, prefix="/api/v1/trade-in", tags=["trade-in"])
app.include_router(landing.router, prefix="/api/v1", tags=["landing"])
app.include_router(pilot.router, prefix="/api/v1/pilot", tags=["pilot"])
app.include_router(users.router, prefix="/api/v1", tags=["users"])
app.include_router(me.router, prefix="/api/v1", tags=["me"])
app.include_router(ping.router, prefix="/api/v1", tags=["ping"])


@app.get("/health")
async def health() -> dict[str, str]:
    return {
        "status": "ok",
        "environment": settings.environment,
        "version": app.version,
    }
