"""
BAN YARO — Datenbank
SQLite mit WAL-Modus (bewährt von akku-werkstatt).
"""

import sqlite3
import os
import logging
import unicodedata
from contextlib import contextmanager

logger   = logging.getLogger(__name__)
DB_PATH  = os.getenv("DB_PATH", "/data/banyaro.db")


def _norm(s):
    """Diakritika entfernen + lowercase — für akzentunabhängige Suche."""
    if not s:
        return ''
    return unicodedata.normalize('NFKD', str(s)).encode('ascii', 'ignore').decode('ascii').lower()


def get_connection() -> sqlite3.Connection:
    conn = sqlite3.connect(DB_PATH, check_same_thread=False)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA foreign_keys=ON")
    conn.execute("PRAGMA busy_timeout=5000")
    conn.execute("PRAGMA cache_size = -32000")   # 32MB Page-Cache
    conn.execute("PRAGMA temp_store = MEMORY")   # Temp-Tabellen im RAM statt auf Disk
    conn.execute("PRAGMA mmap_size = 268435456") # 256MB Memory-Mapped I/O
    conn.create_function('norm', 1, _norm)
    return conn


@contextmanager
def db():
    conn = get_connection()
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()


def init_db():
    """Erstellt alle Tabellen falls nicht vorhanden."""
    logger.info(f"Initialisiere Datenbank: {DB_PATH}")
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)

    with db() as conn:
        conn.executescript("""

        -- USERS
        CREATE TABLE IF NOT EXISTS users (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            email       TEXT    NOT NULL UNIQUE,
            pw_hash     TEXT    NOT NULL,
            name        TEXT    NOT NULL,
            rolle       TEXT    NOT NULL DEFAULT 'user',
            is_premium  INTEGER NOT NULL DEFAULT 0,
            push_sub    TEXT,
            created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
            last_login  TEXT
        );

        -- HUNDE
        CREATE TABLE IF NOT EXISTS dogs (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            name        TEXT    NOT NULL,
            rasse       TEXT,
            geburtstag  TEXT,
            geschlecht  TEXT,
            gewicht_kg  REAL,
            chip_nr     TEXT,
            foto_url    TEXT,
            bio         TEXT,
            is_public   INTEGER NOT NULL DEFAULT 0,
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );

        -- TAGEBUCH
        CREATE TABLE IF NOT EXISTS diary (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            dog_id      INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
            datum       TEXT    NOT NULL DEFAULT (date('now')),
            typ         TEXT    NOT NULL DEFAULT 'eintrag',
            titel       TEXT,
            text        TEXT,
            media_url   TEXT,
            tags        TEXT,   -- JSON Array
            gps_lat     REAL,
            gps_lon     REAL,
            is_milestone INTEGER NOT NULL DEFAULT 0,
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_diary_dog ON diary(dog_id, datum DESC);

        -- TAGEBUCH ↔ HUNDE (n:m — ein Eintrag kann mehrere Hunde betreffen)
        CREATE TABLE IF NOT EXISTS diary_dogs (
            diary_id INTEGER NOT NULL REFERENCES diary(id) ON DELETE CASCADE,
            dog_id   INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
            PRIMARY KEY (diary_id, dog_id)
        );
        CREATE INDEX IF NOT EXISTS idx_diary_dogs_dog ON diary_dogs(dog_id);

        -- GESUNDHEIT
        CREATE TABLE IF NOT EXISTS health (
            id           INTEGER PRIMARY KEY AUTOINCREMENT,
            dog_id       INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
            typ          TEXT    NOT NULL,  -- impfung | entwurmung | tierarzt | medikament | gewicht
            bezeichnung  TEXT    NOT NULL,
            datum        TEXT    NOT NULL,
            naechstes    TEXT,
            notiz        TEXT,
            wert         REAL,             -- für Gewicht o.ä.
            einheit      TEXT,
            erinnerung   INTEGER NOT NULL DEFAULT 1,
            created_at   TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_health_dog ON health(dog_id, datum DESC);

        -- GIFTKÖDER-ALARM
        CREATE TABLE IF NOT EXISTS poison (
            id           INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id      INTEGER NOT NULL REFERENCES users(id),
            lat          REAL    NOT NULL,
            lon          REAL    NOT NULL,
            beschreibung TEXT,
            typ          TEXT    DEFAULT 'unbekannt',
            foto_url     TEXT,
            bestaetigt   INTEGER NOT NULL DEFAULT 0,
            bestaetigt_von INTEGER,
            geloest      INTEGER NOT NULL DEFAULT 0,
            expires_at   TEXT    NOT NULL,  -- auto-expire nach 7 Tagen
            created_at   TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_poison_location ON poison(lat, lon);
        CREATE INDEX IF NOT EXISTS idx_poison_active   ON poison(geloest, expires_at);

        -- ORTE (hundefreundliche Orte, Kotbeutelspender, etc.)
        CREATE TABLE IF NOT EXISTS places (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER REFERENCES users(id),
            name        TEXT    NOT NULL,
            typ         TEXT    NOT NULL,  -- restaurant | shop | freilauf | kotbeutel | tierarzt | hundeschule
            lat         REAL    NOT NULL,
            lon         REAL    NOT NULL,
            adresse     TEXT,
            website     TEXT,
            hund_rein   INTEGER,
            leine_pflicht INTEGER,
            wasser_fuer_hunde INTEGER,
            foto_url    TEXT,
            bewertung   REAL    DEFAULT 0,
            anz_bewertungen INTEGER DEFAULT 0,
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_places_location ON places(lat, lon);
        CREATE INDEX IF NOT EXISTS idx_places_typ      ON places(typ);

        -- ROUTEN
        CREATE TABLE IF NOT EXISTS routes (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER NOT NULL REFERENCES users(id),
            name        TEXT    NOT NULL,
            beschreibung TEXT,
            gps_track   TEXT    NOT NULL,  -- JSON Array von {lat, lon}
            distanz_km  REAL,
            dauer_min   INTEGER,
            schwierigkeit TEXT  DEFAULT 'leicht',
            untergrund  TEXT,              -- wald | asphalt | wiese | mix
            schatten    INTEGER,
            leine_empfohlen INTEGER,
            bewertung   REAL    DEFAULT 0,
            anz_bewertungen INTEGER DEFAULT 0,
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS route_walks (
            id         INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            route_id   INTEGER REFERENCES routes(id) ON DELETE SET NULL,
            walked_km  REAL    NOT NULL,
            walked_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_route_walks_user ON route_walks(user_id);

        CREATE TABLE IF NOT EXISTS exercise_progress (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            exercise_id TEXT    NOT NULL,
            status      TEXT,
            updated_at  TEXT NOT NULL DEFAULT (datetime('now')),
            UNIQUE(user_id, exercise_id)
        );
        CREATE INDEX IF NOT EXISTS idx_exercise_progress_user ON exercise_progress(user_id);

        CREATE TABLE IF NOT EXISTS training_plan_progress (
            user_id   INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            item_key  TEXT    NOT NULL,
            checked   INTEGER NOT NULL DEFAULT 1,
            checked_at TEXT NOT NULL DEFAULT (datetime('now')),
            PRIMARY KEY (user_id, item_key)
        );

        -- GASSI-TREFFEN
        CREATE TABLE IF NOT EXISTS walks (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER NOT NULL REFERENCES users(id),
            titel       TEXT    NOT NULL,
            datum       TEXT    NOT NULL,
            uhrzeit     TEXT    NOT NULL,
            lat         REAL    NOT NULL,
            lon         REAL    NOT NULL,
            ort_name    TEXT,
            max_teilnehmer INTEGER DEFAULT 10,
            beschreibung TEXT,
            status      TEXT    NOT NULL DEFAULT 'offen',
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );

        -- GASSI-TREFFEN TEILNEHMER
        CREATE TABLE IF NOT EXISTS walk_participants (
            walk_id   INTEGER NOT NULL REFERENCES walks(id) ON DELETE CASCADE,
            user_id   INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            dog_id    INTEGER REFERENCES dogs(id),
            joined_at TEXT    NOT NULL DEFAULT (datetime('now')),
            PRIMARY KEY (walk_id, user_id)
        );

        -- GASSI-TREFFEN ↔ HUNDE (n:m — Teilnehmer kann mehrere Hunde mitbringen)
        CREATE TABLE IF NOT EXISTS walk_participant_dogs (
            walk_id  INTEGER NOT NULL REFERENCES walks(id)  ON DELETE CASCADE,
            user_id  INTEGER NOT NULL REFERENCES users(id)  ON DELETE CASCADE,
            dog_id   INTEGER NOT NULL REFERENCES dogs(id)   ON DELETE CASCADE,
            PRIMARY KEY (walk_id, user_id, dog_id)
        );
        CREATE INDEX IF NOT EXISTS idx_wpd_dog ON walk_participant_dogs(dog_id);

        -- FORUM
        CREATE TABLE IF NOT EXISTS forum_threads (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER NOT NULL REFERENCES users(id),
            dog_id      INTEGER REFERENCES dogs(id),
            titel       TEXT    NOT NULL,
            kategorie   TEXT    NOT NULL DEFAULT 'allgemein',
            rasse_tag   TEXT,
            plz         TEXT,
            views       INTEGER NOT NULL DEFAULT 0,
            pinned      INTEGER NOT NULL DEFAULT 0,
            locked      INTEGER NOT NULL DEFAULT 0,
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_threads_kat ON forum_threads(kategorie, created_at DESC);

        CREATE TABLE IF NOT EXISTS forum_posts (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            thread_id   INTEGER NOT NULL REFERENCES forum_threads(id) ON DELETE CASCADE,
            user_id     INTEGER NOT NULL REFERENCES users(id),
            text        TEXT    NOT NULL,
            created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
            edited_at   TEXT
        );

        -- PUSH SUBSCRIPTIONS (alternativ zu users.push_sub für mehrere Geräte)
        CREATE TABLE IF NOT EXISTS push_subscriptions (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            endpoint    TEXT    NOT NULL UNIQUE,
            p256dh      TEXT    NOT NULL,
            auth        TEXT    NOT NULL,
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );

        -- PREMIUM-TRANSAKTIONEN (später für Zahlungsabwicklung)
        CREATE TABLE IF NOT EXISTS premium_orders (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER NOT NULL REFERENCES users(id),
            status      TEXT    NOT NULL DEFAULT 'pending',
            betrag_cent INTEGER NOT NULL,
            zahlungsart TEXT,
            valid_until TEXT,
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );

        -- EVENTS (Hundeveranstaltungen)
        CREATE TABLE IF NOT EXISTS events (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER NOT NULL REFERENCES users(id),
            titel       TEXT    NOT NULL,
            datum       TEXT    NOT NULL,  -- YYYY-MM-DD
            uhrzeit     TEXT,
            lat         REAL,
            lon         REAL,
            ort_name    TEXT,
            typ         TEXT    NOT NULL DEFAULT 'sonstiges',
            beschreibung TEXT,
            link        TEXT,
            status      TEXT    NOT NULL DEFAULT 'aktiv',
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_events_datum ON events(datum ASC);

        -- SITTING — Sitter-Profile
        CREATE TABLE IF NOT EXISTS sitters (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id         INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
            beschreibung    TEXT,
            preis_pro_tag   REAL    DEFAULT 0,
            max_hunde       INTEGER DEFAULT 1,
            lat             REAL,
            lon             REAL,
            radius_km       INTEGER DEFAULT 20,
            services        TEXT    DEFAULT '[]',  -- JSON: ['tagesbetreuung','uebernachtung','gassi','hausbesuch']
            aktiv           INTEGER NOT NULL DEFAULT 1,
            created_at      TEXT    NOT NULL DEFAULT (datetime('now'))
        );

        -- SITTING — Anfragen
        CREATE TABLE IF NOT EXISTS sitting_requests (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER NOT NULL REFERENCES users(id),   -- Anfragender
            sitter_id   INTEGER NOT NULL REFERENCES sitters(id),
            dog_ids     TEXT    DEFAULT '[]',  -- JSON Array
            von         TEXT    NOT NULL,      -- YYYY-MM-DD
            bis         TEXT    NOT NULL,
            nachricht   TEXT,
            status      TEXT    NOT NULL DEFAULT 'offen',  -- offen|angenommen|abgelehnt|abgebrochen
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );

        -- OSM POI-Cache (Mülleimer, Hundewiesen, Wasserstellen aus OpenStreetMap)
        CREATE TABLE IF NOT EXISTS osm_pois (
            osm_id      INTEGER NOT NULL,
            type        TEXT    NOT NULL,
            lat         REAL    NOT NULL,
            lon         REAL    NOT NULL,
            name        TEXT,
            cached_at   TEXT    NOT NULL DEFAULT (datetime('now')),
            PRIMARY KEY (osm_id, type)
        );
        CREATE INDEX IF NOT EXISTS idx_osm_pois_loc ON osm_pois(type, lat, lon);

        -- VERLORENE HUNDE
        CREATE TABLE IF NOT EXISTS lost_dogs (
            id           INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id      INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            dog_id       INTEGER REFERENCES dogs(id) ON DELETE SET NULL,
            name         TEXT    NOT NULL,
            rasse        TEXT,
            beschreibung TEXT    NOT NULL,
            foto_url     TEXT,
            lat          REAL    NOT NULL,
            lon          REAL    NOT NULL,
            is_active    INTEGER NOT NULL DEFAULT 1,
            gefunden_at  TEXT,
            created_at   TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_lost_active ON lost_dogs(is_active, created_at DESC);

        -- OSM Tile-Cache: welche Kacheln wurden schon geladen?
        CREATE TABLE IF NOT EXISTS osm_tiles (
            type        TEXT NOT NULL,
            tile_key    TEXT NOT NULL,  -- "zoom_x_y"
            cached_at   TEXT NOT NULL DEFAULT (datetime('now')),
            PRIMARY KEY (type, tile_key)
        );

        -- Community-Pins: von Nutzern gesetzte Karten-Marker
        CREATE TABLE IF NOT EXISTS user_map_pois (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER REFERENCES users(id) ON DELETE SET NULL,
            type        TEXT    NOT NULL,  -- waste_basket | drinking_water | dog_park | sonstiges
            lat         REAL    NOT NULL,
            lon         REAL    NOT NULL,
            name        TEXT,
            notiz       TEXT,
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_user_pois_loc ON user_map_pois(type, lat, lon);

        -- Meldungen: ungültige OSM- oder Community-Marker
        CREATE TABLE IF NOT EXISTS osm_reports (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id     INTEGER REFERENCES users(id) ON DELETE SET NULL,
            osm_id      INTEGER,           -- gesetzt wenn OSM-Marker gemeldet
            user_poi_id INTEGER,           -- gesetzt wenn Community-Marker gemeldet
            type        TEXT,
            grund       TEXT    NOT NULL,  -- existiert_nicht | falsche_position | spam | sonstiges
            created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
        );

        -- TIERÄRZTE (user-level, nie löschen — Historien-Erhalt bei Umzug)
        CREATE TABLE IF NOT EXISTS tieraerzte (
            id               INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id          INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            name             TEXT    NOT NULL,
            strasse          TEXT,
            plz              TEXT,
            ort              TEXT,
            telefon          TEXT,
            notfall_telefon  TEXT,
            email            TEXT,
            website          TEXT,
            notizen          TEXT,
            ist_notfallpraxis INTEGER NOT NULL DEFAULT 0,
            aktiv            INTEGER NOT NULL DEFAULT 1,
            created_at       TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_tieraerzte_user ON tieraerzte(user_id, aktiv);

        """)

    # Migrations: neue Spalten zu bestehenden Tabellen hinzufügen (idempotent)
    _migrate(conn_factory=db)

    logger.info("Datenbank initialisiert.")


