"""BAN YARO — Tagebuch Routes"""

import os, uuid, json, math, logging
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from typing import Optional
from database import db
from auth import get_current_user, require_admin
import ki as KI
import httpx
import weather as weather_mod
from media_utils import convert_media, extract_video_thumb, safe_media_path, validate_upload, extract_gps_from_exif, generate_preview, preview_url_from
from timeutils import safe_client_time

logger = logging.getLogger(__name__)

router    = APIRouter()
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")


class DiaryCreate(BaseModel):
    datum:         Optional[str]       = None   # ISO date, default heute
    client_time:   Optional[str]       = None   # lokale Uhrzeit des Geräts (YYYY-MM-DDTHH:MM:SS)
    typ:           str                 = "eintrag"
    titel:         Optional[str]       = None
    text:          Optional[str]       = None
    tags:          Optional[list]      = None
    gps_lat:       Optional[float]     = None
    gps_lon:       Optional[float]     = None
    location_name: Optional[str]       = None
    is_milestone:  bool                = False
    dog_ids:       Optional[list[int]] = None   # alle Hunde inkl. primär; None = nur primary


class DiaryUpdate(BaseModel):
    titel:         Optional[str]       = None
    text:          Optional[str]       = None
    tags:          Optional[list]      = None
    gps_lat:       Optional[float]     = None
    gps_lon:       Optional[float]     = None
    location_name: Optional[str]       = None
    is_milestone:  Optional[bool]      = None
    dog_ids:       Optional[list[int]] = None   # wenn gesetzt: Hunde-Zuweisung ersetzen


def _own_dog(dog_id: int, user_id: int, conn):
    """Eigener Hund ODER geteilter Hund (angenommene Einladung)."""
    dog = conn.execute(
        "SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user_id)
    ).fetchone()
    if not dog:
        dog = conn.execute(
            """SELECT d.id FROM dogs d
               JOIN dog_shares ds ON ds.dog_id = d.id
               WHERE d.id=? AND ds.shared_with_id=? AND ds.accepted_at IS NOT NULL""",
            (dog_id, user_id)
        ).fetchone()
    if not dog:
        raise HTTPException(404, "Hund nicht gefunden.")
    return dog


def _can_read_dog(dog_id: int, user_id: int, conn):
    """Eigener Hund ODER geteilter Hund ODER aktiver Sitter-Zugang (Lesezugriff)."""
    dog = conn.execute(
        "SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user_id)
    ).fetchone()
    if not dog:
        dog = conn.execute(
            """SELECT d.id FROM dogs d
               JOIN dog_shares ds ON ds.dog_id = d.id
               WHERE d.id=? AND ds.shared_with_id=? AND ds.accepted_at IS NOT NULL""",
            (dog_id, user_id)
        ).fetchone()
    if not dog:
        dog = conn.execute(
            """SELECT 1 FROM sitting_subscriptions
               WHERE dog_id=? AND sitter_id=? AND valid_until >= date('now')""",
            (dog_id, user_id)
        ).fetchone()
    if not dog:
        raise HTTPException(404, "Hund nicht gefunden.")
    return dog


def _validate_dog_ids(dog_ids: list[int], primary: int, user_id: int, conn) -> list[int]:
    """Stellt sicher dass alle IDs dem User gehören. Gibt die bereinigte Liste zurück."""
    all_ids = list({primary} | set(dog_ids))
    for did in all_ids:
        _own_dog(did, user_id, conn)
    return all_ids


def _fetch_dog_ids(conn, entry_ids: list[int]) -> dict:
    """Gibt {entry_id: [dog_id, ...]} zurück."""
    if not entry_ids:
        return {}
    ph = ",".join("?" * len(entry_ids))
    rows = conn.execute(
        f"SELECT diary_id, dog_id FROM diary_dogs WHERE diary_id IN ({ph})",
        entry_ids
    ).fetchall()
    result = {}
    for r in rows:
        result.setdefault(r["diary_id"], []).append(r["dog_id"])
    return result


def _set_dog_ids(conn, entry_id: int, dog_ids: list[int]):
    conn.execute("DELETE FROM diary_dogs WHERE diary_id=?", (entry_id,))
    for did in dog_ids:
        conn.execute(
            "INSERT OR IGNORE INTO diary_dogs (diary_id, dog_id) VALUES (?,?)",
            (entry_id, did)
        )


