# coding: utf-8

import os
import sys
import gb_common as common
import urllib.request
import urllib.parse
import json
import xbmc
import xbmcgui
import xbmcaddon
import xbmcplugin
import os
import xbmcvfs
import ssl
import hashlib
import datetime as dt_module
import random
import traceback
import re

# Optional: paralleles Bild-Caching
try:
    from concurrent.futures import ThreadPoolExecutor, as_completed
    _HAS_FUTURES = True
except Exception:
    _HAS_FUTURES = False


# ---------------------------------------------------------------------------------------
# Konfiguration / Konstanten
# ---------------------------------------------------------------------------------------

SERIES_FAV_JSON_FILENAME = "seriesFav.json"
LOCAL_IMAGE_CACHE_SUBDIR_FAV = "cache_fav_series_images"
LOCAL_IMAGE_CACHE_DIR_FAV = ""

# Favoriten: bewusst KEIN watched-status / kein Playback-Monitoring.
# (Random-Play kann optional einen Monitor starten, ist aber default aus.)
ENABLE_FAV_SERIES_MONITOR = False

# Log-Helper
LOGP = f"[{common.addon_id} favorites.py]"

def log(msg, lvl=xbmc.LOGINFO):
    try:
        xbmc.log(f"{LOGP} {msg}", lvl)
    except Exception:
        pass


def _safe_end_of_directory(succeeded=True, update_listing=False, cache_to_disc=True, handle=None):
    try:
        h = common.addon_handle if handle is None else handle
        h = int(h)
    except Exception as e:
        log(f"endOfDirectory: invalid handle ({handle}): {e}", xbmc.LOGDEBUG)
        return False
    if h < 0:
        log(f"endOfDirectory skipped in script mode (handle={h})", xbmc.LOGDEBUG)
        return False
    try:
        xbmcplugin.endOfDirectory(h, succeeded=succeeded, updateListing=update_listing, cacheToDisc=cache_to_disc)
        return True
    except Exception as e:
        log(f"endOfDirectory failed (handle={h}): {e}", xbmc.LOGDEBUG)
        return False


def start_series_monitor(playable_path: str, identifier_for_status: str = None):
    """
    Optional: startet den Playback-Monitor (series.monitor_playback_series) für eine Episode,
    die aus den Favoriten gestartet wird.

    Standard ist AUS (ENABLE_FAV_SERIES_MONITOR=False), damit Favoriten:
      - keinen watched-status setzen/ändern
      - keinerlei Nebenwirkungen im Hintergrund haben
    """
    if not ENABLE_FAV_SERIES_MONITOR:
        return

    try:
        if not playable_path:
            return

        import threading

        try:
            import series  # lazy import (nur wenn Feature aktiv)
        except Exception:
            log("Monitor: series.py nicht importierbar -> skip", xbmc.LOGDEBUG)
            return

        ident = (identifier_for_status or playable_path or "").strip() or playable_path

        t = threading.Thread(
            target=series.monitor_playback_series,
            args=(playable_path, ident),
        )
        t.daemon = True
        t.start()
        try:
            p_mask = common.mask_url(playable_path)
            i_mask = common.mask_url(ident)
        except Exception:
            p_mask = playable_path
            i_mask = ident
        log(f"Monitor gestartet für Favorit-Playback: {p_mask} (id={i_mask})", xbmc.LOGDEBUG)
    except Exception as e:
        log(f"Monitor Fehler: {e}", xbmc.LOGDEBUG)

# ---------------------------------------------------------------------------------------
# Safe-Getter für Addon-Settings (ohne settings.xml)
# ---------------------------------------------------------------------------------------
def _get_setting_str(key, default=""):
    try:
        val = common.addon.getSetting(key)
        return val if isinstance(val, str) else default
    except Exception:
        return default

def _get_setting_bool(key, default=False):
    try:
        raw = common.addon.getSetting(key)
    except Exception:
        raw = ""
    s = (raw or "").strip().lower()
    if s in ("true", "1", "yes", "on"):
        return True
    if s in ("false", "0", "no", "off"):
        return False
    return default

def _get_setting_int(key, default=0):
    try:
        raw = common.addon.getSetting(key)
        return int(raw)
    except Exception:
        return default


# ---------------------------------------------------------------------------------------
# Netz / Timeouts / SSL
# ---------------------------------------------------------------------------------------
NET_TIMEOUT = max(5, _get_setting_int("net_timeout", 20))
IMG_TIMEOUT = max(5, _get_setting_int("image_timeout", 15))

PARALLEL_IMAGES = _get_setting_bool("image_cache_parallel", True)
IMAGE_WORKERS = max(1, min(8, _get_setting_int("image_workers", 4)))

try:
    # bevorzugt: globaler SSL-Kontext aus common.py (spart CPU, thread-safe)
    _SSL_CTX = common.get_ssl_context() if hasattr(common, "get_ssl_context") else None
except Exception:
    _SSL_CTX = None

# Fallback: wenn common.get_ssl_context nicht existiert oder fehlschlägt
if _SSL_CTX is None:
    try:
        _SSL_CTX = ssl.create_default_context()
    except Exception:
        _SSL_CTX = None

def _robust_urlopen(req, timeout=NET_TIMEOUT):
    """urllib.request.urlopen mit wiederverwendetem SSL-Context."""
    ctx = None
    try:
        get_ctx = getattr(common, "get_ssl_context", None)
        if callable(get_ctx):
            ctx = get_ctx()
    except Exception:
        ctx = None

    ctx = ctx or _SSL_CTX
    if ctx:
        return urllib.request.urlopen(req, context=ctx, timeout=timeout)
    return urllib.request.urlopen(req, timeout=timeout)

def unquote2(s):
    return urllib.parse.unquote(urllib.parse.unquote(s or ""))

def _prepare_playback_path(path):
    """Prepare HTTP(S) URLs for Kodi playback (strip userinfo + auth via headers)."""
    try:
        p = (path or "").strip()
    except Exception:
        p = ""
    if not p:
        return ""
    if p.startswith(("http://", "https://")):
        try:
            return common.kodi_prepare_play_url(p)
        except Exception as e:
            log(f"playback path hardening fallback (raw URL used): {e}", xbmc.LOGDEBUG)
            return p
    return p

def _playback_compare_key(path):
    """Normalize player paths for robust comparison (raw URL vs Kodi prepared URL)."""
    try:
        p = unquote2(path or "")
    except Exception:
        p = str(path or "")
    if not p:
        return ""
    if "|" in p:
        p = p.split("|", 1)[0]
    try:
        sp = urllib.parse.urlsplit(p)
        if (sp.scheme or "").lower() in ("http", "https"):
            host = sp.hostname or ""
            if sp.port:
                host = f"{host}:{sp.port}"
            p = urllib.parse.urlunsplit((sp.scheme, host, sp.path, sp.query, sp.fragment))
    except Exception:
        pass
    try:
        return unquote2(p)
    except Exception:
        return p


# ---------------------------------------------------------------------------------------
# Payload Cache (pid:...) + Details-Cache
# ---------------------------------------------------------------------------------------

_PAYLOAD_PREFIX = "pid:"
_PAYLOAD_CACHE_SUBDIR = "payload_cache_fav"
_PAYLOAD_TTL_SECONDS = max(3600, _get_setting_int("payload_ttl_seconds", 7 * 24 * 3600))  # 7 Tage
_PAYLOAD_MAX_BYTES = max(50_000, _get_setting_int("payload_max_bytes", 2_000_000))        # 2MB

_DETAILS_CACHE_SUBDIR = "fav_details_cache"
_DETAILS_TTL_SECONDS = max(3600, _get_setting_int("fav_details_ttl_seconds", 3 * 24 * 3600))  # 3 Tage


# Payload-GC nicht bei jedem Öffnen laufen lassen (Performance)
_PAYLOAD_GC_INTERVAL_SECONDS = 6 * 3600  # 6 Stunden
_LAST_PAYLOAD_GC_TS = 0.0


def _vfs_mtime(path: str) -> float:
    """mtime für VFS/Local Pfade (best effort)."""
    try:
        st = xbmcvfs.Stat(path)
        mt = getattr(st, "st_mtime", None)
        if callable(mt):
            mt = mt()
        if mt is not None:
            return float(mt)
    except Exception:
        pass
    try:
        return float(os.path.getmtime(common.fs_local(path)))
    except Exception:
        return 0.0



def _safe_mkdirs(path: str) -> None:
    try:
        if path and not common.fs_exists(path):
            common.fs_mkdirs(path)
    except Exception:
        pass


def _payload_cache_dir() -> str:
    try:
        base = common.addon_data_dir
    except Exception:
        base = xbmcvfs.translatePath("special://temp/")
    p = os.path.join(base, _PAYLOAD_CACHE_SUBDIR)
    _safe_mkdirs(p)
    return p


def _payload_path_from_pid(pid: str) -> str:
    pid_clean = (pid or "").strip().replace("/", "").replace("\\", "")
    return os.path.join(_payload_cache_dir(), f"{pid_clean}.json")


def cleanup_payload_cache(ttl_seconds: int = None) -> None:
    """Löscht alte pid-Payloads aus dem Cache (best effort) – throttled."""
    ttl = int(ttl_seconds or _PAYLOAD_TTL_SECONDS)
    if ttl <= 0:
        return

    # Throttle: wenn ohne explizites ttl aufgerufen, nur alle X Stunden laufen lassen
    global _LAST_PAYLOAD_GC_TS
    now_ts = dt_module.datetime.utcnow().timestamp()
    try:
        if ttl_seconds is None and (now_ts - float(_LAST_PAYLOAD_GC_TS or 0.0)) < float(_PAYLOAD_GC_INTERVAL_SECONDS):
            return
        _LAST_PAYLOAD_GC_TS = now_ts
    except Exception:
        pass

    try:
        cache_dir = _payload_cache_dir()
        if not cache_dir or not xbmcvfs.exists(cache_dir):
            return

        _, files = xbmcvfs.listdir(cache_dir)
        for fn in files or []:
            if not fn.lower().endswith(".json"):
                continue
            fp = os.path.join(cache_dir, fn)
            mtime = _vfs_mtime(fp)
            if not mtime:
                continue
            if (now_ts - float(mtime)) > ttl:
                try:
                    xbmcvfs.delete(fp)
                except Exception:
                    pass
    except Exception:
        pass


def _payload_file_is_valid(fp: str) -> bool:
    """Best-effort Prüfung: existiert, nicht leer, JSON parsebar."""
    try:
        if not fp or not xbmcvfs.exists(fp):
            return False
        try:
            st = xbmcvfs.Stat(fp)
            if st and hasattr(st, "st_size") and int(st.st_size()) < 5:
                return False
        except Exception:
            pass

        with xbmcvfs.File(fp, "r") as fh:
            raw = fh.read()

        if raw is None:
            return False
        if isinstance(raw, bytes):
            raw = raw.decode("utf-8", errors="replace")
        raw = (raw or "").lstrip("\ufeff").strip()
        if not raw:
            return False

        obj = json.loads(raw)
        return isinstance(obj, (dict, list))
    except Exception:
        return False


