"""
api.py — Routes FastAPI de l'agent immobilier.

Démarrage :
    python -m uvicorn api:app --host 0.0.0.0 --port 8000 --reload
"""

import logging
from contextlib import asynccontextmanager

from dotenv import load_dotenv
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, Response, StreamingResponse
from fastapi.staticfiles import StaticFiles

load_dotenv()

import domain.log_handler as log_handler  # noqa: F401 — triggers SSE + console logging setup
import domain.agent.memory as memory
import domain.core.monitoring as monitoring
import domain.services.agent_service as agent_service
import domain.services.analysis_service as analysis_service
import domain.services.geo_service as geo_service
import domain.services.isochrone_service as isochrone_service
import domain.services.map_service as map_service
import domain.services.risques_service as risques_service
import domain.services.ui_service as ui_service
from config import MODEL
from domain.core.security import InputSecurityError
from domain.geo_utils import flood_polygons_from_db, flood_zones_from_db
from domain.log_handler import subscribe as log_subscribe
from domain.management import router as mgmt_router
from domain.schemas import (
    ERREURS_COMMUNES, OPENAPI_TAGS,
    DemandeAnalysis, DemandeQuestion,
    ErreurAPI, EtatService, Metriques, ReponseAnalysis, ReponseQuestion,
)
from domain.tools.analysis import init_analysis
from domain.tools.database import setup_db

logger = logging.getLogger("api")


# ---------------------------------------------------------------------------
# Lifespan
# ---------------------------------------------------------------------------

@asynccontextmanager
async def lifespan(app: FastAPI):
    logger.info("Initialisation de la base de données...")
    setup_db()
    memory.clear()
    logger.info("Base de données prête.")
    await init_analysis()
    logger.info("Préchauffage du cache GeoJSON IRIS…")
    await map_service.get_iris_geojson()
    logger.info("Cache GeoJSON IRIS prêt.")
    yield
    logger.info("Arrêt du serveur.")


# ---------------------------------------------------------------------------
# Application
# ---------------------------------------------------------------------------

app = FastAPI(
    title="Agent Immobilier",
    description="""\
API d'analyse immobilière combinant un **agent ReAct LLM** et une **estimation géographique sans LLM**.

## Endpoints principaux

| Endpoint | Méthode | Description |
|---|---|---|
| `/ask` | POST | Question en langage naturel → agent ReAct |
| `/analysis` | POST | Estimation de prix (20 transactions DVF proches) |
| `/health` | GET | État du service |
| `/metrics` | GET | Métriques de performance |
| `/logs/stream` | GET | Stream SSE des logs en temps réel |

## Sources de données

| Dataset | Zone | Période |
|---|---|---|
| DVF (Demandes de Valeurs Foncières) | Nord — département 59 | 2025 |
| DVF | Haute-Garonne — département 31 | 2025 |
| Observatoire des Loyers | Agglomération de Toulouse | 2025 |

## Authentification

Si la variable d'environnement `API_KEY` est définie, inclure le header `X-API-Key` sur les \
endpoints `/ask` et `/analysis`. Cliquez sur **Authorize** ci-dessus pour renseigner votre clé.
""",
    version="1.1.0",
    openapi_tags=OPENAPI_TAGS,
    contact={
        "name": "Agent Immobilier",
        "email": "contact@agent-immobilier.local",
    },
    license_info={
        "name": "Données DVF — Licence Ouverte Etalab 2.0",
        "url": "https://www.etalab.gouv.fr/licence-ouverte-open-licence",
    },
    lifespan=lifespan,
)

app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/assets", StaticFiles(directory="frontend/v2/dist/assets"), name="v2-assets")
app.include_router(mgmt_router, prefix="/management")


# ---------------------------------------------------------------------------
# Error handlers
# ---------------------------------------------------------------------------

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
    detail = "; ".join(
        f"{' → '.join(str(l) for l in e['loc'])}: {e['msg']}"
        for e in exc.errors()
    )
    return JSONResponse(
        status_code=422,
        content=ErreurAPI(code="validation_error", detail=detail).model_dump(),
    )


