"""TV-App Routes - Seiten und API fuer Streaming-Frontend"""
import asyncio
import io
import json
import logging
import secrets
import time
from functools import wraps
from aiohttp import web
import aiohttp_jinja2
import aiomysql

from app.config import Config
from app.services.auth import AuthService
from app.services.library import LibraryService
from app.services.hls import HLSSessionManager
from app.services.i18n import set_request_lang, get_all_translations


def setup_tv_routes(app: web.Application, config: Config,
                    auth_service: AuthService,
                    library_service: LibraryService,
                    hls_manager: HLSSessionManager = None) -> None:
    """Registriert alle TV-App Routes"""

    # --- Poster-URL Lokalisierung ---
    # TVDB-URLs durch lokalen Endpunkt ersetzen (schnelleres Laden)

    _POSTER_WIDTH = 300  # Max. Breite fuer Poster-Thumbnails

    def _localize_posters(rows, content_type="series"):
        """poster_url durch lokalen Resize-Endpunkt ersetzen.
        Vermeidet externe TVDB-Requests, Bilder werden lokal gecacht."""
        for row in rows:
            if content_type == "series":
                row["poster_url"] = (
                    f"/api/library/metadata/{row['id']}"
                    f"/poster.jpg?w={_POSTER_WIDTH}"
                )
            # Filme: noch keine lokale Metadaten -> URL beibehalten

    # --- Stream-Tokens (fuer AVPlay / Native Player ohne Cookie-Jar) ---
    # Token -> {video_id, user_id, expires}
    _stream_tokens: dict[str, dict] = {}
    _TOKEN_LIFETIME = 3600  # 1 Stunde

    def _cleanup_tokens():
        """Abgelaufene Tokens entfernen"""
        now = time.time()
        expired = [k for k, v in _stream_tokens.items() if v["expires"] < now]
        for k in expired:
            del _stream_tokens[k]

    def _create_stream_token(video_id: int, user_id: int) -> str:
        """Temporaeren Stream-Token erstellen"""
        _cleanup_tokens()
        token = secrets.token_urlsafe(32)
        _stream_tokens[token] = {
            "video_id": video_id,
            "user_id": user_id,
            "expires": time.time() + _TOKEN_LIFETIME,
        }
        return token

    def _validate_stream_token(token: str, video_id: int) -> bool:
        """Prueft ob Token gueltig ist fuer das angegebene Video"""
        info = _stream_tokens.get(token)
        if not info:
            return False
        if info["expires"] < time.time():
            del _stream_tokens[token]
            return False
        if info["video_id"] != video_id:
            return False
        return True

    # --- Auth-Hilfsfunktionen ---

    def _cookie_params(request: web.Request) -> dict:
        """SameSite-Parameter je nach Protokoll und Client.
        Tizen-WGT-App: iframe-Context -> SameSite weglassen (Lax blockiert Cookies in iframes).
        HTTPS: SameSite=None+Secure. HTTP normal: Lax."""
        ua = request.headers.get("User-Agent", "")
        is_https = (request.secure
                    or request.headers.get("X-Forwarded-Proto") == "https")
        if is_https:
            return {"samesite": "None", "secure": True}
        # Tizen-App laeuft im iframe -> SameSite weglassen damit Cookie gesetzt wird
        if "Tizen" in ua:
            return {}
        return {"samesite": "Lax"}

    async def get_tv_user(request: web.Request) -> dict | None:
        """Prueft Session-Cookie, gibt User zurueck oder None"""
        session_id = request.cookies.get("vk_session")
        if not session_id:
            return None
        return await auth_service.validate_session(session_id)

    def require_auth(handler):
        """Decorator: Leitet auf Login um wenn nicht eingeloggt.
        Setzt i18n-Sprache aus User-Einstellungen."""
        @wraps(handler)
        async def wrapper(request):
            user = await get_tv_user(request)
            if not user:
                raise web.HTTPFound("/tv/login")
            request["tv_user"] = user
            # i18n: Sprache des Users setzen
            set_request_lang(request.app, user.get("ui_lang", "de"))
            return await handler(request)
        return wrapper

    # --- Login / Logout ---

    async def get_login(request: web.Request) -> web.Response:
        """GET /tv/login - Login-Seite.
        Wenn bereits eingeloggt -> weiter.
        Wenn Profile auf dem Geraet -> Profilauswahl.
        Wenn genau ein Profil -> direkt einloggen."""
        user = await get_tv_user(request)
        if user:
            raise web.HTTPFound("/tv/")
        # Pruefen ob Profile auf diesem Client existieren
        client_id = request.cookies.get("vk_client_id")
        if client_id:
            profiles = await auth_service.get_client_profiles(client_id)
            if len(profiles) == 1:
                # Nur ein Profil -> direkt Session wechseln
                session_id = profiles[0]["session_id"]
                check_user = await auth_service.validate_session(session_id)
                if check_user:
                    resp = web.HTTPFound("/tv/")
                    resp.set_cookie(
                        "vk_session", session_id,
                        max_age=10 * 365 * 24 * 3600,
                        httponly=True, path="/",
                        **_cookie_params(request),
                    )
                    return resp
            elif len(profiles) > 1:
                # Mehrere Profile -> Profilauswahl
                raise web.HTTPFound("/tv/profiles")
        return aiohttp_jinja2.render_template(
            "tv/login.html", request, {"error": None}
        )

    async def post_login(request: web.Request) -> web.Response:
        """POST /tv/login - Login verarbeiten.
        Unterstuetzt 'remember' Checkbox fuer permanente Sessions
        und Client-ID fuer Multi-User Quick-Switch."""
        data = await request.post()
        username = data.get("username", "").strip()
        password = data.get("password", "")
        remember = data.get("remember", "") == "on"

        if not username or not password:
            return aiohttp_jinja2.render_template(
                "tv/login.html", request,
                {"error": "Benutzername und Passwort eingeben"}
            )

        user = await auth_service.verify_login(username, password)
        if not user:
            return aiohttp_jinja2.render_template(
                "tv/login.html", request,
                {"error": "Falscher Benutzername oder Passwort"}
            )

        # Client-ID ermitteln/erstellen (fuer Multi-User pro Geraet)
        client_id = request.cookies.get("vk_client_id")
        client_id = await auth_service.get_or_create_client(client_id)

        # Session erstellen (persistent wenn "Angemeldet bleiben")
        ua = request.headers.get("User-Agent", "")
        session_id = await auth_service.create_session(
            user["id"], ua, client_id=client_id, persistent=remember
        )

        resp = web.HTTPFound("/tv/")
        # Session-Cookie
        max_age = 10 * 365 * 24 * 3600 if remember else 30 * 24 * 3600
        resp.set_cookie(
            "vk_session", session_id,
            max_age=max_age,
            httponly=True, path="/",
            **_cookie_params(request),
        )
        # Client-ID Cookie (immer permanent)
        resp.set_cookie(
            "vk_client_id", client_id,
            max_age=10 * 365 * 24 * 3600,  # 10 Jahre
            httponly=True, path="/",
            **_cookie_params(request),
        )
        return resp

    async def get_logout(request: web.Request) -> web.Response:
        """GET /tv/logout - Session loeschen"""
        session_id = request.cookies.get("vk_session")
        if session_id:
            await auth_service.delete_session(session_id)
        resp = web.HTTPFound("/tv/login")
        resp.del_cookie("vk_session", path="/")
        return resp

    # --- TV-Seiten ---

    @require_auth
    async def get_home(request: web.Request) -> web.Response:
        """GET /tv/ - Startseite mit konfigurierbaren Rubriken"""
        user = request["tv_user"]
        uid = user["id"]

        # User-Einstellungen fuer Startseite
        show_continue = user.get("home_show_continue", 1)
        show_new = user.get("home_show_new", 1)
        hide_watched = user.get("home_hide_watched", 1)
        show_watched = user.get("home_show_watched", 1)

        # Daten-Container
        continue_watching = []
        new_series = []
        new_movies = []
        series = []
        movies = []
        watched_series = []
        watched_movies = []

        # Berechtigungs-WHERE fuer Pfad-Einschraenkung
        def _path_filter(alias: str, paths: list):
            if paths:
                ph = ",".join(["%s"] * len(paths))
                return f" AND {alias}.library_path_id IN ({ph})", list(paths)
            return "", []

        pool = library_service._db_pool
        if pool:
            async with pool.acquire() as conn:
                async with conn.cursor(aiomysql.DictCursor) as cur:

                    # --- Serien ---
                    if user.get("can_view_series"):
                        path_sql, path_params = _path_filter(
                            "s", user.get("allowed_paths"))

                        # Neu hinzugefuegt (letzte 10, nach Scan-Datum)
                        if show_new:
                            await cur.execute(f"""
                                SELECT s.id, s.title, s.folder_name,
                                       s.poster_url, s.last_updated,
                                       COUNT(v.id) as episode_count
                                FROM library_series s
                                LEFT JOIN library_videos v
                                    ON v.series_id = s.id
                                WHERE s.last_updated IS NOT NULL
                                    {path_sql}
                                GROUP BY s.id
                                ORDER BY s.last_updated DESC
                                LIMIT 10
                            """, path_params)
                            new_series = await cur.fetchall()

                        # Serien (ohne gesehene, falls aktiviert)
                        watched_join = ""
                        watched_where = ""
                        if hide_watched:
                            watched_join = (
                                " LEFT JOIN tv_watch_status ws"
                                " ON ws.series_id = s.id"
                                f" AND ws.user_id = {int(uid)}"
                                " AND ws.status = 'watched'"
                            )
                            watched_where = " AND ws.id IS NULL"

                        await cur.execute(f"""
                            SELECT s.id, s.title, s.folder_name,
                                   s.poster_url, s.genres, s.tvdb_id,
                                   COUNT(v.id) as episode_count
                            FROM library_series s
                            LEFT JOIN library_videos v
                                ON v.series_id = s.id
                            {watched_join}
                            WHERE 1=1 {path_sql} {watched_where}
                            GROUP BY s.id
                            ORDER BY s.title
                            LIMIT 20
                        """, path_params)
                        series = await cur.fetchall()

                        # Schon gesehen
                        if show_watched:
                            await cur.execute(f"""
                                SELECT s.id, s.title, s.folder_name,
                                       s.poster_url,
                                       COUNT(v.id) as episode_count
                                FROM library_series s
                                LEFT JOIN library_videos v
                                    ON v.series_id = s.id
                                INNER JOIN tv_watch_status ws
                                    ON ws.series_id = s.id
                                    AND ws.user_id = %s
                                    AND ws.status = 'watched'
                                WHERE 1=1 {path_sql}
                                GROUP BY s.id
                                ORDER BY ws.updated_at DESC
                                LIMIT 20
                            """, [uid] + path_params)
                            watched_series = await cur.fetchall()

                    # --- Filme ---
                    if user.get("can_view_movies"):
                        path_sql, path_params = _path_filter(
                            "m", user.get("allowed_paths"))

                        # Neu hinzugefuegt
                        if show_new:
                            await cur.execute(f"""
                                SELECT m.id, m.title, m.folder_name,
                                       m.poster_url, m.year, m.genres,
                                       m.last_updated
                                FROM library_movies m
                                WHERE m.last_updated IS NOT NULL
                                    {path_sql}
                                ORDER BY m.last_updated DESC
                                LIMIT 10
                            """, path_params)
                            new_movies = await cur.fetchall()

                        # Filme (ohne gesehene, falls aktiviert)
                        # Filme gelten als gesehen wenn watch_progress
                        # completed = 1
                        watched_join = ""
                        watched_where = ""
                        if hide_watched:
                            watched_join = f"""
                                LEFT JOIN (
                                    SELECT DISTINCT v2.movie_id
                                    FROM tv_watch_progress wp
                                    JOIN library_videos v2
                                        ON wp.video_id = v2.id
                                    WHERE wp.user_id = {int(uid)}
                                        AND wp.completed = 1
                                        AND v2.movie_id IS NOT NULL
                                ) wm ON wm.movie_id = m.id
                            """
                            watched_where = " AND wm.movie_id IS NULL"

                        await cur.execute(f"""
                            SELECT m.id, m.title, m.folder_name,
                                   m.poster_url, m.year, m.genres
                            FROM library_movies m
                            {watched_join}
                            WHERE 1=1 {path_sql} {watched_where}
                            ORDER BY m.title
                            LIMIT 20
                        """, path_params)
                        movies = await cur.fetchall()

                        # Schon gesehen (Filme)
                        if show_watched:
                            await cur.execute(f"""
                                SELECT DISTINCT m.id, m.title,
                                       m.folder_name, m.poster_url,
                                       m.year, m.genres
                                FROM library_movies m
                                JOIN library_videos v ON v.movie_id = m.id
                                JOIN tv_watch_progress wp
                                    ON wp.video_id = v.id
                                    AND wp.user_id = %s
                                    AND wp.completed = 1
                                WHERE 1=1 {path_sql}
                                ORDER BY wp.updated_at DESC
                                LIMIT 20
                            """, [uid] + path_params)
                            watched_movies = await cur.fetchall()

            # Poster-URLs lokalisieren
            for lst in (series, new_series, watched_series):
                _localize_posters(lst, "series")

            # Weiterschauen
            if show_continue:
                continue_watching = await auth_service.get_continue_watching(
                    uid)

        return aiohttp_jinja2.render_template(
            "tv/home.html", request, {
                "user": user,
                "active": "home",
                "continue_watching": continue_watching,
                "new_series": new_series,
                "new_movies": new_movies,
                "series": series,
                "movies": movies,
                "watched_series": watched_series,
                "watched_movies": watched_movies,
            }
        )

    @require_auth
    async def get_series_list(request: web.Request) -> web.Response:
        """GET /tv/series?source=&genre=&sort=&rating= - Alle Serien mit Filtern"""
        user = request["tv_user"]
        if not user.get("can_view_series"):
            raise web.HTTPFound("/tv/")

        # Filter-Parameter
        source_filter = request.query.get("source", "")
        genre_filter = request.query.get("genre", "")
        sort_by = request.query.get("sort", "title")
        rating_filter = request.query.get("rating", "")

        series = []
        sources = []
        all_genres = set()
        pool = library_service._db_pool
        if pool:
            async with pool.acquire() as conn:
                async with conn.cursor(aiomysql.DictCursor) as cur:
                    # Verfuegbare Quellen laden
                    src_query = "SELECT id, name FROM library_paths WHERE media_type = 'series'"
                    src_params = []
                    if user.get("allowed_paths"):
                        ph = ",".join(["%s"] * len(user["allowed_paths"]))
                        src_query += f" AND id IN ({ph})"
                        src_params = user["allowed_paths"]
                    await cur.execute(src_query, src_params)
                    sources = await cur.fetchall()

                    # Serien-Query mit Filtern + Durchschnittsbewertung
                    query = """
                        SELECT s.id, s.title, s.folder_name, s.poster_url,
                               s.genres, s.tvdb_id, s.overview, s.status,
                               s.library_path_id, s.tvdb_score,
                               COUNT(DISTINCT v.id) as episode_count,
                               COALESCE(AVG(r.rating), 0) as avg_rating,
                               COUNT(DISTINCT r.id) as rating_count
                        FROM library_series s
                        LEFT JOIN library_videos v ON v.series_id = s.id
                        LEFT JOIN tv_ratings r ON r.series_id = s.id
                            AND r.rating > 0
                    """
                    conditions = []
                    params = []

                    # Pfad-Berechtigung
                    if user.get("allowed_paths"):
                        ph = ",".join(["%s"] * len(user["allowed_paths"]))
                        conditions.append(
                            f"s.library_path_id IN ({ph})")
                        params.extend(user["allowed_paths"])

                    # Quellen-Filter
                    if source_filter:
                        conditions.append("s.library_path_id = %s")
                        params.append(int(source_filter))

                    # Genre-Filter
                    if genre_filter:
                        conditions.append("s.genres LIKE %s")
                        params.append(f"%{genre_filter}%")

                    if conditions:
                        query += " WHERE " + " AND ".join(conditions)

                    query += " GROUP BY s.id"

                    # Rating-Filter (nach GROUP BY mit HAVING)
                    if rating_filter:
                        min_stars = int(rating_filter)
                        query += " HAVING avg_rating >= %s"
                        params.append(min_stars)

                    # Sortierung
                    sort_map = {
                        "title": " ORDER BY s.title",
                        "title_desc": " ORDER BY s.title DESC",
                        "newest": " ORDER BY s.id DESC",
                        "episodes": " ORDER BY episode_count DESC",
                        "rating": " ORDER BY avg_rating DESC, rating_count DESC",
                    }
                    query += sort_map.get(sort_by, " ORDER BY s.title")
                    await cur.execute(query, params)
                    series = await cur.fetchall()

                    # Alle verfuegbaren Genres extrahieren + Rating runden
                    for s in series:
                        s["avg_rating"] = round(
                            float(s.get("avg_rating") or 0), 1)
                        if s.get("genres"):
                            for g in s["genres"].split(","):
                                g = g.strip()
                                if g:
                                    all_genres.add(g)

        # Poster-URLs lokalisieren (kein TVDB-Laden)
        _localize_posters(series, "series")

        # Ordner-Daten aufbereiten (gruppiert nach Quelle, sortiert nach Ordnername)
        folder_data = []
        src_map = {str(src["id"]): src["name"] for src in sources}
        if source_filter:
            # Nur gefilterte Quelle
            items = sorted(
                [s for s in series
                 if str(s.get("library_path_id")) == source_filter],
                key=lambda x: (x.get("folder_name") or "").lower()
            )
            src_name = src_map.get(source_filter, "")
            if items:
                folder_data.append({"name": src_name, "entries": items})
        else:
            for src in sources:
                items = sorted(
                    [s for s in series
                     if s.get("library_path_id") == src["id"]],
                    key=lambda x: (x.get("folder_name") or "").lower()
                )
                if items:
                    folder_data.append({
                        "name": src["name"], "entries": items})
            # Serien ohne Quelle (Fallback)
            src_ids = {src["id"] for src in sources}
            orphans = sorted(
                [s for s in series
                 if s.get("library_path_id") not in src_ids],
                key=lambda x: (x.get("folder_name") or "").lower()
            )
            if orphans:
                folder_data.append({"name": "Sonstige", "entries": orphans})

        return aiohttp_jinja2.render_template(
            "tv/series.html", request, {
                "user": user,
                "active": "series",
                "series": series,
                "view": user.get("series_view") or "grid",
                "sources": sources,
                "folder_data": folder_data,
                "genres": sorted(all_genres),
                "current_source": source_filter,
                "current_genre": genre_filter,
                "current_sort": sort_by,
                "current_rating": rating_filter,
            }
        )

    @require_auth
    async def get_series_detail(request: web.Request) -> web.Response:
        """GET /tv/series/{id} - Serien-Detail mit Staffeln"""
        user = request["tv_user"]
        if not user.get("can_view_series"):
            raise web.HTTPFound("/tv/")

        series_id = int(request.match_info["id"])

        series = None
        seasons = {}
        in_watchlist = False
        pool = library_service._db_pool
        if pool:
            async with pool.acquire() as conn:
                async with conn.cursor(aiomysql.DictCursor) as cur:
                    await cur.execute("""
                        SELECT id, title, folder_name, poster_url,
                               overview, genres, tvdb_id, tvdb_score
                        FROM library_series WHERE id = %s
                    """, (series_id,))
                    series = await cur.fetchone()

                    if series:
                        # Episoden mit TVDB-Beschreibung und Watch-Progress
                        await cur.execute("""
                            SELECT v.id, v.file_name, v.season_number,
                                   v.episode_number, v.episode_title,
                                   v.duration_sec, v.file_size,
                                   v.width, v.height, v.video_codec,
                                   v.container,
                                   tc.overview AS ep_overview,
                                   wp.position_sec, wp.duration_sec AS wp_duration
                            FROM library_videos v
                            LEFT JOIN tvdb_episode_cache tc
                                ON tc.series_tvdb_id = %s
                                AND tc.season_number = v.season_number
                                AND tc.episode_number = v.episode_number
                            LEFT JOIN tv_watch_progress wp
                                ON wp.video_id = v.id
                                AND wp.user_id = %s
                            WHERE v.series_id = %s
                            ORDER BY v.season_number, v.episode_number,
                                     v.file_name
                        """, (series.get("tvdb_id") or 0,
                              user["id"], series_id))
                        episodes = await cur.fetchall()

                        for ep in episodes:
                            # Fortschritt berechnen
                            if ep.get("position_sec") and ep.get("wp_duration"):
                                ep["progress_pct"] = min(100, int(
                                    ep["position_sec"] / ep["wp_duration"]
                                    * 100))
                            else:
                                ep["progress_pct"] = 0
                            sn = ep.get("season_number") or 0
                            if sn not in seasons:
                                seasons[sn] = []
                            seasons[sn].append(ep)

                        # Duplikat-Episoden markieren (gleiche Episodennummer)
                        for sn, eps in seasons.items():
                            ep_count = {}
                            for ep in eps:
                                en = ep.get("episode_number")
                                if en is not None:
                                    ep_count[en] = ep_count.get(en, 0) + 1
                            for ep in eps:
                                en = ep.get("episode_number")
                                ep["is_duplicate"] = (
                                    en is not None and ep_count.get(en, 0) > 1
                                )

                        # Watchlist-Status pruefen
                        in_watchlist = await auth_service.is_in_watchlist(
                            user["id"], series_id=series_id)

            # Bewertungen laden
            user_rating = await auth_service.get_rating(
                user["id"], series_id=series_id)
            avg_rating = await auth_service.get_avg_rating(
                series_id=series_id)

        if not series:
            raise web.HTTPFound("/tv/series")

        # Poster-URL lokalisieren
        _localize_posters([series], "series")

        # Watch-Threshold aus Config (Standard: 90%, wie Plex)
        watched_threshold = config.tv_config.get("watched_threshold_pct", 90)

        # Pro Staffel: Anzahl gesamt und gesehen berechnen
        season_watched = {}
        for sn, eps in seasons.items():
            total = len(eps)
            seen = sum(1 for ep in eps
                       if ep.get("progress_pct", 0) >= watched_threshold)
            season_watched[sn] = {
                "total": total, "seen": seen,
                "all_seen": total > 0 and seen == total,
            }

        # Post-Play Parameter (vom Player nach Episoden-Ende)
        post_play = request.query.get("post_play") == "1"
        next_video_id = request.query.get("next_video", "")
        countdown = int(request.query.get("countdown", "10") or "10")
        last_watched_id = request.query.get("last_watched", "")

        return aiohttp_jinja2.render_template(
            "tv/series_detail.html", request, {
                "user": user,
                "active": "series",
                "series": series,
                "seasons": dict(sorted(seasons.items())),
                "season_watched": season_watched,
                "in_watchlist": in_watchlist,
                "user_rating": user_rating,
                "avg_rating": avg_rating,
                "tvdb_score": series.get("tvdb_score"),
                "watched_threshold_pct": watched_threshold,
                "post_play": post_play,
                "next_video_id": next_video_id,
                "countdown": countdown,
                "last_watched_id": last_watched_id,
            }
        )

    @require_auth
    async def get_movies_list(request: web.Request) -> web.Response:
        """GET /tv/movies?source=&genre=&sort=&rating= - Alle Filme mit Filtern"""
        user = request["tv_user"]
        if not user.get("can_view_movies"):
            raise web.HTTPFound("/tv/")

        # Filter-Parameter
        source_filter = request.query.get("source", "")
        genre_filter = request.query.get("genre", "")
        sort_by = request.query.get("sort", "title")
        rating_filter = request.query.get("rating", "")

        movies = []
        sources = []
        all_genres = set()
        pool = library_service._db_pool
        if pool:
            async with pool.acquire() as conn:
                async with conn.cursor(aiomysql.DictCursor) as cur:
                    # Verfuegbare Quellen
                    src_query = "SELECT id, name FROM library_paths WHERE media_type = 'movie'"
                    src_params = []
                    if user.get("allowed_paths"):
                        ph = ",".join(["%s"] * len(user["allowed_paths"]))
                        src_query += f" AND id IN ({ph})"
                        src_params = user["allowed_paths"]
                    await cur.execute(src_query, src_params)
                    sources = await cur.fetchall()

                    # Film-Query mit Filtern + Durchschnittsbewertung
                    query = """
                        SELECT m.id, m.title, m.folder_name, m.poster_url,
                               m.year, m.genres, m.overview,
                               m.library_path_id, m.tvdb_score,
                               COALESCE(AVG(r.rating), 0) as avg_rating,
                               COUNT(DISTINCT r.id) as rating_count
                        FROM library_movies m
                        LEFT JOIN tv_ratings r ON r.movie_id = m.id
                            AND r.rating > 0
                    """
                    conditions = []
                    params = []

                    if user.get("allowed_paths"):
                        ph = ",".join(["%s"] * len(user["allowed_paths"]))
                        conditions.append(
                            f"m.library_path_id IN ({ph})")
                        params.extend(user["allowed_paths"])

                    if source_filter:
                        conditions.append("m.library_path_id = %s")
                        params.append(int(source_filter))

                    if genre_filter:
                        conditions.append("m.genres LIKE %s")
                        params.append(f"%{genre_filter}%")

                    if conditions:
                        query += " WHERE " + " AND ".join(conditions)

                    query += " GROUP BY m.id"

                    # Rating-Filter (nach GROUP BY)
                    if rating_filter:
                        min_stars = int(rating_filter)
                        query += " HAVING avg_rating >= %s"
                        params.append(min_stars)

                    sort_map = {
                        "title": " ORDER BY m.title",
                        "title_desc": " ORDER BY m.title DESC",
                        "newest": " ORDER BY m.id DESC",
                        "year": " ORDER BY m.year DESC",
                        "rating": " ORDER BY avg_rating DESC, rating_count DESC",
                    }
                    query += sort_map.get(sort_by, " ORDER BY m.title")
                    await cur.execute(query, params)
                    movies = await cur.fetchall()

                    for m in movies:
                        m["avg_rating"] = round(
                            float(m.get("avg_rating") or 0), 1)
                        if m.get("genres"):
                            for g in m["genres"].split(","):
                                g = g.strip()
                                if g:
                                    all_genres.add(g)

        # Ordner-Daten aufbereiten (gruppiert nach Quelle, sortiert nach Ordnername)
        folder_data = []
        src_map = {str(src["id"]): src["name"] for src in sources}
        if source_filter:
            items = sorted(
                [m for m in movies
                 if str(m.get("library_path_id")) == source_filter],
                key=lambda x: (x.get("folder_name") or "").lower()
            )
            src_name = src_map.get(source_filter, "")
            if items:
                folder_data.append({"name": src_name, "entries": items})
        else:
            for src in sources:
                items = sorted(
                    [m for m in movies
                     if m.get("library_path_id") == src["id"]],
                    key=lambda x: (x.get("folder_name") or "").lower()
                )
                if items:
                    folder_data.append({
                        "name": src["name"], "entries": items})
            # Filme ohne Quelle (Fallback)
            src_ids = {src["id"] for src in sources}
            orphans = sorted(
                [m for m in movies
                 if m.get("library_path_id") not in src_ids],
                key=lambda x: (x.get("folder_name") or "").lower()
            )
            if orphans:
                folder_data.append({"name": "Sonstige", "entries": orphans})

        return aiohttp_jinja2.render_template(
            "tv/movies.html", request, {
                "user": user,
                "active": "movies",
                "movies": movies,
                "view": user.get("movies_view") or "grid",
                "sources": sources,
                "folder_data": folder_data,
                "genres": sorted(all_genres),
                "current_source": source_filter,
                "current_genre": genre_filter,
                "current_sort": sort_by,
                "current_rating": rating_filter,
            }
        )

    @require_auth
    async def get_movie_detail(request: web.Request) -> web.Response:
        """GET /tv/movies/{id} - Film-Detail"""
        user = request["tv_user"]
        if not user.get("can_view_movies"):
            raise web.HTTPFound("/tv/")

        movie_id = int(request.match_info["id"])
        movie = None
        videos = []
        pool = library_service._db_pool
        if pool:
            async with pool.acquire() as conn:
                async with conn.cursor(aiomysql.DictCursor) as cur:
                    await cur.execute("""
                        SELECT id, title, folder_name, poster_url,
                               year, overview, genres, tvdb_score
                        FROM library_movies WHERE id = %s
                    """, (movie_id,))
                    movie = await cur.fetchone()

                    if movie:
                        await cur.execute("""
                            SELECT id, file_name, duration_sec, file_size,
                                   width, height, video_codec,
                                   container
                            FROM library_videos WHERE movie_id = %s
                        """, (movie_id,))
                        videos = await cur.fetchall()

        if not movie:
            raise web.HTTPFound("/tv/movies")

        in_watchlist = await auth_service.is_in_watchlist(
            user["id"], movie_id=movie_id)

        # Bewertungen laden
        user_rating = await auth_service.get_rating(
            user["id"], movie_id=movie_id)
        avg_rating = await auth_service.get_avg_rating(
            movie_id=movie_id)

        return aiohttp_jinja2.render_template(
            "tv/movie_detail.html", request, {
                "user": user,
                "active": "movies",
                "movie": movie,
                "videos": videos,
                "in_watchlist": in_watchlist,
                "user_rating": user_rating,
                "avg_rating": avg_rating,
                "tvdb_score": movie.get("tvdb_score"),
            }
        )

    @require_auth
    async def get_player(request: web.Request) -> web.Response:
        """GET /tv/player?v={video_id} - Video-Player
        Laedt Video-Info, naechste Episode und Client-Einstellungen."""
        user = request["tv_user"]
        video_id = int(request.query.get("v", 0))
        if not video_id:
            raise web.HTTPFound("/tv/")

        # Wiedergabe-Position laden
        progress = await auth_service.get_progress(user["id"], video_id)
        start_pos = 0
        if progress and not progress.get("completed"):
            start_pos = progress.get("position_sec", 0)

        # Video-Info + naechste Episode laden
        video = None
        next_video = None
        pool = library_service._db_pool
        if pool:
            async with pool.acquire() as conn:
                async with conn.cursor(aiomysql.DictCursor) as cur:
                    await cur.execute("""
                        SELECT v.id, v.file_name, v.duration_sec,
                               v.series_id,
                               s.title as series_title,
                               v.season_number, v.episode_number,
                               v.episode_title
                        FROM library_videos v
                        LEFT JOIN library_series s ON v.series_id = s.id
                        WHERE v.id = %s
                    """, (video_id,))
                    video = await cur.fetchone()

                    # Naechste Episode ermitteln (gleiche Serie)
                    if video and video.get("series_id"):
                        await cur.execute("""
                            SELECT id, season_number, episode_number,
                                   episode_title, file_name
                            FROM library_videos
                            WHERE series_id = %s
                              AND (season_number > %s
                                   OR (season_number = %s
                                       AND episode_number > %s))
                            ORDER BY season_number ASC, episode_number ASC
                            LIMIT 1
                        """, (video["series_id"],
                              video.get("season_number", 0),
                              video.get("season_number", 0),
                              video.get("episode_number", 0)))
                        next_video = await cur.fetchone()

        if not video:
            raise web.HTTPFound("/tv/")

        # Titel zusammenbauen
        title = video.get("file_name", "Video")
        if video.get("series_title"):
            sn = video.get("season_number", 0)
            en = video.get("episode_number", 0)
            ep_title = video.get("episode_title", "")
            title = f"{video['series_title']} - S{sn:02d}E{en:02d}"
            if ep_title:
                title += f" - {ep_title}"

        # Naechste Episode Titel
        next_title = ""
        if next_video:
            sn2 = next_video.get("season_number", 0)
            en2 = next_video.get("episode_number", 0)
            next_title = f"S{sn2:02d}E{en2:02d}"
            if next_video.get("episode_title"):
                next_title += f" - {next_video['episode_title']}"

        # Client-Einstellungen
        client_id = request.cookies.get("vk_client_id")
        client = None
        if client_id:
            client = await auth_service.get_client_settings(client_id)

        # URL zur Seriendetail-Seite (fuer Post-Play Navigation)
        series_detail_url = ""
        if video.get("series_id"):
            series_detail_url = f"/tv/series/{video['series_id']}"

        return aiohttp_jinja2.render_template(
            "tv/player.html", request, {
                "user": user,
                "video": video,
                "title": title,
                "start_pos": start_pos,
                "next_video": next_video,
                "next_title": next_title,
                "series_detail_url": series_detail_url,
                "client_sound_mode": client.get("sound_mode", "stereo") if client else "stereo",
                "client_stream_quality": client.get("stream_quality", "hd") if client else "hd",
                "client_audio_compressor": client.get("audio_compressor", False) if client else False,
            }
        )

    @require_auth
    async def get_search(request: web.Request) -> web.Response:
        """GET /tv/search?q=... - Suchseite mit History/Autocomplete"""
        user = request["tv_user"]
        query = request.query.get("q", "").strip()
        results_series = []
        results_movies = []
        history = []

        if query and len(query) >= 2:
            pool = library_service._db_pool
            if pool:
                async with pool.acquire() as conn:
                    async with conn.cursor(aiomysql.DictCursor) as cur:
                        search_term = f"%{query}%"

                        if user.get("can_view_series"):
                            await cur.execute("""
                                SELECT id, title, folder_name, poster_url,
                                       genres
                                FROM library_series
                                WHERE title LIKE %s OR folder_name LIKE %s
                                ORDER BY title LIMIT 50
                            """, (search_term, search_term))
                            results_series = await cur.fetchall()

                        if user.get("can_view_movies"):
                            await cur.execute("""
                                SELECT id, title, folder_name, poster_url,
                                       year, genres
                                FROM library_movies
                                WHERE title LIKE %s OR folder_name LIKE %s
                                ORDER BY title LIMIT 50
                            """, (search_term, search_term))
                            results_movies = await cur.fetchall()

            # Poster-URLs lokalisieren
            _localize_posters(results_series, "series")

            # Such-History speichern
            await auth_service.save_search(user["id"], query)
        else:
            # Ohne Query: History anzeigen
            history = await auth_service.get_search_history(user["id"])

        return aiohttp_jinja2.render_template(
            "tv/search.html", request, {
                "user": user,
                "active": "search",
                "query": query,
                "series": results_series,
                "movies": results_movies,
                "history": history,
            }
        )

    # --- TV-API Endpoints ---

    @require_auth
    async def post_watch_progress(request: web.Request) -> web.Response:
        """POST /tv/api/watch-progress - Position speichern"""
        user = request["tv_user"]
        try:
            data = await request.json()
        except Exception:
            return web.json_response({"error": "Ungueltiges JSON"}, status=400)

        video_id = data.get("video_id")
        position = data.get("position_sec", 0)
        duration = data.get("duration_sec", 0)
        completed = data.get("completed", False)

        if not video_id:
            return web.json_response(
                {"error": "video_id fehlt"}, status=400)

        # Bei explizitem completed-Flag: Position = Duration setzen
        if completed and duration > 0:
            position = duration

        await auth_service.save_progress(
            user["id"], video_id, position, duration
        )

        # Bei completed: Episode automatisch als "watched" markieren
        if completed:
            await auth_service.set_watch_status(
                user["id"], "watched", video_id=video_id
            )

        return web.json_response({"ok": True})

    @require_auth
    async def get_watch_progress(request: web.Request) -> web.Response:
        """GET /tv/api/watch-progress/{video_id}"""
        user = request["tv_user"]
        video_id = int(request.match_info["video_id"])
        progress = await auth_service.get_progress(user["id"], video_id)
        return web.json_response(progress or {"position_sec": 0})

    # --- QR-Code ---

    async def get_qrcode(request: web.Request) -> web.Response:
        """GET /api/tv/qrcode - QR-Code als PNG"""
        try:
            import qrcode
        except ImportError:
            return web.json_response(
                {"error": "qrcode nicht installiert"}, status=500)

        # URL ermitteln
        srv = config.server_config
        ext_url = srv.get("external_url", "")
        if ext_url:
            proto = "https" if srv.get("use_https") else "http"
            base = f"{proto}://{ext_url}"
        else:
            base = f"http://{request.host}"
        tv_url = f"{base}/tv/"

        qr = qrcode.QRCode(version=1, box_size=10, border=4)
        qr.add_data(tv_url)
        qr.make(fit=True)
        img = qr.make_image(fill_color="white", back_color="#0f0f0f")

        buf = io.BytesIO()
        img.save(buf, format="PNG")
        return web.Response(
            body=buf.getvalue(), content_type="image/png"
        )

    async def get_tv_url(request: web.Request) -> web.Response:
        """GET /api/tv/url - TV-App URL als JSON"""
        srv = config.server_config
        ext_url = srv.get("external_url", "")
        if ext_url:
            proto = "https" if srv.get("use_https") else "http"
            base = f"{proto}://{ext_url}"
        else:
            base = f"http://{request.host}"
        return web.json_response({"url": f"{base}/tv/"})

    # --- User-Verwaltung (fuer Admin-UI) ---

    async def get_users(request: web.Request) -> web.Response:
        """GET /api/tv/users - Alle User auflisten"""
        users = await auth_service.list_users()
        return web.json_response({"users": users})

    async def post_user(request: web.Request) -> web.Response:
        """POST /api/tv/users - Neuen User erstellen"""
        try:
            data = await request.json()
        except Exception:
            return web.json_response({"error": "Ungueltiges JSON"}, status=400)

        username = data.get("username", "").strip()
        password = data.get("password", "")
        if not username or not password:
            return web.json_response(
                {"error": "Username und Passwort noetig"}, status=400)

        user_id = await auth_service.create_user(
            username=username,
            password=password,
            display_name=data.get("display_name", ""),
            is_admin=data.get("is_admin", False),
            can_view_series=data.get("can_view_series", True),
            can_view_movies=data.get("can_view_movies", True),
            show_tech_info=data.get("show_tech_info", False),
            allowed_paths=data.get("allowed_paths"),
        )

        if not user_id:
            return web.json_response(
                {"error": "User konnte nicht erstellt werden "
                          "(Name bereits vergeben?)"}, status=400)

        return web.json_response({"id": user_id, "message": "User erstellt"})

    async def put_user(request: web.Request) -> web.Response:
        """PUT /api/tv/users/{id} - User aendern"""
        user_id = int(request.match_info["id"])
        try:
            data = await request.json()
        except Exception:
            return web.json_response({"error": "Ungueltiges JSON"}, status=400)

        success = await auth_service.update_user(user_id, **data)
        if success:
            return web.json_response({"message": "User aktualisiert"})
        return web.json_response(
            {"error": "Aktualisierung fehlgeschlagen"}, status=400)

    async def delete_user(request: web.Request) -> web.Response:
        """DELETE /api/tv/users/{id} - User loeschen"""
        user_id = int(request.match_info["id"])
        success = await auth_service.delete_user(user_id)
        if success:
            return web.json_response({"message": "User geloescht"})
        return web.json_response(
            {"error": "User nicht gefunden"}, status=404)

    # --- Profilauswahl (Multi-User Quick-Switch) ---

    async def get_profiles(request: web.Request) -> web.Response:
        """GET /tv/profiles - Profilauswahl (wer schaut?)
        Zeigt alle User an. Aktuelle Session wird hervorgehoben."""
        client_id = request.cookies.get("vk_client_id")
        current_session = request.cookies.get("vk_session")
        current_user_id = None
        if current_session:
            user = await auth_service.validate_session(current_session)
            if user:
                current_user_id = user.get("id")

        # Alle User laden (nicht nur die mit Sessions auf diesem Client)
        all_users = await auth_service.get_all_users()
        return aiohttp_jinja2.render_template(
            "tv/profiles.html", request, {
                "profiles": all_users,
                "current_user_id": current_user_id,
            }
        )

    async def post_switch_profile(request: web.Request) -> web.Response:
        """POST /tv/switch-profile - Auf anderen User wechseln.
        Erstellt neue Session fuer den gewaehlten User."""
        data = await request.post()
        user_id = data.get("user_id", "")
        if not user_id:
            raise web.HTTPFound("/tv/profiles")

        # Client-ID ermitteln/erstellen
        client_id = request.cookies.get("vk_client_id")
        client_id = await auth_service.get_or_create_client(client_id)

        # Neue Session fuer den User erstellen
        ua = request.headers.get("User-Agent", "")
        session_id = await auth_service.create_session(
            int(user_id), ua, client_id=client_id, persistent=True
        )
        if not session_id:
            raise web.HTTPFound("/tv/login")

        resp = web.HTTPFound("/tv/")
        resp.set_cookie(
            "vk_session", session_id,
            max_age=10 * 365 * 24 * 3600,
            httponly=True, path="/",
            **_cookie_params(request),
        )
        resp.set_cookie(
            "vk_client_id", client_id,
            max_age=10 * 365 * 24 * 3600,
            httponly=True, path="/",
            **_cookie_params(request),
        )
        return resp

    # --- User-Einstellungen ---

    @require_auth
    async def get_settings(request: web.Request) -> web.Response:
        """GET /tv/settings - Benutzer-Einstellungen"""
        user = request["tv_user"]
        client_id = request.cookies.get("vk_client_id")
        client = None
        if client_id:
            client = await auth_service.get_client_settings(client_id)
        return aiohttp_jinja2.render_template(
            "tv/settings.html", request, {
                "user": user,
                "client": client,
                "active": "settings",
            }
        )

    @require_auth
    async def post_settings(request: web.Request) -> web.Response:
        """POST /tv/settings - Benutzer-Einstellungen speichern
        Unterstuetzt sowohl vollstaendige Form-Submits als auch
        einzelne AJAX-Updates (nur gesetzte Felder aendern)."""
        user = request["tv_user"]
        data = await request.post()
        is_ajax = "X-Requested-With" in request.headers or \
            len(data) <= 2

        # Nur uebergebene Felder sammeln (kein Ueberschreiben)
        user_kwargs = {}
        field_map = {
            "display_name": lambda v: v,
            "preferred_audio_lang": lambda v: v,
            "preferred_subtitle_lang": lambda v: v or None,
            "subtitles_enabled": lambda v: v == "on",
            "ui_lang": lambda v: v,
            "series_view": lambda v: v,
            "movies_view": lambda v: v,
            "avatar_color": lambda v: v,
            "theme": lambda v: v,
            "autoplay_enabled": lambda v: v == "on",
            "autoplay_countdown_sec": lambda v: int(v),
            "autoplay_max_episodes": lambda v: int(v),
            "home_show_continue": lambda v: v == "on",
            "home_show_new": lambda v: v == "on",
            "home_hide_watched": lambda v: v == "on",
            "home_show_watched": lambda v: v == "on",
        }
        # Checkbox-Felder: Wenn nicht in data -> False (bei Full-Form)
        checkbox_fields = {
            "subtitles_enabled", "autoplay_enabled",
            "home_show_continue", "home_show_new",
            "home_hide_watched", "home_show_watched",
        }
        for key, transform in field_map.items():
            if key in data:
                user_kwargs[key] = transform(data[key])
            elif not is_ajax and key in checkbox_fields:
                user_kwargs[key] = False

        if user_kwargs:
            await auth_service.update_user_settings(
                user["id"], **user_kwargs)

        # Client-Einstellungen (nur wenn Felder vorhanden)
        client_id = request.cookies.get("vk_client_id")
        client_kwargs = {}
        if client_id:
            if "client_name" in data:
                client_kwargs["name"] = data["client_name"]
            if "sound_mode" in data:
                client_kwargs["sound_mode"] = data["sound_mode"]
            if "stream_quality" in data:
                client_kwargs["stream_quality"] = data["stream_quality"]
            # audio_compressor: Checkbox-Handling
            if "audio_compressor" in data:
                client_kwargs["audio_compressor"] = (
                    data["audio_compressor"] == "on")
            elif not is_ajax:
                client_kwargs["audio_compressor"] = False
            if client_kwargs:
                await auth_service.update_client_settings(
                    client_id, **client_kwargs)

        # AJAX: JSON zurueckgeben, sonst Redirect
        if is_ajax:
            return web.json_response({"ok": True})
        raise web.HTTPFound("/tv/settings?saved=1")

    @require_auth
    async def post_reset_progress(request: web.Request) -> web.Response:
        """POST /tv/settings/reset - Alle Fortschritte zuruecksetzen"""
        user = request["tv_user"]
        await auth_service.reset_all_progress(user["id"])
        raise web.HTTPFound("/tv/settings?reset=1")

    # --- Watchlist ---

    @require_auth
    async def get_watchlist(request: web.Request) -> web.Response:
        """GET /tv/watchlist - Merkliste anzeigen"""
        user = request["tv_user"]
        wl = await auth_service.get_watchlist(user["id"])
        # Poster-URLs lokalisieren
        _localize_posters(wl["series"], "series")
        return aiohttp_jinja2.render_template(
            "tv/watchlist.html", request, {
                "user": user,
                "active": "watchlist",
                "series": wl["series"],
                "movies": wl["movies"],
            }
        )

    @require_auth
    async def post_watchlist_toggle(request: web.Request) -> web.Response:
        """POST /tv/api/watchlist - Toggle Merkliste (JSON)"""
        user = request["tv_user"]
        data = await request.json()
        series_id = data.get("series_id")
        movie_id = data.get("movie_id")
        in_list = await auth_service.toggle_watchlist(
            user["id"],
            series_id=int(series_id) if series_id else None,
            movie_id=int(movie_id) if movie_id else None,
        )
        return web.json_response({"in_watchlist": in_list})

    # --- Watch-Status ---

    @require_auth
    async def post_watch_status(request: web.Request) -> web.Response:
        """POST /tv/api/watch-status - Status setzen (JSON)"""
        user = request["tv_user"]
        data = await request.json()
        status = data.get("status", "unwatched")
        success = await auth_service.set_watch_status(
            user["id"], status,
            video_id=data.get("video_id"),
            series_id=data.get("series_id"),
            season_key=data.get("season_key"),
        )
        return web.json_response({"success": success})

    # --- Such-API ---

    @require_auth
    async def get_search_suggestions(request: web.Request) -> web.Response:
        """GET /tv/api/search/suggest?q=... - Autocomplete-Vorschlaege"""
        user = request["tv_user"]
        prefix = request.query.get("q", "").strip()
        suggestions = await auth_service.get_search_suggestions(
            user["id"], prefix)
        return web.json_response({"suggestions": suggestions})

    @require_auth
    async def get_search_history(request: web.Request) -> web.Response:
        """GET /tv/api/search/history - Such-History"""
        user = request["tv_user"]
        history = await auth_service.get_search_history(user["id"])
        return web.json_response({"history": history})

    @require_auth
    async def delete_search_history(request: web.Request) -> web.Response:
        """DELETE /tv/api/search/history - Such-History loeschen"""
        user = request["tv_user"]
        await auth_service.clear_search_history(user["id"])
        return web.json_response({"success": True})

    # --- Rating API ---

    @require_auth
    async def post_rating(request: web.Request) -> web.Response:
        """POST /tv/api/rating - Bewertung setzen/loeschen (JSON)
        Body: { series_id|movie_id: int, rating: 0-5 }"""
        user = request["tv_user"]
        try:
            data = await request.json()
        except Exception:
            return web.json_response({"error": "Ungueltiges JSON"}, status=400)

        rating = int(data.get("rating", 0))
        series_id = data.get("series_id")
        movie_id = data.get("movie_id")

        if not series_id and not movie_id:
            return web.json_response(
                {"error": "series_id oder movie_id noetig"}, status=400)

        success = await auth_service.set_rating(
            user["id"], rating,
            series_id=int(series_id) if series_id else None,
            movie_id=int(movie_id) if movie_id else None,
        )

        # Durchschnitt zurueckgeben
        avg = await auth_service.get_avg_rating(
            series_id=int(series_id) if series_id else None,
            movie_id=int(movie_id) if movie_id else None,
        )

        return web.json_response({
            "success": success,
            "user_rating": rating,
            "avg_rating": avg["avg"],
            "rating_count": avg["count"],
        })

    # --- i18n API (fuer JavaScript) ---

    async def get_i18n(request: web.Request) -> web.Response:
        """GET /tv/api/i18n?lang=de - Alle Uebersetzungen als JSON"""
        lang = request.query.get("lang", "de")
        return web.json_response(get_all_translations(lang))

    # --- Direct-Stream API (fuer AVPlay / native Wiedergabe) ---

    async def get_direct_stream(request: web.Request) -> web.Response:
        """GET /tv/api/direct-stream/{video_id}
        Liefert Videodatei direkt als FileResponse (kein Transcoding).
        Unterstuetzt HTTP Range Requests fuer Seeking.
        Auth: Cookie ODER ?token=xxx (fuer AVPlay ohne Cookie-Jar)."""
        video_id = int(request.match_info["video_id"])

        # Auth pruefen: Cookie oder Stream-Token
        user = await get_tv_user(request)
        if not user:
            # Fallback: Stream-Token pruefen
            token = request.query.get("token")
            if not token or not _validate_stream_token(token, video_id):
                return web.json_response(
                    {"error": "Nicht autorisiert"}, status=401)

        pool = await library_service._get_pool()
        if not pool:
            return web.json_response(
                {"error": "Keine DB-Verbindung"}, status=500)

        try:
            async with pool.acquire() as conn:
                async with conn.cursor(aiomysql.DictCursor) as cur:
                    await cur.execute(
                        "SELECT file_path FROM library_videos WHERE id = %s",
                        (video_id,))
                    row = await cur.fetchone()
        except Exception as e:
            return web.json_response({"error": str(e)}, status=500)

        if not row or not row.get("file_path"):
            return web.json_response(
                {"error": "Video nicht gefunden"}, status=404)

        import os
        file_path = row["file_path"]
        if not os.path.isfile(file_path):
            return web.json_response(
                {"error": "Datei nicht gefunden"}, status=404)

        return web.FileResponse(file_path)

    @require_auth
    async def post_stream_token(request: web.Request) -> web.Response:
        """POST /tv/api/stream-token
        Erstellt temporaeren Token fuer Direct-Stream (AVPlay).
        Body: { video_id: int }"""
        try:
            data = await request.json()
        except Exception:
            return web.json_response(
                {"error": "Ungueltiges JSON"}, status=400)

        video_id = int(data.get("video_id", 0))
        if not video_id:
            return web.json_response(
                {"error": "video_id fehlt"}, status=400)

        user = request["tv_user"]
        token = _create_stream_token(video_id, user["id"])
        return web.json_response({"token": token})

    @require_auth
    async def get_video_info_tv(request: web.Request) -> web.Response:
        """GET /tv/api/video-info/{video_id}
        Erweiterte Video-Infos inkl. Direct-Play-Kompatibilitaet."""
        video_id = int(request.match_info["video_id"])

        pool = await library_service._get_pool()
        if not pool:
            return web.json_response(
                {"error": "Keine DB-Verbindung"}, status=500)

        try:
            async with pool.acquire() as conn:
                async with conn.cursor(aiomysql.DictCursor) as cur:
                    await cur.execute("""
                        SELECT id, file_name, file_path, width, height,
                               video_codec, audio_tracks, subtitle_tracks,
                               container, duration_sec, video_bitrate,
                               is_10bit, hdr, series_id,
                               season_number, episode_number
                        FROM library_videos WHERE id = %s
                    """, (video_id,))
                    video = await cur.fetchone()
                    if not video:
                        return web.json_response(
                            {"error": "Video nicht gefunden"}, status=404)
        except Exception as e:
            return web.json_response({"error": str(e)}, status=500)

        # JSON-Felder parsen
        for field in ("audio_tracks", "subtitle_tracks"):
            val = video.get(field)
            if isinstance(val, str):
                video[field] = json.loads(val)
            elif val is None:
                video[field] = []

        # Bild-basierte Untertitel rausfiltern
        video["subtitle_tracks"] = [
            s for s in video["subtitle_tracks"]
            if s.get("codec") not in (
                "hdmv_pgs_subtitle", "dvd_subtitle", "pgs", "vobsub"
            )
        ]

        # Direct-Play-Infos hinzufuegen
        video["direct_play_url"] = f"/tv/api/direct-stream/{video_id}"

        # Audio-Codecs extrahieren (fuer Client-seitige Kompatibilitaetspruefung)
        audio_codecs = []
        for track in video.get("audio_tracks", []):
            codec = track.get("codec", "").lower()
            if codec and codec not in audio_codecs:
                audio_codecs.append(codec)
        video["audio_codecs"] = audio_codecs

        # Video-Codec normalisieren
        vc = (video.get("video_codec") or "").lower()
        codec_map = {
            "h264": "h264", "avc": "h264", "avc1": "h264",
            "hevc": "hevc", "h265": "hevc", "hev1": "hevc",
            "av1": "av1", "av01": "av1",
            "vp9": "vp9", "vp09": "vp9",
        }
        video["video_codec_normalized"] = codec_map.get(vc, vc)

        # Dateigroesse fuer Puffer-Abschaetzung (ExoPlayer)
        file_path = video.get("file_path")
        try:
            import os
            video["file_size"] = os.path.getsize(file_path) if file_path and os.path.isfile(file_path) else 0
        except Exception:
            video["file_size"] = 0

        # file_path nicht an den Client senden
        video.pop("file_path", None)

        return web.json_response(video)

    # --- HLS Streaming API ---

    @require_auth
    async def post_hls_start(request: web.Request) -> web.Response:
        """POST /tv/api/hls/start - HLS-Session starten
        Body: { video_id, quality?, audio?, sound?, t? }"""
        if not hls_manager:
            return web.json_response(
                {"error": "HLS nicht verfuegbar"}, status=503)

        try:
            data = await request.json()
        except Exception:
            return web.json_response(
                {"error": "Ungueltiges JSON"}, status=400)

        video_id = int(data.get("video_id", 0))
        if not video_id:
            return web.json_response(
                {"error": "video_id erforderlich"}, status=400)

        quality = data.get("quality", "hd")
        audio_idx = int(data.get("audio", 0))
        sound_mode = data.get("sound", "stereo")
        seek_sec = float(data.get("t", 0))
        client_codecs = data.get("codecs")  # z.B. ["h264", "av1", "vp9"]

        session = await hls_manager.create_session(
            video_id, quality, audio_idx, sound_mode, seek_sec,
            client_codecs=client_codecs)

        if not session:
            return web.json_response(
                {"error": "Session konnte nicht erstellt werden"}, status=500)

        if session.error:
            return web.json_response(
                {"error": f"ffmpeg Fehler: {session.error[:200]}"}, status=500)

        return web.json_response({
            "session_id": session.session_id,
            "playlist_url": f"/tv/api/hls/{session.session_id}/playlist.m3u8",
            "ready": session.ready,
        })

    async def get_hls_playlist(request: web.Request) -> web.Response:
        """GET /tv/api/hls/{session_id}/playlist.m3u8 - HLS Manifest"""
        if not hls_manager:
            return web.Response(status=503)

        session_id = request.match_info["session_id"]
        session = hls_manager.get_session(session_id)
        if not session:
            return web.Response(status=404, text="Session nicht gefunden")

        if not session.playlist_path.exists():
            # Playlist noch nicht bereit - leere HLS-Playlist zurueckgeben
            # hls.js und natives HLS laden sie automatisch erneut
            return web.Response(
                text="#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:4\n",
                content_type="application/vnd.apple.mpegurl",
                headers={
                    "Cache-Control": "no-cache, no-store",
                    "Access-Control-Allow-Origin": "*",
                })

        return web.FileResponse(
            session.playlist_path,
            headers={
                "Content-Type": "application/vnd.apple.mpegurl",
                "Cache-Control": "no-cache, no-store",
                "Access-Control-Allow-Origin": "*",
            })

    async def get_hls_segment(request: web.Request) -> web.Response:
        """GET /tv/api/hls/{session_id}/{segment} - HLS Segment (.m4s/.mp4)"""
        if not hls_manager:
            return web.Response(status=503)

        session_id = request.match_info["session_id"]
        segment = request.match_info["segment"]
        session = hls_manager.get_session(session_id)
        if not session:
            return web.Response(status=404, text="Session nicht gefunden")

        # Path-Traversal verhindern (z.B. ../../etc/passwd)
        seg_path = (session.dir / segment).resolve()
        if not str(seg_path).startswith(str(session.dir.resolve())):
            return web.Response(status=403, text="Zugriff verweigert")

        # Warten bis Segment existiert und fertig geschrieben ist
        # (ffmpeg schreibt Segmente inkrementell - bei AV1 copy bis 34 MB)
        for _ in range(50):  # max 5 Sekunden warten (AV1-Segmente sind gross)
            if seg_path.exists() and seg_path.stat().st_size > 0:
                # Kurz warten und nochmal pruefen ob Dateigroesse stabil
                size1 = seg_path.stat().st_size
                await asyncio.sleep(0.15)
                if seg_path.exists() and seg_path.stat().st_size == size1:
                    break  # Segment fertig geschrieben
            await asyncio.sleep(0.1)
        else:
            if not seg_path.exists():
                return web.Response(status=404,
                                    text="Segment nicht verfuegbar")

        # Content-Type je nach Dateiendung
        if segment.endswith(".mp4") or segment.endswith(".m4s"):
            content_type = "video/mp4"
        elif segment.endswith(".ts"):
            content_type = "video/mp2t"
        else:
            content_type = "application/octet-stream"

        return web.FileResponse(
            seg_path,
            headers={
                "Content-Type": content_type,
                "Cache-Control": "public, max-age=86400, immutable",
                "Access-Control-Allow-Origin": "*",
            })

    @require_auth
    async def delete_hls_session(request: web.Request) -> web.Response:
        """DELETE /tv/api/hls/{session_id} - Session beenden"""
        if not hls_manager:
            return web.json_response({"error": "HLS nicht verfuegbar"},
                                     status=503)

        session_id = request.match_info["session_id"]
        await hls_manager.destroy_session(session_id)
        return web.json_response({"success": True})

    async def post_hls_stop(request: web.Request) -> web.Response:
        """POST /tv/api/hls/{session_id}/stop - Session beenden (fuer sendBeacon)"""
        if not hls_manager:
            return web.Response(status=204)
        session_id = request.match_info["session_id"]
        await hls_manager.destroy_session(session_id)
        return web.Response(status=204)

    # --- HLS Admin-API (fuer TV Admin-Center) ---

    async def get_hls_sessions(request: web.Request) -> web.Response:
        """GET /api/tv/hls-sessions - Aktive HLS-Sessions auflisten"""
        if not hls_manager:
            return web.json_response({"sessions": []})
        sessions = hls_manager.get_all_sessions_info()
        # Video-Namen aus DB nachladen fuer Anzeige
        pool = await library_service._get_pool()
        if pool:
            try:
                async with pool.acquire() as conn:
                    async with conn.cursor(aiomysql.DictCursor) as cur:
                        for s in sessions:
                            await cur.execute(
                                "SELECT file_name FROM library_videos "
                                "WHERE id = %s", (s["video_id"],))
                            row = await cur.fetchone()
                            s["video_name"] = row["file_name"] if row else ""
            except Exception:
                pass
        return web.json_response({"sessions": sessions})

    async def delete_hls_session_admin(request: web.Request) -> web.Response:
        """DELETE /api/tv/hls-sessions/{sid} - HLS-Session beenden (Admin)"""
        if not hls_manager:
            return web.json_response({"error": "HLS nicht verfuegbar"},
                                     status=400)
        sid = request.match_info["sid"]
        await hls_manager.destroy_session(sid)
        return web.json_response({"success": True})

    # --- Tizen Debug-Log ---

    async def post_tizen_log(request):
        """Empfaengt Log-Eintraege von der Tizen WGT-App und speichert in DB"""
        try:
            data = await request.json()
            entries = data.get("entries", [])
            ua = data.get("userAgent", "")[:500]
            if not entries:
                return web.json_response({"ok": True, "count": 0})
            pool = library_service._db_pool
            async with pool.acquire() as conn:
                async with conn.cursor() as cur:
                    for e in entries[:50]:  # Max 50 pro Request
                        await cur.execute(
                            "INSERT INTO tizen_logs (level, message, user_agent, client_ts) "
                            "VALUES (%s, %s, %s, %s)",
                            (e.get("l", "I"), e.get("m", "")[:2000],
                             ua, e.get("t", ""))
                        )
                await conn.commit()
            return web.json_response({"ok": True, "count": len(entries)})
        except Exception as ex:
            logging.warning(f"Tizen-Log Fehler: {ex}")
            return web.json_response({"ok": False, "error": str(ex)}, status=400)

    async def get_tizen_log(request):
        """Gibt die letzten Tizen-Logs zurueck (fuer Debugging)"""
        limit = int(request.query.get("limit", "100"))
        level = request.query.get("level", "")
        pool = library_service._db_pool
        async with pool.acquire() as conn:
            async with conn.cursor(aiomysql.DictCursor) as cur:
                if level:
                    await cur.execute(
                        "SELECT * FROM tizen_logs WHERE level = %s "
                        "ORDER BY id DESC LIMIT %s", (level, limit))
                else:
                    await cur.execute(
                        "SELECT * FROM tizen_logs ORDER BY id DESC LIMIT %s",
                        (limit,))
                rows = await cur.fetchall()
        # Chronologisch (aelteste zuerst)
        rows.reverse()
        # Timestamps serialisierbar machen
        for r in rows:
            if r.get("created_at"):
                r["created_at"] = str(r["created_at"])
        return web.json_response(rows)

    # --- Routes registrieren ---

    # TV-Seiten (mit Auth via Decorator)
    app.router.add_get("/tv/login", get_login)
    app.router.add_post("/tv/login", post_login)
    app.router.add_get("/tv/logout", get_logout)
    app.router.add_get("/tv/profiles", get_profiles)
    app.router.add_post("/tv/switch-profile", post_switch_profile)
    app.router.add_get("/tv", lambda r: web.HTTPFound("/tv/"))
    app.router.add_get("/tv/", get_home)
    app.router.add_get("/tv/series", get_series_list)
    app.router.add_get("/tv/series/{id}", get_series_detail)
    app.router.add_get("/tv/movies", get_movies_list)
    app.router.add_get("/tv/movies/{id}", get_movie_detail)
    app.router.add_get("/tv/player", get_player)
    app.router.add_get("/tv/search", get_search)
    app.router.add_get("/tv/watchlist", get_watchlist)
    app.router.add_get("/tv/settings", get_settings)
    app.router.add_post("/tv/settings", post_settings)
    app.router.add_post("/tv/settings/reset", post_reset_progress)

    # TV-API (Watch-Progress, Watchlist, Status, Suche, i18n)
    app.router.add_post("/tv/api/watch-progress", post_watch_progress)
    app.router.add_get(
        "/tv/api/watch-progress/{video_id}", get_watch_progress)
    app.router.add_post("/tv/api/watchlist", post_watchlist_toggle)
    app.router.add_post("/tv/api/watch-status", post_watch_status)
    app.router.add_get("/tv/api/search/suggest", get_search_suggestions)
    app.router.add_get("/tv/api/search/history", get_search_history)
    app.router.add_delete("/tv/api/search/history", delete_search_history)
    app.router.add_get("/tv/api/i18n", get_i18n)
    app.router.add_post("/tv/api/rating", post_rating)

    # Direct-Stream API (AVPlay)
    app.router.add_get(
        "/tv/api/direct-stream/{video_id}", get_direct_stream)
    app.router.add_get(
        "/tv/api/video-info/{video_id}", get_video_info_tv)
    app.router.add_post(
        "/tv/api/stream-token", post_stream_token)

    # HLS Streaming API
    app.router.add_post("/tv/api/hls/start", post_hls_start)
    app.router.add_get(
        "/tv/api/hls/{session_id}/playlist.m3u8", get_hls_playlist)
    app.router.add_get(
        "/tv/api/hls/{session_id}/{segment}", get_hls_segment)
    app.router.add_delete(
        "/tv/api/hls/{session_id}", delete_hls_session)
    app.router.add_post(
        "/tv/api/hls/{session_id}/stop", post_hls_stop)

    # Admin-API (QR-Code, User-Verwaltung)
    app.router.add_get("/api/tv/qrcode", get_qrcode)
    app.router.add_get("/api/tv/url", get_tv_url)
    app.router.add_get("/api/tv/users", get_users)
    app.router.add_post("/api/tv/users", post_user)
    app.router.add_put("/api/tv/users/{id}", put_user)
    app.router.add_delete("/api/tv/users/{id}", delete_user)
    app.router.add_get("/api/tv/hls-sessions", get_hls_sessions)
    app.router.add_delete("/api/tv/hls-sessions/{sid}", delete_hls_session_admin)

    # Tizen Debug-Log API (kein Auth, Daten von WGT-App)
    app.router.add_post("/api/tizen-log", post_tizen_log)
    app.router.add_get("/api/tizen-log", get_tizen_log)