def pack_payload(obj) -> str:
    """
    Speichert obj als JSON in addon_data_dir/.../payload_cache_fav/<pid>.json und gibt 'pid:<pid>' zurück.
    Robust:
      - wenn Datei existiert aber leer/kaputt -> wird neu geschrieben
      - wenn Schreiben/Validieren scheitert -> Fallback: JSON inline zurückgeben
    (Stable: sort_keys=True => bessere Cache-Hitrate)
    """
    try:
        raw = json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
    except Exception:
        raw = json.dumps({"_payload_error": "json_dumps_failed"}, ensure_ascii=False, sort_keys=True, separators=(",", ":"))

    raw_b = raw.encode("utf-8", errors="replace")
    if len(raw_b) > _PAYLOAD_MAX_BYTES:
        log(f"Payload sehr groß ({len(raw_b)} bytes) – wird trotzdem gecached.", xbmc.LOGWARNING)

    pid = hashlib.sha256(raw_b).hexdigest()[:24]
    fp = _payload_path_from_pid(pid)

    # Wenn schon vorhanden und gültig -> direkt nutzen
    try:
        if _payload_file_is_valid(fp):
            return f"{_PAYLOAD_PREFIX}{pid}"
    except Exception:
        pass

    # Sonst neu schreiben (atomisch, Cache ist nicht kritisch -> ohne heavy fsync)
    try:
        _safe_mkdirs(os.path.dirname(fp))

        try:
            common.atomic_write_bytes(fp, raw_b, backup=False, durable=False)
        except Exception:
            with xbmcvfs.File(fp, "w") as fh:
                fh.write(raw)

        # Validieren – wenn immer noch kaputt, lieber inline zurückgeben (damit Navigation nicht bricht)
        if _payload_file_is_valid(fp):
            return f"{_PAYLOAD_PREFIX}{pid}"

        try:
            if xbmcvfs.exists(fp):
                xbmcvfs.delete(fp)
        except Exception:
            pass
        return raw

    except Exception as e:
        log(f"pack_payload write failed: {e}", xbmc.LOGWARNING)
        return raw



def unpack_payload(data_str):
    """
    Akzeptiert entweder:
      - 'pid:<id>'  -> lädt JSON aus Datei (payload_cache_fav/<pid>.json)
      - JSON String -> json.loads

    Robust gegen seltene '0-Byte' / Race-Cases:
      - kurzer Retry-Loop
      - löscht eine leere/kaputte Cache-Datei nur, wenn sie "alt" ist (>=10s),
        damit ein paralleler Writer nicht torpediert wird.
    """
    if data_str is None:
        return None

    s = str(data_str)

    candidates = [s]
    try:
        uq = unquote2(s)
        if uq != s:
            candidates.append(uq)
    except Exception:
        pass

    def _read_vfs_file(path: str) -> str:
        try:
            with xbmcvfs.File(path, "r") as fh:
                raw = fh.read()
            if raw is None:
                return ""
            if isinstance(raw, bytes):
                raw = raw.decode("utf-8", errors="replace")
            return (raw or "").lstrip("\ufeff").strip()
        except Exception:
            return ""

    def _vfs_size(path: str) -> int:
        try:
            st = xbmcvfs.Stat(path)
            sz = getattr(st, "st_size", 0)
            if callable(sz):
                sz = sz()
            return int(sz or 0)
        except Exception:
            return 0

    retry_monitor = None
    try:
        retry_monitor = xbmc.Monitor()
    except Exception:
        retry_monitor = None

    for cand in candidates:
        c = (cand or "").strip()
        if not c:
            continue

        # pid:<hash> -> load from cache
        if c.startswith(_PAYLOAD_PREFIX):
            pid = c[len(_PAYLOAD_PREFIX):].strip()
            fp = _payload_path_from_pid(pid)

            try:
                # quick guard: exists but 0 bytes
                if xbmcvfs.exists(fp) and _vfs_size(fp) < 5:
                    # Retry kurz – evtl. wird gerade geschrieben
                    raw = ""
                    for _i in range(3):
                        if common.wait_or_abort(retry_monitor, 0.05):
                            return None
                        raw = _read_vfs_file(fp)
                        if raw:
                            break

                    if not raw:
                        # Falls "alt" -> löschen, sonst stehen lassen
                        try:
                            now_ts = dt_module.datetime.utcnow().timestamp()
                            st = xbmcvfs.Stat(fp)
                            mt = getattr(st, "st_mtime", None)
                            if callable(mt):
                                mt = mt()
                            mt = float(mt or 0.0)
                            if mt and (now_ts - mt) >= 10.0:
                                try:
                                    xbmcvfs.delete(fp)
                                except Exception:
                                    pass
                        except Exception:
                            pass

                # Normaler Read (mit kurzem Retry falls leer)
                raw = _read_vfs_file(fp) if xbmcvfs.exists(fp) else ""
                if not raw:
                    for _i in range(2):
                        if common.wait_or_abort(retry_monitor, 0.05):
                            return None
                        raw = _read_vfs_file(fp)
                        if raw:
                            break

                # Fallback: .bak / .seed (best-effort)
                if not raw:
                    for alt in (fp + ".bak", fp + ".seed"):
                        if xbmcvfs.exists(alt):
                            raw = _read_vfs_file(alt)
                            if raw:
                                break

                if not raw:
                    return None

                return json.loads(raw) if raw else None

            except Exception as e:
                log(f"unpack_payload pid read failed ({pid}): {e}", xbmc.LOGWARNING)
                return None

        # inline JSON
        try:
            c = (c or "").lstrip("\ufeff")
            return json.loads(c)
        except Exception:
            continue

    return None


def _details_cache_dir() -> str:
    try:
        base = common.addon_data_dir
    except Exception:
        base = xbmcvfs.translatePath("special://temp/")
    p = os.path.join(base, _DETAILS_CACHE_SUBDIR)
    _safe_mkdirs(p)
    return p


def _details_cache_path(details_url: str) -> str:
    """Stable cache filename based on canonical details_url_key."""
    try:
        key = common.details_url_key(details_url)
    except Exception:
        key = (details_url or "").strip()
    try:
        h = hashlib.sha1((key or '').encode('utf-8', errors='ignore')).hexdigest()
    except Exception:
        h = hashlib.sha1(str(key).encode('utf-8', errors='ignore')).hexdigest()
    return os.path.join(_details_cache_dir(), f"{h}.json")


def _details_cache_path_legacy(details_url: str) -> str:
    """Legacy cache filename (older versions): md5(full_url)."""
    h = hashlib.md5((details_url or '').encode('utf-8', errors='ignore')).hexdigest()
    return os.path.join(_details_cache_dir(), f"{h}.json")
def _load_details_from_cache(details_url: str):
    fp = _details_cache_path(details_url)
    fp_legacy = _details_cache_path_legacy(details_url)
    try:
        if (not common.fs_exists(fp)) and (not common.fs_exists(fp_legacy)):
            return None
        if (not common.fs_exists(fp)) and common.fs_exists(fp_legacy):
            fp = fp_legacy
        try:
            age = dt_module.datetime.utcnow().timestamp() - float(_vfs_mtime(fp))
            if age > _DETAILS_TTL_SECONDS:
                return None
        except Exception:
            pass

        d = common.safe_load_json_file(fp, default=None)
        return d if isinstance(d, dict) else None
    except Exception:
        return None


def _save_details_to_cache(details_url: str, details_json: dict) -> None:
    fp = _details_cache_path(details_url)
    try:
        common.atomic_write_json(fp, details_json, indent=None, durable=True, backup=True)
    except Exception:
        pass


def fetch_details_json(details_url: str, expected_sha1: str = None):
    """
    Lädt details.json:
      - Wenn expected_sha1 vorhanden: disk cache wird genutzt, wenn lokal bekannter SHA passt,
        andernfalls wird sofort neu vom Server geladen.
      - Ohne expected_sha1: Fallback TTL-Cache.
    Rückgabe: (dict_or_empty, err_or_None)
    """
    if not details_url:
        return {}, "details_url fehlt"

    # Hash-map (shared with series.py) -> erlaubt Update ohne "warte 5 min".
    details_key = None
    try:
        details_key = common.details_url_key(details_url)
    except Exception:
        details_key = None

    if expected_sha1 and details_key:
        try:
            m = common.load_details_sha1_map()
            local_sha = (m.get(details_key) or "").strip()

            # Backward-Compat: ältere Versionen nutzten sha1(full_url) als Key
            if (not local_sha) and details_url:
                try:
                    legacy_k = common.details_url_key_legacy_sha1(details_url)
                    legacy_sha = (m.get(legacy_k) or "").strip() if legacy_k else ""
                    if legacy_sha:
                        local_sha = legacy_sha
                        # migrate -> neuer Key
                        try:
                            m[details_key] = legacy_sha
                            if legacy_k:
                                m.pop(legacy_k, None)
                            common.save_details_sha1_map(m)
                        except Exception:
                            pass
                except Exception:
                    pass

            # Wenn Hash passt: Cache darf verwendet werden (TTL ignoriert)
            if local_sha and local_sha == expected_sha1:
                fp = _details_cache_path(details_url)
                if xbmcvfs.exists(fp):
                    with xbmcvfs.File(fp, "r") as fh:
                        raw = fh.read()
                    d = json.loads(raw) if raw else None
                    if isinstance(d, dict):
                        return d, None
        except Exception:
            pass

    # Fallback: TTL cache (oder kein expected_sha1)
    if not expected_sha1:
        cached = _load_details_from_cache(details_url)
        if isinstance(cached, dict) and cached:
            return cached, None

    try:
        req = common.get_authenticated_request(details_url)
        if not req:
            return {}, "Kein authentifizierter Request."
        with _robust_urlopen(req, timeout=NET_TIMEOUT) as response:
            if response.getcode() != 200:
                return {}, f"HTTP {response.getcode()}"
            raw = response.read()
            body_sha = None
            try:
                body_sha = common.sha1_hex(raw)
            except Exception:
                body_sha = None
            d = json.loads(raw.decode("utf-8-sig", errors="replace"))
            if not isinstance(d, dict):
                d = {}
            if d:
                _save_details_to_cache(details_url, d)
                # persist hash
                if details_key:
                    try:
                        m = common.load_details_sha1_map()
                        m[details_key] = expected_sha1 or body_sha or m.get(details_key)
                        common.save_details_sha1_map(m)
                    except Exception:
                        pass
            return d, None
    except Exception as e:
        return {}, str(e)


# ---------------------------------------------------------------------------------------
# Init: Cache-Verzeichnis (nur EINMAL)
# ---------------------------------------------------------------------------------------
try:
    if not common.addon_data_dir or not isinstance(common.addon_data_dir, str):
        raise ValueError("common.addon_data_dir ist ungültig oder nicht initialisiert.")
    LOCAL_IMAGE_CACHE_DIR_FAV = os.path.join(common.addon_data_dir, LOCAL_IMAGE_CACHE_SUBDIR_FAV)
    if LOCAL_IMAGE_CACHE_DIR_FAV:
        cache_exists = os.path.isdir(LOCAL_IMAGE_CACHE_DIR_FAV) or xbmcvfs.exists(LOCAL_IMAGE_CACHE_DIR_FAV)
        if not cache_exists:
            xbmcvfs.mkdirs(LOCAL_IMAGE_CACHE_DIR_FAV)
            cache_exists = os.path.isdir(LOCAL_IMAGE_CACHE_DIR_FAV) or xbmcvfs.exists(LOCAL_IMAGE_CACHE_DIR_FAV)
            if cache_exists:
                log(f"Favoriten-Cache-Verzeichnis erstellt: {LOCAL_IMAGE_CACHE_DIR_FAV}")
            else:
                log(f"WARNUNG: Favoriten-Cache-Verzeichnis konnte nicht angelegt werden: {LOCAL_IMAGE_CACHE_DIR_FAV}", xbmc.LOGWARNING)
