from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Query, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, ConfigDict
from typing import List, Optional
from datetime import datetime, timedelta
import os
from pathlib import Path
from dotenv import load_dotenv
import psycopg
from psycopg.rows import dict_row
from psycopg_pool import ConnectionPool
import logging

# Logging konfigurieren
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# Umgebungsvariablen laden
env_path = Path(__file__).parent.parent / '.env'
load_dotenv(dotenv_path=env_path)

# Datenbank-Konfiguration
DB_HOST = os.getenv('DB_HOST', 'localhost')
DB_PORT = int(os.getenv('DB_PORT', 5432))
DB_NAME = os.getenv('DB_NAME', 'wetterstation')
DB_USER = os.getenv('DB_USER')
DB_PASSWORD = os.getenv('DB_PASSWORD')

# --------------------------------------------------------------------------- #
# Connection Pool + Lifespan
# --------------------------------------------------------------------------- #

DB_POOL_MIN = int(os.getenv("DB_POOL_MIN", 2))
DB_POOL_MAX = int(os.getenv("DB_POOL_MAX", 10))

pool: Optional["ConnectionPool"] = None


def _build_conninfo() -> str:
    return (
        f"host={DB_HOST} port={DB_PORT} dbname={DB_NAME} "
        f"user={DB_USER} password={DB_PASSWORD}"
    )


@asynccontextmanager
async def lifespan(app: FastAPI):
    global pool
    if not DB_USER or not DB_PASSWORD:
        raise RuntimeError("DB_USER/DB_PASSWORD nicht gesetzt")

    pool = ConnectionPool(
        conninfo=_build_conninfo(),
        min_size=DB_POOL_MIN,
        max_size=DB_POOL_MAX,
        timeout=10,
        kwargs={"row_factory": dict_row, "autocommit": True},
    )
    pool.wait()
    logger.info("DB-Pool initialisiert (min=%d, max=%d)", DB_POOL_MIN, DB_POOL_MAX)
    try:
        yield
    finally:
        if pool is not None:
            pool.close()
            logger.info("DB-Pool geschlossen")


# FastAPI App erstellen
app = FastAPI(
    title="Wetterstation API",
    description="API zum Auslesen von Wetterdaten",
    version="1.1.0",
    lifespan=lifespan,
)

# CORS Middleware — auf bekannte Frontend-Domains beschraenkt.
# Zusaetzliche Origins koennen via ENV CORS_EXTRA_ORIGINS (Komma-separiert) gesetzt werden.
_default_origins = [
    "https://stwwetter.fuerst-stuttgart.de",
    "https://sternwarte-welzheim.de",
    "http://localhost:3000",
    "http://localhost:5173",
]
_extra = os.getenv("CORS_EXTRA_ORIGINS", "")
_extra_list = [o.strip() for o in _extra.split(",") if o.strip()]
ALLOWED_ORIGINS = _default_origins + _extra_list

app.add_middleware(
    CORSMiddleware,
    allow_origins=ALLOWED_ORIGINS,
    allow_credentials=False,           # API liest nur, keine Cookies/Auth noetig
    allow_methods=["GET", "OPTIONS"],  # API ist read-only
    allow_headers=["Content-Type"],
    max_age=600,
)
logger.info("CORS aktiv fuer: %s", ALLOWED_ORIGINS)