# ---------------------------------------------------------------------------
# Routes — pages HTML
# ---------------------------------------------------------------------------

@app.get("/", include_in_schema=False)
async def root():
    return RedirectResponse(url="/docs")


@app.get("/dashboard", include_in_schema=False)
async def dashboard():
    return FileResponse("static/dashboard.html")


@app.get("/map", include_in_schema=False)
async def map_view():
    return FileResponse("static/map.html")


@app.get("/duration-map", include_in_schema=False)
async def duration_map_view():
    return FileResponse("static/duration-map.html")


@app.get("/flood-map", include_in_schema=False)
async def flood_map_view():
    return FileResponse("static/flood-map.html")


@app.get("/mercator", include_in_schema=False)
async def mercator_view():
    return FileResponse("static/mercator.html")


# ---------------------------------------------------------------------------
# Routes — monitoring
# ---------------------------------------------------------------------------

@app.get(
    "/health",
    tags=["Monitoring"],
    response_model=EtatService,
    summary="État du service",
    description="Retourne `ok` si le service est opérationnel, avec le modèle LLM actif.",
    responses={200: {"description": "Service opérationnel"}},
)
async def health() -> EtatService:
    return EtatService(statut="ok", modele=MODEL)


@app.get(
    "/metrics",
    tags=["Monitoring"],
    response_model=Metriques,
    summary="Métriques de monitoring",
    description=(
        "Agrégats de performance depuis le démarrage du serveur : "
        "latence, tokens consommés, coût LLM estimé et ROI du cache."
    ),
    responses={200: {"description": "Métriques agrégées"}},
)
async def metrics() -> Metriques:
    return Metriques(**monitoring.get_metrics())


@app.get(
    "/map-data",
    tags=["Monitoring"],
    summary="Données GeoJSON sécurité — Métropole de Lille",
    description=(
        "Retourne un **GeoJSON FeatureCollection** des 17 communes de la métropole lilloise "
        "avec leurs contours (IGN Admin Express via geo.api.gouv.fr) et leur indice de sécurité "
        "issu des statistiques publiques de délinquance 2025 (Ministère de l'Intérieur)."
    ),
    responses={200: {"description": "GeoJSON FeatureCollection"}},
)
async def map_data() -> Response:
    try:
        data_bytes = await map_service.get_iris_geojson_bytes()
        return Response(content=data_bytes, media_type="application/json")
    except RuntimeError as exc:
        logger.error("Erreur construction GeoJSON carte : %s", exc)
        raise HTTPException(status_code=500, detail=str(exc))


@app.post(
    "/map-refresh",
    tags=["Monitoring"],
    summary="Vide le cache GeoJSON pour forcer un rechargement des données IRIS",
)
async def map_refresh() -> JSONResponse:
    map_service.reset_iris_cache()
    return JSONResponse(content={"status": "cache cleared"})


# ---------------------------------------------------------------------------
# Routes — streaming
# ---------------------------------------------------------------------------

@app.get(
    "/logs/stream",
    tags=["Streaming"],
    summary="Stream SSE des logs en temps réel",
    description="""\
Flux **Server-Sent Events** des logs Python de l'application.

Chaque événement est un objet JSON stringifié (ligne de log formatée).
Un heartbeat (`: heartbeat`) est envoyé toutes les 50 ms pour maintenir la connexion ouverte.

**Utilisation depuis JavaScript :**
```js
const es = new EventSource('/logs/stream');
es.onmessage = e => console.log(JSON.parse(e.data));
```
""",
    responses={
        200: {
            "description": "Flux SSE ouvert",
            "content": {
                "text/event-stream": {
                    "example": 'data: "10:00:01 [INFO    ] api — Question reçue : \'Quel est le prix…\'"'
                }
            },
        }
    },
)
async def logs_stream():
    client_q = log_subscribe()
    return StreamingResponse(
        log_handler.sse_generator(client_q),
        media_type="text/event-stream",
        headers={
            "Cache-Control":    "no-cache",
            "Connection":       "keep-alive",
            "X-Accel-Buffering": "no",
        },
    )