except Exception as e:
    log(f"FATALER FEHLER beim Initialisieren der Favoriten-Pfade: {e}", xbmc.LOGERROR)
    xbmcgui.Dialog().notification(
        "Addon Fehler",
        "Favoriten-Datenpfade konnten nicht initialisiert werden.",
        xbmcgui.NOTIFICATION_ERROR,
        8000
    )
    LOCAL_IMAGE_CACHE_DIR_FAV = ""

# Favoriten: bewusst KEIN watched-status / kein Playback-Monitoring.
# (Random-Play kann optional einen Monitor starten, ist aber default aus.)
ENABLE_FAV_SERIES_MONITOR = False


# ---------------------------------------------------------------------------------------
# Datei I/O
# ---------------------------------------------------------------------------------------
def _serialize_json_to_file(obj, dest_path):
    """Schreibt JSON atomisch (best effort)."""
    try:
        # Kompakt schreiben + atomischer Replace (durable, weil Basis-Cache)
        data = json.dumps(obj, ensure_ascii=False, separators=(",", ":"), sort_keys=False).encode("utf-8", errors="replace")
        try:
            common.atomic_write_bytes(dest_path, data, backup=True, durable=True)
            return True
        except Exception:
            # Fallback: VFS write
            with xbmcvfs.File(dest_path, 'w') as fh:
                fh.write(data.decode("utf-8", errors="replace"))
            return True
    except Exception as e:
        log(f"Speichern der Cache-Datei fehlgeschlagen: {dest_path} -> {e}", xbmc.LOGWARNING)
        return False

def _load_json_from_file(src_path):
    try:
        if not xbmcvfs.exists(src_path):
            return None
        with xbmcvfs.File(src_path, 'r') as fh:
            data = fh.read()
        if not data:
            return None
        # BOM robust
        if isinstance(data, str):
            data = data.lstrip("\ufeff")
        return json.loads(data)
    except Exception as e:
        log(f"Lesen der Cache-Datei fehlgeschlagen: {src_path} -> {e}", xbmc.LOGWARNING)
        return None

def _get_seriesfav_cache_path():
    try:
        return os.path.join(common.addon_data_dir, "seriesFav.cache.json")
    except Exception:
        return os.path.join(xbmcvfs.translatePath("special://temp/"), "seriesFav.cache.json")

def _get_seriesfav_remote_meta_path():
    """Meta-Cache für HEAD/Content-Length der Favoritenliste."""
    try:
        return os.path.join(common.addon_data_dir, "seriesFav.remote_meta.json")
    except Exception:
        return os.path.join(xbmcvfs.translatePath("special://temp/"), "seriesFav.remote_meta.json")


def _load_seriesfav_remote_meta():
    meta_path = _get_seriesfav_remote_meta_path()
    meta = _load_json_from_file(meta_path)
    return meta if isinstance(meta, dict) else {}


def _save_seriesfav_remote_meta(meta: dict) -> None:
    try:
        meta_path = _get_seriesfav_remote_meta_path()
        _serialize_json_to_file(meta or {}, meta_path)
    except Exception as e:
        log(f"save seriesFav remote meta failed: {e}", xbmc.LOGDEBUG)


# ---------------------------------------------------------------------------------------
# Bild-Cache-Funktionen (direkt auf common, nur mit Local-Path-Fallback)
# ---------------------------------------------------------------------------------------

def _cached_or_local_image(url):
    if not url or not isinstance(url, str):
        return None
    u = url.strip()
    if not u:
        return None
    if not u.startswith("http"):
        return u if xbmcvfs.exists(u) else None
    try:
        return common.get_cached_image(u, cache_dir=LOCAL_IMAGE_CACHE_DIR_FAV)
    except Exception as e:
        log(f"cached image lookup failed for {common.mask_url(u)}: {e}", xbmc.LOGDEBUG)
        return None


def _download_or_local_image(url, timeout_s=15):
    """Silent download into cache. Returns cached file path or None."""
    if not url or not isinstance(url, str):
        return None
    u = url.strip()
    if not u:
        return None
    if not u.startswith("http"):
        return u if xbmcvfs.exists(u) else None
    try:
        return common.download_and_cache_image(u, cache_dir=LOCAL_IMAGE_CACHE_DIR_FAV, timeout_s=timeout_s)
    except Exception as e:
        log(f"image download/cache failed for {common.mask_url(u)}: {e}", xbmc.LOGDEBUG)
        return None

# ---------------------------------------------------------------------------------------
# Artwork Helfer (MQ8)
# ---------------------------------------------------------------------------------------
def _force_window_fanart(img_path: str):
    def _ok(s): return isinstance(s, str) and s.strip()
    if not _ok(img_path):
        return
    try:
        if common.addon_handle != -1:
            xbmcplugin.setProperty(common.addon_handle, "fanart_image", img_path)
    except Exception as e:
        log(f"setProperty(fanart_image) failed: {e}", xbmc.LOGDEBUG)
    for win_id in (10025, 10028, 10000):
        try:
            win = xbmcgui.Window(win_id)
            for key in ("fanart_image", "Fanart_Image", "background_fanart"):
                win.setProperty(key, img_path)
        except Exception as e:
            log(f"window fanart set failed (win={win_id}): {e}", xbmc.LOGDEBUG)


try:
    _ADDON = xbmcaddon.Addon()
    _ADDON_FANART_PATH = (_ADDON.getAddonInfo("fanart") or "").replace("\\", "/").lower()
except Exception:
    _ADDON = None
    _ADDON_FANART_PATH = ""


def _set_art_mq8(li, *, thumb=None, icon=None, poster=None, fanart=None, tvshow_fanart=None, **extra):
    def _ok(s): return isinstance(s, str) and s.strip()

    _addon_fanart = _ADDON_FANART_PATH

    def _clean(p):
        if not _ok(p):
            return ""
        p2 = p.replace("\\", "/").lower()
        if _addon_fanart and p2 == _addon_fanart:
            return ""
        return p

    thumb = _clean(thumb)
    icon = _clean(icon)
    poster = _clean(poster)
    fanart = _clean(fanart)
    tvshow_fanart = _clean(tvshow_fanart)

    fanart_final = fanart or tvshow_fanart or poster or thumb or icon
    poster_final = poster or thumb or icon

    art = {}

    if _ok(poster_final):
        art["poster"] = poster_final
        art["thumb"] = poster_final
        art["icon"] = poster_final
        art["landscape"] = poster_final
    else:
        fallback = os.path.join(common.addon_path, "series.png")
        if not xbmcvfs.exists(fallback):
            fallback = common.addon.getAddonInfo('icon')
        art["icon"] = art["thumb"] = art["poster"] = art["landscape"] = fallback

    if _ok(fanart_final):
        art["fanart"] = fanart_final
        for i in range(1, 10):
            art[f"fanart{i}"] = fanart_final
        art["tvshow.fanart"] = fanart_final
        for i in range(1, 10):
            art[f"tvshow.fanart{i}"] = fanart_final

    for k, v in (extra or {}).items():
        if _ok(v):
            art[k] = v

    if art:
        li.setArt(art)

    if _ok(fanart_final):
        try:
            li.setProperty("fanart_image", fanart_final)
            li.setProperty("Fanart_Image", fanart_final)
        except Exception:
            pass
        _force_window_fanart(fanart_final)


def _apply_fanart_to_listitem(
    li,
    *,
    poster: str = None,
    fanart: str = None,
    tvshow_poster: str = None,
    tvshow_fanart: str = None,
    still: str = None
) -> None:
    def _ok(s): return isinstance(s, str) and s.strip()

    poster_final = None
    for cand in (still, poster, tvshow_poster):
        if _ok(cand):
            poster_final = cand
            break

    fanart_final = None
    for cand in (fanart, still, poster, tvshow_fanart, tvshow_poster):
        if _ok(cand):
            fanart_final = cand
            break

    _set_art_mq8(
        li,
        thumb=poster_final or "",
        icon=poster_final or "",
        poster=poster_final or "",
        fanart=fanart_final or "",
        tvshow_fanart=tvshow_fanart or ""
    )


# ---------------------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------------------
def _parse_year_from_air_date_string(air_date_str, info_tag_ref, item_label_for_log="Item"):
    if not (air_date_str and isinstance(air_date_str, str) and air_date_str.strip()):
        return False

    did_set_year = False
    try:
        try:
            parsed_date = dt_module.datetime.strptime(air_date_str, "%Y-%m-%d")
            info_tag_ref.setYear(parsed_date.year)
            return True
        except Exception:
            pass

        if len(air_date_str) >= 4 and air_date_str[:4].isdigit():
            info_tag_ref.setYear(int(air_date_str[:4]))
            did_set_year = True
    except Exception:
        pass

    return did_set_year


def _split_and_set_list(info_tag, setter_name, value):
    if not value:
        return
    try:
        items = [x.strip() for x in str(value).split(',') if x.strip()]
        if not items:
            return
        getattr(info_tag, setter_name)(items)
    except Exception:
        pass


# ---------------------------------------------------------------------------------------
# Laden von seriesFav.json (mit lokalem Fallback-Cache)
# ---------------------------------------------------------------------------------------
def _normalize_host_and_basepath():
    """Einheitliche Normalisierung (delegiert an common.normalize_ftp_host_basepath)."""
    ftp_username = _get_setting_str("ftp_username", "").strip()
    ftp_password = _get_setting_str("ftp_password", "").strip()

    try:
        host_final, base_path, scheme = common.normalize_ftp_host_basepath()
        return scheme, host_final, ftp_username, ftp_password, base_path
    except Exception:
        # Fallback (sollte praktisch nie passieren)
        ftp_host_raw = _get_setting_str("ftp_host", "").strip()
        base_path_setting = _get_setting_str("ftp_base_path", "").strip()

        scheme = "https"
        ftp_host_cleaned = ftp_host_raw

        if ftp_host_raw.startswith(("http://", "https://")):
            scheme = ftp_host_raw.split("://", 1)[0].lower()
            ftp_host_cleaned = ftp_host_raw.split("://", 1)[1]

        if "@" in ftp_host_cleaned:
            ftp_host_cleaned = ftp_host_cleaned.split("@", 1)[-1]

        ftp_host_cleaned = ftp_host_cleaned.split("/")[0].split("?")[0].split("#")[0].rstrip(":/ ")

        base_path = "/user/downloads/kodiAddon/"
        try:
            if base_path_setting:
                temp_path = base_path_setting.strip()
                if temp_path and temp_path != "/":
                    base_path = temp_path.rstrip("/") + "/"
        except Exception:
            pass

        if not base_path.startswith("/"):
            base_path = "/" + base_path

        return scheme, ftp_host_cleaned, ftp_username, ftp_password, base_path



