#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
stream_controller_ordered.py

Spielt lokale HLS-Playlists geordnet nacheinander ab und gibt sie per UDP an OBS aus.

Ordnerstruktur z.B.:
D:\
  78f573d7-29c8-46eb-90ef-f46c49f4987b\
      playlist.m3u8
      segment000.ts
      segment001.ts
      ...
  anderer-ordner\
      playlist.m3u8
      ...

Wichtig:
- FFmpeg bekommt immer nur EINE playlist.m3u8 gleichzeitig.
- Erst wenn FFmpeg diese komplett beendet hat, wird die nächste gestartet.
- Neue Ordner werden automatisch erkannt und hinten in die Queue gestellt.
- Bereits abgespielte Playlists werden in played_playlists.txt gespeichert.
"""

import os
import sys
import time
import json
import signal
import logging
import subprocess
from pathlib import Path
from typing import Optional
import urllib.request
import urllib.error
import urllib.parse
import http.server
import socketserver
import threading

# ---------------------------------------------------------------------------
# KONFIGURATION
# ---------------------------------------------------------------------------

ROOT_DIR = Path("D:/")
WORK_DIR = Path("D:/ffmpeg_stream")

PLAYLIST_NAME = "playlist.m3u8"

FFMPEG = "ffmpeg"

UDP_OUTPUT = "udp://127.0.0.1:12345?pkt_size=1316"

SCAN_INTERVAL_SECONDS = 5

# Nur vollständige HLS-VODs abspielen.
# True = playlist.m3u8 muss #EXT-X-ENDLIST enthalten.
# Sehr empfohlen, wenn Dateien noch während des Downloads geschrieben werden.
REQUIRE_ENDLIST = False

# Falls deine m3u8 KEIN #EXT-X-ENDLIST bekommt, kannst du REQUIRE_ENDLIST=False
# setzen. Dann wird stattdessen geprüft, ob die Playlist eine gewisse Zeit
# unverändert geblieben ist.
STABLE_SECONDS = 20

# Bereits abgespielte Streams nach Neustart erneut abspielen (Endlos-Loop).
REMEMBER_PLAYED = False

# Wenn True, werden am Ende alle vorhandenen Playlists erneut von vorne gespielt.
# Notwendig für einen 24/7-Stream, der nach dem letzten Video wieder von vorne anfängt.
LOOP_ALL = True

# Transcoding sorgt dafür, dass OBS immer einen gleichartigen Stream bekommt.
# Das ist stabiler als -c copy, besonders bei unterschiedlichen HLS-Dateien.
VIDEO_CODEC = "libx264"
VIDEO_PRESET = "veryfast"
VIDEO_CRF = "20"
AUDIO_CODEC = "aac"
AUDIO_BITRATE = "192k"

OUTPUT_FPS = "60"

# Falls alle Quellen bereits identisch sind und CPU gespart werden soll,
# kann USE_STREAM_COPY=True getestet werden.
# Bei Timestamp-/Codec-Problemen wieder False setzen.
USE_STREAM_COPY = False

# Twitch-API (nur relevant, wenn Titel/Kategorie automatisch geändert werden sollen)
# Über Umgebungsvariablen setzen, z. B.:
#   set TWITCH_CLIENT_ID=...
#   set TWITCH_ACCESS_TOKEN=...
#   set BROADCASTER_ID=...
#   set TWITCH_CONTENT_LABELS=SexualThemes (optional, komma-getrennt)
TWITCH_CLIENT_ID = os.environ.get("TWITCH_CLIENT_ID", "")
TWITCH_ACCESS_TOKEN = os.environ.get("TWITCH_ACCESS_TOKEN", "")
TWITCH_REFRESH_TOKEN = os.environ.get("TWITCH_REFRESH_TOKEN", "")
TWITCH_CONTENT_LABELS = [
    l.strip()
    for l in os.environ.get("TWITCH_CONTENT_LABELS", "").split(",")
    if l.strip()
]

TWITCH_TOKEN_FILE = WORK_DIR / "twitch_token.json"

# OAuth Konfiguration
OAUTH_REDIRECT_URI = "http://localhost:9999/callback"
OAUTH_SCOPES = "channel:manage:broadcast"

# Resume-Konfiguration
# True = Skript fährt dort weiter, wo es gestoppt wurde (Playlist-Position merken)
# False = Skript fängt immer bei der ersten Playlist an
RESUME_ENABLED = True

# ---------------------------------------------------------------------------

WORK_DIR.mkdir(parents=True, exist_ok=True)

LOG_FILE = WORK_DIR / "stream_controller.log"
PLAYED_FILE = WORK_DIR / "played_playlists.txt"
STATE_FILE = WORK_DIR / "queue_state.json"
RESUME_FILE = WORK_DIR / "resume_state.json"

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
    handlers=[
        logging.FileHandler(LOG_FILE, encoding="utf-8"),
        logging.StreamHandler(sys.stdout),
    ],
)

stop_requested = False
current_process: Optional[subprocess.Popen] = None


def request_stop(signum=None, frame=None):
    global stop_requested, current_process
    stop_requested = True
    logging.info("Beenden angefordert.")

    if current_process and current_process.poll() is None:
        try:
            current_process.terminate()
        except Exception:
            pass


signal.signal(signal.SIGINT, request_stop)
if hasattr(signal, "SIGTERM"):
    signal.signal(signal.SIGTERM, request_stop)


def norm_path(path: Path) -> str:
    """Normalisierte Pfaddarstellung für State-Dateien."""
    try:
        return str(path.resolve()).lower()
    except Exception:
        return str(path.absolute()).lower()


def load_played() -> set[str]:
    if not REMEMBER_PLAYED or not PLAYED_FILE.exists():
        return set()

    result = set()
    try:
        for line in PLAYED_FILE.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if line:
                result.add(line.lower())
    except Exception as exc:
        logging.warning("Konnte played_playlists.txt nicht lesen: %s", exc)

    return result


def mark_played(path: Path, played: set[str]) -> None:
    key = norm_path(path)
    played.add(key)

    if not REMEMBER_PLAYED:
        return

    try:
        with PLAYED_FILE.open("a", encoding="utf-8") as f:
            f.write(key + "\n")
    except Exception as exc:
        logging.warning("Konnte Playlist nicht als abgespielt speichern: %s", exc)


def playlist_has_endlist(path: Path) -> bool:
    try:
        text = path.read_text(encoding="utf-8", errors="ignore")
        return "#EXT-X-ENDLIST" in text
    except Exception:
        return False


def playlist_is_stable(path: Path) -> bool:
    """
    Fallback für Playlists ohne ENDLIST:
    Datei muss STABLE_SECONDS lang unverändert sein.
    """
    try:
        age = time.time() - path.stat().st_mtime
        return age >= STABLE_SECONDS
    except OSError:
        return False


def playlist_ready(path: Path) -> bool:
    if not path.is_file():
        return False

    if REQUIRE_ENDLIST:
        return playlist_has_endlist(path)

    return playlist_is_stable(path)


def discover_playlists() -> list[Path]:
    """
    Sucht rekursiv nach playlist.m3u8.
    D:\ffmpeg_stream wird absichtlich ignoriert.
    Sortierung:
      1. Änderungszeit des übergeordneten Ordners
      2. Pfadname als stabiler Tie-Breaker
    """
    found: list[Path] = []

    try:
        for path in ROOT_DIR.rglob(PLAYLIST_NAME):
            try:
                # Arbeitsordner ausschließen
                if WORK_DIR in path.parents:
                    continue

                if playlist_ready(path):
                    found.append(path)
            except (OSError, RuntimeError):
                continue
    except Exception as exc:
        logging.error("Fehler beim Scannen von %s: %s", ROOT_DIR, exc)
        return []

    def sort_key(p: Path):
        try:
            # Ordner-Zeit statt einzelner TS-Zeit verwenden
            ts = p.parent.stat().st_mtime
        except OSError:
            ts = 0
        return (ts, str(p).lower())

    found.sort(key=sort_key)
    return found


def write_state(queue: list[Path], current: Optional[Path]) -> None:
    try:
        data = {
            "current": str(current) if current else None,
            "queue": [str(p) for p in queue],
            "updated": time.strftime("%Y-%m-%d %H:%M:%S"),
        }
        STATE_FILE.write_text(
            json.dumps(data, indent=2, ensure_ascii=False),
            encoding="utf-8",
        )
    except Exception:
        pass


def write_resume_state(current: Optional[Path]) -> None:
    """Speichert die aktuelle Playlist für Resume beim nächsten Start."""
    if not RESUME_ENABLED:
        return

    try:
        data = {
            "current": str(current) if current else None,
            "updated": time.strftime("%Y-%m-%d %H:%M:%S"),
        }
        RESUME_FILE.write_text(
            json.dumps(data, indent=2, ensure_ascii=False),
            encoding="utf-8",
        )
    except Exception:
        pass


def load_resume_state() -> Optional[Path]:
    """Lädt die zuletzt gespielte Playlist für Resume."""
    if not RESUME_ENABLED or not RESUME_FILE.exists():
        return None

    try:
        data = json.loads(RESUME_FILE.read_text(encoding="utf-8"))
        current_str = data.get("current")
        if current_str:
            path = Path(current_str)
            if path.exists():
                return path
    except Exception as exc:
        logging.warning("Konnte Resume-Status nicht laden: %s", exc)

    return None


def build_ffmpeg_command(playlist: Path) -> list[str]:
    """
    FFmpeg liest die lokale m3u8 direkt.
    Relative .ts-Pfade werden dadurch relativ zum Ordner der m3u8 aufgelöst.
    """

    cmd = [
        FFMPEG,
        "-hide_banner",
        "-loglevel", "warning",

        # Echtzeit-Wiedergabe, damit eine 20-Minuten-Datei auch ~20 Min. läuft
        "-re",

        # Hilft bei problematischen MPEG-TS-Zeitstempeln
        "-fflags", "+genpts+discardcorrupt",

        # HLS/MPEG-TS Input
        "-i", str(playlist),
    ]

    if USE_STREAM_COPY:
        cmd += [
            "-map", "0:v:0?",
            "-map", "0:a:0?",
            "-c", "copy",
            "-avoid_negative_ts", "make_zero",
            "-muxdelay", "0",
            "-muxpreload", "0",
        ]
    else:
        cmd += [
            "-map", "0:v:0?",
            "-map", "0:a:0?",

            # Video vereinheitlichen
            "-c:v", VIDEO_CODEC,
            "-preset", VIDEO_PRESET,
            "-crf", VIDEO_CRF,
            "-pix_fmt", "yuv420p",
            "-r", OUTPUT_FPS,

            # OBS-/Streaming-freundliche GOP
            "-g", str(int(OUTPUT_FPS) * 2),
            "-keyint_min", str(int(OUTPUT_FPS) * 2),

            # Audio vereinheitlichen
            "-c:a", AUDIO_CODEC,
            "-b:a", AUDIO_BITRATE,
            "-ar", "48000",
            "-ac", "2",

            # Zeitstempel bereinigen
            "-avoid_negative_ts", "make_zero",
            "-start_at_zero",
        ]

    cmd += [
        "-f", "mpegts",
        UDP_OUTPUT,
    ]

    return cmd


def play_playlist(playlist: Path) -> bool:
    global current_process

    logging.info("=" * 72)
    logging.info("STARTE STREAM:")
    logging.info("Ordner   : %s", playlist.parent)
    logging.info("Playlist : %s", playlist)
    logging.info("=" * 72)

    cmd = build_ffmpeg_command(playlist)

    logging.info("FFmpeg: %s", subprocess.list2cmdline(cmd))

    try:
        current_process = subprocess.Popen(
            cmd,
            cwd=str(playlist.parent),
        )
    except FileNotFoundError:
        logging.error(
            "FFmpeg wurde nicht gefunden. Prüfe, ob ffmpeg.exe im PATH liegt "
            "oder trage oben den vollständigen Pfad in FFMPEG ein."
        )
        return False
    except Exception as exc:
        logging.error("FFmpeg konnte nicht gestartet werden: %s", exc)
        return False

    while current_process.poll() is None and not stop_requested:
        time.sleep(0.5)

    if stop_requested:
        try:
            current_process.terminate()
            current_process.wait(timeout=5)
        except Exception:
            try:
                current_process.kill()
            except Exception:
                pass
        return False

    return_code = current_process.returncode
    current_process = None

    if return_code == 0:
        logging.info("Stream vollständig beendet: %s", playlist.parent.name)
        return True

    logging.error(
        "FFmpeg wurde mit Fehlercode %s beendet: %s",
        return_code,
        playlist,
    )
    return False


class OAuthCallbackHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path.startswith("/callback"):
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(b"<h1>OAuth erfolgreich!</h1><p>Du kannst dieses Fenster schliessen.</p>")

            query = urllib.parse.urlparse(self.path).query
            params = urllib.parse.parse_qs(query)
            code = params.get("code", [None])[0]
            if code:
                self.server.auth_code = code
            else:
                error = params.get("error", [None])[0]
                self.server.auth_error = error or "Unbekannter Fehler"
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, format, *args):
        pass


def start_oauth_server(port: int = 9999) -> tuple[socketserver.TCPServer, threading.Thread]:
    server = socketserver.TCPServer(("localhost", port), OAuthCallbackHandler)
    server.auth_code = None
    server.auth_error = None
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    return server, thread


def exchange_auth_code_for_token(auth_code: str) -> Optional[dict]:
    """Tauscht Authorization Code gegen Access Token und Refresh Token."""
    url = "https://id.twitch.tv/oauth2/token"
    data = {
        "client_id": TWITCH_CLIENT_ID,
        "client_secret": "",  # Twitch OAuth Flow ohne Client Secret ist fuer public clients
        "code": auth_code,
        "grant_type": "authorization_code",
        "redirect_uri": OAUTH_REDIRECT_URI,
    }
    encoded_data = urllib.parse.urlencode(data).encode("utf-8")

    try:
        with urllib.request.urlopen(url, data=encoded_data, timeout=15) as resp:
            response = json.loads(resp.read().decode("utf-8", errors="ignore"))
            return {
                "access_token": response.get("access_token"),
                "refresh_token": response.get("refresh_token"),
                "expires_at": time.time() + response.get("expires_in", 3600),
            }
    except urllib.error.HTTPError as exc:
        logging.error("Twitch OAuth HTTP %s: %s", exc.code, exc.read().decode("utf-8", errors="ignore"))
    except urllib.error.URLError as exc:
        logging.error("Twitch OAuth URL-Fehler: %s", exc.reason)
    except Exception as exc:
        logging.error("Twitch OAuth Fehler: %s", exc)
    return None


def get_broadcaster_id(access_token: str) -> Optional[str]:
    """Ruft die Broadcaster ID fuer den eingeloggten User ab."""
    url = "https://api.twitch.tv/helix/users"
    headers = {
        "Client-Id": TWITCH_CLIENT_ID,
        "Authorization": f"Bearer {access_token}",
    }
    req = urllib.request.Request(url, headers=headers)
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            response = json.loads(resp.read().decode("utf-8", errors="ignore"))
            if response.get("data"):
                return response["data"][0].get("id")
    except Exception as exc:
        logging.error("Konnte Broadcaster ID nicht abrufen: %s", exc)
    return None


def perform_oauth_flow() -> bool:
    """Fuehrt den kompletten OAuth Flow durch."""
    if not TWITCH_CLIENT_ID:
        logging.error("TWITCH_CLIENT_ID muss gesetzt sein fuer OAuth Flow.")
        return False

    logging.info("Starte OAuth Flow...")
    server, thread = start_oauth_server()

    auth_url = (
        f"https://id.twitch.tv/oauth2/authorize"
        f"?client_id={TWITCH_CLIENT_ID}"
        f"&redirect_uri={urllib.parse.quote(OAUTH_REDIRECT_URI)}"
        f"&response_type=code"
        f"&scope={urllib.parse.quote(OAUTH_SCOPES)}"
    )

    logging.info("Oeffne folgenden Link im Browser:")
    logging.info(auth_url)
    logging.info("Warte auf OAuth Callback...")

    for _ in range(120):  # 2 Minuten Timeout
        time.sleep(1)
        if hasattr(server, "auth_code") and server.auth_code:
            break
        if hasattr(server, "auth_error") and server.auth_error:
            logging.error("OAuth Fehler: %s", server.auth_error)
            server.shutdown()
            return False
    else:
        logging.error("OAuth Timeout: Kein Callback erhalten.")
        server.shutdown()
        return False

    server.shutdown()
    thread.join(timeout=5)

    token_data = exchange_auth_code_for_token(server.auth_code)
    if not token_data:
        return False

    broadcaster_id = get_broadcaster_id(token_data["access_token"])
    if not broadcaster_id:
        logging.warning("Konnte Broadcaster ID nicht abrufen, speichere ohne.")
    else:
        token_data["broadcaster_id"] = broadcaster_id
        logging.info("Broadcaster ID: %s", broadcaster_id)

    save_twitch_token(token_data)
    logging.info("OAuth erfolgreich abgeschlossen.")
    return True


def load_twitch_token() -> dict:
    """Lädt Twitch-Token aus JSON-Datei oder initialisiert aus Umgebungsvariablen / OAuth."""
    if TWITCH_TOKEN_FILE.exists():
        try:
            return json.loads(TWITCH_TOKEN_FILE.read_text(encoding="utf-8"))
        except Exception as exc:
            logging.warning("Konnte Twitch-Token-Datei nicht lesen: %s", exc)

    if TWITCH_ACCESS_TOKEN and TWITCH_REFRESH_TOKEN:
        logging.info("Initialisiere Twitch-Token aus Umgebungsvariablen.")
        token_data = {
            "access_token": TWITCH_ACCESS_TOKEN,
            "refresh_token": TWITCH_REFRESH_TOKEN,
            "expires_at": time.time() + 3600,  # Vorläufig, wird beim ersten Refresh korrigiert
        }
        save_twitch_token(token_data)
        return token_data

    if TWITCH_CLIENT_ID:
        logging.info("Kein Token vorhanden, starte OAuth Flow...")
        if perform_oauth_flow():
            if TWITCH_TOKEN_FILE.exists():
                return json.loads(TWITCH_TOKEN_FILE.read_text(encoding="utf-8"))
        return {}

    logging.warning(
        "Kein Twitch-Token vorhanden. Bitte TWITCH_CLIENT_ID setzen "
        "oder TWITCH_ACCESS_TOKEN und TWITCH_REFRESH_TOKEN manuell."
    )
    return {}


def save_twitch_token(token_data: dict) -> None:
    """Speichert Twitch-Token in JSON-Datei."""
    try:
        TWITCH_TOKEN_FILE.write_text(
            json.dumps(token_data, indent=2, ensure_ascii=False),
            encoding="utf-8",
        )
    except Exception as exc:
        logging.error("Konnte Twitch-Token nicht speichern: %s", exc)


def is_token_expired(token_data: dict, buffer_seconds: int = 300) -> bool:
    """Prüft, ob der Token in den nächsten buffer_seconds Sekunden abläuft."""
    expires_at = token_data.get("expires_at")
    if not expires_at:
        return True
    return time.time() + buffer_seconds >= expires_at


def refresh_twitch_token(token_data: dict) -> Optional[dict]:
    """Erneuert Twitch-Token mit Refresh Token."""
    refresh_token = token_data.get("refresh_token")
    if not refresh_token:
        logging.error("Kein Refresh Token vorhanden, kann Token nicht erneuern.")
        return None

    url = "https://id.twitch.tv/oauth2/token"
    data = {
        "client_id": TWITCH_CLIENT_ID,
        "grant_type": "refresh_token",
        "refresh_token": refresh_token,
    }
    encoded_data = urllib.parse.urlencode(data).encode("utf-8")

    try:
        with urllib.request.urlopen(url, data=encoded_data, timeout=15) as resp:
            response = json.loads(resp.read().decode("utf-8", errors="ignore"))
            new_token_data = {
                "access_token": response.get("access_token"),
                "refresh_token": response.get("refresh_token"),
                "expires_at": time.time() + response.get("expires_in", 3600),
            }
            # Broadcaster ID übernehmen, falls vorhanden
            if "broadcaster_id" in token_data:
                new_token_data["broadcaster_id"] = token_data["broadcaster_id"]
            save_twitch_token(new_token_data)
            logging.info("Twitch-Token erfolgreich erneuert.")
            return new_token_data
    except urllib.error.HTTPError as exc:
        logging.error("Twitch-Token-Refresh HTTP %s: %s", exc.code, exc.read().decode("utf-8", errors="ignore"))
    except urllib.error.URLError as exc:
        logging.error("Twitch-Token-Refresh URL-Fehler: %s", exc.reason)
    except Exception as exc:
        logging.error("Twitch-Token-Refresh Fehler: %s", exc)
    return None


def get_valid_access_token() -> Optional[str]:
    """Gibt einen gültigen Access Token zurück, erneuert bei Bedarf."""
    token_data = load_twitch_token()
    if not token_data:
        return None

    if is_token_expired(token_data):
        logging.info("Twitch-Token läuft bald ab, erneuere...")
        token_data = refresh_twitch_token(token_data)
        if not token_data:
            return None

    return token_data.get("access_token")


def find_chat_json(playlist: Path) -> Optional[Path]:
    """Sucht im Ordner der Playlist nach einer Chat-JSON-Datei."""
    for path in playlist.parent.glob("*Chat.json"):
        if path.is_file():
            return path
    return None


def load_metadata_from_chat(chat_path: Path) -> Optional[dict]:
    """Liest Titel und Spiel aus der Twitch-Chat-Export-JSON."""
    try:
        data = json.loads(chat_path.read_text(encoding="utf-8", errors="ignore"))
    except Exception as exc:
        logging.warning("Konnte Chat-Datei nicht parsen (%s): %s", chat_path, exc)
        return None

    video = data.get("video", {})
    title = video.get("title")
    game = video.get("game")
    if not game and video.get("chapters"):
        game = video["chapters"][0].get("gameDisplayName")

    if not title and not game:
        logging.warning("Keine Metadaten in Chat-Datei: %s", chat_path)
        return None

    return {"title": title, "game": game}


def _twitch_request(url: str, method: str = "GET", data: Optional[bytes] = None) -> Optional[dict]:
    if not TWITCH_CLIENT_ID:
        return None

    access_token = get_valid_access_token()
    if not access_token:
        return None

    headers = {
        "Client-Id": TWITCH_CLIENT_ID,
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json",
    }
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            body = resp.read().decode("utf-8", errors="ignore")
            if body:
                return json.loads(body)
            return {}
    except urllib.error.HTTPError as exc:
        logging.error("Twitch-API HTTP %s: %s", exc.code, exc.read().decode("utf-8", errors="ignore"))
    except urllib.error.URLError as exc:
        logging.error("Twitch-API URL-Fehler: %s", exc.reason)
    except Exception as exc:
        logging.error("Twitch-API Anfragefehler: %s", exc)
    return None


def get_twitch_game_id(game_name: str) -> Optional[str]:
    if not game_name:
        return None
    encoded = urllib.parse.quote(game_name)
    url = f"https://api.twitch.tv/helix/games?name={encoded}"
    result = _twitch_request(url)
    if result and result.get("data"):
        return result["data"][0].get("id")
    logging.warning("Spiel nicht auf Twitch gefunden: %s", game_name)
    return None


def update_twitch_channel(title: Optional[str], game_name: Optional[str]) -> None:
    """Aktualisiert Twitch-Titel und Kategorie über die Helix-API."""
    if not TWITCH_CLIENT_ID:
        logging.debug("Twitch-API nicht konfiguriert, überspringe Update.")
        return

    broadcaster_id = BROADCASTER_ID
    if not broadcaster_id:
        token_data = load_twitch_token()
        broadcaster_id = token_data.get("broadcaster_id") if token_data else None

    if not broadcaster_id:
        logging.warning("Keine Broadcaster ID verfügbar, überspringe Twitch-Update.")
        return

    payload: dict[str, object] = {}
    if title:
        payload["title"] = title
    game_id = get_twitch_game_id(game_name) if game_name else None
    if game_id:
        payload["game_id"] = game_id
    if TWITCH_CONTENT_LABELS:
        payload["content_classification_labels"] = TWITCH_CONTENT_LABELS

    if not payload:
        return

    url = f"https://api.twitch.tv/helix/channels?broadcaster_id={broadcaster_id}"
    data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    result = _twitch_request(url, method="PATCH", data=data)
    if result is not None:
        logging.info("Twitch-Streaminfo aktualisiert: title='%s', game='%s'", title, game_name)
    else:
        logging.error("Twitch-Streaminfo konnte nicht aktualisiert werden.")


def main():
    logging.info("Stream Controller gestartet.")
    logging.info("Suche unter: %s", ROOT_DIR)
    logging.info("OBS-Quelle: %s", UDP_OUTPUT)
    logging.info("REQUIRE_ENDLIST=%s", REQUIRE_ENDLIST)
    logging.info("USE_STREAM_COPY=%s", USE_STREAM_COPY)
    logging.info("RESUME_ENABLED=%s", RESUME_ENABLED)

    played = load_played()
    queue: list[Path] = []
    queued_keys: set[str] = set()

    # Resume: Zuletzt gespielte Playlist wieder an die Queue-Position setzen
    resume_playlist = load_resume_state()
    if resume_playlist:
        logging.info("Resume: Setze fort bei %s", resume_playlist.parent.name)

    while not stop_requested:
        discovered = discover_playlists()

        # Neue Playlists hinten anstellen
        for playlist in discovered:
            key = norm_path(playlist)

            if key in queued_keys:
                continue

            if not LOOP_ALL and key in played:
                continue

            queue.append(playlist)
            queued_keys.add(key)

            logging.info(
                "Neu in Queue: %s",
                playlist.parent.name,
            )

        write_state(queue, None)

        if not queue:
            logging.info(
                "Keine neue vollständige Playlist. Neuer Scan in %ss.",
                SCAN_INTERVAL_SECONDS,
            )
            time.sleep(SCAN_INTERVAL_SECONDS)

            # Bei LOOP_ALL nach vollständigem Durchlauf played zurücksetzen
            if LOOP_ALL and discovered:
                played.clear()

            continue

        # Resume: Wenn die zuletzt gespielte Playlist in der Queue ist, springe zu ihr
        if resume_playlist:
            resume_key = norm_path(resume_playlist)
            for i, playlist in enumerate(queue):
                if norm_path(playlist) == resume_key:
                    logging.info("Resume: Springe zu Position %d (%s)", i, playlist.parent.name)
                    queue = queue[i:]
                    resume_playlist = None
                    break
            else:
                logging.warning("Resume: Playlist nicht mehr gefunden, starte von vorne.")
                resume_playlist = None

        playlist = queue.pop(0)
        key = norm_path(playlist)
        queued_keys.discard(key)

        write_state(queue, playlist)
        write_resume_state(playlist)

        # Vor Start nochmal prüfen, ob Datei noch da ist
        if not playlist.exists():
            logging.warning("Playlist existiert nicht mehr: %s", playlist)
            continue

        chat_path = find_chat_json(playlist)
        if chat_path:
            meta = load_metadata_from_chat(chat_path)
            if meta:
                logging.info(
                    "Chat-Metadaten: title='%s', game='%s'",
                    meta.get("title"),
                    meta.get("game"),
                )
                update_twitch_channel(meta.get("title"), meta.get("game"))

        success = play_playlist(playlist)

        if stop_requested:
            break

        if success:
            mark_played(playlist, played)
        else:
            # Fehlerhafte Playlist NICHT sofort in Endlosschleife erneut starten.
            # Sie wird als gespielt markiert, damit die Queue weiterläuft.
            logging.error(
                "Playlist übersprungen, damit die Queue nicht hängen bleibt: %s",
                playlist,
            )
            mark_played(playlist, played)

        # Nach jedem Stream sofort neu scannen.
        time.sleep(1)

    write_state([], None)
    write_resume_state(None)
    logging.info("Stream Controller beendet.")


if __name__ == "__main__":
    main()