# Pydantic Models
class WeatherData(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    
    id: int
    datetime: datetime
    temperature: Optional[float] = None
    humidity: Optional[int] = None
    pressure: Optional[float] = None
    wind_speed: Optional[float] = None
    wind_gust: Optional[float] = None
    wind_dir: Optional[float] = None
    rain: Optional[float] = None
    rain_rate: Optional[float] = None
    bar_trend: Optional[int] = None
    received_at: datetime


class WeatherStats(BaseModel):
    avg_temperature: Optional[float] = None
    min_temperature: Optional[float] = None
    max_temperature: Optional[float] = None
    avg_humidity: Optional[float] = None
    avg_pressure: Optional[float] = None
    avg_wind_speed: Optional[float] = None
    max_wind_gust: Optional[float] = None
    total_rain: Optional[float] = None
    data_points: int


class HealthResponse(BaseModel):
    status: str
    database: str
    timestamp: datetime


# Datenbankverbindung — aus dem Pool, als FastAPI-Dependency.
def get_db_conn():
    """Yieldet eine Connection aus dem Pool und gibt sie automatisch zurueck."""
    if pool is None:
        raise HTTPException(status_code=503, detail="DB-Pool nicht initialisiert")
    try:
        with pool.connection() as conn:
            yield conn
    except psycopg.Error:
        logger.exception("DB-Fehler beim Pool-Zugriff")
        raise HTTPException(status_code=503, detail="Datenbank nicht erreichbar")


# API Endpoints
@app.get("/", tags=["General"])
async def root():
    """Root Endpoint"""
    return {
        "message": "Wetterstation API",
        "version": "1.1.0",
        "docs": "/docs"
    }


@app.get("/health", response_model=HealthResponse, tags=["General"])
async def health_check():
    """Health Check Endpoint"""
    db_status = "disconnected"
    try:
        if pool is not None:
            with pool.connection() as conn:
                with conn.cursor() as cursor:
                    cursor.execute("SELECT 1")
            db_status = "connected"
    except Exception:
        logger.exception("Health-Check DB-Test fehlgeschlagen")
    
    return {
        "status": "ok" if db_status == "connected" else "error",
        "database": db_status,
        "timestamp": datetime.now()
    }


@app.get("/weather/latest", response_model=WeatherData, tags=["Weather Data"])
async def get_latest_weather(conn = Depends(get_db_conn)):
    """Gibt die neuesten Wetterdaten zurück"""
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT id, datetime, temperature, humidity, pressure,
                   wind_speed * 1.60934 as wind_speed,
                   wind_gust * 1.60934 as wind_gust,
                   wind_dir, rain, rain_rate, bar_trend, received_at
            FROM weather_data 
            ORDER BY datetime DESC 
            LIMIT 1
        """)
        result = cursor.fetchone()
        
        if not result:
            raise HTTPException(status_code=404, detail="Keine Daten verfügbar")
        
        return dict(result)


@app.get("/weather/current", response_model=WeatherData, tags=["Weather Data"])
async def get_current_weather(conn = Depends(get_db_conn)):
    """Alias für /weather/latest - gibt aktuelle Wetterdaten zurück"""
    return await get_latest_weather(conn=conn)


@app.get("/weather/history", response_model=List[WeatherData], tags=["Weather Data"])
async def get_weather_history(
    hours: int = Query(24, ge=1, le=168, description="Anzahl Stunden zurück (max 168 = 7 Tage)"),
    limit: int = Query(1000, ge=1, le=10000, description="Maximale Anzahl Datensätze"),
    conn = Depends(get_db_conn),
):
    """Gibt historische Wetterdaten der letzten X Stunden zurück"""
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT id, datetime, temperature, humidity, pressure,
                   wind_speed * 1.60934 as wind_speed,
                   wind_gust * 1.60934 as wind_gust,
                   wind_dir, rain, rain_rate, bar_trend, received_at
            FROM weather_data 
            WHERE datetime >= NOW() - make_interval(hours => %s)
            ORDER BY datetime DESC
            LIMIT %s
        """, (hours, limit))
        results = cursor.fetchall()
        
        return [dict(row) for row in results]