def load_favorites_json():
    scheme, ftp_host, ftp_username, ftp_password, base_path = _normalize_host_and_basepath()
    if not ftp_host:
        return None, "FTP/HTTP Host fehlt!"

    if not (ftp_username and ftp_password) and not scheme.startswith("http"):
        return None, "FTP Benutzername/Passwort fehlt!"

    url = f"{scheme}://{ftp_host}{base_path}{SERIES_FAV_JSON_FILENAME}"
    cache_path = _get_seriesfav_cache_path()
    meta = _load_seriesfav_remote_meta()

    # NEW: prefer tiny .sha1 sidecar (seriesFav.json.sha1) for change detection
    remote_sha = None
    try:
        if scheme.startswith("http"):
            remote_sha = common.fetch_remote_sha1(url, timeout_s=8)
    except Exception:
        remote_sha = None

    # Fast path: if sha matches and cache exists -> no HEAD/GET needed
    try:
        prev_sha = (meta.get("sha1") or "").strip()
        if remote_sha and prev_sha and remote_sha == prev_sha and xbmcvfs.exists(cache_path):
            cached = _load_json_from_file(cache_path)
            if isinstance(cached, list):
                log("Favoritenliste laut .sha1 unverändert – verwende lokalen Cache.", xbmc.LOGINFO)
                return cached, None
    except Exception:
        pass

    if not (ftp_username and ftp_password):
        log("Keine FTP-Zugangsdaten gesetzt – versuche ohne Auth.", xbmc.LOGDEBUG)

    # Fallback: HEAD/Content-Length Check (wenn .sha1 fehlt)
    try:
        if scheme.startswith("http") and (not remote_sha) and xbmcvfs.exists(cache_path):
            req_h = common.get_authenticated_request(url)
            if req_h:
                # method override kompatibel mit älterem urllib in Kodi
                try:
                    req_h.get_method = lambda: "HEAD"
                except Exception:
                    pass
                with _robust_urlopen(req_h, timeout=NET_TIMEOUT) as resp_h:
                    cl = None
                    try:
                        cl_raw = resp_h.headers.get("Content-Length")
                        if cl_raw is not None:
                            cl = int(cl_raw)
                    except Exception:
                        cl = None

                    prev_cl = meta.get("content_length")
                    if cl is not None and prev_cl is not None and int(prev_cl) == int(cl):
                        cached = _load_json_from_file(cache_path)
                        if isinstance(cached, list) and cached:
                            log("Favoritenliste laut HEAD/Content-Length unverändert – verwende lokalen Cache.", xbmc.LOGINFO)
                            return cached, None
    except Exception as e:
        # HEAD darf niemals die Funktion kaputt machen – dann einfach normal weiter
        log(f"HEAD/Content-Length Check fehlgeschlagen: {e}", xbmc.LOGDEBUG)

    log(f"Lade Favoriten-JSON von: {common.mask_url(url)}", xbmc.LOGDEBUG)
    try:
        req = common.get_authenticated_request(url)
        if not req:
            raise ValueError("Konnte Auth Request nicht erstellen.")
        with _robust_urlopen(req, timeout=NET_TIMEOUT) as response:
            if response.getcode() == 200:
                raw = response.read()
                body_sha = None
                try:
                    body_sha = common.sha1_hex(raw)
                except Exception:
                    body_sha = None
                # BOM robust (utf-8-sig)
                try:
                    text = raw.decode("utf-8-sig")
                except Exception:
                    text = raw.decode("utf-8", errors="replace")
                data = json.loads(text)
                if not isinstance(data, list):
                    data = []

                # Cache + Meta speichern
                _serialize_json_to_file(data, cache_path)
                try:
                    cl = response.headers.get("Content-Length")
                    meta["content_length"] = int(cl) if cl is not None else meta.get("content_length")
                except Exception:
                    pass
                try:
                    body_sha = common.sha1_hex(raw)
                    meta["sha1"] = body_sha or remote_sha or meta.get("sha1")
                except Exception:
                    if remote_sha:
                        meta["sha1"] = remote_sha
                meta["last_ok_utc"] = dt_module.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
                _save_seriesfav_remote_meta(meta)

                return data, None
            else:
                raise ValueError(f"HTTP Error {response.getcode()}")
    except Exception as e:
        log(f"Netzwerkfehler bei {SERIES_FAV_JSON_FILENAME}: {e}", xbmc.LOGWARNING)
        cached = _load_json_from_file(cache_path)
        if isinstance(cached, list) and cached:
            log(f"Offline: Verwende lokale {os.path.basename(cache_path)}", xbmc.LOGINFO)
            return cached, None
        return None, f"Laden/Parsen von {SERIES_FAV_JSON_FILENAME} fehlgeschlagen: {e}"

# ---------------------------------------------------------------------------------------
# Favoriten-Root anzeigen
# ---------------------------------------------------------------------------------------
def show_favorite_series():
    xbmcplugin.setContent(common.addon_handle, "tvshows")

    # pid-Cache aufräumen (best effort)
    cleanup_payload_cache()

    data, err = load_favorites_json()
    if err:
        xbmcgui.Dialog().notification("Fehler", err, "")
        _safe_end_of_directory(succeeded=False, cache_to_disc=False)
        return

    favorite_series_list = data or []

    # 1) Lokale Cache-Pfade direkt einsetzen (kein Netzwerk, kein Blocking)
    missing_images = []
    if LOCAL_IMAGE_CACHE_DIR_FAV:
        for serie in favorite_series_list:
            if not isinstance(serie, dict):
                continue
            serie.pop("action", None)
            for key in ("poster", "backcover"):
                url_val = (serie.get(key) or "").strip()
                if url_val.startswith("http"):
                    local_path = _cached_or_local_image(url_val)
                    if local_path:
                        serie[key] = local_path
                    else:
                        missing_images.append((serie, url_val, key))

    # 2) Fehlende Bilder asynchron im Hintergrund nachladen (UI nicht blockieren)
    if missing_images and LOCAL_IMAGE_CACHE_DIR_FAV:
        def _bg_download_missing():
            mon = xbmc.Monitor()
            ok_cnt, fail_cnt = 0, 0
            if PARALLEL_IMAGES and _HAS_FUTURES and IMAGE_WORKERS > 1:
                try:
                    with ThreadPoolExecutor(max_workers=IMAGE_WORKERS) as ex:
                        futures = {ex.submit(_download_or_local_image, url): (serie, key)
                                   for serie, url, key in missing_images}
                        for fut in as_completed(futures):
                            if mon.abortRequested():
                                break
                            try:
                                path = fut.result()
                                if path:
                                    ok_cnt += 1
                                else:
                                    fail_cnt += 1
                            except Exception:
                                fail_cnt += 1
                except Exception as e:
                    log(f"Favoriten BG-Downloader Fehler: {e}", xbmc.LOGWARNING)
            else:
                for serie, image_url, key in missing_images:
                    if mon.abortRequested():
                        break
                    try:
                        if _download_or_local_image(image_url):
                            ok_cnt += 1
                        else:
                            fail_cnt += 1
                    except Exception as e:
                        fail_cnt += 1
                        log(f"Favoriten BG Imagecache Fehler ({key}): {e}", xbmc.LOGDEBUG)
            log(f"Favoriten BG-ImageCache: ok={ok_cnt}, fail={fail_cnt}, missing={len(missing_images)}")

        import threading as _threading
        _t = _threading.Thread(target=_bg_download_missing, daemon=True)
        _t.start()

    # 3) Sortierung
    sort_method_param = "added"
    if len(sys.argv) > 2 and sys.argv[2].startswith('?'):
        try:
            plugin_params = dict(urllib.parse.parse_qsl(sys.argv[2][1:]))
            sort_method_param = plugin_params.get("sort", sort_method_param).lower()
        except Exception:
            pass

    if sort_method_param in ("date", "added"):
        def parse_date_robust_fav(s_item):
            d_str = s_item.get("added", "")
            for f in ("%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
                try:
                    return dt_module.datetime.strptime(d_str, f)
                except Exception:
                    continue
            return dt_module.datetime(1970, 1, 1)
        favorite_series_list = sorted(favorite_series_list, key=parse_date_robust_fav, reverse=True)

    elif sort_method_param == "year":
        def get_year_fav(s_item):
            year_str = str(s_item.get("start_year", "0"))[:4]
            return int(year_str) if year_str.isdigit() else 0
        favorite_series_list = sorted(favorite_series_list, key=get_year_fav, reverse=True)

    else:
        favorite_series_list = sorted(favorite_series_list, key=lambda s: s.get("name", "").lower())

    # 4) Erstes Fanart für Container setzen
    first_fanart = ""
    for s in favorite_series_list:
        cand = (s.get("backcover") or s.get("poster") or "").strip()
        if cand:
            first_fanart = cand
            break
    _force_window_fanart(first_fanart or common.addon.getAddonInfo('icon'))

    # 5) Random-Item oben
    random_item = xbmcgui.ListItem(label="Zufällige Serie starten", offscreen=True)
    random_item.setProperty("specialsort", "top")
    vi_r = random_item.getVideoInfoTag()
    vi_r.setPlot("Startet die Wiedergabe einer zufälligen Episode aus deinen Favoriten-Serien.")
    vi_r.setMediaType("video")

    rand_icon = os.path.join(common.addon_path, "random.png")
    series_icon = os.path.join(common.addon_path, "series.png")
    icon_final = rand_icon if xbmcvfs.exists(rand_icon) else (series_icon if xbmcvfs.exists(series_icon) else common.addon.getAddonInfo('icon'))

    _apply_fanart_to_listitem(
        random_item,
        poster=icon_final,
        fanart=first_fanart or icon_final,
        tvshow_poster=icon_final,
        tvshow_fanart=first_fanart or icon_final
    )

    random_url = common.build_url({'action': 'play_random_favorite_series'})
    xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=random_url, listitem=random_item, isFolder=False)

    if not favorite_series_list:
        xbmcgui.Dialog().notification(common.addon_name, "Keine Favoriten gefunden.", "")

    # 6) Serien listen
    for serie_item in favorite_series_list:
        if not isinstance(serie_item, dict):
            continue

        name = (serie_item.get("name") or "Unbekannt").strip()
        li = xbmcgui.ListItem(label=name, offscreen=True)

        final_poster_raw = (serie_item.get("poster") or "").strip()
        final_fanart_raw = (serie_item.get("backcover") or "").strip()
        if not final_fanart_raw:
            final_fanart_raw = final_poster_raw
        # Bevorzugt lokalen Cache-Pfad, Fallback auf Remote-URL
        final_poster = _cached_or_local_image(final_poster_raw) or final_poster_raw
        final_fanart = _cached_or_local_image(final_fanart_raw) or final_fanart_raw

        _apply_fanart_to_listitem(
            li,
            poster=final_poster or final_fanart,
            fanart=final_fanart or final_poster,
            tvshow_poster=final_poster or final_fanart,
            tvshow_fanart=final_fanart or final_poster
        )

        info_tag = li.getVideoInfoTag()
        info_tag.setTitle(name)
        info_tag.setPlot(serie_item.get("overview", ""))
        info_tag.setMediaType("tvshow")
        info_tag.setTvShowTitle(name)
        info_tag.setPlaycount(0)

        y_str = str(serie_item.get("start_year", ""))
        year_val, premiered = 0, ""
        if len(y_str) >= 4 and y_str[:4].isdigit():
            year_val = int(y_str[:4])
        if len(y_str) == 10 and y_str[4] == '-' and y_str[7] == '-':
            premiered = y_str
        elif year_val > 0:
            premiered = f"{year_val}-01-01"
        if premiered:
            info_tag.setPremiered(premiered)
        if year_val > 0:
            info_tag.setYear(year_val)

        series_vote_avg_str = serie_item.get("vote_average")
        if series_vote_avg_str is not None:
            try:
                rv = float(str(series_vote_avg_str).replace(",", "."))
                if rv > 0:
                    info_tag.setRating(rv)
            except Exception:
                pass
        else:
            imdb_rating_str = serie_item.get("imdbRating")
            if imdb_rating_str is not None:
                try:
                    rv = float(str(imdb_rating_str).replace(",", "."))
                    if rv > 0:
                        info_tag.setRating(rv)
                except Exception:
                    pass

        # Votes (IMDb bevorzugt, sonst TMDB)
        votes_val = 0
        for imdb_key in ("imdbVotes", "imdb_votes", "imdbvotes", "votes_imdb"):
            raw = serie_item.get(imdb_key)
            if raw:
                try:
                    votes_val = int(re.sub(r"[^\d]", "", str(raw)))
                except Exception:
                    votes_val = 0
                if votes_val > 0:
                    break
        if votes_val <= 0:
            for tmdb_key in ("vote_count", "votes", "tmdb_vote_count"):
                raw = serie_item.get(tmdb_key)
                if raw is not None:
                    try:
                        votes_val = int(str(raw))
                    except Exception:
                        votes_val = 0
                    if votes_val > 0:
                        break
        if votes_val > 0:
            try:
                info_tag.setVotes(votes_val)
            except Exception:
                pass

        fsk_val = serie_item.get("fsk")
        fsk_processed = str(fsk_val).strip() if fsk_val is not None else ""
        if fsk_processed:
            info_tag.setMpaa(f"FSK {fsk_processed}")

        if serie_item.get("added"):
            info_tag.setDateAdded(serie_item.get("added"))

        _split_and_set_list(info_tag, "setGenres", serie_item.get("genre", ""))
        _split_and_set_list(info_tag, "setStudios", serie_item.get("studio", ""))

        details_url_nav = (serie_item.get("details_url") or "").strip()
        if not details_url_nav:
            log(f"Serie ohne details_url ignoriert: {name}", xbmc.LOGDEBUG)
            continue

        nav_data = serie_item.copy()
        nav_data['series_vote_average'] = series_vote_avg_str

        # WICHTIG: data= pack_payload statt json.dumps (kurze URLs)
        target_url = common.build_url({"action": "show_fav_seasons", "data": pack_payload(nav_data)})
        li.setProperty('IsPlayable', 'false')

        xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=target_url, listitem=li, isFolder=True)

    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_LABEL)
    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_DATEADDED)
    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR)

    _safe_end_of_directory(succeeded=True, cache_to_disc=True)
    log("Favoriten-Verzeichnis erfolgreich aufgebaut.")


