"""
code_first.py — FastAPI CRUD Beispiel (Code First OpenAPI)

Dieses Skript zeigt, wie man mit FastAPI eine kleine CRUD-API baut,
die automatisch eine OpenAPI-Dokumentation erzeugt (Swagger UI /docs).

Wichtige Idee (Code First):
- Wir schreiben zuerst den Code (Endpoints + Typen + Modelle).
- FastAPI generiert daraus automatisch die OpenAPI-Spezifikation.
- Swagger UI zeigt die Dokumentation interaktiv an.

Starten:
    python3 -m uvicorn code_first:app --reload

Öffnen im Browser:
    Swagger UI:   http://127.0.0.1:8000/docs
    OpenAPI JSON: http://127.0.0.1:8000/openapi.json
"""

# -----------------------------------------------------------------------------
# 1) Imports: Welche Bausteine brauchen wir?
# -----------------------------------------------------------------------------
# FastAPI: das Framework
# HTTPException: um sauber Fehler (z.B. 404) zurückzugeben
# Path, Query: um Parameter in URL und Query sauber zu beschreiben/validieren
# status: komfortable HTTP-Statuscodes (z.B. 201, 204)
from fastapi import FastAPI, HTTPException, Path, Query, status

# Pydantic: Modelle für Datenvalidierung (Request/Response-Schemas)
# BaseModel: Basis für Datenmodelle
# Field: Metadaten zu Feldern (Beispiele, Constraints, Beschreibung)
from pydantic import BaseModel, Field

# typing: Typen für Listen etc. (OpenAPI profitiert davon)
from typing import List


# -----------------------------------------------------------------------------
# 2) App-Objekt: Das ist "die API"
# -----------------------------------------------------------------------------
# `app` ist der Einstiegspunkt:
# - Alle Endpoints (@app.get, @app.post, ...) hängen an diesem Objekt.
# - FastAPI sammelt daraus automatisch die OpenAPI-Doku.
app = FastAPI(
    title="User Service API",
    description="Simple CRUD API to demonstrate Code First OpenAPI generation with FastAPI",
    version="1.0.0",
)


# -----------------------------------------------------------------------------
# 3) Datenmodelle (Pydantic): Der "Vertrag" der API
# -----------------------------------------------------------------------------
# FastAPI nutzt diese Modelle für:
# - Validierung von eingehenden Daten (Request Body)
# - automatische Erzeugung der OpenAPI-Schemas
# - Ausgabeformat (Response) über response_model
#
# WICHTIG:
# "Pydantic macht unsere Typannotationen ausführbar: es prüft Daten zur Laufzeit."


class UserBase(BaseModel):
    """
    Basis-Modell für einen User.

    Enthält die Felder, die sowohl beim Erstellen als auch beim Zurückgeben
    eines Users (Response) vorkommen.

    Warum "Base"?
    - Wir können Modelle wiederverwenden und Duplikate vermeiden.
    """

    # Field(...): bedeutet "Pflichtfeld" (required)
    # json_schema_extra: Beispielwerte für Swagger/OpenAPI (Pydantic v2-konform)
    name: str = Field(
        ...,
        description="Der Name des Users",
        json_schema_extra={"example": "Alice"},
    )
    age: int = Field(
        ...,
        description="Das Alter des Users (als Zahl)",
        json_schema_extra={"example": 22},
        ge=0,  # ge = greater or equal: verhindert negative Alterswerte
    )
    email: str = Field(
        ...,
        description="E-Mail-Adresse des Users",
        json_schema_extra={"example": "alice@example.com"},
    )


class UserCreate(UserBase):
    """
    Modell für das Erstellen eines Users.

    In diesem Beispiel ist es identisch zu UserBase.
    Warum trotzdem eine eigene Klasse?
    - In echten Projekten unterscheiden sich Create-Modelle oft (z.B. kein 'id').
    - So bleibt die API sauber erweiterbar, ohne später alles umzubauen.
    """

    pass


class User(UserBase):
    """
    Modell für den User, so wie er von der API zurückgegeben wird.

    Unterschied zu UserCreate:
    - 'id' kommt hinzu (die wird serverseitig vergeben).
    """

    id: int = Field(
        ...,
        description="Eindeutige ID des Users",
        json_schema_extra={"example": 1},
        ge=1,
    )