# ---------------------------------------------------------------------------
# Routes — estimation sans LLM
# ---------------------------------------------------------------------------

@app.post(
    "/analysis",
    tags=["Analyse"],
    response_model=ReponseAnalysis,
    summary="Estimation immobilière sans LLM",
    description="""\
Estime le prix d'un bien immobilier dans le **Nord (département 59)** sans appel LLM.

### Algorithme

1. **Géocodage** de l'adresse via l'[API BAN](https://api-adresse.data.gouv.fr) → coordonnées GPS
2. **Calcul de distance** Haversine vers chaque transaction DVF (parcel GPS ou centroïde commune)
3. **Top 20** transactions DVF les plus proches (2021-2025)
4. **Prix/m²** pondéré (1/distance × récence) projeté au marché courant × **coefficients**

### Coefficients d'état

| Statut | Coefficient |
|---|---|
| `terrible` | ×0.50 |
| `bad` | ×0.70 |
| `light renovation` | ×0.85 |
| `medium` | ×0.95 |
| `renovated` | ×1.00 *(référence)* |
| `good` | ×1.10 |
| `premium` | ×1.15 |
| `premium plus` | ×1.30 |
""",
    responses={
        200: {"description": "Estimation et 20 transactions de référence"},
        **ERREURS_COMMUNES,
    }
)
async def analysis(demande: DemandeAnalysis) -> ReponseAnalysis:
    try:
        result = await analysis_service.estimer_bien(
            adresse=demande.adresse,
            code_postal=demande.code_postal,
            ville=demande.ville,
            surface_m2=demande.surface_m2,
            statut=demande.statut.value,
            type_bien=demande.type_bien.value,
            orientation=demande.orientation.value,
            taille_terrain=demande.taille_terrain,
        )
        return ReponseAnalysis(**result)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc))
    except RuntimeError as exc:
        raise HTTPException(status_code=503, detail=str(exc))


# ---------------------------------------------------------------------------
# Routes — géo / cartographie
# ---------------------------------------------------------------------------

@app.get("/iris-evolution/{code_iris}", include_in_schema=False)
async def iris_evolution(code_iris: str) -> JSONResponse:
    try:
        data = map_service.get_iris_evolution(code_iris)
        return JSONResponse(content=data)
    except RuntimeError as exc:
        raise HTTPException(status_code=500, detail=str(exc))


@app.get("/flood-zones", include_in_schema=False)
async def flood_zones_proxy(lat: float, lng: float) -> JSONResponse:
    return JSONResponse(content=flood_zones_from_db(lng, lat))


@app.get("/flood-polygons", include_in_schema=False)
async def flood_polygons_proxy(
    min_lng: float, min_lat: float,
    max_lng: float, max_lat: float,
    scenario: str = "moyen",
) -> JSONResponse:
    return JSONResponse(
        content=flood_polygons_from_db(scenario, min_lng, min_lat, max_lng, max_lat)
    )


@app.get("/isochrone", include_in_schema=False)
async def isochrone_proxy(lat: float, lng: float, mode: str = "car") -> JSONResponse:
    data = await isochrone_service.get_isochrone(lat, lng, mode)
    return JSONResponse(content=data)


@app.get("/seveso-sites", include_in_schema=False)
def seveso_sites_proxy(departement: str = "") -> JSONResponse:
    data = risques_service.get_seveso_sites(departement)
    return JSONResponse(content=data)


@app.get("/noise-polygons", include_in_schema=False)
def noise_polygons_proxy(
    min_lng: float, min_lat: float,
    max_lng: float, max_lat: float,
    sources: str = "routier,ferroviaire",
    min_lden: int = 55,
) -> JSONResponse:
    try:
        data = geo_service.get_noise_polygons(min_lng, min_lat, max_lng, max_lat, sources, min_lden)
        return JSONResponse(content=data)
    except RuntimeError as exc:
        logger.warning("noise_polygons error: %s", exc)
        return JSONResponse(status_code=500, content={"error": str(exc)})