@app.get("/weather/range", response_model=List[WeatherData], tags=["Weather Data"])
async def get_weather_by_date_range(
    start: datetime = Query(..., description="Startdatum (ISO 8601)"),
    end: datetime = Query(..., description="Enddatum (ISO 8601)"),
    limit: int = Query(10000, ge=1, le=50000, description="Maximale Anzahl Datensätze"),
    conn = Depends(get_db_conn),
):
    """Gibt Wetterdaten für einen bestimmten Zeitraum zurück"""
    if start >= end:
        raise HTTPException(status_code=400, detail="Startdatum muss vor Enddatum liegen")
    
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT * FROM weather_data 
            WHERE datetime BETWEEN %s AND %s
            ORDER BY datetime ASC
            LIMIT %s
        """, (start, end, limit))
        results = cursor.fetchall()
        
        return [dict(row) for row in results]


@app.get("/weather/stats", response_model=WeatherStats, tags=["Statistics"])
async def get_weather_statistics(
    hours: int = Query(24, ge=1, le=168, description="Zeitraum in Stunden für Statistiken"),
    conn = Depends(get_db_conn),
):
    """Gibt aggregierte Statistiken für den angegebenen Zeitraum zurück"""
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT 
                AVG(temperature) as avg_temperature,
                MIN(temperature) as min_temperature,
                MAX(temperature) as max_temperature,
                AVG(humidity) as avg_humidity,
                AVG(pressure) as avg_pressure,
                AVG(wind_speed * 1.60934) as avg_wind_speed,
                MAX(wind_gust * 1.60934) as max_wind_gust,
                (SELECT COALESCE(SUM(daily_max), 0)
                 FROM (
                     SELECT MAX(rain) as daily_max
                     FROM weather_data d2
                     WHERE d2.datetime >= NOW() - make_interval(hours => %s)
                     GROUP BY DATE(d2.datetime)
                 ) sub) as total_rain,
                COUNT(*) as data_points
            FROM weather_data
            WHERE datetime >= NOW() - make_interval(hours => %s)
        """, (hours, hours))
        result = cursor.fetchone()
        
        if not result or result['data_points'] == 0:
            raise HTTPException(status_code=404, detail="Keine Daten für den Zeitraum verfügbar")
        
        return dict(result)


@app.get("/weather/daily", response_model=List[WeatherStats], tags=["Statistics"])
async def get_daily_statistics(
    days: int = Query(7, ge=1, le=90, description="Anzahl Tage zurück (max 90)"),
    conn = Depends(get_db_conn),
):
    """Gibt tägliche Statistiken für die letzten X Tage zurück"""
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT 
                DATE(datetime) as date,
                AVG(temperature) as avg_temperature,
                MIN(temperature) as min_temperature,
                MAX(temperature) as max_temperature,
                AVG(humidity) as avg_humidity,
                AVG(pressure) as avg_pressure,
                AVG(wind_speed * 1.60934) as avg_wind_speed,
                MAX(wind_gust * 1.60934) as max_wind_gust,
                MAX(rain) as total_rain,
                COUNT(*) as data_points
            FROM weather_data
            WHERE datetime >= NOW() - make_interval(days => %s)
            GROUP BY DATE(datetime)
            ORDER BY date DESC
        """, (days,))
        results = cursor.fetchall()
        
        return [dict(row) for row in results]


@app.get("/weather/temperature", response_model=List[dict], tags=["Weather Data"])
async def get_temperature_data(
    hours: int = Query(24, ge=1, le=168, description="Anzahl Stunden zurück"),
    conn = Depends(get_db_conn),
):
    """Gibt nur Temperatur-Zeitreihen zurück (optimiert für Diagramme)"""
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT datetime, temperature 
            FROM weather_data 
            WHERE datetime >= NOW() - make_interval(hours => %s)
              AND temperature IS NOT NULL
            ORDER BY datetime ASC
        """, (hours,))
        results = cursor.fetchall()
        
        return [dict(row) for row in results]