# ---------------------------------------------------------------------------------------
# Favoriten-Staffeln anzeigen
# ---------------------------------------------------------------------------------------
def show_fav_seasons(data_json_str):
    xbmcplugin.setContent(common.addon_handle, "seasons")

    try:
        series_info = unpack_payload(data_json_str)
        if not isinstance(series_info, dict):
            raise ValueError("Ungültige Seriendaten.")
        series_name = series_info.get("name", "Unbekannte Serie")
        series_backcover = series_info.get("backcover", "")
        series_poster = series_info.get("poster", "")
        current_series_genre = series_info.get("genre", "")
        current_series_studio = series_info.get("studio", "")
        series_start_year_str = str(series_info.get("start_year", ""))
        current_series_fsk_raw = series_info.get("fsk")
        current_series_overall_vote_average = series_info.get("series_vote_average")
    except Exception as e:
        xbmcgui.Dialog().notification("Fehler", f"Ungültige Seriendaten: {e}", xbmcgui.NOTIFICATION_ERROR)
        log(f"Fehler in show_fav_seasons (Datenparsen): {e}", xbmc.LOGERROR)
        _safe_end_of_directory(succeeded=False, cache_to_disc=False)
        return

    series_fanart_for_win = _cached_or_local_image(series_backcover) or series_backcover or series_poster
    _force_window_fanart(series_fanart_for_win)

    details_url = series_info.get("details_url")
    if not details_url:
        xbmcgui.Dialog().notification("Fehler", "Keine Details-URL gefunden.", xbmcgui.NOTIFICATION_ERROR)
        _safe_end_of_directory(succeeded=False, cache_to_disc=False)
        return

    # Zufall oben
    random_item = xbmcgui.ListItem(label="Zufällige Episode starten", offscreen=True)
    random_item.setProperty("specialsort", "top")
    vi = random_item.getVideoInfoTag()
    vi.setPlot(f"Startet die Wiedergabe einer zufälligen Episode aus der Serie '{series_name}'.")
    vi.setMediaType('video')

    icon_r = os.path.join(common.addon_path, "random.png")
    if not xbmcvfs.exists(icon_r):
        icon_r = os.path.join(common.addon_path, "series.png")
        if not xbmcvfs.exists(icon_r):
            icon_r = common.addon.getAddonInfo('icon')

    _set_art_mq8(random_item, thumb=icon_r, icon=icon_r, fanart=series_fanart_for_win)

    random_series_url = common.build_url({
    "action": "play_random_fav_episode_from_this_series",
    "data": pack_payload(series_info.copy())
})

    xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=random_series_url, listitem=random_item, isFolder=False)

    # Details laden (mit Cache)
    details_json, err = fetch_details_json(details_url, expected_sha1=(series_info.get("details_sha1") or None))
    if err:
        xbmcgui.Dialog().notification("Fehler", f"Details laden fehlgeschlagen: {err}", xbmcgui.NOTIFICATION_ERROR)
        log(f"Fehler beim Laden von details.json für '{series_name}': {err}", xbmc.LOGERROR)
        details_json = {}

    # FSK ggf. aus Details übernehmen
    fsk_from_seriesfav = str(current_series_fsk_raw).strip() if current_series_fsk_raw is not None else ""
    if not fsk_from_seriesfav and details_json:
        details_series_info_node = details_json.get("series_info", {})
        fsk_from_details_node = details_series_info_node.get("fsk")
        fsk_from_details_processed = str(fsk_from_details_node).strip() if fsk_from_details_node is not None else ""
        if fsk_from_details_processed:
            current_series_fsk_raw = fsk_from_details_node

    season_keys = sorted(
        [k for k in details_json if isinstance(k, str) and k.lower().startswith("season")],
        key=lambda s_key: int(s_key.lower().replace("season", "").strip()) if s_key.lower().replace("season", "").strip().isdigit() else float('inf')
    )

    for season_key in season_keys:
        season_data = details_json.get(season_key, {})
        if not isinstance(season_data, dict):
            continue

        tmdb_season_details = season_data.get("tmdb_season_details", {})
        season_num_str = season_key.lower().replace("season", "").strip()
        season_num = int(season_num_str) if season_num_str.isdigit() else -1
        season_label_name = tmdb_season_details.get("name", f"Staffel {season_num}" if season_num != -1 else season_key)

        episode_count_tmdb = tmdb_season_details.get("episode_count")
        label = f"{season_label_name} ({episode_count_tmdb} Episoden)" if isinstance(episode_count_tmdb, int) and episode_count_tmdb > 0 else season_label_name

        li = xbmcgui.ListItem(label=label, offscreen=True)

        season_poster_url = season_data.get("poster", series_poster)
        final_poster_path = _cached_or_local_image(season_poster_url) or season_poster_url

        _set_art_mq8(
            li,
            thumb=final_poster_path,
            icon=final_poster_path,
            poster=final_poster_path,
            fanart=series_fanart_for_win
        )

        info_tag = li.getVideoInfoTag()
        info_tag.setMediaType("season")
        info_tag.setTitle(season_label_name)
        if season_num != -1:
            info_tag.setSeason(season_num)
        info_tag.setTvShowTitle(series_name)
        info_tag.setPlot(tmdb_season_details.get("overview", season_data.get("overview", "")))

        s_air_date_str = str(tmdb_season_details.get("air_date", season_data.get("air_date", "")))
        s_year = 0
        s_premiered = ""
        if s_air_date_str and len(s_air_date_str) >= 4 and s_air_date_str[:4].isdigit():
            s_year = int(s_air_date_str[:4])
        if len(s_air_date_str) == 10:
            s_premiered = s_air_date_str
        elif s_year > 0:
            s_premiered = f"{s_year}-01-01"
        if not s_premiered and series_start_year_str:
            if len(series_start_year_str) == 10:
                s_premiered = series_start_year_str
            elif len(series_start_year_str) >= 4 and series_start_year_str[:4].isdigit():
                s_premiered = f"{int(series_start_year_str[:4])}-01-01"
        if s_premiered:
            info_tag.setPremiered(s_premiered)
        if s_year > 0:
            info_tag.setYear(s_year)
        elif s_air_date_str:
            _parse_year_from_air_date_string(s_air_date_str, info_tag, label)

        determined_season_rating_value = 0.0
        season_specific_vote_average_str = tmdb_season_details.get("vote_average")
        if season_specific_vote_average_str is not None:
            try:
                rating_val = float(str(season_specific_vote_average_str).replace(",", "."))
                if rating_val > 0:
                    determined_season_rating_value = rating_val
            except Exception:
                pass

        if determined_season_rating_value > 0:
            info_tag.setRating(determined_season_rating_value)
        elif current_series_overall_vote_average is not None:
            try:
                series_rating_float = float(str(current_series_overall_vote_average).replace(",", "."))
                if series_rating_float > 0:
                    info_tag.setRating(series_rating_float)
                    determined_season_rating_value = series_rating_float
            except Exception:
                pass

        fsk_processed_value_season = str(current_series_fsk_raw).strip() if current_series_fsk_raw is not None else ""
        if fsk_processed_value_season:
            info_tag.setMpaa(f"FSK {fsk_processed_value_season}")

        _split_and_set_list(info_tag, "setGenres", current_series_genre)
        _split_and_set_list(info_tag, "setStudios", current_series_studio)

        # WICHTIG: NICHT season_data + episodes in data= mitschleppen
        season_nav_data = {
            "details_url": details_url,
            "details_sha1": (series_info.get("details_sha1") or None),
            "season_key": season_key,
            "season_number": season_num,

            "series_fanart": series_backcover,
            "series_poster": series_poster,
            "series_name": series_name,
            "series_genre": current_series_genre,
            "series_studio": current_series_studio,

            "fsk_from_series": current_series_fsk_raw,
            "series_vote_average_from_series": current_series_overall_vote_average,
            "season_vote_average_from_season": determined_season_rating_value if determined_season_rating_value > 0 else None
        }

        folder_url = common.build_url({"action": "show_fav_episodes", "data": pack_payload(season_nav_data)})
        xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=folder_url, listitem=li, isFolder=True)

    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_LABEL)
    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR)
    _safe_end_of_directory(succeeded=True, cache_to_disc=True)