# -----------------------------------------------------------------------------
# 4) Fake-Datenbank (In-Memory)
# -----------------------------------------------------------------------------
# Für den Workshop nutzen wir absichtlich keine echte Datenbank,
# damit jeder die API sofort lokal starten kann.
#
# Wichtig:
# - Daten sind weg, sobald der Server neu startet.
# - Nicht threadsafe/produktionsreif.
# - Für Demo/Workshop absolut perfekt.
users_db: dict[int, User] = {}


# -----------------------------------------------------------------------------
# 5) CRUD Endpoints
# -----------------------------------------------------------------------------
# CRUD = Create, Read, Update, Delete
#
# Wir benutzen HTTP-Methoden:
# - POST   => Create
# - GET    => Read (list / detail)
# - PUT    => Update
# - DELETE => Delete
#
# FastAPI generiert aus den folgenden Decorators automatisch OpenAPI:
# - Pfade (/users, /users/{user_id})
# - Methoden (GET/POST/PUT/DELETE)
# - Parameter (Path/Query)
# - Request Body (Pydantic-Model)
# - Response-Model (response_model=...)
# - Statuscodes, Summary, Description, Tags


@app.post(
    "/users",
    response_model=User,
    status_code=status.HTTP_201_CREATED,
    summary="Create a new user",
    description="Creates a new user and returns the created resource",
    tags=["Users"],
)
def create_user(user: UserCreate):
    """
    CREATE: Einen neuen User anlegen.

    Request:
    - Body enthält ein UserCreate JSON (name, age, email)
    - FastAPI validiert automatisch (z.B. age muss int sein)

    Response:
    - 201 Created
    - User (inkl. id)

    Hinweis:
    - Wir generieren die ID hier simpel über `len(users_db) + 1`.
      Das ist für Demo ok, aber in Produktion würde das die Datenbank übernehmen.
    """
    user_id = len(users_db) + 1

    # Pydantic v2: model_dump() statt dict()
    new_user = User(id=user_id, **user.model_dump())

    users_db[user_id] = new_user
    return new_user


@app.get(
    "/users",
    response_model=List[User],
    summary="List all users",
    description="Returns a list of all users",
    tags=["Users"],
)
def list_users(
    limit: int = Query(
        10,
        ge=1,
        le=100,
        description="Maximum number of users to return",
    )
):
    """
    READ (List): Alle Users zurückgeben.

    Query-Parameter:
    - limit: wie viele Users maximal zurückgegeben werden
      - Default: 10
      - Minimum: 1
      - Maximum: 100

    Response:
    - 200 OK
    - Liste von Users
    """
    return list(users_db.values())[:limit]


@app.get(
    "/users/{user_id}",
    response_model=User,
    summary="Get user by ID",
    description="Returns a single user by its ID",
    tags=["Users"],
)
def get_user(
    user_id: int = Path(
        ...,
        ge=1,
        description="The ID of the user",
    )
):
    """
    READ (Detail): Einen User anhand seiner ID holen.

    Path-Parameter:
    - user_id: ID in der URL, z.B. /users/3

    Wenn der User nicht existiert:
    - 404 Not Found mit {"detail": "User not found"}
    """
    user = users_db.get(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user


@app.put(
    "/users/{user_id}",
    response_model=User,
    summary="Update a user",
    description="Updates an existing user",
    tags=["Users"],
)
def update_user(
    user_id: int = Path(..., ge=1, description="The ID of the user"),
    user: UserCreate = ...,
):
    """
    UPDATE: Einen bestehenden User vollständig überschreiben.

    PUT bedeutet in der Regel:
    - Der Client schickt den kompletten Datensatz (name, age, email)
    - Der Server ersetzt den existierenden Eintrag vollständig

    Wenn der User nicht existiert:
    - 404 Not Found

    Response:
    - 200 OK
    - Aktualisierter User (inkl. id)
    """
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")

    updated_user = User(id=user_id, **user.model_dump())
    users_db[user_id] = updated_user
    return updated_user


@app.delete(
    "/users/{user_id}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Delete a user",
    description="Deletes a user by its ID",
    tags=["Users"],
)
def delete_user(
    user_id: int = Path(..., ge=1, description="The ID of the user"),
):
    """
    DELETE: Einen User löschen.

    Wenn der User nicht existiert:
    - 404 Not Found

    Response:
    - 204 No Content (kein Response-Body)
      -> Das ist üblich bei erfolgreichen DELETEs.
    """
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")

    del users_db[user_id]
    # Bei 204 geben wir keinen Inhalt zurück.
    return