@app.get("/weather/wind", response_model=List[dict], tags=["Weather Data"])
async def get_wind_data(
    hours: int = Query(24, ge=1, le=168, description="Anzahl Stunden zurück"),
    conn = Depends(get_db_conn),
):
    """Gibt nur Wind-Daten zurück (Geschwindigkeit, Richtung, Böen)"""
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT datetime, 
                   wind_speed * 1.60934 as wind_speed,
                   wind_gust * 1.60934 as wind_gust,
                   wind_dir 
            FROM weather_data 
            WHERE datetime >= NOW() - make_interval(hours => %s)
            ORDER BY datetime ASC
        """, (hours,))
        results = cursor.fetchall()
        
        return [dict(row) for row in results]


@app.get("/weather/rain", response_model=List[dict], tags=["Weather Data"])
async def get_rain_data(
    hours: int = Query(24, ge=1, le=168, description="Anzahl Stunden zurück"),
    conn = Depends(get_db_conn),
):
    """Gibt nur Regen-Daten zurück"""
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT datetime, rain, rain_rate 
            FROM weather_data 
            WHERE datetime >= NOW() - make_interval(hours => %s)
            ORDER BY datetime ASC
        """, (hours,))
        results = cursor.fetchall()
        
        return [dict(row) for row in results]


@app.get("/weather/hourly-aggregated", response_model=List[WeatherData], tags=["Aggregated Data"])
async def get_hourly_aggregated_data(
    days: int = Query(7, ge=1, le=60, description="Anzahl Tage zurück (max 60)"),
    conn = Depends(get_db_conn),
):
    """Gibt stündlich aggregierte Wetterdaten zurück (Stundenmittel)"""
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT 
                0 as id,
                date_trunc('hour', datetime) as datetime,
                AVG(temperature) as temperature,
                ROUND(AVG(humidity)) as humidity,
                AVG(pressure) as pressure,
                AVG(wind_speed * 1.60934) as wind_speed,
                MAX(wind_gust * 1.60934) as wind_gust,
                AVG(wind_dir) as wind_dir,
                MAX(rain) as rain,
                AVG(rain_rate) as rain_rate,
                MAX(received_at) as received_at
            FROM weather_data
            WHERE datetime >= NOW() - make_interval(days => %s)
            GROUP BY date_trunc('hour', datetime)
            ORDER BY datetime ASC
        """, (days,))
        results = cursor.fetchall()
        
        return [dict(row) for row in results]


@app.get("/weather/daily-aggregated", response_model=List[dict], tags=["Aggregated Data"])
async def get_daily_aggregated_data(
    days: int = Query(365, ge=1, le=730, description="Anzahl Tage zurück (max 730)"),
    conn = Depends(get_db_conn),
):
    """Gibt täglich aggregierte Wetterdaten zurück (Tagesmittel mit Min/Max-Temperaturen)"""
    with conn.cursor() as cursor:
        cursor.execute("""
                SELECT
                    date_trunc('day', datetime) as datetime,
                    AVG(temperature)::float as temperature,
                    MIN(temperature)::float as min_temperature,
                    MAX(temperature)::float as max_temperature,
                    (array_agg(datetime ORDER BY temperature ASC NULLS LAST))[1] as min_temperature_time,
                    (array_agg(datetime ORDER BY temperature DESC NULLS LAST))[1] as max_temperature_time,
                    ROUND(AVG(humidity))::int as humidity,
                    MIN(humidity)::int as min_humidity,
                    MAX(humidity)::int as max_humidity,
                    (array_agg(datetime ORDER BY humidity ASC NULLS LAST))[1] as min_humidity_time,
                    (array_agg(datetime ORDER BY humidity DESC NULLS LAST))[1] as max_humidity_time,
                    AVG(pressure)::float as pressure,
                    MIN(pressure)::float as min_pressure,
                    MAX(pressure)::float as max_pressure,
                    (array_agg(datetime ORDER BY pressure ASC NULLS LAST))[1] as min_pressure_time,
                    (array_agg(datetime ORDER BY pressure DESC NULLS LAST))[1] as max_pressure_time,
                    AVG(wind_speed * 1.60934)::float as wind_speed,
                    MAX(wind_gust * 1.60934)::float as wind_gust,
                    (array_agg(datetime ORDER BY wind_gust DESC NULLS LAST))[1] as max_wind_gust_time,
                    AVG(wind_dir)::float as wind_dir,
                    MAX(rain)::float as total_rain
                FROM weather_data
                WHERE datetime >= NOW() - make_interval(days => %s)
                GROUP BY date_trunc('day', datetime)
                ORDER BY datetime ASC
            """, (days,))
        results = cursor.fetchall()

        return [dict(row) for row in results]


@app.get("/weather/daily-with-minmax", response_model=List[dict], tags=["Aggregated Data"])
async def get_daily_with_minmax_data(
    days: int = Query(30, ge=1, le=90, description="Anzahl Tage zurück (max 90)"),
    conn = Depends(get_db_conn),
):
    """Gibt täglich aggregierte Wetterdaten mit Min/Max-Temperaturen zurück"""
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT
                date_trunc('day', datetime) as datetime,
                AVG(temperature)::float as temperature,
                MIN(temperature)::float as min_temperature,
                MAX(temperature)::float as max_temperature,
                (array_agg(datetime ORDER BY temperature ASC NULLS LAST))[1] as min_temperature_time,
                (array_agg(datetime ORDER BY temperature DESC NULLS LAST))[1] as max_temperature_time,
                ROUND(AVG(humidity))::int as humidity,
                MIN(humidity)::int as min_humidity,
                MAX(humidity)::int as max_humidity,
                (array_agg(datetime ORDER BY humidity ASC NULLS LAST))[1] as min_humidity_time,
                (array_agg(datetime ORDER BY humidity DESC NULLS LAST))[1] as max_humidity_time,
                AVG(pressure)::float as pressure,
                MIN(pressure)::float as min_pressure,
                MAX(pressure)::float as max_pressure,
                (array_agg(datetime ORDER BY pressure ASC NULLS LAST))[1] as min_pressure_time,
                (array_agg(datetime ORDER BY pressure DESC NULLS LAST))[1] as max_pressure_time,
                AVG(wind_speed * 1.60934)::float as wind_speed,
                MAX(wind_gust * 1.60934)::float as wind_gust,
                (array_agg(datetime ORDER BY wind_gust DESC NULLS LAST))[1] as max_wind_gust_time,
                AVG(wind_dir)::float as wind_dir,
                MAX(rain)::float as total_rain
            FROM weather_data
            WHERE datetime >= NOW() - make_interval(days => %s)
            GROUP BY date_trunc('day', datetime)
            ORDER BY datetime ASC
        """, (days,))
        results = cursor.fetchall()

        return [dict(row) for row in results]