# ---------------------------------------------------------------------------------------
# Episoden anzeigen
# ---------------------------------------------------------------------------------------
def show_fav_episodes(data_json_str):
    xbmcplugin.setContent(common.addon_handle, "episodes")

    try:
        season_info = unpack_payload(data_json_str)
        if not isinstance(season_info, dict):
            raise ValueError("Ungültige Staffeldaten.")
    except Exception as e:
        xbmcgui.Dialog().notification("Fehler", f"Ungültige Staffeldaten: {e}", xbmcgui.NOTIFICATION_ERROR)
        _safe_end_of_directory(succeeded=False, cache_to_disc=False)
        return

    # Legacy: falls season_data noch inline mitgegeben wurde, nutze es.
    season_data_from_nav = season_info.get("season_data") if isinstance(season_info, dict) else None
    episodes = []
    if isinstance(season_data_from_nav, dict):
        episodes = season_data_from_nav.get("episodes", []) or []

    # Neu: über details_url + season_key nachladen
    if not episodes:
        details_url = (season_info or {}).get("details_url")
        season_key = (season_info or {}).get("season_key")
        details_json, derr = fetch_details_json(details_url, expected_sha1=(season_info.get("details_sha1") or None))
        if derr:
            log(f"show_fav_episodes: Details laden fehlgeschlagen: {derr}", xbmc.LOGWARNING)
            details_json = {}
        season_data_from_nav = details_json.get(season_key, {}) if isinstance(details_json, dict) else {}
        episodes = (season_data_from_nav.get("episodes", []) or []) if isinstance(season_data_from_nav, dict) else []

    if not episodes or not isinstance(episodes, list):
        xbmcgui.Dialog().notification("Info", "Keine Episoden in dieser Staffel gefunden.", "")
        _safe_end_of_directory(succeeded=True, cache_to_disc=True)
        return

    series_name_for_ep = season_info.get("series_name", "Unbekannte Serie")
    current_season_number = season_info.get("season_number", -1)
    series_genre_str_for_ep = season_info.get("series_genre", "")
    series_studio_str_for_ep = season_info.get("series_studio", "")
    series_fanart_for_ep = season_info.get("series_fanart", "")
    series_poster_for_ep = season_info.get("series_poster", "")

    series_fsk_raw_for_ep = season_info.get("fsk_from_series")
    series_overall_vote_average_for_ep = season_info.get("series_vote_average_from_series")
    season_vote_average_for_ep = season_info.get("season_vote_average_from_season")

    series_fanart_cached = _cached_or_local_image(series_fanart_for_ep) or series_fanart_for_ep
    series_poster_cached = _cached_or_local_image(series_poster_for_ep) or series_poster_for_ep

    _force_window_fanart(series_fanart_cached or series_poster_cached)

    # Zufall oben
    random_item = xbmcgui.ListItem(label="Zufällige Episode starten", offscreen=True)
    random_item.setProperty("specialsort", "top")
    vi = random_item.getVideoInfoTag()
    vi.setPlot(
        f"Startet die Wiedergabe einer zufälligen Episode aus Staffel {current_season_number}."
        if current_season_number != -1 else "Startet die Wiedergabe einer zufälligen Episode aus dieser Staffel."
    )
    vi.setMediaType('video')

    # data_json_str kann pid:... sein -> passt
    random_url = common.build_url({"action": "play_random_favorite_episode_from_season", "data": data_json_str})

    icon_r = os.path.join(common.addon_path, "random.png")
    if not xbmcvfs.exists(icon_r):
        icon_r = os.path.join(common.addon_path, "series.png")
        if not xbmcvfs.exists(icon_r):
            icon_r = common.addon.getAddonInfo('icon')

    _set_art_mq8(random_item, thumb=icon_r, icon=icon_r, fanart=series_fanart_cached or series_poster_cached)
    xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=random_url, listitem=random_item, isFolder=False)

    episodes = sorted(episodes, key=lambda ep_item: int(ep_item.get("episode_number", 0)) if str(ep_item.get("episode_number", "0")).isdigit() else float('inf'))

    for ep in episodes:
        if not isinstance(ep, dict):
            continue

        ep_title = ep.get("name", "Unbekannte Episode")
        ep_number_val = ep.get("episode_number", "")
        label = f"E{ep_number_val}: {ep_title}" if ep_number_val else ep_title
        if current_season_number != -1:
            label = f"S{str(current_season_number).zfill(2)}{label}"

        li = xbmcgui.ListItem(label=label, offscreen=True)
        li.setProperty("IsPlayable", "true")

        playable_path = unquote2(ep.get("file_ort", ""))

        still_url = ep.get("still", "")
        final_still_path = _cached_or_local_image(still_url) or still_url if still_url else ""
        final_thumb = final_still_path or series_poster_cached

        _set_art_mq8(
            li,
            thumb=final_thumb,
            icon=final_thumb,
            poster=final_thumb,
            fanart=series_fanart_cached or final_thumb,
            **{
                "landscape": final_thumb,
                "tvshow.poster": series_poster_cached,
                "tvshow.fanart": series_fanart_cached
            }
        )

        info_tag = li.getVideoInfoTag()
        info_tag.setMediaType("episode")
        info_tag.setTitle(ep_title)
        info_tag.setPlot(ep.get("overview", ""))
        info_tag.setTvShowTitle(series_name_for_ep)

        if current_season_number != -1:
            info_tag.setSeason(current_season_number)
        if ep_number_val:
            try:
                info_tag.setEpisode(int(ep_number_val))
            except Exception:
                info_tag.setEpisode(0)

        air_date_str = ep.get("air_date", "")
        if air_date_str and air_date_str.strip():
            info_tag.setPremiered(air_date_str)
            _parse_year_from_air_date_string(air_date_str, info_tag, label)

        episode_runtime_minutes = ep.get("runtime")
        if isinstance(episode_runtime_minutes, int) and episode_runtime_minutes > 0:
            try:
                info_tag.setDuration(episode_runtime_minutes * 60)
            except Exception:
                pass

        final_rating = 0.0
        for cand in (ep.get("rating"), season_vote_average_for_ep, series_overall_vote_average_for_ep):
            if cand is not None:
                try:
                    final_rating = float(str(cand).replace(",", "."))
                except Exception:
                    pass
            if final_rating > 0:
                break
        if final_rating > 0:
            info_tag.setRating(final_rating)

        fsk_processed_value_ep = str(series_fsk_raw_for_ep).strip() if series_fsk_raw_for_ep is not None else ""
        if fsk_processed_value_ep:
            info_tag.setMpaa(f"FSK {fsk_processed_value_ep}")

        _split_and_set_list(info_tag, "setDirectors", ep.get("directors", ""))
        _split_and_set_list(info_tag, "setGenres", series_genre_str_for_ep)
        _split_and_set_list(info_tag, "setStudios", series_studio_str_for_ep)

        ep_play_url = common.build_url({
            "action": "play_series",
            "serie_key": playable_path, "title": label, "tmdbid": playable_path
        })
        xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=ep_play_url, listitem=li, isFolder=False)

    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_EPISODE)
    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_LABEL)
    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_DATE)
    _safe_end_of_directory(succeeded=True, cache_to_disc=True)


# ---------------------------------------------------------------------------------------
# Zufallsfunktionen
# ---------------------------------------------------------------------------------------
def play_random_episode_from_season(data_json_str):
    try:
        season_info = unpack_payload(data_json_str)
        if not isinstance(season_info, dict):
            raise ValueError("Geparste Daten sind kein Dictionary.")
    except Exception as e:
        xbmc.log(f"[{common.addon_id}] Fehler beim Parsen der Staffeldaten: {e}", xbmc.LOGERROR)
        xbmcgui.Dialog().notification("Fehler", "Ungültige Staffeldaten.", xbmcgui.NOTIFICATION_ERROR)
        return

    # Legacy / neu kombinieren
    season_data = (season_info or {}).get("season_data") if isinstance(season_info, dict) else None
    episodes = (season_data.get("episodes", []) or []) if isinstance(season_data, dict) else []

    if not episodes:
        details_url = (season_info or {}).get("details_url")
        season_key = (season_info or {}).get("season_key")
        details_json, derr = fetch_details_json(details_url, expected_sha1=(season_info.get("details_sha1") or None))
        if derr:
            xbmcgui.Dialog().notification("Fehler", f"Details laden fehlgeschlagen: {derr}", xbmcgui.NOTIFICATION_ERROR)
            return
        season_data = details_json.get(season_key, {}) if isinstance(details_json, dict) else {}
        episodes = (season_data.get("episodes", []) or []) if isinstance(season_data, dict) else []

    if not episodes or not isinstance(episodes, list):
        xbmcgui.Dialog().notification("Fehler", "Keine Episoden in dieser Staffel gefunden.", xbmcgui.NOTIFICATION_ERROR)
        return

    season_no = season_info.get("season_number", -1)
    series_name = season_info.get("series_name", "Unbekannte Serie")
    series_poster = season_info.get("series_poster", "")
    series_fanart = season_info.get("series_fanart", "")
    series_genre = season_info.get("series_genre", "")
    series_studio = season_info.get("series_studio", "")
    fsk_raw = season_info.get("fsk_from_series")
    series_vote_avg = season_info.get("series_vote_average_from_series")
    season_vote_avg = season_info.get("season_vote_average_from_season")

    autoplay_random = _get_setting_bool("autoplay_random", False)

    def _apply_common_tags(li, ep):
        info = li.getVideoInfoTag()
        info.setMediaType("episode")
        info.setTitle(ep.get("name", "Zufällige Episode"))
        info.setTvShowTitle(series_name)
        info.setPlot(ep.get("overview", ""))

        if season_no != -1:
            info.setSeason(season_no)
        ep_no = ep.get("episode_number", 0)
        if ep_no:
            try:
                info.setEpisode(int(ep_no))
            except Exception:
                pass

        air_date = ep.get("air_date", "")
        if air_date:
            info.setPremiered(air_date)
            _parse_year_from_air_date_string(air_date, info, ep.get("name", ""))

        runtime_min = ep.get("runtime")
        if isinstance(runtime_min, int) and runtime_min > 0:
            try:
                info.setDuration(runtime_min * 60)
            except Exception:
                pass

        final_rating = 0.0
        for cand in (ep.get("rating"), season_vote_avg, series_vote_avg):
            if cand is not None:
                try:
                    final_rating = float(str(cand).replace(",", "."))
                except Exception:
                    pass
            if final_rating > 0:
                break
        if final_rating > 0:
            info.setRating(final_rating)

        if fsk_raw is not None:
            fsk_s = str(fsk_raw).strip()
            if fsk_s:
                info.setMpaa(f"FSK {fsk_s}")

        _split_and_set_list(info, "setDirectors", ep.get("directors", ""))
        _split_and_set_list(info, "setGenres", series_genre)
        _split_and_set_list(info, "setStudios", series_studio)

    def _apply_art(li, still_url):
        still_cached = _cached_or_local_image(still_url) or still_url
        _set_art_mq8(
            li,
            thumb=still_cached,
            icon=still_cached,
            poster=_cached_or_local_image(series_poster) or series_poster,
            fanart=_cached_or_local_image(series_fanart) or series_fanart
        )

    if autoplay_random:
        playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
        playlist.clear()

        chosen = random.sample(episodes, min(10, len(episodes)))
        added_paths = []

        for ep in chosen:
            path = unquote2(ep.get("file_ort", ""))
            title = ep.get("name")
            play_path = _prepare_playback_path(path)
            if not (play_path and title):
                continue
            li = xbmcgui.ListItem(path=play_path, offscreen=True)
            li.setProperty("IsPlayable", "true")
            _apply_common_tags(li, ep)
            _apply_art(li, ep.get("still", ""))
            playlist.add(play_path, li)
            added_paths.append(path)

        if playlist.size() > 0:
            xbmc.Player().play(playlist)

            def _playlist_watcher(expected_paths):
                try:
                    mon = xbmc.Monitor()
                    player = xbmc.Player()

                    expected_norm = [_playback_compare_key(p) for p in expected_paths if p]
                    started_for = set()

                    while not mon.abortRequested():
                        if player.isPlayingVideo():
                            try:
                                current_file = _playback_compare_key(player.getPlayingFile() or "")
                            except Exception:
                                current_file = ""

                            if current_file:
                                for p_raw, p_norm in zip(expected_paths, expected_norm):
                                    if p_norm == current_file and p_raw not in started_for:
                                        start_series_monitor(p_raw)
                                        started_for.add(p_raw)
                                        break

                        if not player.isPlayingVideo() and not xbmc.getCondVisibility("Player.HasMedia"):
                            break

                        if common.wait_or_abort(mon, 1.0):
                            return
                except Exception as e:
                    log(f"Playlist-Watcher Fehler: {e}", xbmc.LOGDEBUG)

            try:
                import threading
                t_mon = threading.Thread(target=_playlist_watcher, args=(added_paths,))
                t_mon.daemon = True
                t_mon.start()
            except Exception as e:
                log(f"Playlist-Watcher Startfehler: {e}", xbmc.LOGDEBUG)

        else:
            xbmcgui.Dialog().notification("Zufall", "Keine passende Episode gefunden.", xbmcgui.NOTIFICATION_INFO)

    else:
        ep = random.choice(episodes)
        path = unquote2(ep.get("file_ort", ""))
        title = ep.get("name")
        play_path = _prepare_playback_path(path)
        if not (play_path and title):
            xbmcgui.Dialog().notification("Zufall", "Unvollständige Episodendaten.", xbmcgui.NOTIFICATION_WARNING)
            return
        li = xbmcgui.ListItem(path=play_path, offscreen=True)
        li.setProperty("IsPlayable", "true")
        _apply_common_tags(li, ep)
        _apply_art(li, ep.get("still", ""))
        xbmc.Player().play(item=play_path, listitem=li)

        start_series_monitor(path)