def _migrate(conn_factory):
    """Fügt fehlende Spalten hinzu und führt Datenmigration durch (idempotent)."""
    migrations = [
        # Giftköder: Auflösungs-Details für spätere KI-Analyse
        ("poison", "geloest_von",   "INTEGER"),
        ("poison", "geloest_at",    "TEXT"),
        ("poison", "geloest_grund", "TEXT"),
        # Gesundheit: erweiterte Felder je nach Eintragstyp
        ("health", "charge_nr",     "TEXT"),
        ("health", "tierarzt_name", "TEXT"),
        ("health", "kosten",        "REAL"),
        ("health", "diagnose",      "TEXT"),
        ("health", "dosierung",     "TEXT"),
        ("health", "haeufigkeit",   "TEXT"),
        ("health", "aktiv",         "INTEGER NOT NULL DEFAULT 1"),
        ("health", "bis_datum",     "TEXT"),
        ("health", "schweregrad",   "TEXT"),
        ("health", "reaktion",      "TEXT"),
        ("health", "datei_url",     "TEXT"),
        ("health", "datei_typ",     "TEXT"),
        ("health", "tierarzt_id",   "INTEGER"),
        # Tierärzte: Adresse aufgeteilt in Strasse/PLZ/Ort
        ("tieraerzte", "strasse",       "TEXT"),
        ("tieraerzte", "plz",           "TEXT"),
        ("tieraerzte", "ort",           "TEXT"),
        ("tieraerzte", "opening_hours", "TEXT"),
        ("tieraerzte", "lat",           "REAL"),
        ("tieraerzte", "lon",           "REAL"),
        ("tieraerzte", "osm_id",        "TEXT"),
        # Gesundheit: Erinnerungsintervall für wiederkehrende Einträge
        ("health", "intervall_tage", "INTEGER"),
        # Routen: neue Felder
        ("routes", "is_public",          "INTEGER NOT NULL DEFAULT 1"),
        ("routes", "hunde_tauglichkeit", "TEXT"),
        ("routes", "foto_urls",          "TEXT NOT NULL DEFAULT '[]'"),
        # OSM POIs: Kontaktdaten
        ("osm_pois", "opening_hours", "TEXT"),
        ("osm_pois", "phone",         "TEXT"),
        ("osm_pois", "website",       "TEXT"),
        ("osm_pois", "user_edited",   "INTEGER NOT NULL DEFAULT 0"),
        # Forum: Threads brauchen text + antworten-Zähler
        ("forum_threads", "text",      "TEXT NOT NULL DEFAULT ''"),
        ("forum_threads", "antworten", "INTEGER NOT NULL DEFAULT 0"),
        # Forum Sprint 11: erweiterte Thread-Felder
        ("forum_threads", "foto_urls",  "TEXT"),
        ("forum_threads", "is_pinned",  "INTEGER NOT NULL DEFAULT 0"),
        ("forum_threads", "is_locked",  "INTEGER NOT NULL DEFAULT 0"),
        ("forum_threads", "is_deleted", "INTEGER NOT NULL DEFAULT 0"),
        ("forum_threads", "likes",      "INTEGER NOT NULL DEFAULT 0"),
        # Forum Sprint 11: erweiterte Post-Felder
        ("forum_posts", "foto_urls",  "TEXT"),
        ("forum_posts", "is_deleted", "INTEGER NOT NULL DEFAULT 0"),
        ("forum_posts", "likes",      "INTEGER NOT NULL DEFAULT 0"),
        # Users: Moderator-Flag + Forum-Standort
        ("users", "is_moderator",          "INTEGER NOT NULL DEFAULT 0"),
        ("users", "forum_lat",             "REAL"),
        ("users", "forum_lon",             "REAL"),
        ("users", "forum_show_location",   "INTEGER NOT NULL DEFAULT 0"),
        # Events: Quelle + externe ID für gescrapte Events
        ("events", "quelle",      "TEXT NOT NULL DEFAULT 'nutzer'"),
        ("events", "external_id", "TEXT"),
        # Admin: User-Sperre
        ("users", "is_banned",    "INTEGER NOT NULL DEFAULT 0"),
        ("users", "ban_reason",   "TEXT"),
        # WebCal: Kalender-Abo-Token
        ("users", "calendar_token", "TEXT"),
        # User-Profil-Felder
        ("users", "email_verified",        "INTEGER NOT NULL DEFAULT 0"),
        ("users", "verification_token",    "TEXT"),
        ("users", "bio",                  "TEXT"),
        ("users", "wohnort",              "TEXT"),
        ("users", "erfahrung",            "TEXT"),
        ("users", "social_link",          "TEXT"),
        ("users", "profil_sichtbarkeit",  "TEXT NOT NULL DEFAULT 'public'"),
        ("users", "avatar_url",           "TEXT"),
        ("places", "telefon",             "TEXT"),
        # Chat: Foto-Versand + Read Receipts — nur wenn Tabelle existiert (wird später per CREATE IF NOT EXISTS angelegt)
        # Hinweis: Wird nach dem direct_messages-CREATE unten nochmal als separate Migration behandelt
        # Chat: Online-Indikator
        ("users", "last_seen",            "TEXT"),
        # Foto-Editor: Zoom + Position
        ("dogs", "foto_zoom",     "REAL NOT NULL DEFAULT 1.0"),
        ("dogs", "foto_offset_x", "REAL NOT NULL DEFAULT 0.0"),
        ("dogs", "foto_offset_y", "REAL NOT NULL DEFAULT 0.0"),
        # Tagebuch: Ortsname (POI/Adresse)
        ("diary", "location_name", "TEXT"),
        # Bewertungen: walks + sitters brauchen bewertung + anz_bewertungen
        ("walks",   "bewertung",        "REAL DEFAULT 0"),
        ("walks",   "anz_bewertungen",  "INTEGER DEFAULT 0"),
        ("sitters", "bewertung",        "REAL DEFAULT 0"),
        ("sitters", "anz_bewertungen",  "INTEGER DEFAULT 0"),
        # Tagebuch-Medien: Cover-Bild markieren
        ("diary_media", "is_cover", "INTEGER NOT NULL DEFAULT 0"),
        # Forum-Threads: optionaler Standort
        ("forum_threads", "thread_lat", "REAL"),
        ("forum_threads", "thread_lon", "REAL"),
        ("forum_threads", "thread_ort", "TEXT"),
        # Referral (idempotent, falls try/except-Block oben fehlgeschlagen ist)
        ("users", "referral_code", "TEXT"),
        ("users", "referred_by",   "INTEGER"),
        # Routen: Original-Werte für Gamification (bleiben nach Kürzen erhalten)
        ("routes", "original_km",       "REAL"),
        ("routes", "original_dauer_min","INTEGER"),
        # Gamification: Streaks
        ("users", "current_streak",    "INTEGER NOT NULL DEFAULT 0"),
        ("users", "max_streak",        "INTEGER NOT NULL DEFAULT 0"),
        ("users", "last_activity_date","TEXT"),
        # Social Media Manager
        ("users", "is_social_media",   "INTEGER NOT NULL DEFAULT 0"),
        ("social_content", "coaching",     "TEXT"),
        ("social_content", "media_url",    "TEXT"),
        ("social_content", "category",     "TEXT"),
        ("social_content", "exercise_id",  "TEXT"),
        ("social_content", "post_url",     "TEXT"),
        ("dogs",           "rasse_id",     "INTEGER"),
        # Pflege: Schere vs. Trimmen unterscheiden
        ("pflege_tipps",   "fell_pflege_art", "TEXT"),
        # Wiki-Foto-Einreichungen: Bildrechte-Bestätigung
        ("wiki_foto_submissions", "rights_confirmed", "INTEGER NOT NULL DEFAULT 0"),
        # Tagebuch: Wetter + POI-Metadaten beim Eintrag
        ("diary", "weather_json", "TEXT"),
        ("diary", "poi_json",     "TEXT"),
        # Notizen: Ort + Label + KI-Assistent User-Setting
        ("notes", "location_name", "TEXT"),
        ("notes", "parent_label",  "TEXT"),
        ("users", "notes_ki_enabled", "INTEGER NOT NULL DEFAULT 1"),
        # Züchter-Rolle
        ("users",   "breeder_status", "TEXT"),
        # Würfe: Verknüpfung mit Zuchtkartei-Hunden + Welfare
        ("litters", "vater_id",                "INTEGER"),
        ("litters", "mutter_id",               "INTEGER"),
        ("litters", "welfare_level",           "TEXT"),
        ("litters", "welfare_acknowledged",    "INTEGER NOT NULL DEFAULT 0"),
        # KI-Züchter-Features (pro User an/abschaltbar, außer Tierschutz)
        ("users", "ki_zucht_wurfankuendigung",  "INTEGER NOT NULL DEFAULT 1"),
        ("users", "ki_zucht_genetik",           "INTEGER NOT NULL DEFAULT 1"),
        ("users", "ki_zucht_paarung",           "INTEGER NOT NULL DEFAULT 1"),
        ("users", "ki_zucht_beschreibung",      "INTEGER NOT NULL DEFAULT 1"),
        ("users", "ki_zucht_jahresbericht",     "INTEGER NOT NULL DEFAULT 1"),
        # Partner-Code + Gründer-Lizenz
        ("users", "is_founder",          "INTEGER NOT NULL DEFAULT 0"),
        ("users", "is_partner",          "INTEGER NOT NULL DEFAULT 0"),
        ("users", "founder_number",      "INTEGER"),
        ("users", "is_founder_pending",  "INTEGER NOT NULL DEFAULT 0"),
        # Passwort-Zurücksetzen
        ("users", "password_reset_token",   "TEXT"),
        ("users", "password_reset_expires", "TEXT"),
    ]
    with conn_factory() as conn:
        for table, column, col_type in migrations:
            existing = [
                row[1] for row in
                conn.execute(f"PRAGMA table_info({table})").fetchall()
            ]
            if column not in existing:
                conn.execute(
                    f"ALTER TABLE {table} ADD COLUMN {column} {col_type}"
                )
                logger.info(f"Migration: {table}.{column} hinzugefügt.")

        # Social Media Manager
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS social_content (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                created_at      TEXT    NOT NULL DEFAULT (datetime('now')),
                created_by      INTEGER REFERENCES users(id) ON DELETE SET NULL,
                platform        TEXT    NOT NULL DEFAULT 'both',
                format          TEXT    NOT NULL DEFAULT 'post',
                topic           TEXT    NOT NULL,
                caption         TEXT,
                hashtags        TEXT,
                visual_brief    TEXT,
                image_prompt    TEXT,
                canva_notes     TEXT,
                script          TEXT,
                hook            TEXT,
                cta             TEXT,
                unsplash_query  TEXT,
                ai_score        INTEGER,
                status          TEXT    NOT NULL DEFAULT 'idea',
                scheduled_at    TEXT,
                published_at    TEXT,
                source          TEXT    NOT NULL DEFAULT 'generated',
                breed_id        INTEGER REFERENCES wiki_rassen(id) ON DELETE SET NULL,
                coaching        TEXT,
                notes           TEXT,
                media_url       TEXT,
                category        TEXT,
                exercise_id     TEXT
            );
            CREATE INDEX IF NOT EXISTS idx_social_content_status
                ON social_content(status);
        """)

        # Training-Übungen (für Social Media + Auswertung)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS training_exercises (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                exercise_id     TEXT    NOT NULL UNIQUE,
                name            TEXT    NOT NULL,
                kategorie       TEXT    NOT NULL,
                schwierigkeit   TEXT,
                alter_ab        TEXT,
                dauer           TEXT,
                beschreibung    TEXT,
                schritte        TEXT,
                tipp            TEXT,
                created_at      TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_training_exercises_kat
                ON training_exercises(kategorie);
        """)

        # Pflege-Tipps (für Social Media + User-spezifisch)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS pflege_tipps (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                tipp_id         TEXT    NOT NULL UNIQUE,
                titel           TEXT    NOT NULL,
                kategorie       TEXT    NOT NULL,
                beschreibung    TEXT,
                schritte        TEXT,
                materialien     TEXT,
                haeufigkeit     TEXT,
                fell_typ        TEXT,
                saison          TEXT,
                rassengruppe    TEXT,
                tipp            TEXT,
                created_at      TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_pflege_tipps_kat
                ON pflege_tipps(kategorie);
        """)

        # Knigge: Community-Votes
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS knigge_votes (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                szenario_id TEXT    NOT NULL,
                user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                answer      TEXT    NOT NULL,
                created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
                UNIQUE(szenario_id, user_id)
            );
        """)

        # Forum Sprint 11: neue Tabellen
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS forum_likes (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                target_type TEXT    NOT NULL,
                target_id   INTEGER NOT NULL,
                UNIQUE(user_id, target_type, target_id)
            );

            CREATE TABLE IF NOT EXISTS forum_reports (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                target_type TEXT    NOT NULL,
                target_id   INTEGER NOT NULL,
                grund       TEXT    NOT NULL,
                created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
                resolved    INTEGER NOT NULL DEFAULT 0
            );
        """)

        # Wiki: Community-Berichte
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS wiki_berichte (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                rasse       TEXT    NOT NULL,
                titel       TEXT    NOT NULL,
                text        TEXT    NOT NULL,
                created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_wiki_berichte_rasse ON wiki_berichte(rasse, created_at DESC);
        """)

        # Hunde-Filme: Katalog + Bewertungen + Hund des Monats
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS movies (
                id              TEXT    PRIMARY KEY,
                titel           TEXT    NOT NULL,
                originaltitel   TEXT,
                jahr            INTEGER,
                genre           TEXT,
                typ             TEXT    NOT NULL DEFAULT 'film',
                hund_rasse      TEXT,
                stirbt_der_hund INTEGER NOT NULL DEFAULT 0,
                beschreibung    TEXT,
                bild_emoji      TEXT    DEFAULT '🐾',
                imdb_rating     REAL,
                streaming       TEXT,
                sort_order      INTEGER DEFAULT 0,
                created_at      TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_movies_typ  ON movies(typ);
            CREATE INDEX IF NOT EXISTS idx_movies_jahr ON movies(jahr DESC);
        """)

        conn.executescript("""
            CREATE TABLE IF NOT EXISTS movie_votes (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                film_id     TEXT    NOT NULL,
                bewertung   INTEGER NOT NULL,
                created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
                UNIQUE(user_id, film_id)
            );

            CREATE TABLE IF NOT EXISTS hund_des_monats_votes (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                dog_id      INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                monat       TEXT    NOT NULL,
                created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
                UNIQUE(user_id, monat)
            );
        """)

        # Events: Unique-Index für externe IDs (idempotent)
        conn.executescript("""
            CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external
            ON events(external_id) WHERE external_id IS NOT NULL;
        """)

        # Users: Eindeutiger Benutzername (case-insensitive via COLLATE NOCASE)
        conn.executescript("""
            CREATE UNIQUE INDEX IF NOT EXISTS idx_users_name_unique
            ON users(name COLLATE NOCASE);
        """)

        # Wiki: User-Foto-Einreichungen
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS wiki_foto_submissions (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id      INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                rasse_id     INTEGER NOT NULL REFERENCES wiki_rassen(id) ON DELETE CASCADE,
                foto_url     TEXT NOT NULL,
                status       TEXT NOT NULL DEFAULT 'pending',
                created_at   TEXT NOT NULL DEFAULT (datetime('now')),
                reviewed_by  INTEGER REFERENCES users(id),
                reviewed_at  TEXT,
                reject_reason TEXT
            );
            CREATE INDEX IF NOT EXISTS idx_wfs_status ON wiki_foto_submissions(status, created_at DESC);
        """)

        # Freundschaften + Direktnachrichten
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS friendships (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                requester_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                addressee_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                status       TEXT NOT NULL DEFAULT 'pending',
                created_at   TEXT NOT NULL DEFAULT (datetime('now')),
                updated_at   TEXT,
                UNIQUE(requester_id, addressee_id)
            );
            CREATE INDEX IF NOT EXISTS idx_friendships_addressee ON friendships(addressee_id, status);
            CREATE INDEX IF NOT EXISTS idx_friendships_requester ON friendships(requester_id, status);

            CREATE TABLE IF NOT EXISTS conversations (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                user_a_id   INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                user_b_id   INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                last_msg_at TEXT,
                a_read_at   TEXT,
                b_read_at   TEXT,
                created_at  TEXT NOT NULL DEFAULT (datetime('now')),
                UNIQUE(user_a_id, user_b_id)
            );

            CREATE TABLE IF NOT EXISTS direct_messages (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
                sender_id       INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                text            TEXT NOT NULL,
                is_deleted      INTEGER NOT NULL DEFAULT 0,
                created_at      TEXT NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_dm_conv ON direct_messages(conversation_id, created_at ASC);
        """)
        # Chat-Spalten sicher nach CREATE hinzufügen
        _dm_cols = [r[1] for r in conn.execute("PRAGMA table_info(direct_messages)").fetchall()]
        for _col, _typedef in [("media_url", "TEXT"), ("media_type", "TEXT"), ("read_at", "TEXT")]:
            if _col not in _dm_cols:
                conn.execute(f"ALTER TABLE direct_messages ADD COLUMN {_col} {_typedef}")

        # Wiki: Rassen-Datenbank (TheDogAPI)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS wiki_rassen (
              id              INTEGER PRIMARY KEY AUTOINCREMENT,
              external_id     INTEGER UNIQUE,
              name            TEXT NOT NULL,
              name_de         TEXT,
              gruppe          TEXT,
              herkunft        TEXT,
              temperament     TEXT,
              gewicht_min_kg  REAL,
              gewicht_max_kg  REAL,
              groesse         TEXT,
              lebensdauer     TEXT,
              foto_url        TEXT,
              bred_for        TEXT,
              aktivitaet      TEXT,
              wohnung_geeignet INTEGER DEFAULT 0,
              kinder_geeignet  INTEGER DEFAULT 1,
              erfahrung        TEXT DEFAULT 'anfaenger',
              slug             TEXT UNIQUE,
              created_at       TEXT DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_wiki_rassen_slug   ON wiki_rassen(slug);
            CREATE INDEX IF NOT EXISTS idx_wiki_rassen_gruppe ON wiki_rassen(gruppe);
        """)

        # Datenmigration: diary_dogs für bestehende Einträge befüllen
        conn.execute("""
            INSERT OR IGNORE INTO diary_dogs (diary_id, dog_id)
            SELECT id, dog_id FROM diary
        """)
        logger.info("Migration: diary_dogs Backfill abgeschlossen.")

        # Benachrichtigungs-Center
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS notifications (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                type       TEXT    NOT NULL,
                title      TEXT    NOT NULL,
                body       TEXT,
                data       TEXT,
                read_at    TEXT,
                created_at TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id, created_at DESC);
        """)

        # Hund-Teilen: Einladungssystem
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS dog_shares (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                dog_id          INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                owner_id        INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                shared_with_id  INTEGER REFERENCES users(id) ON DELETE SET NULL,
                invite_token    TEXT    NOT NULL UNIQUE,
                role            TEXT    NOT NULL DEFAULT 'editor',
                accepted_at     TEXT,
                created_at      TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_dog_shares_dog   ON dog_shares(dog_id);
            CREATE INDEX IF NOT EXISTS idx_dog_shares_token ON dog_shares(invite_token);
            CREATE INDEX IF NOT EXISTS idx_dog_shares_user  ON dog_shares(shared_with_id);
        """)
        logger.info("Migration: dog_shares Tabelle bereit.")

        # Event-RSVP
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS event_rsvp (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                event_id   INTEGER NOT NULL,
                user_id    INTEGER NOT NULL,
                status     TEXT    DEFAULT 'going',
                created_at TEXT    DEFAULT (datetime('now')),
                UNIQUE(event_id, user_id)
            );
            CREATE INDEX IF NOT EXISTS idx_event_rsvp_event ON event_rsvp(event_id);
        """)
        logger.info("Migration: event_rsvp Tabelle bereit.")

        # Events: user_id NOT NULL Constraint entfernen (für Scheduler-Imports ohne User)
        _ev_cols = {r[1]: r[3] for r in conn.execute("PRAGMA table_info(events)").fetchall()}
        if _ev_cols.get("user_id") == 1:
            conn.executescript("""
                CREATE TABLE IF NOT EXISTS events_new (
                    id          INTEGER PRIMARY KEY AUTOINCREMENT,
                    user_id     INTEGER,
                    titel       TEXT    NOT NULL,
                    datum       TEXT    NOT NULL,
                    uhrzeit     TEXT,
                    lat         REAL,
                    lon         REAL,
                    ort_name    TEXT,
                    typ         TEXT    NOT NULL DEFAULT 'sonstiges',
                    beschreibung TEXT,
                    link        TEXT,
                    status      TEXT    NOT NULL DEFAULT 'aktiv',
                    created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
                    quelle      TEXT    NOT NULL DEFAULT 'nutzer',
                    external_id TEXT
                );
                INSERT OR IGNORE INTO events_new SELECT * FROM events;
                DROP TABLE events;
                ALTER TABLE events_new RENAME TO events;
                CREATE INDEX IF NOT EXISTS idx_events_datum ON events(datum ASC);
                CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external
                    ON events(external_id) WHERE external_id IS NOT NULL;
            """)
            logger.info("Migration: events.user_id NOT NULL Constraint entfernt.")

        # Service-Angebote: Sitting + Walks Matching
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS service_offers (
                id            INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id       INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                type          TEXT    NOT NULL,   -- 'sitting' oder 'walks'
                beschreibung  TEXT,
                preis_pro_tag REAL,
                lat           REAL,
                lon           REAL,
                radius_km     INTEGER DEFAULT 10,
                aktiv         INTEGER DEFAULT 1,
                created_at    TEXT    DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_service_offers_type  ON service_offers(type, aktiv);
            CREATE INDEX IF NOT EXISTS idx_service_offers_user  ON service_offers(user_id, type);
        """)

        # Ratings — einheitliches Bewertungssystem
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS ratings (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                target_type TEXT    NOT NULL,
                target_id   INTEGER NOT NULL,
                stars       INTEGER NOT NULL,
                kommentar   TEXT,
                created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
                UNIQUE(user_id, target_type, target_id)
            );
            CREATE INDEX IF NOT EXISTS idx_ratings_target ON ratings(target_type, target_id);
        """)
        logger.info("Migration: ratings Tabelle bereit.")

        # Tagebuch: mehrere Mediendateien pro Eintrag
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS diary_media (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                diary_id   INTEGER NOT NULL REFERENCES diary(id) ON DELETE CASCADE,
                url        TEXT    NOT NULL,
                media_type TEXT    NOT NULL DEFAULT 'image',
                sort_order INTEGER NOT NULL DEFAULT 0,
                created_at TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_diary_media_entry ON diary_media(diary_id, sort_order);
        """)
        logger.info("Migration: diary_media Tabelle bereit.")

        # Gesundheit: mehrere Mediendateien pro Eintrag
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS health_media (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                health_id  INTEGER NOT NULL REFERENCES health(id) ON DELETE CASCADE,
                url        TEXT    NOT NULL,
                media_type TEXT    NOT NULL DEFAULT 'image',
                sort_order INTEGER NOT NULL DEFAULT 0,
                created_at TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_health_media_entry ON health_media(health_id, sort_order);
        """)
        logger.info("Migration: health_media Tabelle bereit.")

        # Gasthund-Zugang: Sitter darf temporär für Hund schreiben
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS sitting_subscriptions (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                dog_id      INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                owner_id    INTEGER NOT NULL REFERENCES users(id),
                sitter_id   INTEGER NOT NULL REFERENCES users(id),
                valid_until TEXT    NOT NULL,
                created_at  TEXT    DEFAULT (datetime('now')),
                UNIQUE(dog_id, sitter_id)
            );
            CREATE INDEX IF NOT EXISTS idx_sitting_sub_sitter ON sitting_subscriptions(sitter_id, valid_until);
            CREATE INDEX IF NOT EXISTS idx_sitting_sub_owner  ON sitting_subscriptions(owner_id);
        """)
        logger.info("Migration: sitting_subscriptions Tabelle bereit.")

        # Walk-Einladungen (RSVP)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS walk_invitations (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                walk_id      INTEGER NOT NULL REFERENCES walks(id) ON DELETE CASCADE,
                user_id      INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                status       TEXT    NOT NULL DEFAULT 'invited',
                invited_at   TEXT    NOT NULL DEFAULT (datetime('now')),
                responded_at TEXT,
                UNIQUE(walk_id, user_id)
            );
            CREATE INDEX IF NOT EXISTS idx_walk_inv_walk ON walk_invitations(walk_id);
            CREATE INDEX IF NOT EXISTS idx_walk_inv_user ON walk_invitations(user_id, status);
        """)
        logger.info("Migration: walk_invitations Tabelle bereit.")

        # Referral-Code für jeden User (einmalig generiert)
        try:
            conn.execute("ALTER TABLE users ADD COLUMN referral_code TEXT UNIQUE")
            conn.execute("ALTER TABLE users ADD COLUMN referred_by INTEGER REFERENCES users(id)")
            # Bestehende User bekommen einen Code
            import secrets, string
            rows = conn.execute("SELECT id FROM users WHERE referral_code IS NULL").fetchall()
            for r in rows:
                code = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8))
                conn.execute("UPDATE users SET referral_code=? WHERE id=?", (code, r['id']))
            logger.info("Migration: referral_code + referred_by zu users hinzugefügt.")
        except Exception:
            pass

        # Gamification: Badge-Tabelle
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS user_badges (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                badge_id   TEXT    NOT NULL,
                earned_at  TEXT    NOT NULL DEFAULT (datetime('now')),
                UNIQUE(user_id, badge_id)
            );
            CREATE INDEX IF NOT EXISTS idx_user_badges_user ON user_badges(user_id);
        """)
        logger.info("Migration: user_badges Tabelle bereit.")

        # Username / Real-Name Trennung
        try:
            conn.execute("ALTER TABLE users ADD COLUMN real_name TEXT")
            # Bestehende User: Leerzeichen im name → name enthält echten Namen → in real_name verschieben
            rows = conn.execute("SELECT id, name FROM users WHERE name LIKE '% %'").fetchall()
            for r in rows:
                first_word = r['name'].split()[0]
                conn.execute("UPDATE users SET real_name=?, name=? WHERE id=?",
                             (r['name'], first_word, r['id']))
            logger.info("Migration: real_name Spalte hinzugefügt, %d User migriert.", len(rows))
        except Exception:
            pass

        # Wiki: Rassen-Anreicherung
        for col, typedef in [
            ("beschreibung",    "TEXT"),
            ("vorkommen_de",    "TEXT"),
            ("wikipedia_url_de","TEXT"),
            ("ki_enriched",     "INTEGER DEFAULT 0"),
            ("ki_model",        "TEXT"),
            ("ki_source",       "TEXT"),
        ]:
            try:
                conn.execute(f"ALTER TABLE wiki_rassen ADD COLUMN {col} {typedef}")
            except Exception:
                pass
        logger.info("Migration: wiki_rassen Anreicherungs-Felder bereit.")

        # Moderation-Logging: resolved_by/at für forum_reports, verified_by/at/reject für wiki_zuchter
        for table, col, typedef in [
            ("forum_reports", "resolved_by",  "INTEGER"),
            ("forum_reports", "resolved_at",  "TEXT"),
            ("wiki_zuchter",  "verified_by",   "INTEGER"),
            ("wiki_zuchter",  "verified_at",   "TEXT"),
            ("wiki_zuchter",  "reject_reason", "TEXT"),
        ]:
            try:
                conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}")
            except Exception:
                pass

        # Wiki: Züchter-Verzeichnis
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS wiki_zuchter (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                rasse_slug  TEXT NOT NULL,
                name        TEXT NOT NULL,
                zwingername TEXT,
                ort         TEXT,
                plz         TEXT,
                bundesland  TEXT,
                vdh_mitglied INTEGER DEFAULT 0,
                website     TEXT,
                telefon     TEXT,
                beschreibung TEXT,
                verified    INTEGER DEFAULT 0,
                user_id     INTEGER REFERENCES users(id) ON DELETE SET NULL,
                created_at  TEXT DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_wiki_zuchter_rasse ON wiki_zuchter(rasse_slug, verified);
        """)
        logger.info("Migration: wiki_zuchter Tabelle bereit.")

        # Wiki: Rasse-Interesse ("So einen hab ich" / "Interessiert mich")
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS wiki_breed_interest (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                rasse_slug  TEXT    NOT NULL,
                typ         TEXT    NOT NULL DEFAULT 'hat',
                created_at  TEXT    DEFAULT (datetime('now')),
                UNIQUE(user_id, rasse_slug)
            );
            CREATE INDEX IF NOT EXISTS idx_wbi_rasse ON wiki_breed_interest(rasse_slug, typ);
            CREATE INDEX IF NOT EXISTS idx_wbi_user  ON wiki_breed_interest(user_id);
        """)
        logger.info("Migration: wiki_breed_interest Tabelle bereit.")

        # Training: Session-Protokoll
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS training_sessions (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id         INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                dog_id          INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                exercise_id     TEXT    NOT NULL,
                exercise_name   TEXT    NOT NULL,
                datum           TEXT    NOT NULL DEFAULT (date('now')),
                wiederholungen  INTEGER DEFAULT 1,
                erfolgsquote    INTEGER DEFAULT 50,
                hund_stimmung   TEXT    DEFAULT 'aufmerksam',
                zufriedenheit   INTEGER DEFAULT 3,
                notiz           TEXT,
                ist_top         INTEGER DEFAULT 0,
                diary_entry_id  INTEGER REFERENCES diary(id) ON DELETE SET NULL,
                created_at      TEXT    DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_ts_user_dog  ON training_sessions(user_id, dog_id, datum DESC);
            CREATE INDEX IF NOT EXISTS idx_ts_exercise   ON training_sessions(exercise_id, user_id);
        """)
        logger.info("Migration: training_sessions Tabelle bereit.")

        # Training: KI-Feedback-Cache
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS training_ki_cache (
                dog_id       INTEGER PRIMARY KEY,
                feedback     TEXT    NOT NULL,
                generated_at TEXT    NOT NULL DEFAULT (datetime('now'))
            );
        """)
        logger.info("Migration: training_ki_cache Tabelle bereit.")

        # Fortschritts-Lober: wöchentliche Lob-Karten
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS weekly_praise (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id      INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                dog_id       INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                week_key     TEXT NOT NULL,  -- ISO-Woche, z.B. "2026-W17"
                praise_text  TEXT NOT NULL,
                stats_json   TEXT,           -- JSON der gesammelten Stats (für Debugging)
                generated_at TEXT NOT NULL DEFAULT (datetime('now')),
                UNIQUE(dog_id, week_key)
            );
            CREATE INDEX IF NOT EXISTS idx_wp_dog_week ON weekly_praise(dog_id, week_key DESC);
        """)
        logger.info("Migration: weekly_praise Tabelle bereit.")

        # Push: Standort-Filter (last_lat/lon für geo-basierte Alerts)
        for col in ["last_lat REAL", "last_lon REAL"]:
            try:
                conn.execute(f"ALTER TABLE push_subscriptions ADD COLUMN {col}")
            except Exception:
                pass
        logger.info("Migration: push_subscriptions last_lat/lon bereit.")

        # Routen: Geschwindigkeits-Validierung
        try:
            conn.execute("ALTER TABLE routes ADD COLUMN is_valid INTEGER NOT NULL DEFAULT 1")
        except Exception:
            pass
        logger.info("Migration: routes.is_valid bereit.")

        # ki_daily_calls: source-Spalte + PK auf (user_id, date, source) erweitern
        ki_cols = {r[1] for r in conn.execute("PRAGMA table_info(ki_daily_calls)").fetchall()}
        if "source" not in ki_cols:
            conn.executescript("""
                ALTER TABLE ki_daily_calls RENAME TO ki_daily_calls_old;
                CREATE TABLE ki_daily_calls (
                    user_id INTEGER NOT NULL,
                    date    TEXT    NOT NULL,
                    count   INTEGER NOT NULL DEFAULT 0,
                    source  TEXT    NOT NULL DEFAULT 'cloud',
                    PRIMARY KEY (user_id, date, source)
                );
                INSERT INTO ki_daily_calls (user_id, date, count, source)
                    SELECT user_id, date, count, 'cloud' FROM ki_daily_calls_old;
                DROP TABLE ki_daily_calls_old;
                CREATE INDEX IF NOT EXISTS idx_ki_daily_source ON ki_daily_calls(date, source);
            """)
        logger.info("Migration: ki_daily_calls.source bereit.")

        # Notizen: generische polymorphe Notiz-Tabelle
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS notes (
                id            INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id       INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                parent_type   TEXT    NOT NULL,
                parent_id     INTEGER NOT NULL,
                text          TEXT    NOT NULL DEFAULT '',
                meta_json     TEXT,
                created_at    TEXT    NOT NULL DEFAULT (datetime('now')),
                updated_at    TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_notes_parent
                ON notes(parent_type, parent_id, created_at DESC);
            CREATE INDEX IF NOT EXISTS idx_notes_user
                ON notes(user_id, created_at DESC);
        """)
        logger.info("Migration: notes Tabelle bereit.")

        conn.execute("""
            CREATE TABLE IF NOT EXISTS ki_health_reports (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                dog_id     INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                bericht    TEXT    NOT NULL,
                erstellt_at TEXT   NOT NULL DEFAULT (datetime('now'))
            )
        """)
        conn.execute("""
            CREATE INDEX IF NOT EXISTS idx_ki_health_reports_dog
                ON ki_health_reports(dog_id, erstellt_at DESC)
        """)
        logger.info("Migration: ki_health_reports Tabelle bereit.")

        conn.execute("""
            CREATE TABLE IF NOT EXISTS osm_poi_edits (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                osm_id      TEXT    NOT NULL,
                poi_name    TEXT    NOT NULL,
                field       TEXT    NOT NULL DEFAULT 'opening_hours',
                old_value   TEXT,
                new_value   TEXT    NOT NULL,
                user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                status      TEXT    NOT NULL DEFAULT 'pending',
                mod_id      INTEGER REFERENCES users(id),
                created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
                resolved_at TEXT
            )
        """)
        conn.execute("""
            CREATE INDEX IF NOT EXISTS idx_osm_poi_edits_status
                ON osm_poi_edits(status, created_at DESC)
        """)
        logger.info("Migration: osm_poi_edits Tabelle bereit.")

        # Einmalige Datenmigration: dogs.gewicht_kg mit aktuellem Gesundheits-Gewicht synchronisieren
        conn.execute("""
            UPDATE dogs SET gewicht_kg = (
                SELECT wert FROM health
                WHERE health.dog_id = dogs.id AND health.typ = 'gewicht' AND health.wert IS NOT NULL
                ORDER BY health.datum DESC, health.id DESC LIMIT 1
            )
            WHERE EXISTS (
                SELECT 1 FROM health
                WHERE health.dog_id = dogs.id AND health.typ = 'gewicht' AND health.wert IS NOT NULL
            )
        """)
        logger.info("Migration: dogs.gewicht_kg aus health synchronisiert.")

        # Performance-Indizes für häufige Queries
        conn.execute("CREATE INDEX IF NOT EXISTS idx_health_naechstes ON health(naechstes) WHERE naechstes IS NOT NULL")
        conn.execute("CREATE INDEX IF NOT EXISTS idx_ki_calls_user_date ON ki_daily_calls(user_id, date)")
        conn.execute("CREATE INDEX IF NOT EXISTS idx_events_user_datum ON events(user_id, datum)")
        conn.execute("CREATE INDEX IF NOT EXISTS idx_diary_created ON diary(dog_id, created_at DESC)")
        conn.execute("CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(user_id, created_at DESC)")
        logger.info("Migration: Performance-Indizes bereit.")

        # Züchter-Tabellen
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS breeder_profiles (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id         INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
                zwingername     TEXT    NOT NULL,
                rasse_text      TEXT    NOT NULL,
                verein          TEXT    NOT NULL,
                vdh_mitglied    INTEGER NOT NULL DEFAULT 0,
                stadt           TEXT    NOT NULL,
                website         TEXT,
                beschreibung    TEXT,
                location_lat    REAL,
                location_lng    REAL,
                verified_at     TEXT,
                created_at      TEXT    NOT NULL DEFAULT (datetime('now'))
            );

            CREATE TABLE IF NOT EXISTS breeder_documents (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id         INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                dokument_typ    TEXT    NOT NULL,
                file_path       TEXT    NOT NULL,
                uploaded_at     TEXT    NOT NULL DEFAULT (datetime('now'))
            );
        """)
        logger.info("Migration: breeder_profiles + breeder_documents bereit.")

        # Würfe + Welpen
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS litters (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                breeder_id      INTEGER NOT NULL REFERENCES breeder_profiles(id) ON DELETE CASCADE,
                vater_name      TEXT,
                mutter_name     TEXT,
                geburt_datum    TEXT,
                erwartetes_datum TEXT,
                welpen_gesamt   INTEGER,
                welpen_verfuegbar INTEGER,
                beschreibung    TEXT,
                gesundheitstests TEXT,
                preis_spanne    TEXT,
                status          TEXT NOT NULL DEFAULT 'geplant',
                sichtbar        INTEGER NOT NULL DEFAULT 1,
                sichtbar_bis    TEXT,
                created_at      TEXT NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_litters_breeder ON litters(breeder_id, created_at DESC);
            CREATE INDEX IF NOT EXISTS idx_litters_status  ON litters(status, sichtbar);

            CREATE TABLE IF NOT EXISTS puppies (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                wurf_id         INTEGER NOT NULL REFERENCES litters(id) ON DELETE CASCADE,
                name            TEXT,
                geschlecht      TEXT,
                farbe           TEXT,
                chip_nr         TEXT,
                geburtsgewicht  REAL,
                status          TEXT NOT NULL DEFAULT 'verfuegbar',
                status_sichtbar INTEGER NOT NULL DEFAULT 1,
                notiz           TEXT,
                created_at      TEXT NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_puppies_wurf ON puppies(wurf_id);

            CREATE TABLE IF NOT EXISTS puppy_weights (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                welpe_id    INTEGER NOT NULL REFERENCES puppies(id) ON DELETE CASCADE,
                gewicht_g   REAL    NOT NULL,
                gemessen_am TEXT    NOT NULL
            );
        """)
        logger.info("Migration: litters + puppies + puppy_weights bereit.")

        # Züchter-Fotos
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS breeder_photos (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                breeder_id      INTEGER NOT NULL REFERENCES breeder_profiles(id) ON DELETE CASCADE,
                entity_type     TEXT    NOT NULL,
                entity_id       INTEGER NOT NULL,
                file_path       TEXT    NOT NULL,
                thumbnail_path  TEXT,
                caption         TEXT,
                is_primary      INTEGER NOT NULL DEFAULT 0,
                visibility      TEXT    NOT NULL DEFAULT 'public',
                sort_order      INTEGER NOT NULL DEFAULT 0,
                uploaded_at     TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_breeder_photos_entity
                ON breeder_photos(entity_type, entity_id);
        """)
        logger.info("Migration: breeder_photos bereit.")

        # Züchter-Hunde-Stammdaten (Stammbaum, Gesundheit, Genetik, Titel)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS zucht_hunde (
                id                INTEGER PRIMARY KEY AUTOINCREMENT,
                breeder_id        INTEGER REFERENCES breeder_profiles(id) ON DELETE SET NULL,
                name              TEXT    NOT NULL,
                rufname           TEXT,
                geschlecht        TEXT,
                geburtsdatum      TEXT,
                sterbedatum       TEXT,
                chip_nr           TEXT,
                taetowiernummer   TEXT,
                zuchtbuchnummer   TEXT,
                farbe             TEXT,
                vater_id          INTEGER REFERENCES zucht_hunde(id),
                mutter_id         INTEGER REFERENCES zucht_hunde(id),
                zuechter_name     TEXT,
                eigentuemer_name  TEXT,
                is_public         INTEGER NOT NULL DEFAULT 1,
                notiz             TEXT,
                foto_url          TEXT,
                created_at        TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_zucht_hunde_breeder ON zucht_hunde(breeder_id);
            CREATE INDEX IF NOT EXISTS idx_zucht_hunde_eltern  ON zucht_hunde(vater_id, mutter_id);

            CREATE TABLE IF NOT EXISTS dog_health_tests (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                hund_id         INTEGER NOT NULL REFERENCES zucht_hunde(id) ON DELETE CASCADE,
                test_typ        TEXT    NOT NULL,
                test_name       TEXT,
                ergebnis        TEXT    NOT NULL,
                untersuch_am    TEXT    NOT NULL,
                gueltig_bis     TEXT,
                untersucher     TEXT,
                labor           TEXT,
                zertifikat_nr   TEXT,
                is_public       INTEGER NOT NULL DEFAULT 1,
                created_at      TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_dog_health_tests_hund ON dog_health_tests(hund_id);

            CREATE TABLE IF NOT EXISTS dog_genetic_tests (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                hund_id         INTEGER NOT NULL REFERENCES zucht_hunde(id) ON DELETE CASCADE,
                marker_name     TEXT    NOT NULL,
                marker_kategorie TEXT,
                genotyp         TEXT,
                ergebnis_klasse TEXT,
                getestet_am     TEXT    NOT NULL,
                labor           TEXT,
                zertifikat_nr   TEXT,
                is_public       INTEGER NOT NULL DEFAULT 1,
                created_at      TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_dog_genetic_tests_hund ON dog_genetic_tests(hund_id);

            CREATE TABLE IF NOT EXISTS dog_titles (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                hund_id         INTEGER NOT NULL REFERENCES zucht_hunde(id) ON DELETE CASCADE,
                titel_typ       TEXT    NOT NULL,
                titel_name      TEXT    NOT NULL,
                verliehen_am    TEXT    NOT NULL,
                ort             TEXT,
                richter         TEXT,
                ausstellung     TEXT,
                formwert        TEXT,
                is_public       INTEGER NOT NULL DEFAULT 1,
                created_at      TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_dog_titles_hund ON dog_titles(hund_id);
        """)
        logger.info("Migration: zucht_hunde + dog_health_tests + dog_genetic_tests + dog_titles bereit.")

        # Läufigkeit: Deckdatum + Wurftermin
        existing_h = [row[1] for row in conn.execute("PRAGMA table_info(health)").fetchall()]
        for col, typedef in [("deckdatum", "TEXT"), ("wurftermin", "TEXT")]:
            if col not in existing_h:
                conn.execute(f"ALTER TABLE health ADD COLUMN {col} {typedef}")
                logger.info(f"Migration: health.{col} hinzugefügt.")

        # Route-Suggest Rate-Limiting
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS route_suggest_usage (
                user_id  INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                week     TEXT    NOT NULL,
                count    INTEGER NOT NULL DEFAULT 0,
                PRIMARY KEY (user_id, week)
            );
        """)
        logger.info("Migration: route_suggest_usage Tabelle bereit.")

        # ORS tägliche Gesamtaufrufe (für Admin-Dashboard)
        existing_ors = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='ors_daily_total'").fetchone()
        if not existing_ors:
            conn.executescript("""
                CREATE TABLE ors_daily_total (
                    date  TEXT PRIMARY KEY,
                    count INTEGER NOT NULL DEFAULT 0
                );
            """)
            logger.info("Migration: ors_daily_total erstellt.")

        # Fehlende Legacy-JS-Übungen nachträglich einfügen
        import json as _json
        _legacy = [
            ('trick_platz_decke', 'Platz auf Decke / Matte', 'Trick', 'leicht', '5-10 min',
             'Der Hund geht auf Kommando auf seine Decke oder Matte und legt sich hin.',
             _json.dumps(['Decke auslegen', 'Hund mit Leckerli auf die Matte locken', 'Markerwort + Leckerli wenn er drauf steht', 'Platz-Signal einbauen', 'Distanz schrittweise erhöhen']),
             'Die Matte zum positiven Ort machen, nie erzwingen.'),
            ('trick_suchspiel', 'Suchspiel / Nasenarbeit', 'Trick', 'leicht', '10-15 min',
             'Der Hund sucht versteckte Leckerlis per Nase — mentale Auslastung pur.',
             _json.dumps(['Leckerli offen zeigen', 'Hund kurz festhalten, Leckerli verstecken', 'Freigabe-Wort und suchen lassen', 'Schwierigkeit langsam steigern']),
             'Nasenarbeit erschöpft mehr als körperliche Aktivität.'),
            ('pb_nicht_springen2', 'Nicht springen / Begrüßung', 'Problemverhalten', 'leicht', '5-10 min',
             'Der Hund begrüßt Menschen ohne hochzuspringen.',
             _json.dumps(['Hund ignorieren wenn er springt (Rücken zudrehen)', 'Erst belohnen wenn alle vier Pfoten am Boden', 'Gäste in die Übung einbeziehen', 'Konsequent sein']),
             'Alle Familienmitglieder müssen gleich reagieren.'),
            ('pb_leinenfuehrigkeit', 'Leinenführigkeit — Nicht ziehen', 'Problemverhalten', 'mittel', '10-20 min',
             'Der Hund läuft locker an der Leine ohne zu ziehen.',
             _json.dumps(['Bei Leinenzug stehen bleiben oder Richtung wechseln', 'Lockere Leine immer belohnen', 'Kurzstrecken üben, nicht lange Spaziergänge']),
             'Dauert Wochen konsequentes Üben — Geduld.'),
            ('pb_bellen_klaffen', 'Bellen / Kläffen', 'Problemverhalten', 'mittel', '10 min',
             'Übermäßiges Bellen auf Kommando reduzieren.',
             _json.dumps(['Auslöser identifizieren', '"Ruhig"-Kommando einführen wenn Pause entsteht', 'Ruhige Pausen direkt belohnen']),
             'Bellen nie durch Schreien lösen — das wirkt wie Mitmachen.'),
            ('pb_enttriggern', 'Enttriggern / Desensibilisierung', 'Problemverhalten', 'schwer', '15-30 min',
             'Den Hund langsam an angstauslösende Reize gewöhnen — für reaktive Hunde.',
             _json.dumps(['Angst-Auslöser in großer Distanz zeigen', 'Bei Ruhe belohnen (kein Stress sichtbar)', 'Distanz sehr langsam verringern', 'Niemals erzwingen']),
             'Immer unterhalb der Stressschwelle bleiben.'),
        ]
        for ex in _legacy:
            if not conn.execute('SELECT 1 FROM training_exercises WHERE exercise_id=?', (ex[0],)).fetchone():
                conn.execute(
                    'INSERT INTO training_exercises (exercise_id,name,kategorie,schwierigkeit,dauer,beschreibung,schritte,tipp) VALUES (?,?,?,?,?,?,?,?)',
                    ex
                )
                logger.info(f"Migration: Übung '{ex[1]}' eingefügt.")

        # Gespeicherte KI-Jahresberichte für Züchter
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS breeder_jahresberichte (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id     INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                breeder_id  INTEGER NOT NULL,
                jahr        INTEGER NOT NULL,
                text        TEXT NOT NULL,
                created_at  TEXT NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_bj_user ON breeder_jahresberichte(user_id, jahr DESC);
        """)

        # Partner-Codes (Influencer-Kooperationen + Gründer-Lizenz)
        try:
            conn.executescript("""
                CREATE TABLE IF NOT EXISTS partner_codes (
                    id          INTEGER PRIMARY KEY AUTOINCREMENT,
                    code        TEXT NOT NULL UNIQUE COLLATE NOCASE,
                    label       TEXT NOT NULL,
                    grants_founder INTEGER NOT NULL DEFAULT 1,
                    max_uses    INTEGER DEFAULT NULL,
                    uses        INTEGER NOT NULL DEFAULT 0,
                    created_by  INTEGER REFERENCES users(id),
                    created_at  TEXT NOT NULL DEFAULT (datetime('now'))
                );
                CREATE INDEX IF NOT EXISTS idx_partner_codes_code ON partner_codes(code);
            """)
            logger.info("Migration: partner_codes Tabelle bereit.")
        except Exception as e:
            logger.warning(f"Migration partner_codes: {e}")

        # Outreach-Log (Admin-E-Mail-Versand)
        try:
            conn.executescript("""
                CREATE TABLE IF NOT EXISTS outreach_log (
                    id         INTEGER PRIMARY KEY AUTOINCREMENT,
                    sent_by    INTEGER REFERENCES users(id),
                    recipient  TEXT NOT NULL,
                    subject    TEXT NOT NULL,
                    body       TEXT NOT NULL,
                    sent_at    TEXT NOT NULL DEFAULT (datetime('now'))
                );
            """)
        except Exception as e:
            logger.warning(f"Migration outreach_log: {e}")

        # E-Mail-Vorlagen (DB-gespeichert, CRUD über Admin)
        try:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS email_templates (
                    id           INTEGER PRIMARY KEY AUTOINCREMENT,
                    key          TEXT UNIQUE NOT NULL,
                    label        TEXT NOT NULL,
                    subject      TEXT NOT NULL,
                    body         TEXT NOT NULL,
                    from_account TEXT NOT NULL DEFAULT 'partner',
                    created_at   TEXT NOT NULL DEFAULT (datetime('now')),
                    updated_at   TEXT
                )
            """)
            # Startwert-Vorlage einspielen wenn Tabelle noch leer
            count = conn.execute("SELECT COUNT(*) FROM email_templates").fetchone()[0]
            if count == 0:
                conn.execute("""
                    INSERT INTO email_templates (key, label, subject, body, from_account) VALUES
                    ('influencer_de',
                     'Influencer-Ansprache (DE)',
                     'Ban Yaro — 100 Gründer-Plätze, einer davon für deine Community',
                     'Hallo {name},\n\nich bin René und habe Ban Yaro gebaut — eine Hunde-App für Tagebuch, Gesundheit, Giftköder-Alarm und Community. Kostenlos, ohne App Store, direkt als PWA.\n\nIch kontaktiere gerade einige Influencer aus der deutschen Hunde-Community mit einem konkreten Angebot:\n\nWas deine Follower bekommen: Wer sich mit deinem persönlichen Code registriert, sichert sich einen der 100 Gründer-Plätze — eine nummerierte Badge ("Gründer #42") die dauerhaft im Profil sichtbar ist. Diese 100 Plätze gibt es genau einmal.\n\nWas du bekommst: Partner-Badge in der App, eigener Code, öffentliches Ranking wer die meisten Gründer bringt.\n\nKein Geld, kein verpflichtender Post — aber eine echte Exklusivität die du deiner Community geben kannst.\n\nAlle Infos: https://banyaro.app/partner\n\nWenn dich das interessiert, antworte einfach kurz — ich richte deinen Code binnen 24h ein.\n\nViele Grüße,\nRené\nbanyaro.app',
                     'partner')
                """)
        except Exception as e:
            logger.warning(f"Migration email_templates: {e}")

        # from_account-Spalte in outreach_log nachträglich hinzufügen
        existing_ol = [row[1] for row in conn.execute("PRAGMA table_info(outreach_log)").fetchall()]
        if 'from_account' not in existing_ol:
            conn.execute("ALTER TABLE outreach_log ADD COLUMN from_account TEXT DEFAULT 'partner'")

        # Job-Bewerbungen + Luna-Probezugang
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS job_applications (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id      INTEGER REFERENCES users(id) ON DELETE SET NULL,
                name         TEXT    NOT NULL,
                email        TEXT    NOT NULL,
                dog_name     TEXT,
                dog_rasse    TEXT,
                social_handle TEXT,
                motivation   TEXT    NOT NULL,
                status       TEXT    NOT NULL DEFAULT 'pending',
                admin_note   TEXT,
                created_at   TEXT    NOT NULL DEFAULT (datetime('now')),
                reviewed_at  TEXT
            );
            CREATE INDEX IF NOT EXISTS idx_job_apps_status ON job_applications(status, created_at DESC);
            CREATE TABLE IF NOT EXISTS job_application_docs (
                id             INTEGER PRIMARY KEY AUTOINCREMENT,
                application_id INTEGER NOT NULL REFERENCES job_applications(id) ON DELETE CASCADE,
                filename       TEXT    NOT NULL,
                file_path      TEXT    NOT NULL,
                uploaded_at    TEXT    NOT NULL DEFAULT (datetime('now'))
            );
        """)
        existing_u = [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()]
        if 'luna_trial_until' not in existing_u:
            conn.execute("ALTER TABLE users ADD COLUMN luna_trial_until TEXT")

        # js_exercise_id zu training_exercises — verbindet training_exercises mit exercise_progress
        existing_te = [row[1] for row in conn.execute("PRAGMA table_info(training_exercises)").fetchall()]
        if 'js_exercise_id' not in existing_te:
            import re as _re
            _CAT_TO_TAB = {
                'Grundkommando': 'grundkommandos', 'Trick': 'tricks',
                'Problemverhalten': 'problemverhalten', 'Mentale Auslastung': 'mentale-auslastung',
                'Hundesport': 'hundesport', 'Körperpflege': 'koerperpflege',
                'Welpe Basics': 'welpe-basics', 'Grundlagen': 'grundlagen',
            }
            conn.execute("ALTER TABLE training_exercises ADD COLUMN js_exercise_id TEXT")
            # "Fuß (Leinenführigkeit)" → "Fuß" (damit es mit alten exercise_progress-Einträgen matcht)
            conn.execute("UPDATE training_exercises SET name='Fuß' WHERE exercise_id='gk_fuss'")
            rows = conn.execute("SELECT id, name, kategorie FROM training_exercises").fetchall()
            for row in rows:
                tab   = _CAT_TO_TAB.get(row['kategorie'], row['kategorie'].lower().replace(' ', '-'))
                js_id = tab + '_' + _re.sub(r'[\s/]+', '_', row['name'])
                conn.execute("UPDATE training_exercises SET js_exercise_id=? WHERE id=?", (js_id, row['id']))
            conn.execute("CREATE INDEX IF NOT EXISTS idx_te_js_id ON training_exercises(js_exercise_id)")
            logger.info("Migration: training_exercises.js_exercise_id hinzugefügt, 'Fuß' bereinigt.")

        # Hund des Monats — dauerhafte Gewinner-Tabelle
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS hund_des_monats_wins (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                dog_id     INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                monat      TEXT    NOT NULL,
                stimmen    INTEGER NOT NULL DEFAULT 0,
                created_at TEXT    NOT NULL DEFAULT (datetime('now')),
                UNIQUE(dog_id, monat)
            );
            CREATE INDEX IF NOT EXISTS idx_hdm_wins_dog ON hund_des_monats_wins(dog_id);
        """)

        # Trainings-Streak-Tabelle
        conn.execute("""
            CREATE TABLE IF NOT EXISTS training_streaks (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id         INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                dog_id          INTEGER NOT NULL REFERENCES dogs(id)  ON DELETE CASCADE,
                current_streak  INTEGER NOT NULL DEFAULT 0,
                longest_streak  INTEGER NOT NULL DEFAULT 0,
                last_training_date TEXT,
                UNIQUE(user_id, dog_id)
            )
        """)
        conn.execute("CREATE INDEX IF NOT EXISTS idx_streaks_user ON training_streaks(user_id)")

        # Ausgaben-Tracker
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS expenses (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                dog_id     INTEGER REFERENCES dogs(id) ON DELETE SET NULL,
                kategorie  TEXT NOT NULL,
                betrag     REAL NOT NULL,
                datum      TEXT NOT NULL,
                notiz      TEXT,
                created_at TEXT NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_expenses_user ON expenses(user_id, datum DESC);
        """)

        # KI-Tierarztfragen Rate-Limit-Log
        conn.execute("""
            CREATE TABLE IF NOT EXISTS ki_tierarzt_log (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                dog_id     INTEGER,
                created_at TEXT NOT NULL DEFAULT (datetime('now'))
            )
        """)

        # KI Rassen-Erkennungs-Log (Rate-Limit: 10/Tag pro User)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS ki_rasse_log (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                created_at TEXT NOT NULL DEFAULT (datetime('now'))
            )
        """)
        conn.execute("""
            CREATE INDEX IF NOT EXISTS idx_ki_rasse_log_user
                ON ki_rasse_log(user_id, created_at DESC)
        """)

        # feed_recalls — Rückruf-Alarm für Tierfutter (RASFF)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS feed_recalls (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                external_id TEXT NOT NULL UNIQUE,
                titel       TEXT NOT NULL,
                produkt     TEXT,
                gefahr      TEXT,
                herkunft    TEXT,
                datum       TEXT NOT NULL,
                quelle      TEXT NOT NULL DEFAULT 'rasff',
                url         TEXT,
                created_at  TEXT NOT NULL DEFAULT (datetime('now'))
            )
        """)
        conn.execute("CREATE INDEX IF NOT EXISTS idx_recalls_datum ON feed_recalls(datum DESC)")

        # Adoption-Cache
        conn.execute("""
            CREATE TABLE IF NOT EXISTS adoption_cache (
                id            INTEGER PRIMARY KEY AUTOINCREMENT,
                external_id   TEXT    NOT NULL UNIQUE,
                name          TEXT    NOT NULL,
                rasse         TEXT,
                alter_jahre   REAL,
                geschlecht    TEXT,
                foto_url      TEXT,
                tierheim      TEXT,
                tierheim_plz  TEXT,
                tierheim_lat  REAL,
                tierheim_lon  REAL,
                adoptions_url TEXT    NOT NULL,
                created_at    TEXT    NOT NULL DEFAULT (datetime('now')),
                expires_at    TEXT    NOT NULL
            )
        """)

        conn.execute("""
            CREATE TABLE IF NOT EXISTS community_adoption (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id      INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                dog_id       INTEGER REFERENCES dogs(id) ON DELETE SET NULL,
                name         TEXT    NOT NULL,
                rasse        TEXT,
                alter_jahre  REAL,
                geschlecht   TEXT,
                foto_url     TEXT,
                beschreibung TEXT    NOT NULL,
                gruende      TEXT,
                ort          TEXT,
                plz          TEXT,
                lat          REAL,
                lon          REAL,
                status       TEXT    NOT NULL DEFAULT 'active',
                created_at   TEXT    NOT NULL DEFAULT (datetime('now')),
                updated_at   TEXT    NOT NULL DEFAULT (datetime('now'))
            )
        """)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS community_adoption_interest (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                listing_id INTEGER NOT NULL REFERENCES community_adoption(id) ON DELETE CASCADE,
                user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                nachricht  TEXT,
                created_at TEXT NOT NULL DEFAULT (datetime('now')),
                UNIQUE(listing_id, user_id)
            )
        """)

        # ---- Wetter-Log (historische Vorhersage-Daten) ----
        conn.execute("""
            CREATE TABLE IF NOT EXISTS weather_log (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                logged_at    TEXT    NOT NULL DEFAULT (datetime('now')),
                date         TEXT    NOT NULL,
                lat_r        REAL    NOT NULL,
                lon_r        REAL    NOT NULL,
                temp_max     REAL,
                temp_min     REAL,
                feels_max    REAL,
                precip_prob  INTEGER,
                precip_sum   REAL,
                wind_kmh     REAL,
                wind_dir     TEXT,
                uv_index     REAL,
                weathercode  INTEGER,
                weatherdesc  TEXT,
                sunrise      TEXT,
                sunset       TEXT,
                asphalt_temp REAL,
                asphalt_warn TEXT,
                zecken       TEXT,
                pollen_erle      INTEGER,
                pollen_birke     INTEGER,
                pollen_graeser   INTEGER,
                pollen_beifuss   INTEGER,
                pollen_ambrosia  INTEGER,
                forecast_json    TEXT,
                UNIQUE(date, lat_r, lon_r)
            )
        """)

        # ---- Favoriten-Tierarzt + Gesundheitsdokumente ----
        conn.execute("""
            CREATE TABLE IF NOT EXISTS favorite_vets (
                user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                vet_id  INTEGER NOT NULL REFERENCES tieraerzte(id) ON DELETE CASCADE,
                PRIMARY KEY (user_id, vet_id)
            )
        """)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS health_documents (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                dog_id       INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                user_id      INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                typ          TEXT NOT NULL,
                titel        TEXT NOT NULL,
                beschreibung TEXT,
                file_path    TEXT NOT NULL,
                file_type    TEXT NOT NULL,
                datum        TEXT,
                vet_id       INTEGER REFERENCES tieraerzte(id),
                created_at   TEXT NOT NULL DEFAULT (datetime('now'))
            )
        """)
        conn.execute("CREATE INDEX IF NOT EXISTS idx_health_docs_dog ON health_documents(dog_id, created_at DESC)")

        # Digitaler Hundepass — Impfungen, Medikamente, Metadaten, Share-Links
        try:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS vaccinations (
                    id         INTEGER PRIMARY KEY AUTOINCREMENT,
                    dog_id     INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                    krankheit  TEXT NOT NULL,
                    datum      TEXT NOT NULL,
                    naechste   TEXT,
                    tierarzt   TEXT,
                    charge_nr  TEXT,
                    created_at TEXT NOT NULL DEFAULT (datetime('now'))
                )
            """)
            conn.execute("""
                CREATE TABLE IF NOT EXISTS medications (
                    id         INTEGER PRIMARY KEY AUTOINCREMENT,
                    dog_id     INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                    name       TEXT NOT NULL,
                    dosierung  TEXT,
                    von        TEXT,
                    bis        TEXT,
                    notiz      TEXT,
                    created_at TEXT NOT NULL DEFAULT (datetime('now'))
                )
            """)
            conn.execute("""
                CREATE TABLE IF NOT EXISTS dog_passport_meta (
                    dog_id         INTEGER PRIMARY KEY REFERENCES dogs(id) ON DELETE CASCADE,
                    blutgruppe     TEXT,
                    allergien      TEXT,
                    besonderheiten TEXT,
                    updated_at     TEXT NOT NULL DEFAULT (datetime('now'))
                )
            """)
            conn.execute("""
                CREATE TABLE IF NOT EXISTS passport_shares (
                    id          INTEGER PRIMARY KEY AUTOINCREMENT,
                    dog_id      INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                    token       TEXT NOT NULL UNIQUE,
                    valid_until TEXT NOT NULL,
                    created_at  TEXT NOT NULL DEFAULT (datetime('now'))
                )
            """)
            conn.execute("""
                CREATE INDEX IF NOT EXISTS idx_passport_shares_token ON passport_shares(token)
            """)
            logger.info("Migration: Hundepass-Tabellen bereit.")
        except Exception as e:
            logger.warning(f"Migration Hundepass: {e}")

        # ---- Playdate ----
        conn.execute("""
            CREATE TABLE IF NOT EXISTS playdate_listings (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                dog_id       INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                user_id      INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                lat          REAL NOT NULL,
                lon          REAL NOT NULL,
                ort_name     TEXT,
                radius_km    INTEGER NOT NULL DEFAULT 10,
                beschreibung TEXT,
                aktiv        INTEGER NOT NULL DEFAULT 1,
                created_at   TEXT NOT NULL DEFAULT (datetime('now')),
                updated_at   TEXT NOT NULL DEFAULT (datetime('now')),
                UNIQUE(dog_id)
            )
        """)
        conn.execute("""
            CREATE INDEX IF NOT EXISTS idx_playdate_listings_geo
            ON playdate_listings(lat, lon) WHERE aktiv=1
        """)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS playdate_requests (
                id           INTEGER PRIMARY KEY AUTOINCREMENT,
                from_dog_id  INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                to_dog_id    INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE,
                from_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                to_user_id   INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                status       TEXT NOT NULL DEFAULT 'pending',
                nachricht    TEXT,
                created_at   TEXT NOT NULL DEFAULT (datetime('now')),
                UNIQUE(from_dog_id, to_dog_id)
            )
        """)

        # Welten-Chip-Konfiguration pro User
        existing_u = [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()]
        if 'world_config' not in existing_u:
            conn.execute("ALTER TABLE users ADD COLUMN world_config TEXT")

        # Tagessprüche-Pool
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS daily_quotes (
                id         INTEGER PRIMARY KEY AUTOINCREMENT,
                text       TEXT    NOT NULL,
                autor      TEXT,
                kategorie  TEXT,
                created_at TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_dq_kategorie ON daily_quotes(kategorie);
        """)

        # Wiederkehrende Ausgaben (Daueraufträge)
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS recurring_expenses (
                id                   INTEGER PRIMARY KEY AUTOINCREMENT,
                user_id              INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
                dog_id               INTEGER REFERENCES dogs(id) ON DELETE SET NULL,
                kategorie            TEXT    NOT NULL,
                betrag               REAL    NOT NULL,
                haeufigkeit          TEXT    NOT NULL,  -- monatlich|quartalsweise|jaehrlich
                startdatum           TEXT    NOT NULL,
                naechste_faelligkeit TEXT    NOT NULL,
                notiz                TEXT,
                aktiv                INTEGER NOT NULL DEFAULT 1,
                created_at           TEXT    NOT NULL DEFAULT (datetime('now'))
            );
            CREATE INDEX IF NOT EXISTS idx_recurring_user ON recurring_expenses(user_id, aktiv);
        """)