@app.get("/nature-polygons", include_in_schema=False)
def nature_polygons_proxy(
    min_lng: float, min_lat: float,
    max_lng: float, max_lat: float,
    category: str = "",
    zoom: int = 14,
) -> JSONResponse:
    try:
        data = geo_service.get_nature_polygons(min_lng, min_lat, max_lng, max_lat, category, zoom)
        return JSONResponse(content=data)
    except RuntimeError as exc:
        logger.warning("nature_polygons error: %s", exc)
        return JSONResponse(status_code=500, content={"error": str(exc)})


@app.get("/flood-risk", include_in_schema=False)
async def flood_risk_proxy(lat: float, lng: float) -> JSONResponse:
    data = await risques_service.get_flood_risk(lat, lng)
    return JSONResponse(content=data)


@app.get("/radon-layer", include_in_schema=False)
def radon_layer_proxy(
    min_lng: float, min_lat: float,
    max_lng: float, max_lat: float,
) -> JSONResponse:
    try:
        data = risques_service.get_radon_communes_geojson(min_lng, min_lat, max_lng, max_lat)
        return JSONResponse(content=data)
    except RuntimeError as exc:
        logger.warning("radon_layer error: %s", exc)
        return JSONResponse(status_code=500, content={"error": str(exc)})


@app.get("/rga-layer", include_in_schema=False)
def rga_layer_proxy(
    min_lng: float, min_lat: float,
    max_lng: float, max_lat: float,
) -> JSONResponse:
    try:
        data = risques_service.get_rga_communes_geojson(min_lng, min_lat, max_lng, max_lat)
        return JSONResponse(content=data)
    except RuntimeError as exc:
        logger.warning("rga_layer error: %s", exc)
        return JSONResponse(status_code=500, content={"error": str(exc)})


@app.get("/nearby-sales", include_in_schema=False)
def nearby_sales_endpoint(
    lat: float,
    lng: float,
    type_local: str,
    limit: int = 10,
) -> JSONResponse:
    try:
        data = map_service.get_nearby_sales(lat, lng, type_local, min(limit, 20))
        return JSONResponse(content=data)
    except RuntimeError as exc:
        logger.warning("nearby_sales error: %s", exc)
        return JSONResponse(status_code=500, content={"error": str(exc)})


@app.get("/poi", include_in_schema=False)
def poi_endpoint(types: str = "mairie,gare,metro,tram") -> JSONResponse:
    try:
        data = geo_service.get_poi_geojson(types)
        return JSONResponse(content=data)
    except RuntimeError as exc:
        logger.warning("poi_endpoint error: %s", exc)
        return JSONResponse(status_code=500, content={"error": str(exc)})


# ---------------------------------------------------------------------------
# Routes — v2 (MapLibre GL JS frontend)
# ---------------------------------------------------------------------------

@app.get("/v2", include_in_schema=False)
@app.get("/v2/", include_in_schema=False)
async def v2_view():
    return FileResponse("frontend/v2/dist/index.html")


@app.get("/api/v2/ui-features", include_in_schema=False)
def ui_features_endpoint() -> JSONResponse:
    data = ui_service.get_ui_features()
    return JSONResponse(content=data, headers={"Cache-Control": "no-cache"})


@app.get("/api/v2/iris-pressure/{code_iris}", include_in_schema=False)
def iris_pressure_endpoint(code_iris: str) -> JSONResponse:
    try:
        data = map_service.get_iris_pressure(code_iris)
        return JSONResponse(content=data)
    except KeyError:
        return JSONResponse(status_code=404, content={"error": "IRIS not found"})
    except RuntimeError as exc:
        logger.warning("iris_pressure error: %s", exc)
        return JSONResponse(status_code=500, content={"error": str(exc)})


@app.get("/api/v2/iris", include_in_schema=False)
async def iris_v2() -> JSONResponse:
    try:
        data = await map_service.get_iris_geojson_v2()
    except RuntimeError as exc:
        raise HTTPException(status_code=500, detail=str(exc))
    return JSONResponse(
        content=data,
        headers={"Cache-Control": "public, max-age=3600"},
    )