def play_random_episode_from_specific_series(series_info_json_str):
    try:
        series_info = unpack_payload(series_info_json_str)
        if not isinstance(series_info, dict):
            raise ValueError("Ungültige Seriendaten für spezifische Zufallswiedergabe.")

        series_name = series_info.get("name", "Unbekannte Serie")
        details_url = series_info.get("details_url")
        series_fsk_raw = series_info.get("fsk")

        # kompatibel: manchmal kommt series_vote_average statt vote_average
        series_overall_vote_avg = series_info.get("vote_average")
        if series_overall_vote_avg is None:
            series_overall_vote_avg = series_info.get("series_vote_average")

        series_poster_for_ep = series_info.get("poster")
        series_fanart_for_ep = series_info.get("backcover")
        series_genre_for_ep = series_info.get("genre", "")
        series_studio_for_ep = series_info.get("studio", "")

        if not details_url:
            xbmcgui.Dialog().notification("Fehler", f"Keine Details-URL für Serie '{series_name}' gefunden.", xbmcgui.NOTIFICATION_ERROR)
            return

        details_json, derr = fetch_details_json(details_url, expected_sha1=(series_info.get("details_sha1") or None))
        if derr:
            xbmcgui.Dialog().notification("Fehler", f"Details laden fehlgeschlagen: {derr}", xbmcgui.NOTIFICATION_ERROR)
            return

        all_episodes = []
        for season_key, season_data_val in details_json.items():
            if isinstance(season_data_val, dict) and season_key.lower().startswith("season"):
                s_num = 0
                try:
                    s_num = int(re.search(r'(\d+)', season_key).group(1))
                except Exception:
                    pass
                tmdb_s_details = season_data_val.get("tmdb_season_details", {})
                season_vote_avg = tmdb_s_details.get("vote_average")

                for ep_data_item in season_data_val.get("episodes", []):
                    if isinstance(ep_data_item, dict):
                        ep_copy = ep_data_item.copy()
                        ep_copy['parsed_season_number'] = s_num
                        ep_copy['current_season_vote_average'] = season_vote_avg
                        all_episodes.append(ep_copy)

        if not all_episodes:
            xbmcgui.Dialog().notification("Info", f"Keine Episoden in Serie '{series_name}' gefunden.", xbmcgui.NOTIFICATION_INFO)
            return

        random_episode = random.choice(all_episodes)

        playable_path_raw = unquote2(random_episode.get("file_ort"))
        playable_path = _prepare_playback_path(playable_path_raw)
        episode_title = random_episode.get("name", "Zufällige Episode")
        if not playable_path:
            xbmcgui.Dialog().notification("Fehler", "Kein Abspielpfad gefunden.", xbmcgui.NOTIFICATION_WARNING)
            return

        li = xbmcgui.ListItem(path=playable_path, offscreen=True)
        li.setProperty("IsPlayable", "true")
        info_tag = li.getVideoInfoTag()
        info_tag.setTitle(episode_title)
        info_tag.setTvShowTitle(series_name)
        info_tag.setMediaType("episode")
        info_tag.setPlot(random_episode.get("overview", ""))

        ep_season_num = random_episode.get('parsed_season_number', 0)
        if ep_season_num > 0:
            info_tag.setSeason(ep_season_num)

        ep_num_val = random_episode.get("episode_number", 0)
        if ep_num_val:
            try:
                info_tag.setEpisode(int(ep_num_val))
            except Exception:
                pass

        ep_air_date = random_episode.get("air_date", "")
        if ep_air_date:
            info_tag.setPremiered(ep_air_date)
            _parse_year_from_air_date_string(ep_air_date, info_tag, episode_title)

        ep_runtime = random_episode.get("runtime")
        if isinstance(ep_runtime, int) and ep_runtime > 0:
            try:
                info_tag.setDuration(ep_runtime * 60)
            except Exception:
                pass

        final_ep_rating = 0.0
        for cand in (random_episode.get("rating"),
                     random_episode.get("current_season_vote_average"),
                     series_overall_vote_avg):
            if cand is not None:
                try:
                    final_ep_rating = float(str(cand).replace(",", "."))
                except Exception:
                    pass
            if final_ep_rating > 0:
                break
        if final_ep_rating > 0:
            info_tag.setRating(final_ep_rating)

        fsk_processed = str(series_fsk_raw).strip() if series_fsk_raw is not None else ""
        if fsk_processed:
            info_tag.setMpaa(f"FSK {fsk_processed}")

        _split_and_set_list(info_tag, "setGenres", series_genre_for_ep)
        _split_and_set_list(info_tag, "setStudios", series_studio_for_ep)
        _split_and_set_list(info_tag, "setDirectors", random_episode.get("directors", ""))

        still_url = random_episode.get("still", "")
        still_cached = _cached_or_local_image(still_url) or still_url

        _set_art_mq8(
            li,
            thumb=still_cached,
            icon=still_cached,
            poster=_cached_or_local_image(series_poster_for_ep) or series_poster_for_ep,
            fanart=_cached_or_local_image(series_fanart_for_ep) or series_fanart_for_ep
        )

        xbmc.Player().play(item=playable_path, listitem=li)

        start_series_monitor(playable_path_raw)

    except Exception as e:
        log(f"Fehler in play_random_episode_from_specific_series: {e}\n{traceback.format_exc()}",
            xbmc.LOGERROR)
        xbmcgui.Dialog().notification("Fehler", "Zufallsepisode konnte nicht gestartet werden.", xbmcgui.NOTIFICATION_ERROR)


def find_random_episode_info(count=1, max_retries=20):
    log(f"BG: Suche zufällige Episode(n) (Anzahl: {count})")
    all_series_data, err = load_favorites_json()
    if err:
        log(f"find_random_episode_info: load_favorites_json Fehler: {err}", xbmc.LOGWARNING)
        return None
    if not all_series_data:
        log("find_random_episode_info: keine Favoriten vorhanden.", xbmc.LOGINFO)
        return None

    results = []
    attempts = 0
    max_total_attempts = count * max_retries
    available_series_indices = list(range(len(all_series_data)))

    # In-Memory Details Cache (zusätzlich zum Disk-Cache)
    processed_series_details_cache = {}

    while len(results) < count and attempts < max_total_attempts and available_series_indices:
        attempts += 1
        try:
            chosen_idx = random.choice(available_series_indices)
            random_serie = all_series_data[chosen_idx].copy()
            random_serie.pop("action", None)

            series_title_fav = random_serie.get("name", "Unbekannte Serie")
            details_url = random_serie.get("details_url")
            if not details_url:
                available_series_indices.remove(chosen_idx)
                continue

            details_json = processed_series_details_cache.get(details_url)
            if details_json is None:
                d, derr = fetch_details_json(details_url, expected_sha1=(random_serie.get("details_sha1") or None))
                if derr:
                    processed_series_details_cache[details_url] = False
                    available_series_indices.remove(chosen_idx)
                    continue
                details_json = d if isinstance(d, dict) else {}
                processed_series_details_cache[details_url] = details_json
            elif details_json is False:
                available_series_indices.remove(chosen_idx)
                continue

            all_episodes = []
            for season_key, season_data_val in details_json.items():
                if isinstance(season_data_val, dict) and season_key.lower().startswith("season"):
                    s_num = 0
                    try:
                        s_num = int(re.search(r'(\d+)', season_key).group(1))
                    except Exception:
                        pass

                    tmdb_s_details = season_data_val.get("tmdb_season_details", {})
                    season_vote_avg_for_this_season = tmdb_s_details.get("vote_average")

                    for ep_data_item in season_data_val.get("episodes", []):
                        if isinstance(ep_data_item, dict):
                            ep_copy = ep_data_item.copy()
                            ep_copy['parsed_season_number'] = s_num
                            ep_copy['current_season_vote_average'] = season_vote_avg_for_this_season
                            all_episodes.append(ep_copy)

            if not all_episodes:
                available_series_indices.remove(chosen_idx)
                continue

            random_episode_data = random.choice(all_episodes)

            info = {
                "playable_path": random_episode_data.get("file_ort"),
                "episode_title": random_episode_data.get("name"),
                "overview": random_episode_data.get("overview", ""),
                "air_date": random_episode_data.get("air_date", ""),
                "still": random_episode_data.get("still", ""),
                "series_title": series_title_fav,
                "series_poster": random_serie.get("poster", ""),
                "series_fanart": random_serie.get("backcover", ""),
                "season_number": random_episode_data.get('parsed_season_number', 0),
                "episode_number": random_episode_data.get("episode_number", 0),
                "genre": random_serie.get("genre", ""),
                "studio": random_serie.get("studio", ""),
                "directors": random_episode_data.get("directors", ""),
                "runtime": random_episode_data.get("runtime"),
                "fsk": random_serie.get("fsk"),
                "episode_rating": random_episode_data.get("rating"),
                "season_vote_average": random_episode_data.get("current_season_vote_average"),
                "series_vote_average": random_serie.get("vote_average")
            }
            if info["playable_path"] and info["episode_title"] and not any(item.get("playable_path") == info["playable_path"] for item in results):
                results.append(info)

        except Exception as e:
            log(f"Fehler im Zufallsversuch ({attempts}/{max_total_attempts}): {e}", xbmc.LOGDEBUG)
            try:
                if chosen_idx in available_series_indices:
                    available_series_indices.remove(chosen_idx)
            except Exception:
                pass
            continue

    if not results:
        return None
    return results[0] if count == 1 else results