def _fetch_media_items(conn, entry_ids: list[int]) -> dict:
    """Gibt {entry_id: [{url, media_type, sort_order, id, is_cover}, ...]} zurück."""
    if not entry_ids:
        return {}
    ph = ",".join("?" * len(entry_ids))
    rows = conn.execute(
        f"SELECT id, diary_id, url, media_type, sort_order, is_cover FROM diary_media "
        f"WHERE diary_id IN ({ph}) ORDER BY diary_id, sort_order",
        entry_ids
    ).fetchall()
    result = {}
    for r in rows:
        url = r["url"]
        result.setdefault(r["diary_id"], []).append({
            "id": r["id"], "url": url,
            "preview_url": preview_url_from(url),
            "media_type": r["media_type"], "sort_order": r["sort_order"],
            "is_cover": r["is_cover"],
        })
    return result


def _entry_dict(row, dog_ids_map: dict, media_map: dict = None) -> dict:
    e = dict(row)
    e["tags"]        = json.loads(e["tags"]) if e["tags"] else []
    e["dog_ids"]     = dog_ids_map.get(e["id"], [e["dog_id"]])
    items            = (media_map or {}).get(e["id"], [])
    e["media_items"] = items
    # cover_url: Item mit is_cover=1, Fallback auf erstes Item
    cover = next((m for m in items if m.get("is_cover")), items[0] if items else None)
    e["cover_url"]         = cover["url"] if cover else None
    e["cover_preview_url"] = preview_url_from(e["cover_url"])
    return e


@router.get("/{dog_id}/diary/stats")
async def diary_stats(dog_id: int, user=Depends(get_current_user)):
    """Gesamtstatistik für das Tagebuch (unabhängig von Pagination)."""
    with db() as conn:
        _can_read_dog(dog_id, user["id"], conn)
        total = conn.execute(
            "SELECT COUNT(*) FROM diary d LEFT JOIN diary_dogs dd ON dd.diary_id=d.id "
            "WHERE (d.dog_id=? OR dd.dog_id=?)", (dog_id, dog_id)
        ).fetchone()[0]
        photos = conn.execute(
            "SELECT COUNT(*) FROM diary_media dm "
            "JOIN diary d ON d.id=dm.diary_id LEFT JOIN diary_dogs dd ON dd.diary_id=d.id "
            "WHERE (d.dog_id=? OR dd.dog_id=?)", (dog_id, dog_id)
        ).fetchone()[0]
        days = conn.execute(
            "SELECT COUNT(DISTINCT d.datum) FROM diary d LEFT JOIN diary_dogs dd ON dd.diary_id=d.id "
            "WHERE d.datum IS NOT NULL AND (d.dog_id=? OR dd.dog_id=?)", (dog_id, dog_id)
        ).fetchone()[0]
    return {"entries": total, "photos": photos, "days": days}


@router.get("/{dog_id}/diary/calendar")
async def diary_calendar(dog_id: int, user=Depends(get_current_user)):
    """Alle Einträge minimal für Kalenderansicht: id, datum, cover_url."""
    with db() as conn:
        _can_read_dog(dog_id, user["id"], conn)
        rows = conn.execute(
            """SELECT DISTINCT d.id, d.datum,
                      (SELECT dm.url FROM diary_media dm
                       WHERE dm.diary_id=d.id AND dm.media_type='image'
                       ORDER BY dm.is_cover DESC, dm.sort_order LIMIT 1) AS cover_url
               FROM diary d
               LEFT JOIN diary_dogs dd ON dd.diary_id = d.id
               WHERE (d.dog_id=? OR dd.dog_id=?)
                 AND d.datum IS NOT NULL
               ORDER BY d.datum DESC""",
            (dog_id, dog_id)
        ).fetchall()
    result = []
    for r in rows:
        d = dict(r)
        d["cover_preview_url"] = preview_url_from(d.get("cover_url"))
        result.append(d)
    return result


@router.get("/{dog_id}/diary/locations")
async def diary_locations(dog_id: int, user=Depends(get_current_user)):
    """Alle Tagebucheinträge mit GPS — minimal für Karten-Ansicht."""
    with db() as conn:
        _can_read_dog(dog_id, user["id"], conn)
        rows = conn.execute(
            """SELECT DISTINCT d.id, d.datum, d.titel, d.gps_lat, d.gps_lon,
                      d.location_name, d.weather_json,
                      (SELECT dm.url FROM diary_media dm
                       WHERE dm.diary_id=d.id AND dm.media_type='image'
                       ORDER BY dm.is_cover DESC, dm.sort_order LIMIT 1) AS cover_url,
                      (SELECT COUNT(*) FROM diary_media dm WHERE dm.diary_id=d.id) AS media_count
               FROM diary d
               LEFT JOIN diary_dogs dd ON dd.diary_id = d.id
               WHERE (d.dog_id=? OR dd.dog_id=?)
                 AND d.gps_lat IS NOT NULL AND d.gps_lon IS NOT NULL
               ORDER BY d.datum DESC""",
            (dog_id, dog_id)
        ).fetchall()
    result = []
    for r in rows:
        d = dict(r)
        d["cover_preview_url"] = preview_url_from(d.get("cover_url"))
        result.append(d)
    return result