@app.get("/weather/rain-daily", response_model=List[dict], tags=["Aggregated Data"])
async def get_daily_rain_data(
    days: int = Query(30, ge=1, le=365, description="Anzahl Tage zurück"),
    conn = Depends(get_db_conn),
):
    """Gibt tägliche Regensummen zurück"""
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT
                date_trunc('day', datetime) as date,
                MAX(rain) as total_rain
            FROM weather_data
            WHERE datetime >= NOW() - make_interval(days => %s)
            GROUP BY date_trunc('day', datetime)
            ORDER BY date ASC
        """, (days,))
        results = cursor.fetchall()
        
        return [dict(row) for row in results]


@app.get("/weather/rain-weekly", response_model=List[dict], tags=["Aggregated Data"])
async def get_weekly_rain_data(
    days: int = Query(365, ge=1, le=730, description="Anzahl Tage zurück"),
    conn = Depends(get_db_conn),
):
    """Gibt wöchentliche Regensummen zurück (Woche = Mo-So)"""
    with conn.cursor() as cursor:
        # Bei 365 Tagen: alle verfügbaren Daten zurückgeben
        if days >= 365:
            cursor.execute("""
                SELECT
                    date_trunc('week', day) as week_start,
                    SUM(daily_rain) as total_rain
                FROM (
                    SELECT DATE(datetime) as day, MAX(rain) as daily_rain
                    FROM weather_data
                    GROUP BY DATE(datetime)
                ) sub
                GROUP BY date_trunc('week', day)
                ORDER BY week_start ASC
            """)
        else:
            cursor.execute("""
                SELECT
                    date_trunc('week', day) as week_start,
                    SUM(daily_rain) as total_rain
                FROM (
                    SELECT DATE(datetime) as day, MAX(rain) as daily_rain
                    FROM weather_data
                    WHERE datetime >= NOW() - make_interval(days => %s)
                    GROUP BY DATE(datetime)
                ) sub
                GROUP BY date_trunc('week', day)
                ORDER BY week_start ASC
            """, (days,))
        results = cursor.fetchall()
        
        return [dict(row) for row in results]


@app.get("/weather/hourly-aggregated-range", response_model=List[dict], tags=["Aggregated Data"])
async def get_hourly_aggregated_range(
    start: datetime = Query(..., description="Startdatum (ISO 8601)"),
    end: datetime = Query(..., description="Enddatum (ISO 8601)"),
    conn = Depends(get_db_conn),
):
    """Gibt stündlich aggregierte Wetterdaten für einen bestimmten Zeitraum zurück"""
    if start >= end:
        raise HTTPException(status_code=400, detail="Startdatum muss vor Enddatum liegen")
    
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT 
                date_trunc('hour', datetime) as datetime,
                AVG(temperature)::float as temperature,
                ROUND(AVG(humidity))::int as humidity,
                AVG(pressure)::float as pressure,
                AVG(wind_speed * 1.60934)::float as wind_speed,
                MAX(wind_gust * 1.60934)::float as wind_gust,
                AVG(wind_dir)::float as wind_dir
            FROM weather_data
            WHERE datetime BETWEEN %s AND %s
            GROUP BY date_trunc('hour', datetime)
            ORDER BY datetime ASC
        """, (start, end))
        results = cursor.fetchall()
        
        return [dict(row) for row in results]