def play_random_favorite_series():
    autoplay_random = _get_setting_bool("autoplay_random", False)

    if autoplay_random:
        episodes_info_list = find_random_episode_info(count=10)
        if episodes_info_list and isinstance(episodes_info_list, list):
            playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
            playlist.clear()

            added_paths = []

            for episode_info in episodes_info_list:
                playable_path_raw = unquote2(episode_info.get("playable_path") or "")
                if not playable_path_raw:
                    continue
                playable_path = _prepare_playback_path(playable_path_raw)
                if not playable_path:
                    continue

                episode_title = episode_info.get("episode_title", "Zufällige Episode")
                li = xbmcgui.ListItem(path=playable_path, offscreen=True)
                li.setProperty("IsPlayable", "true")
                info_tag = li.getVideoInfoTag()
                info_tag.setTitle(episode_title)
                info_tag.setTvShowTitle(episode_info.get("series_title", ""))
                info_tag.setMediaType("episode")
                info_tag.setPlot(episode_info.get("overview", ""))

                if episode_info.get("season_number"):
                    info_tag.setSeason(episode_info["season_number"])
                if episode_info.get("episode_number"):
                    info_tag.setEpisode(episode_info["episode_number"])

                runtime_min = episode_info.get("runtime")
                if isinstance(runtime_min, int) and runtime_min > 0:
                    try:
                        info_tag.setDuration(runtime_min * 60)
                    except Exception:
                        pass

                air_date_val = episode_info.get("air_date")
                if air_date_val:
                    info_tag.setPremiered(air_date_val)
                    _parse_year_from_air_date_string(air_date_val, info_tag, episode_title)

                final_rating = 0.0
                for cand in (episode_info.get("episode_rating"),
                             episode_info.get("season_vote_average"),
                             episode_info.get("series_vote_average")):
                    if cand is not None:
                        try:
                            final_rating = float(str(cand).replace(",", "."))
                        except Exception:
                            pass
                    if final_rating > 0:
                        break
                if final_rating > 0:
                    info_tag.setRating(final_rating)

                fsk_raw = episode_info.get("fsk")
                if fsk_raw is not None:
                    fsk_s = str(fsk_raw).strip()
                    if fsk_s:
                        info_tag.setMpaa(f"FSK {fsk_s}")

                _split_and_set_list(info_tag, "setDirectors", episode_info.get("directors", ""))
                _split_and_set_list(info_tag, "setGenres", episode_info.get("genre", ""))
                _split_and_set_list(info_tag, "setStudios", episode_info.get("studio", ""))

                still = episode_info.get("still", "")
                still_cached = _cached_or_local_image(still) or still
                poster_cached = _cached_or_local_image(episode_info.get("series_poster")) or episode_info.get("series_poster")
                fanart_cached = _cached_or_local_image(episode_info.get("series_fanart")) or episode_info.get("series_fanart")

                _set_art_mq8(li, thumb=still_cached, icon=still_cached, poster=poster_cached, fanart=fanart_cached)

                playlist.add(playable_path, li)
                added_paths.append(playable_path_raw)

            if playlist.size() > 0:
                xbmc.Player().play(playlist)

                def _playlist_watcher(expected_paths):
                    try:
                        mon = xbmc.Monitor()
                        player = xbmc.Player()

                        expected_norm = [_playback_compare_key(p) for p in expected_paths if p]
                        started_for = set()

                        while not mon.abortRequested():
                            if player.isPlayingVideo():
                                try:
                                    current_file = _playback_compare_key(player.getPlayingFile() or "")
                                except Exception:
                                    current_file = ""

                                if current_file:
                                    for p_raw, p_norm in zip(expected_paths, expected_norm):
                                        if p_norm == current_file and p_raw not in started_for:
                                            start_series_monitor(p_raw)
                                            started_for.add(p_raw)
                                            break

                            if not player.isPlayingVideo() and not xbmc.getCondVisibility("Player.HasMedia"):
                                break

                            if common.wait_or_abort(mon, 1.0):
                                return
                    except Exception as e:
                        log(f"Playlist-Watcher Fehler: {e}", xbmc.LOGDEBUG)

                try:
                    import threading
                    t_mon = threading.Thread(target=_playlist_watcher, args=(added_paths,))
                    t_mon.daemon = True
                    t_mon.start()
                except Exception as e:
                    log(f"Playlist-Watcher Startfehler: {e}", xbmc.LOGDEBUG)

            else:
                xbmcgui.Dialog().notification("Zufall", "Keine passende globale Favoriten-Episode gefunden.", xbmcgui.NOTIFICATION_INFO)
        else:
            xbmcgui.Dialog().notification("Zufall", "Keine passende globale Favoriten-Episode gefunden.", xbmcgui.NOTIFICATION_INFO)

    else:
        episode_info = find_random_episode_info(count=1)
        if not (episode_info and isinstance(episode_info, dict)):
            xbmcgui.Dialog().notification("Zufall", "Keine passende globale Favoriten-Episode gefunden.", xbmcgui.NOTIFICATION_INFO)
            return

        playable_path_raw = unquote2(episode_info.get("playable_path") or "")
        playable_path = _prepare_playback_path(playable_path_raw)
        if not playable_path:
            xbmcgui.Dialog().notification("Zufall", "Globale Episode gefunden, aber kein Pfad.", xbmcgui.NOTIFICATION_WARNING)
            return

        episode_title = episode_info.get("episode_title", "Zufällige Episode")
        li = xbmcgui.ListItem(path=playable_path, offscreen=True)
        li.setProperty("IsPlayable", "true")
        info_tag = li.getVideoInfoTag()
        info_tag.setTitle(episode_title)
        info_tag.setTvShowTitle(episode_info.get("series_title", ""))
        info_tag.setMediaType("episode")
        info_tag.setPlot(episode_info.get("overview", ""))

        if episode_info.get("season_number"):
            info_tag.setSeason(episode_info["season_number"])
        if episode_info.get("episode_number"):
            info_tag.setEpisode(episode_info["episode_number"])

        runtime_min = episode_info.get("runtime")
        if isinstance(runtime_min, int) and runtime_min > 0:
            try:
                info_tag.setDuration(runtime_min * 60)
            except Exception:
                pass

        air_date_val = episode_info.get("air_date")
        if air_date_val:
            info_tag.setPremiered(air_date_val)
            _parse_year_from_air_date_string(air_date_val, info_tag, episode_title)

        final_rating = 0.0
        for cand in (episode_info.get("episode_rating"),
                     episode_info.get("season_vote_average"),
                     episode_info.get("series_vote_average")):
            if cand is not None:
                try:
                    final_rating = float(str(cand).replace(",", "."))
                except Exception:
                    pass
            if final_rating > 0:
                break
        if final_rating > 0:
            info_tag.setRating(final_rating)

        fsk_raw = episode_info.get("fsk")
        if fsk_raw is not None:
            fsk_s = str(fsk_raw).strip()
            if fsk_s:
                info_tag.setMpaa(f"FSK {fsk_s}")

        _split_and_set_list(info_tag, "setDirectors", episode_info.get("directors", ""))
        _split_and_set_list(info_tag, "setGenres", episode_info.get("genre", ""))
        _split_and_set_list(info_tag, "setStudios", episode_info.get("studio", ""))

        still = episode_info.get("still", "")
        still_cached = _cached_or_local_image(still) or still
        poster_cached = _cached_or_local_image(episode_info.get("series_poster")) or episode_info.get("series_poster")
        fanart_cached = _cached_or_local_image(episode_info.get("series_fanart")) or episode_info.get("series_fanart")

        _set_art_mq8(li, thumb=still_cached, icon=still_cached, poster=poster_cached, fanart=fanart_cached)
        xbmc.Player().play(item=playable_path, listitem=li)

        start_series_monitor(playable_path_raw)


# ---------------------------------------------------------------------------------------
# Router
# ---------------------------------------------------------------------------------------
# ---------------------------------------------------------------------------------------
# Router
# ---------------------------------------------------------------------------------------
# ---------------------------------------------------------------------------------------
# Legacy-Entry-Point: wird von RunPlugin direkt aufgerufen für Favoriten-Actions.
# Neue Actions bitte in default.py DISPATCH registrieren.
# Dieser Block bleibt für Rückwärtskompatibilität bestehender RunPlugin-Calls.
# ---------------------------------------------------------------------------------------
if __name__ == '__main__':

    # params robust
    try:
        params = dict(urllib.parse.parse_qsl(sys.argv[2][1:], keep_blank_values=True)) \
            if len(sys.argv) > 2 and sys.argv[2].startswith('?') else {}
    except Exception:
        params = {}

    action = (params.get('action') or '').strip() or None

    def _pick_first(keys):
        for k in keys:
            v = params.get(k)
            if v not in (None, ""):
                return v
        return None

    def _get_data(keys):
        v = _pick_first(keys)
        if not v:
            return None
        try:
            return unquote2(v)   # doppelt unquote
        except Exception:
            return v

    # --- Dispatch ---
    if action is None:
        show_favorite_series()

    elif action == 'show_fav_seasons':
        data = _get_data(('data', 'serie_data', 'series_data'))
        if data:
            show_fav_seasons(data)
        else:
            xbmcgui.Dialog().notification("Fehler", "Keine Seriendaten für Favoriten-Staffelansicht.", "")

    elif action == 'show_fav_episodes':
        data = _get_data(('data', 'season_data', 'staffel_data'))
        if data:
            show_fav_episodes(data)
        else:
            xbmcgui.Dialog().notification("Fehler", "Keine Staffeldaten für Favoriten-Episodenansicht.", "")

    elif action == 'play_random_favorite_series':
        play_random_favorite_series()

    elif action == 'play_random_favorite_episode_from_season':
        data = _get_data(('data', 'season_data', 'staffel_data'))
        if data:
            play_random_episode_from_season(data)
        else:
            xbmcgui.Dialog().notification("Fehler", "Keine Staffeldaten für Zufallsepisode aus Favoriten-Staffel.", "")

    # Random Episode aus DIESER Serie (Favoriten-Staffelansicht oben)
    # - alter Name: play_random_episode_from_this_series
    # - neue (empfohlene) konfliktfreie Namen: play_random_favorite_episode_from_this_series / play_random_fav_episode_from_this_series
    elif action in (
        'play_random_episode_from_this_series',
        'play_random_favorite_episode_from_this_series',
        'play_random_fav_episode_from_this_series'
    ):
        data = _get_data(('data', 'serie_data', 'series_data'))
        if data:
            play_random_episode_from_specific_series(data)
        else:
            xbmcgui.Dialog().notification("Fehler", "Keine Seriendaten für Zufallsepisode aus dieser Serie.", "")

    else:
        log(f"Unbekannte Aktion: {action}", xbmc.LOGWARNING)
        show_favorite_series()