@router.get("/{dog_id}/diary")
async def list_diary(dog_id: int, limit: int = 20, offset: int = 0,
                     q: Optional[str] = None, milestone: int = 0,
                     user=Depends(get_current_user)):
    with db() as conn:
        _can_read_dog(dog_id, user["id"], conn)
        # Sitter darf keine bestehenden Einträge lesen
        dog = conn.execute("SELECT user_id FROM dogs WHERE id=?", (dog_id,)).fetchone()
        is_owner = dog and dog["user_id"] == user["id"]
        if not is_owner:
            # Prüfen ob geteilter Hund (dog_shares) — darf lesen
            shared = conn.execute(
                """SELECT 1 FROM dog_shares WHERE dog_id=? AND shared_with_id=? AND accepted_at IS NOT NULL""",
                (dog_id, user["id"])
            ).fetchone()
            if not shared:
                # Weder Besitzer noch geteilter Nutzer → Sitter → leere Liste
                return []
        extra = "AND (d.is_milestone=1 OR d.typ='meilenstein')" if milestone else ""
        if q:
            pattern = f"%{q}%"
            rows = conn.execute(
                f"""SELECT DISTINCT d.* FROM diary d
                   LEFT JOIN diary_dogs dd ON dd.diary_id = d.id
                   WHERE (d.dog_id = ? OR dd.dog_id = ?)
                     AND (d.titel LIKE ? OR d.text LIKE ? OR d.tags LIKE ?)
                     {extra}
                   ORDER BY d.datum DESC, d.created_at DESC
                   LIMIT ? OFFSET ?""",
                (dog_id, dog_id, pattern, pattern, pattern, limit, offset)
            ).fetchall()
        else:
            rows = conn.execute(
                f"""SELECT DISTINCT d.* FROM diary d
                   LEFT JOIN diary_dogs dd ON dd.diary_id = d.id
                   WHERE (d.dog_id = ? OR dd.dog_id = ?) {extra}
                   ORDER BY d.datum DESC, d.created_at DESC
                   LIMIT ? OFFSET ?""",
                (dog_id, dog_id, limit, offset)
            ).fetchall()
        ids = [r["id"] for r in rows]
        dogs_map  = _fetch_dog_ids(conn, ids)
        media_map = _fetch_media_items(conn, ids)

    return [_entry_dict(r, dogs_map, media_map) for r in rows]