@app.get("/weather/daily-aggregated-range", response_model=List[dict], tags=["Aggregated Data"])
async def get_daily_aggregated_range(
    start: datetime = Query(..., description="Startdatum (ISO 8601)"),
    end: datetime = Query(..., description="Enddatum (ISO 8601)"),
    conn = Depends(get_db_conn),
):
    """Gibt täglich aggregierte Wetterdaten mit Min/Max-Temperaturen für einen bestimmten Zeitraum zurück"""
    if start >= end:
        raise HTTPException(status_code=400, detail="Startdatum muss vor Enddatum liegen")
    
    with conn.cursor() as cursor:
        cursor.execute("""
            SELECT
                date_trunc('day', datetime) as datetime,
                AVG(temperature)::float as temperature,
                MIN(temperature)::float as min_temperature,
                MAX(temperature)::float as max_temperature,
                (array_agg(datetime ORDER BY temperature ASC NULLS LAST))[1] as min_temperature_time,
                (array_agg(datetime ORDER BY temperature DESC NULLS LAST))[1] as max_temperature_time,
                ROUND(AVG(humidity))::int as humidity,
                MIN(humidity)::int as min_humidity,
                MAX(humidity)::int as max_humidity,
                (array_agg(datetime ORDER BY humidity ASC NULLS LAST))[1] as min_humidity_time,
                (array_agg(datetime ORDER BY humidity DESC NULLS LAST))[1] as max_humidity_time,
                AVG(pressure)::float as pressure,
                MIN(pressure)::float as min_pressure,
                MAX(pressure)::float as max_pressure,
                (array_agg(datetime ORDER BY pressure ASC NULLS LAST))[1] as min_pressure_time,
                (array_agg(datetime ORDER BY pressure DESC NULLS LAST))[1] as max_pressure_time,
                AVG(wind_speed * 1.60934)::float as wind_speed,
                MAX(wind_gust * 1.60934)::float as wind_gust,
                (array_agg(datetime ORDER BY wind_gust DESC NULLS LAST))[1] as max_wind_gust_time,
                AVG(wind_dir)::float as wind_dir,
                MAX(rain)::float as total_rain
            FROM weather_data
            WHERE datetime BETWEEN %s AND %s
            GROUP BY date_trunc('day', datetime)
            ORDER BY datetime ASC
        """, (start, end))
        results = cursor.fetchall()
        
        return [dict(row) for row in results]


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