@router.post("/{dog_id}/diary", status_code=201)
async def create_diary(dog_id: int, data: DiaryCreate,
                       user=Depends(get_current_user)):
    tags = data.tags or []

    # KI: Auto-Tags wenn Text vorhanden (lokal, kostenlos)
    if data.text and len(data.text) > 10:
        try:
            ai_tags = await KI.diary_tags(data.text, user_id=user["id"])
            tags = list(set(tags + ai_tags))
        except Exception:
            pass

    with db() as conn:
        # Erlaubnis: eigener Hund ODER aktiver Sitter-Zugang
        dog = conn.execute("SELECT user_id FROM dogs WHERE id=?", (dog_id,)).fetchone()
        is_owner = dog and dog["user_id"] == user["id"]
        is_sitter = conn.execute("""
            SELECT 1 FROM sitting_subscriptions
            WHERE dog_id=? AND sitter_id=? AND valid_until >= date('now')
        """, (dog_id, user["id"])).fetchone()
        if not is_owner and not is_sitter:
            # Fallback: shared dog check
            try:
                _own_dog(dog_id, user["id"], conn)
            except HTTPException:
                raise HTTPException(403, "Kein Zugriff auf diesen Hund.")
        # Sitter darf nur den Gasthund als einzigen Hund eintragen
        if is_sitter and not is_owner:
            all_dogs = [dog_id]
        else:
            all_dogs = _validate_dog_ids(data.dog_ids or [], dog_id, user["id"], conn)

        ct    = safe_client_time(data.client_time)
        datum = data.datum or ct[:10]
        conn.execute(
            """INSERT INTO diary
               (dog_id, datum, typ, titel, text, tags, gps_lat, gps_lon, location_name, is_milestone, created_at)
               VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
            (dog_id, datum, data.typ, data.titel, data.text,
             json.dumps(tags), data.gps_lat, data.gps_lon, data.location_name, int(data.is_milestone), ct)
        )
        entry = conn.execute(
            "SELECT * FROM diary WHERE dog_id=? ORDER BY id DESC LIMIT 1",
            (dog_id,)
        ).fetchone()
        _set_dog_ids(conn, entry["id"], all_dogs)
        dogs_map  = _fetch_dog_ids(conn, [entry["id"]])
        media_map = _fetch_media_items(conn, [entry["id"]])
        entry_id  = entry["id"]

    # Wetter + POIs asynchron nach dem DB-Commit holen (außerhalb des with-Blocks)
    if data.gps_lat is not None and data.gps_lon is not None:
        weather_json = None
        poi_json = None

        # Wetter holen
        try:
            wd = await weather_mod.get_weather_for_location(data.gps_lat, data.gps_lon)
            weather_json = json.dumps(wd)
        except Exception as exc:
            logger.warning("Wetter-Abfrage beim Diary-Create fehlgeschlagen: %s", exc)

        # POIs holen
        try:
            pois = await _fetch_pois_for_coords(data.gps_lat, data.gps_lon, limit=5)
            if pois:
                poi_json = json.dumps(pois)
        except Exception as exc:
            logger.warning("POI-Abfrage beim Diary-Create fehlgeschlagen: %s", exc)

        # In DB speichern und Entry aktualisieren
        if weather_json is not None or poi_json is not None:
            with db() as conn:
                conn.execute(
                    "UPDATE diary SET weather_json=?, poi_json=? WHERE id=?",
                    (weather_json, poi_json, entry_id)
                )
                entry = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone()

    return _entry_dict(entry, dogs_map, media_map)


async def _fetch_pois_for_coords(lat: float, lon: float, limit: int = 5) -> list:
    """Holt POIs für Koordinaten via Overpass (analog zu nearby_places, aber ohne DB/Auth)."""
    results = []
    try:
        async with httpx.AsyncClient(timeout=6) as client:
            def _overpass_q(radius):
                return (
                    f'[out:json][timeout:6];'
                    f'('
                    f'  node["name"]["tourism"](around:{radius},{lat},{lon});'
                    f'  node["name"]["historic"](around:{radius},{lat},{lon});'
                    f'  node["name"]["leisure"](around:{radius},{lat},{lon});'
                    f'  node["name"]["amenity"](around:{radius},{lat},{lon});'
                    f'  node["name"]["shop"](around:{radius},{lat},{lon});'
                    f'  way["name"]["tourism"](around:{radius},{lat},{lon});'
                    f'  way["name"]["historic"](around:{radius},{lat},{lon});'
                    f'  way["name"]["leisure"](around:{radius},{lat},{lon});'
                    f');'
                    f'out center;'
                )
            ov = await client.post(
                "https://overpass-api.de/api/interpreter",
                data={"data": _overpass_q(800)},
                headers={"User-Agent": "BanYaro/1.0"},
            )
            elements = ov.json().get("elements", []) if ov.status_code == 200 else []
            if not elements:
                ov2 = await client.post(
                    "https://overpass-api.de/api/interpreter",
                    data={"data": _overpass_q(2000)},
                    headers={"User-Agent": "BanYaro/1.0"},
                )
                elements = ov2.json().get("elements", []) if ov2.status_code == 200 else []
            seen = set()
            for el in elements:
                n = el.get("tags", {}).get("name")
                if not n or n.lower() in seen:
                    continue
                seen.add(n.lower())
                elat = el.get("lat") or el.get("center", {}).get("lat")
                elon = el.get("lon") or el.get("center", {}).get("lon")
                if elat and elon:
                    km = _haversine_km(lat, lon, elat, elon)
                    typ = next((el["tags"].get(k) for k in
                                ["tourism", "historic", "leisure", "amenity", "shop"]
                                if el["tags"].get(k)), "place")
                    results.append({"name": n, "type": typ,
                                    "distance_m": int(km * 1000)})
                if len(results) >= limit:
                    break
    except Exception as exc:
        logger.debug("_fetch_pois_for_coords Fehler: %s", exc)
    return results[:limit]


def _haversine_km(lat1, lon1, lat2, lon2) -> float:
    R = 6371
    dlat = math.radians(lat2 - lat1)
    dlon = math.radians(lon2 - lon1)
    a = (math.sin(dlat / 2) ** 2
         + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2))
         * math.sin(dlon / 2) ** 2)
    return R * 2 * math.asin(math.sqrt(a))


@router.get("/{dog_id}/diary/nearby")
async def nearby_places(dog_id: int, lat: float, lon: float,
                        user=Depends(get_current_user)):
    results = []

    # 1. User-eigene Places
    with db() as conn:
        _own_dog(dog_id, user["id"], conn)
        places = conn.execute(
            "SELECT name, typ, lat, lon FROM places WHERE user_id=? AND lat IS NOT NULL",
            (user["id"],)
        ).fetchall()
        for p in places:
            km = _haversine_km(lat, lon, p["lat"], p["lon"])
            if km <= 5:
                results.append({"name": p["name"], "type": p["typ"] or "place",
                                 "lat": p["lat"], "lon": p["lon"],
                                 "distance_m": int(km * 1000), "source": "places"})

        # 2. Gecachte OSM-POIs (nur wenn vorhanden)
        osm = conn.execute(
            "SELECT name, type, lat, lon FROM osm_pois WHERE name IS NOT NULL AND name != ''"
        ).fetchall()
        for p in osm:
            km = _haversine_km(lat, lon, p["lat"], p["lon"])
            if km <= 2:
                results.append({"name": p["name"], "type": p["type"],
                                 "lat": p["lat"], "lon": p["lon"],
                                 "distance_m": int(km * 1000), "source": "osm"})

    async with httpx.AsyncClient(timeout=6) as client:
        # 3. Overpass: benannte POIs — 800m, bei leerer Antwort auf 2000m erweitern
        try:
            def _overpass_q(radius):
                return (
                    f'[out:json][timeout:6];'
                    f'('
                    f'  node["name"]["tourism"](around:{radius},{lat},{lon});'
                    f'  node["name"]["historic"](around:{radius},{lat},{lon});'
                    f'  node["name"]["leisure"](around:{radius},{lat},{lon});'
                    f'  node["name"]["amenity"](around:{radius},{lat},{lon});'
                    f'  node["name"]["shop"](around:{radius},{lat},{lon});'
                    f'  way["name"]["tourism"](around:{radius},{lat},{lon});'
                    f'  way["name"]["historic"](around:{radius},{lat},{lon});'
                    f'  way["name"]["leisure"](around:{radius},{lat},{lon});'
                    f');'
                    f'out center;'
                )
            overpass_q = _overpass_q(800)
            ov = await client.post(
                "https://overpass-api.de/api/interpreter",
                data={"data": overpass_q},
                headers={"User-Agent": "BanYaro/1.0"},
            )
            elements = ov.json().get("elements", []) if ov.status_code == 200 else []
            # Kein Ergebnis → Radius auf 2 km erweitern
            if not elements:
                ov2 = await client.post(
                    "https://overpass-api.de/api/interpreter",
                    data={"data": _overpass_q(2000)},
                    headers={"User-Agent": "BanYaro/1.0"},
                )
                elements = ov2.json().get("elements", []) if ov2.status_code == 200 else []
            if True:
                for el in elements:
                    n = el.get("tags", {}).get("name")
                    if not n:
                        continue
                    elat = el.get("lat") or el.get("center", {}).get("lat")
                    elon = el.get("lon") or el.get("center", {}).get("lon")
                    if elat and elon:
                        km = _haversine_km(lat, lon, elat, elon)
                        typ = next((el["tags"].get(k) for k in
                                    ["tourism","historic","leisure","amenity","shop"]
                                    if el["tags"].get(k)), "place")
                        results.append({"name": n, "type": typ,
                                         "lat": elat, "lon": elon,
                                         "distance_m": int(km * 1000), "source": "overpass"})
        except Exception:
            pass

        # 4. Nominatim Reverse-Geocode als Adress-Fallback
        try:
            r = await client.get(
                "https://nominatim.openstreetmap.org/reverse",
                params={"lat": lat, "lon": lon, "format": "json",
                        "zoom": 16, "namedetails": 1, "addressdetails": 1},
                headers={"User-Agent": "BanYaro/1.0"},
            )
            if r.status_code == 200:
                d = r.json()
                # namedetails kann None sein wenn kein benanntes Feature vorhanden
                name = (d.get("name") or
                        (d.get("namedetails") or {}).get("name"))
                if not name:
                    addr = d.get("address", {})
                    name = (addr.get("village") or addr.get("hamlet") or
                            addr.get("town") or addr.get("city") or
                            addr.get("suburb") or
                            d.get("display_name", "").split(",")[0].strip())
                if name:
                    results.append({"name": name, "type": "address",
                                     "lat": lat, "lon": lon,
                                     "distance_m": 0, "source": "nominatim"})
        except Exception:
            pass

    seen, unique = set(), []
    for r in sorted(results, key=lambda x: x["distance_m"]):
        key = r["name"].lower()
        if key not in seen:
            seen.add(key); unique.append(r)
        if len(unique) >= 12:
            break
    return unique


@router.get("/{dog_id}/diary/{entry_id}")
async def get_diary(dog_id: int, entry_id: int, user=Depends(get_current_user)):
    with db() as conn:
        _can_read_dog(dog_id, user["id"], conn)
        row = conn.execute(
            """SELECT DISTINCT d.* FROM diary d
               LEFT JOIN diary_dogs dd ON dd.diary_id = d.id
               WHERE d.id=? AND (d.dog_id=? OR dd.dog_id=?)""",
            (entry_id, dog_id, dog_id)
        ).fetchone()
        if not row:
            raise HTTPException(404, "Eintrag nicht gefunden.")
        dogs_map  = _fetch_dog_ids(conn, [entry_id])
        media_map = _fetch_media_items(conn, [entry_id])

    return _entry_dict(row, dogs_map, media_map)


@router.patch("/{dog_id}/diary/{entry_id}")
async def update_diary(dog_id: int, entry_id: int, data: DiaryUpdate,
                       user=Depends(get_current_user)):
    with db() as conn:
        # Nur Besitzer des Hundes darf bearbeiten, NICHT Sitter
        entry_owner = conn.execute(
            "SELECT d.user_id FROM diary dg JOIN dogs d ON d.id=dg.dog_id WHERE dg.id=?",
            (entry_id,)
        ).fetchone()
        if not entry_owner or entry_owner["user_id"] != user["id"]:
            # Prüfen ob geteilter Hund (dog_shares)
            try:
                _own_dog(dog_id, user["id"], conn)
            except HTTPException:
                raise HTTPException(403, "Nur der Besitzer darf Einträge bearbeiten.")

        # Prüfen ob Eintrag diesem Hund gehört (direkt oder via diary_dogs)
        exists = conn.execute(
            """SELECT 1 FROM diary d
               LEFT JOIN diary_dogs dd ON dd.diary_id = d.id
               WHERE d.id=? AND (d.dog_id=? OR dd.dog_id=?)""",
            (entry_id, dog_id, dog_id)
        ).fetchone()
        if not exists:
            raise HTTPException(404, "Eintrag nicht gefunden.")

        # Felder updaten — location_name/gps_* dürfen explizit auf None gesetzt werden
        raw = data.model_dump(exclude={"dog_ids"})
        NULLABLE = {"location_name", "gps_lat", "gps_lon"}
        fields = {k: v for k, v in raw.items() if v is not None or k in NULLABLE}
        if "tags" in fields and fields["tags"] is not None:
            fields["tags"] = json.dumps(fields["tags"])
        if fields:
            set_clause = ", ".join(f"{k}=?" for k in fields)
            conn.execute(
                f"UPDATE diary SET {set_clause} WHERE id=?",
                list(fields.values()) + [entry_id]
            )

        # Hunde-Zuweisung aktualisieren
        if data.dog_ids is not None:
            # primary dog des Eintrags ermitteln
            primary = conn.execute(
                "SELECT dog_id FROM diary WHERE id=?", (entry_id,)
            ).fetchone()["dog_id"]
            all_dogs = _validate_dog_ids(data.dog_ids, primary, user["id"], conn)
            _set_dog_ids(conn, entry_id, all_dogs)

        row       = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone()
        dogs_map  = _fetch_dog_ids(conn, [entry_id])
        media_map = _fetch_media_items(conn, [entry_id])

    return _entry_dict(row, dogs_map, media_map)


@router.delete("/{dog_id}/diary/{entry_id}", status_code=204)
async def delete_diary(dog_id: int, entry_id: int, user=Depends(get_current_user)):
    with db() as conn:
        # Nur Besitzer des Hundes darf löschen, NICHT Sitter
        entry_owner = conn.execute(
            "SELECT d.user_id FROM diary dg JOIN dogs d ON d.id=dg.dog_id WHERE dg.id=?",
            (entry_id,)
        ).fetchone()
        if not entry_owner or entry_owner["user_id"] != user["id"]:
            try:
                _own_dog(dog_id, user["id"], conn)
            except HTTPException:
                raise HTTPException(403, "Nur der Besitzer darf Einträge löschen.")
        conn.execute(
            "DELETE FROM diary WHERE id=? AND dog_id=?", (entry_id, dog_id)
        )


def _guess_media_type(content_type: str, filename: str) -> str:
    ct = (content_type or "").lower()
    if ct == "application/pdf" or (filename or "").lower().endswith(".pdf"):
        return "pdf"
    if ct.startswith("video/"):
        return "video"
    ext = os.path.splitext(filename or "")[1].lower()
    if ext in {".mp4", ".mov", ".webm", ".m4v", ".avi"}:
        return "video"
    return "image"


@router.post("/{dog_id}/diary/{entry_id}/media")
async def upload_media(dog_id: int, entry_id: int,
                       file: UploadFile = File(...),
                       user=Depends(get_current_user)):
    with db() as conn:
        _own_dog(dog_id, user["id"], conn)
        entry = conn.execute(
            """SELECT d.id FROM diary d
               LEFT JOIN diary_dogs dd ON dd.diary_id = d.id
               WHERE d.id=? AND (d.dog_id=? OR dd.dog_id=?)""",
            (entry_id, dog_id, dog_id)
        ).fetchone()
    if not entry:
        raise HTTPException(404, "Eintrag nicht gefunden.")

    ALLOWED = {
        "image/jpeg", "image/png", "image/gif", "image/webp", "image/heic", "image/heif",
        "video/mp4", "video/quicktime", "video/webm", "video/x-m4v",
        "application/pdf",
    }
    ct = file.content_type or ""
    if ct not in ALLOWED:
        ext_low = os.path.splitext(file.filename or "")[1].lower()
        if ext_low not in {".jpg",".jpeg",".png",".gif",".webp",".heic",".heif",
                           ".mp4",".mov",".webm",".m4v",".pdf"}:
            raise HTTPException(415, "Nur Bilder, Videos und PDFs erlaubt.")

    raw_data   = await file.read()
    try:
        validate_upload(raw_data, file.filename or "")
    except ValueError as e:
        raise HTTPException(415, str(e))
    raw_data, ext = convert_media(raw_data, file.filename or "")
    if not ext:
        ext = ".jpg"
    filename   = f"diary_{entry_id}_{uuid.uuid4().hex[:8]}{ext}"
    path       = os.path.join(MEDIA_DIR, "diary", filename)
    media_type = _guess_media_type(ct, file.filename or "")
    os.makedirs(os.path.dirname(path), exist_ok=True)

    with open(path, "wb") as f:
        f.write(raw_data)

    if media_type == "video":
        extract_video_thumb(path)
    elif media_type == "image":
        preview_bytes = generate_preview(raw_data, ext)
        if preview_bytes:
            preview_path = os.path.splitext(path)[0] + "_preview.webp"
            with open(preview_path, "wb") as f:
                f.write(preview_bytes)

    media_url = f"/media/diary/{filename}"

    # EXIF-GPS aus Bild extrahieren (nur bei Bilddateien)
    exif_gps = None
    if media_type == "image":
        exif_gps = extract_gps_from_exif(raw_data)

    with db() as conn:
        # sort_order = nächste freie Position
        max_order = conn.execute(
            "SELECT COALESCE(MAX(sort_order), -1) FROM diary_media WHERE diary_id=?",
            (entry_id,)
        ).fetchone()[0]
        # Erstes Item eines Eintrags wird automatisch Cover
        is_cover = 1 if max_order == -1 else 0
        conn.execute(
            "INSERT INTO diary_media (diary_id, url, media_type, sort_order, is_cover) VALUES (?,?,?,?,?)",
            (entry_id, media_url, media_type, max_order + 1, is_cover)
        )
        new_id = conn.execute(
            "SELECT id FROM diary_media WHERE diary_id=? ORDER BY id DESC LIMIT 1",
            (entry_id,)
        ).fetchone()["id"]

        # GPS aus EXIF in den Eintrag schreiben, wenn noch keine Koordinaten vorhanden
        gps_written = False
        if exif_gps:
            existing = conn.execute(
                "SELECT gps_lat FROM diary WHERE id=?", (entry_id,)
            ).fetchone()
            if existing and existing["gps_lat"] is None:
                conn.execute(
                    "UPDATE diary SET gps_lat=?, gps_lon=? WHERE id=?",
                    (exif_gps[0], exif_gps[1], entry_id)
                )
                gps_written = True

    # Wetter + POI nachladen wenn GPS frisch gesetzt
    if gps_written and exif_gps:
        try:
            wd = await weather_mod.get_weather_for_location(exif_gps[0], exif_gps[1])
            pois = await _fetch_pois_for_coords(exif_gps[0], exif_gps[1], limit=5)
            with db() as conn:
                conn.execute(
                    "UPDATE diary SET weather_json=COALESCE(weather_json,?), poi_json=COALESCE(poi_json,?) WHERE id=?",
                    (json.dumps(wd) if wd else None, json.dumps(pois) if pois else None, entry_id)
                )
        except Exception as e:
            logger.warning("EXIF-GPS Wetter/POI Fehler: %s", e)

    resp = {"id": new_id, "url": media_url, "media_type": media_type,
            "sort_order": max_order + 1, "is_cover": is_cover}
    if exif_gps:
        resp["exif_lat"] = exif_gps[0]
        resp["exif_lon"] = exif_gps[1]
    return resp


@router.delete("/{dog_id}/diary/{entry_id}/media/{media_id}", status_code=204)
async def delete_media_item(dog_id: int, entry_id: int, media_id: int,
                            user=Depends(get_current_user)):
    with db() as conn:
        _own_dog(dog_id, user["id"], conn)
        row = conn.execute(
            "SELECT dm.id, dm.url FROM diary_media dm "
            "JOIN diary d ON d.id = dm.diary_id "
            "LEFT JOIN diary_dogs dd ON dd.diary_id = d.id "
            "WHERE dm.id=? AND dm.diary_id=? AND (d.dog_id=? OR dd.dog_id=?)",
            (media_id, entry_id, dog_id, dog_id)
        ).fetchone()
        if not row:
            raise HTTPException(404, "Medium nicht gefunden.")
        file_path = safe_media_path(MEDIA_DIR, row["url"])
        if file_path:
            try: os.remove(file_path)
            except OSError: pass
        conn.execute("DELETE FROM diary_media WHERE id=?", (media_id,))


@router.delete("/{dog_id}/diary/{entry_id}/media", status_code=204)
async def delete_media_legacy(dog_id: int, entry_id: int, user=Depends(get_current_user)):
    """Legacy-Endpoint: löscht media_url aus dem diary-Datensatz (Rückwärtskompatibilität)."""
    with db() as conn:
        _own_dog(dog_id, user["id"], conn)
        row = conn.execute(
            "SELECT media_url FROM diary WHERE id=? AND dog_id=?", (entry_id, dog_id)
        ).fetchone()
        if not row:
            raise HTTPException(404, "Eintrag nicht gefunden.")
        if row["media_url"]:
            path = safe_media_path(MEDIA_DIR, row["media_url"])
            if path:
                try: os.remove(path)
                except OSError: pass
        conn.execute("UPDATE diary SET media_url=NULL WHERE id=?", (entry_id,))


@router.patch("/{dog_id}/diary/{entry_id}/media/{media_id}/cover", status_code=200)
async def set_cover_media(dog_id: int, entry_id: int, media_id: int,
                          user=Depends(get_current_user)):
    """Setzt ein Medium als Cover-Bild (is_cover=1), alle anderen auf 0."""
    with db() as conn:
        _own_dog(dog_id, user["id"], conn)
        row = conn.execute(
            "SELECT dm.id FROM diary_media dm "
            "JOIN diary d ON d.id = dm.diary_id "
            "LEFT JOIN diary_dogs dd ON dd.diary_id = d.id "
            "WHERE dm.id=? AND dm.diary_id=? AND (d.dog_id=? OR dd.dog_id=?)",
            (media_id, entry_id, dog_id, dog_id)
        ).fetchone()
        if not row:
            raise HTTPException(404, "Medium nicht gefunden.")
        # Alle Items dieses Eintrags auf is_cover=0, dann das gewählte auf 1
        conn.execute("UPDATE diary_media SET is_cover=0 WHERE diary_id=?", (entry_id,))
        conn.execute("UPDATE diary_media SET is_cover=1 WHERE id=?", (media_id,))
    return {"ok": True}


# ------------------------------------------------------------------
# Admin: retroaktive Metadaten-Anreicherung bestehender Einträge
# ------------------------------------------------------------------
@router.post("/admin/enrich-metadata", status_code=200)
async def admin_enrich_diary_metadata(limit: int = 20, _=Depends(require_admin)):
    """Reichert bestehende Tagebucheinträge mit GPS-Koordinaten mit Wetter + POI nach."""
    with db() as conn:
        rows = conn.execute(
            """SELECT id, gps_lat, gps_lon FROM diary
               WHERE gps_lat IS NOT NULL AND gps_lon IS NOT NULL
                 AND (weather_json IS NULL OR poi_json IS NULL)
               LIMIT ?""",
            (limit,)
        ).fetchall()

    enriched = 0
    skipped  = 0
    for row in rows:
        entry_id, lat, lon = row["id"], row["gps_lat"], row["gps_lon"]
        weather_json = None
        poi_json = None
        try:
            wd = await weather_mod.get_weather_for_location(lat, lon)
            weather_json = json.dumps(wd)
        except Exception as e:
            logger.warning("enrich-metadata Wetter id=%s: %s", entry_id, e)
        try:
            pois = await _fetch_pois_for_coords(lat, lon, limit=5)
            if pois:
                poi_json = json.dumps(pois)
        except Exception as e:
            logger.warning("enrich-metadata POI id=%s: %s", entry_id, e)

        if weather_json is not None or poi_json is not None:
            with db() as conn:
                conn.execute(
                    "UPDATE diary SET weather_json=COALESCE(weather_json,?), poi_json=COALESCE(poi_json,?) WHERE id=?",
                    (weather_json, poi_json, entry_id)
                )
            enriched += 1
        else:
            skipped += 1

    with db() as conn:
        remaining = conn.execute(
            """SELECT COUNT(*) FROM diary
               WHERE gps_lat IS NOT NULL AND (weather_json IS NULL OR poi_json IS NULL)"""
        ).fetchone()[0]

    return {"enriched": enriched, "skipped": skipped, "remaining": remaining}
