# coding: utf-8

import sys
import os
import time
import urllib.request
import urllib.parse
import urllib.error
import json
import xbmc
import xbmcgui
import xbmcplugin


# -------------------------
# Helper: always resolve Kodi addon handle for this invocation
# (some callers import this module without addon_handle set)
# -------------------------

def _resolve_addon_handle():
    try:
        h = getattr(common, 'addon_handle', None)
        if isinstance(h, int) and h >= 0:
            return h
    except Exception:
        pass
    try:
        return int(sys.argv[1])
    except Exception:
        return -1

import xbmcvfs  # Wichtig für translatePath und Dateizugriff
import ssl
import threading  # Für Playback-Monitoring + BG-Worker
import re
import traceback
import random
import hashlib
from datetime import datetime  # Wird an mehreren Stellen benötigt
try:
    from concurrent.futures import ThreadPoolExecutor
    _HAS_FUTURES = True
except Exception:
    ThreadPoolExecutor = None
    _HAS_FUTURES = False

import gb_common as common  # zentrales Addon-Shared-Modul

WATCHED_STATUS_LOCK = threading.Lock()
_MOVIES_EXECUTOR_LOCK = threading.Lock()
_MOVIES_EXECUTOR = None


# --- Logging helper ---
# Kodi logging wrapper for this module
# Usage: _log('message') or _log('warn', xbmc.LOGWARNING)

def _log(msg, level=xbmc.LOGINFO):
    try:
        addon_id = getattr(common, 'addon_id', 'plugin.video.glotzbox')
    except Exception:
        addon_id = 'plugin.video.glotzbox'
    try:
        xbmc.log(f"[{addon_id} movies.py] {msg}", level)
    except Exception:
        # Last resort (should never crash)
        try:
            xbmc.log(str(msg), level)
        except Exception:
            pass


def _safe_end_of_directory(handle=None, succeeded=True, update_listing=False, cache_to_disc=False):
    try:
        h = _resolve_addon_handle() if handle is None else int(handle)
    except Exception as e:
        _log(f"endOfDirectory skipped: 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 _force_movies_container_dateadded_desc():
    """Delegiert an gemeinsamen Helper in gb_common."""
    common.force_container_sort_dateadded_desc("show_filme")

def _prepare_playback_path(path):
    """Prepare HTTP(S) playback URLs to avoid user:pass in Kodi core logs."""
    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 _resolve_movie_codec_hint(movie, playback_path=""):
    """Resolve a stable codec hint for plugin routing/logging.

    Priority:
    1) explicit movie metadata (video_codec / video_codec_label)
    2) path-based detection fallback
    Returns lower-case normalized values like 'h264' / 'h265' when possible.
    """
    try:
        movie = movie if isinstance(movie, dict) else {}
    except Exception:
        movie = {}

    explicit = movie.get('video_codec') or movie.get('video_codec_label') or ''
    try:
        return common.detect_video_codec(codec_hint=explicit, path=playback_path or '') or ''
    except Exception:
        try:
            return (str(explicit).strip().lower() if explicit else '')
        except Exception:
            return ''


def _start_daemon_task(target, *args, task_name="MoviesTask", use_executor=False, **kwargs):
    """Start a background task with bounded pool for short jobs, thread fallback otherwise."""
    global _MOVIES_EXECUTOR
    if use_executor and _HAS_FUTURES:
        try:
            with _MOVIES_EXECUTOR_LOCK:
                if _MOVIES_EXECUTOR is None:
                    _MOVIES_EXECUTOR = ThreadPoolExecutor(max_workers=3, thread_name_prefix="GlotzMovies")
            _MOVIES_EXECUTOR.submit(target, *args, **kwargs)
            return True
        except Exception as e:
            _log(f"{task_name}: executor submit failed, fallback to thread: {e}", xbmc.LOGDEBUG)

    try:
        t = threading.Thread(target=target, args=args, kwargs=kwargs, name=task_name, daemon=True)
        t.start()
        return True
    except Exception as e:
        _log(f"{task_name}: thread start failed: {e}", xbmc.LOGWARNING)
        return False


def shutdown_executor():
    """Shutdown the ThreadPoolExecutor (call on addon exit to avoid blocking Kodi shutdown)."""
    global _MOVIES_EXECUTOR
    with _MOVIES_EXECUTOR_LOCK:
        ex = _MOVIES_EXECUTOR
        _MOVIES_EXECUTOR = None
    if ex is not None:
        try:
            ex.shutdown(wait=False)
            _log("ThreadPoolExecutor shutdown complete", xbmc.LOGDEBUG)
        except Exception as e:
            _log(f"ThreadPoolExecutor shutdown failed: {e}", xbmc.LOGDEBUG)


def _mask_log_value(value):
    """
    Maskiert Credentials in URLs für Logs.
    Behandelt sowohl normale als auch URL-encodete Strings.
    """
    try:
        raw = "" if value is None else str(value)
    except Exception:
        return "***"

    try:
        masked = common.mask_url(raw)
    except Exception:
        masked = raw

    # Manche Werte (z.B. Container.FolderPath) enthalten encodete URLs.
    # Für Log-Zwecke einmal decoden und dann maskieren.
    try:
        decoded = urllib.parse.unquote(raw)
        decoded_masked = common.mask_url(decoded)
        if decoded_masked != decoded:
            return decoded_masked
    except Exception:
        pass

    return masked

# (In-Memory-Artwork-Cache entfernt – vollständig durch common.get_cached_image()
#  mit SHA1-basiertem Disk-Cache abgedeckt.)


def _fs_local(path):
    return common.fs_local(path)


def _fs_exists(path):
    return common.fs_exists(path)


def _fs_mkdirs(path):
    return common.fs_mkdirs(path)


def _fs_delete(path):
    return common.fs_delete(path)


def _fs_rmdir(path):
    return common.fs_rmdir(path)


def _fs_listdir(path):
    return common.fs_listdir(path)


def _fs_rename(src, dst):
    return common.fs_rename(src, dst)



# -------------------------
# Atomic File Write Helpers
# -------------------------
def _atomic_write_bytes(dest_path, data_bytes, backup=False, durable=False):
    """Delegiert auf common.atomic_write_bytes, damit alle Module identisches, robustes Schreibverhalten haben."""
    try:
        return common.atomic_write_bytes(dest_path, data_bytes, backup=backup, durable=durable)
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] atomic_write_bytes delegate failed: {e}", xbmc.LOGWARNING)
        return False

def _atomic_write_json(dest_path, obj, indent=None, encoding="utf-8", durable=True):
    """Delegiert auf common.atomic_write_json, nutzt dessen Backup- und Durability-Logik."""
    try:
        return common.atomic_write_json(dest_path, obj, indent=indent, durable=durable, backup=True)
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] atomic_write_json delegate failed ({dest_path}): {e}", xbmc.LOGWARNING)
        return False









def _safe_load_json_file(path, default):
    """Nutze die zentrale Recovery-Logik aus common.safe_load_json_file."""
    return common.safe_load_json_file(path, default)


# --- IMDb Votes Helpers ---

def _normalize_votes_value(raw_votes):
    """
    Normalisiert imdbVotes in einen int.
    Akzeptiert: int, float, '34.130', '130,248', '130 248', etc.
    """
    if raw_votes is None:
        return None
    try:
        if isinstance(raw_votes, (int, float)):
            v = int(raw_votes)
            return v if v >= 0 else None
        s = str(raw_votes)
        s_digits = re.sub(r"[^\d]", "", s)
        if not s_digits:
            return None
        v = int(s_digits)
        return v if v >= 0 else None
    except Exception:
        return None


def _set_votes_on_info(info_tag, listitem, votes_int):
    """
    Setzt Votes robust:
    - bevorzugt VideoInfoTag.setVotes(int)
    - Fallback: listitem.setInfo('video', {'votes': int})
    - setzt zusätzlich ListItem.Property('imdbVotes')
    """
    if votes_int is None:
        return
    try:
        info_tag.setVotes(int(votes_int))
    except Exception as e_votes:
        try:
            listitem.setInfo('video', {'votes': int(votes_int)})
        except Exception:
            xbmc.log(f"[{common.addon_id} movies.py] Konnte Votes nicht setzen: {e_votes}", xbmc.LOGDEBUG)
    try:
        listitem.setProperty("imdbVotes", str(int(votes_int)))
    except Exception:
        pass


# --- Globale Pfaddefinitionen ---
WATCHED_STATUS_FILENAME_MOVIES = 'watched_status_movies.json'
MOVIES_CACHE_FILENAME = "movies.cache.json"  # vom Service geschriebener Online-Cache

WATCHED_STATUS_FILE_MOVIES = ""
MOVIES_CACHE_PATH = ""

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.")

    WATCHED_STATUS_FILE_MOVIES = os.path.join(common.addon_data_dir, WATCHED_STATUS_FILENAME_MOVIES)
    MOVIES_CACHE_PATH = os.path.join(common.addon_data_dir, MOVIES_CACHE_FILENAME)

except Exception as e_paths:
    xbmc.log(f"[{common.addon_id} movies.py] FATALER FEHLER beim Initialisieren der Filmpfade: {e_paths}", xbmc.LOGERROR)
    MOVIES_CACHE_PATH = ""


# --- Background Refresh (Movies) ---
BG_REFRESH_INTERVAL_HOURS_MOVIES = 1        # hours between automatic background refreshes
MOVIES_REFRESH_PENDING_COOLDOWN_S = 60      # seconds to block re-scheduling after one refresh kick
MOVIES_UI_IDLE_GUARD_S = 180               # seconds of Kodi idle before suppressing UI refreshes

# Movies view (show_filme) last open-state for logging
_MOVIES_VIEW_STATE = {
    'cache_used': False,
    'remote_changed_scheduled': False,
    'items': 0,
}

MOVIES_SIGNATURE_FILE = os.path.join(common.addon_data_dir, "movies_signature.json")
MOVIES_BG_PROP = f"{common.addon_id}.bg.movies.running"
_MOVIES_REFRESH_LOCK = threading.Lock()
MOVIES_REFRESH_TS_PROP = f"{common.addon_id}.movies.refresh.ts"


# zusätzlich zu deinen bisherigen Props:
MOVIES_REFRESH_PENDING_UNTIL_PROP = f"{common.addon_id}.movies.refresh.pending_until"

# Signatur der Daten, mit denen das aktuell sichtbare Listing gebaut wurde.
# Wird am Ende von check_movies_json gesetzt und vom BG-Refresh verglichen,
# um unnötige Container.Refresh-Aufrufe zu vermeiden.
MOVIES_LISTING_DATA_SIG_PROP = f"{common.addon_id}.movies.listing.data_sig"

# --- refresh scheduling throttle (prevents repeated refresh loops) ---

def _get_movies_refresh_pending_until():
    try:
        v = xbmcgui.Window(10000).getProperty(MOVIES_REFRESH_PENDING_UNTIL_PROP)
        return float(v) if v else 0.0
    except Exception:
        return 0.0


def _set_movies_refresh_pending_until(ts):
    try:
        xbmcgui.Window(10000).setProperty(MOVIES_REFRESH_PENDING_UNTIL_PROP, str(ts))
    except Exception:
        pass


def _clear_movies_refresh_pending():
    try:
        xbmcgui.Window(10000).setProperty(MOVIES_REFRESH_PENDING_UNTIL_PROP, "")
    except Exception:
        pass


def _schedule_movies_refresh(req_ts=None, reason="remote_changed"):
    """Start a background movies refresh once, throttled.

    Kodi can call directory builders multiple times in quick succession.
    We therefore mark a short "pending" window and skip re-scheduling
    while that is active.
    """
    now = time.time()
    pending_until = _get_movies_refresh_pending_until()
    if pending_until and pending_until > now:
        _log(f"show_filme: {reason} -> refresh already pending (throttled)")
        return False

    # Mark pending for a short time so repeated calls don't spawn a storm.
    _set_movies_refresh_pending_until(now + MOVIES_REFRESH_PENDING_COOLDOWN_S)

    try:
        # We don't strictly need req_ts here (it's used by maybe_refresh_movies
        # / AlarmClock flow), but keep it to match older call sites.
        if _start_daemon_task(
            background_refresh_movies_once,
            task_name="MoviesRefresh",
            use_executor=False,  # avoid non-daemon executor workers blocking addon shutdown
            source=f"ui:{reason}",
        ):
            _log(f"show_filme: {reason} -> refresh scheduled")
            return True
        _clear_movies_refresh_pending()
        _log(f"show_filme: {reason} schedule failed: task start returned False", xbmc.LOGWARNING)
        return False
    except Exception as e:
        _clear_movies_refresh_pending()
        _log(f"show_filme: {reason} schedule failed: {e}", xbmc.LOGWARNING)
        return False







MOVIES_LISTING_TS_PROP = f"{common.addon_id}.movies.listing.ts"        # wann check_movies_json gebaut wurde
MOVIES_REFRESH_REQ_TS_PROP = f"{common.addon_id}.movies.refresh.req.ts"  # debounce für refresh-requests

def _is_movies_container_folderpath(folder_path: str) -> bool:
    """True nur in der Filme-Ansicht dieses Addons."""
    try:
        fp = (folder_path or "").replace('"', "").strip().lower()
        addon_fp = f"plugin://{common.addon_id}".lower()
        if addon_fp not in fp:
            return False
        # Refresh ausschließlich in der Filme-Liste zulassen.
        return ("action=show_filme" in fp) or ("action=check_movies_json" in fp)
    except Exception:
        return False

def maybe_refresh_movies(req_ts_param=None, reason=""):
    """
    Wird per RunPlugin(...) von AlarmClock aufgerufen.
    Prüft: wurde die Liste seit dem Request schon automatisch neu gebaut?
    -> dann KEIN Container.Refresh mehr (verhindert Doppel-Refresh).
    """
    try:
        # Params aus sys.argv ziehen, wenn nicht direkt übergeben
        if req_ts_param is None:
            try:
                if len(sys.argv) > 2 and sys.argv[2].startswith('?'):
                    p = dict(urllib.parse.parse_qsl(sys.argv[2][1:]))
                    req_ts_param = p.get("req_ts")
                    reason = p.get("reason", reason)
            except Exception:
                pass

        try:
            req_ts = float(req_ts_param) if req_ts_param not in (None, "", "None") else 0.0
        except Exception:
            req_ts = 0.0

        win = xbmcgui.Window(10000)
        now = time.time()

        # WANN wurde die Liste zuletzt gebaut?
        try:
            last_listing_ts = float(win.getProperty(MOVIES_LISTING_TS_PROP) or "0")
        except Exception:
            last_listing_ts = 0.0

        # Wenn check_movies_json NACH unserem Request schon lief -> skip
        if req_ts > 0 and last_listing_ts >= req_ts:
            xbmc.log(
                f"[{common.addon_id} movies.py] maybe_refresh_movies: SKIP (listing already rebuilt) "
                f"req_ts={req_ts:.3f} last_listing_ts={last_listing_ts:.3f} reason={reason}",
                xbmc.LOGINFO
            )
            return

        # Extra-Schutz: wenn die Liste "gerade eben" gebaut wurde, skip
        if last_listing_ts > 0 and (now - last_listing_ts) < 2.0:
            xbmc.log(
                f"[{common.addon_id} movies.py] maybe_refresh_movies: SKIP (listing built recently) "
                f"dt={now-last_listing_ts:.2f}s reason={reason}",
                xbmc.LOGINFO
            )
            return

        # Nur refreshen wenn wir in unserem Container sind
        folder_path = xbmc.getInfoLabel("Container.FolderPath") or ""
        if not _is_movies_container_folderpath(folder_path):
            xbmc.log(
                f"[{common.addon_id} movies.py] maybe_refresh_movies: SKIP (not our container): {_mask_log_value(folder_path)}",
                xbmc.LOGDEBUG
            )
            return
        if "action=play_movie" in folder_path:
            xbmc.log(f"[{common.addon_id} movies.py] maybe_refresh_movies: SKIP (play_movie container).", xbmc.LOGDEBUG)
            return
        if xbmc.getCondVisibility("Window.IsVisible(fullscreenvideo)"):
            xbmc.log(f"[{common.addon_id} movies.py] maybe_refresh_movies: SKIP (fullscreenvideo visible).", xbmc.LOGDEBUG)
            return
        if xbmc.getCondVisibility("System.ScreenSaverActive"):
            xbmc.log(f"[{common.addon_id} movies.py] maybe_refresh_movies: SKIP (screensaver active).", xbmc.LOGDEBUG)
            return
        if xbmc.getCondVisibility("Player.HasMedia"):
            xbmc.log(f"[{common.addon_id} movies.py] maybe_refresh_movies: SKIP (Player.HasMedia).", xbmc.LOGDEBUG)
            return
        if xbmc.getCondVisibility(f"System.IdleTime({int(MOVIES_UI_IDLE_GUARD_S)})"):
            xbmc.log(f"[{common.addon_id} movies.py] maybe_refresh_movies: SKIP (idle >= {MOVIES_UI_IDLE_GUARD_S}s).", xbmc.LOGDEBUG)
            return

        xbmc.log(f"[{common.addon_id} movies.py] maybe_refresh_movies: DO Container.Refresh reason={reason}", xbmc.LOGINFO)
        xbmc.executebuiltin("Container.Refresh")

    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] maybe_refresh_movies error: {e}\n{traceback.format_exc()}", xbmc.LOGERROR)



def request_movies_container_refresh(reason="", delay_sec=1.0, min_interval_sec=3.0):
    """
    Refresh-Request debounced + delayed, ABER:
    Wir triggern NICHT direkt Container.Refresh, sondern AlarmClock -> RunPlugin(maybe_refresh_movies).
    Dadurch kann maybe_refresh_movies erkennen, ob Kodi die Liste bereits automatisch neu geladen hat
    und den 2. Refresh sauber unterdrücken.
    """
    try:
        win = xbmcgui.Window(10000)
        now = time.time()

        # ---- Debounce auf Request-Ebene ----
        try:
            last_req = float(win.getProperty(MOVIES_REFRESH_REQ_TS_PROP) or "0")
        except Exception:
            last_req = 0.0

        if (now - last_req) < float(min_interval_sec):
            xbmc.log(f"[{common.addon_id} movies.py] Refresh request suppressed (debounce) reason={reason}", xbmc.LOGINFO)
            return

        win.setProperty(MOVIES_REFRESH_REQ_TS_PROP, str(now))

        # ---- Nur wenn wir wirklich in unserem Container sind ----
        folder_path = xbmc.getInfoLabel("Container.FolderPath") or ""
        if not _is_movies_container_folderpath(folder_path):
            xbmc.log(
                f"[{common.addon_id} movies.py] Refresh skip (not our container): {_mask_log_value(folder_path)}",
                xbmc.LOGDEBUG
            )
            return

        if "action=play_movie" in folder_path:
            xbmc.log(f"[{common.addon_id} movies.py] Refresh skip (play_movie container).", xbmc.LOGDEBUG)
            return

        if xbmc.getCondVisibility("Window.IsVisible(fullscreenvideo)"):
            xbmc.log(f"[{common.addon_id} movies.py] Refresh skip (fullscreenvideo visible).", xbmc.LOGDEBUG)
            return

        # ---- AlarmClock -> RunPlugin(maybe_refresh_movies) ----
        delay_str = f"00:00:{int(max(0, delay_sec)):02d}"

        run_url = common.build_url({
            "action": "maybe_refresh_movies",
            "req_ts": f"{now:.3f}",
            "reason": reason or ""
        })

        xbmc.executebuiltin(
            f'AlarmClock({common.addon_id}_movies_maybe_refresh,RunPlugin({run_url}),{delay_str},silent)'
        )

        xbmc.log(f"[{common.addon_id} movies.py] Refresh request scheduled reason={reason} delay={delay_sec}s", xbmc.LOGINFO)

    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] request_movies_container_refresh error: {e}", xbmc.LOGERROR)



def _load_online_movies_from_cache(with_status=False):
    """
    Lädt die vom Service oder vom Plugin geschriebene movies.cache.json robust.
    Erwartet eine Liste von Movies.

    Returns:
      - default: list (bei Problemen [])
      - with_status=True: (list, cache_usable: bool)
        cache_usable=True bedeutet: Cache-Datei konnte gültig gelesen werden
        (auch wenn die Liste ggf. leer ist).
    """
    def _ret(lst, ok):
        if with_status:
            return (lst, bool(ok))
        return lst

    if not MOVIES_CACHE_PATH:
        return _ret([], False)

    data = _safe_load_json_file(MOVIES_CACHE_PATH, None)
    if data is None:
        return _ret([], False)

    if isinstance(data, list):
        return _ret(data, True)
    if isinstance(data, dict) and isinstance(data.get("movies"), list):
        return _ret(data.get("movies"), True)

    xbmc.log(f"[{common.addon_id} movies.py] WARN: {MOVIES_CACHE_PATH} enthält keine Liste (Typ: {type(data)}).", xbmc.LOGWARNING)
    return _ret([], False)




def _save_online_movies_to_cache(movies_list):
    """
    Speichert eine Online-Movieliste in movies.cache.json (atomisch).
    """
    if not MOVIES_CACHE_PATH or not isinstance(movies_list, list):
        return False
    try:
        return bool(_atomic_write_json(MOVIES_CACHE_PATH, movies_list, indent=None))
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] Konnte {MOVIES_CACHE_PATH} nicht speichern: {e}", xbmc.LOGDEBUG)
        return False



def _load_movies_signature():
    try:
        data = _safe_load_json_file(MOVIES_SIGNATURE_FILE, {})
        if isinstance(data, dict):
            return data.get("sig", "") or ""
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] BG: Konnte Movies-Signature nicht laden: {e}", xbmc.LOGDEBUG)
    return ""


def _save_movies_signature(sig):
    try:
        payload = {"sig": sig, "ts": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
        return _atomic_write_json(MOVIES_SIGNATURE_FILE, payload, indent=2)
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] BG: Konnte Movies-Signature nicht speichern: {e}", xbmc.LOGWARNING)
        return False


def _compute_movies_signature(movie_list):
    """
    Kompakte Signatur über relevante Felder – erkennt echte Änderungen ohne Full-Hash.
    """
    try:
        sk = []
        for m in movie_list:
            key = str(m.get("original_online_key") or m.get("file_ort") or m.get("name") or "").strip()
            if not key:
                continue
            sk.append({
                "k": key,
                "a": str(m.get("added", "")),
                "rd": str(m.get("release_date", "")),
                "r": str(m.get("imdbRating", "")),
                "v": str(_normalize_votes_value(m.get("imdbVotes", None)) or ""),
                "rt": str(m.get("runtime", "")),
            })
        sk = sorted(sk, key=lambda x: x["k"])
        raw = json.dumps(sk, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
        return hashlib.md5(raw.encode("utf-8")).hexdigest()
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] BG: Fehler beim Signatur-Build: {e}", xbmc.LOGWARNING)
        return ""


def _gather_artwork_sources_from_movie(m):
    """Sammelt alle potentiellen Artwork-URLs (poster, backcover, images[..] etc.)."""
    urls = []
    for art_key in ["poster", "backcover", "thumb", "fanart", "logo", "clearart", "discart", "banner"]:
        u = m.get(art_key)
        if isinstance(u, str) and u.startswith("http"):
            urls.append((art_key, u, False))
    images = m.get("images", {})
    if isinstance(images, dict):
        for k, u in images.items():
            if isinstance(u, str) and u.startswith("http"):
                urls.append((k, u, True))  # True = kommt aus images{}
    return urls


def _prefetch_movie_artwork_silently(movie_list):
    """Stub – Artwork wird via Kodis Texture-Cache geladen, kein Addon-Prefetch noetig."""
    return 0, 0


def background_refresh_movies_once(source=None, **kwargs):
    """
    Einmaliger BG-Run (vom Service oder aus der UI aufgerufen):
    - Liste (Online + lokal) mergen
    - Signatur aktualisieren
    - Wenn der Run aus der UI kam und sich die Liste wirklich geändert hat:
      gezielt Container-Refresh anfordern.

    source: z.B. 'service', 'startup', 'ui:remote_changed' – für Logging.
    """
    _src = source or "unknown"
    xbmc.log(f"[{common.addon_id} movies.py] BG: Movies-Refresh gestartet (source={_src}).", xbmc.LOGINFO)

    try:
        movie_list = _get_merged_movie_list(force=True)

        new_sig = _compute_movies_signature(movie_list)
        old_sig = _load_movies_signature()
        signature_changed = bool(new_sig and new_sig != old_sig)

        if signature_changed:
            _save_movies_signature(new_sig)
            xbmc.log(
                f"[{common.addon_id} movies.py] BG: neue Movies-Signatur gespeichert (source={_src}).",
                xbmc.LOGINFO
            )

            if str(_src).startswith("ui:"):
                try:
                    listing_data_sig = xbmcgui.Window(10000).getProperty(MOVIES_LISTING_DATA_SIG_PROP)
                    if not listing_data_sig or listing_data_sig != new_sig:
                        xbmc.log(
                            f"[{common.addon_id} movies.py] BG: UI-Refresh angefordert – "
                            f"Listing hat veraltete Daten (source={_src}).",
                            xbmc.LOGINFO
                        )
                        request_movies_container_refresh(reason="bg_sig_changed", delay_sec=1.0, min_interval_sec=0.5)
                    else:
                        xbmc.log(
                            f"[{common.addon_id} movies.py] BG: UI-Refresh uebersprungen – "
                            f"Listing zeigt bereits aktuelle Daten (source={_src}).",
                            xbmc.LOGDEBUG
                        )
                except Exception as e:
                    xbmc.log(f"[{common.addon_id} movies.py] BG: UI-Refresh Anfrage fehlgeschlagen: {e}", xbmc.LOGWARNING)
        else:
            xbmc.log(f"[{common.addon_id} movies.py] BG: Keine Movie-Änderung anhand Signatur erkannt (source={_src}).", xbmc.LOGDEBUG)
    finally:
        _clear_movies_refresh_pending()


def _movies_bg_loop():
    """
    Dauerschleife (Kodi Monitor), sofortiger Start + danach alle X Stunden.
    """
    xbmc.log(f"[{common.addon_id} movies.py] BG: Movies-Worker gestartet (Intervall {BG_REFRESH_INTERVAL_HOURS_MOVIES}h).", xbmc.LOGINFO)
    monitor = xbmc.Monitor()
    win = xbmcgui.Window(10000)
    try:
        background_refresh_movies_once(source="startup")
        while not monitor.waitForAbort(BG_REFRESH_INTERVAL_HOURS_MOVIES * 3600):
            background_refresh_movies_once(source="bg_loop")
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] BG: Worker-Fehler: {e}\n{traceback.format_exc()}", xbmc.LOGERROR)
    finally:
        try:
            if win.getProperty(MOVIES_BG_PROP) == "1":
                win.setProperty(MOVIES_BG_PROP, "")
        except Exception:
            pass
        xbmc.log(f"[{common.addon_id} movies.py] BG: Movies-Worker beendet.", xbmc.LOGINFO)


def start_movies_bg_worker():
    """
    Idempotenter Starter – sorgt dafür, dass nur EIN Thread läuft.
    Kann von einem Service oder via Plugin-Action aufgerufen werden.
    """
    try:
        win = xbmcgui.Window(10000)
        if win.getProperty(MOVIES_BG_PROP) == "1":
            xbmc.log(f"[{common.addon_id} movies.py] BG: Movies-Worker läuft bereits – kein Start.", xbmc.LOGDEBUG)
            return
        win.setProperty(MOVIES_BG_PROP, "1")
        if _start_daemon_task(_movies_bg_loop, task_name="MoviesBGWorker", use_executor=False):
            xbmc.log(f"[{common.addon_id} movies.py] BG: Movies-Worker gestartet.", xbmc.LOGINFO)
        else:
            win.setProperty(MOVIES_BG_PROP, "")
            xbmc.log(f"[{common.addon_id} movies.py] BG: Movies-Worker konnte nicht gestartet werden.", xbmc.LOGERROR)
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] BG: Konnte Movies-Worker nicht starten: {e}", xbmc.LOGERROR)



def get_movie_cache_filename(url):
    """Stub – kein Addon-eigener Image-Cache mehr."""
    return ""


def get_cached_movie_image(url):
    """Stub – kein Addon-eigener Image-Cache mehr."""
    return None


def download_and_cache_movie_image(url):
    """Stub – kein Addon-eigener Image-Cache mehr."""
    return None



# -------------------------
# Playback Monitoring (event-basiert, weniger Polling)
# -------------------------

MOVIES_LIST_BUILT_TS_PROP = f"{common.addon_id}.movies.list.built.ts"
MOVIES_WATCHED_UPDATED_TS_PROP = f"{common.addon_id}.movies.watched.updated.ts"


class _PlaybackTracker(xbmc.Player):
    def __init__(self, movie_key_identifier, watched_file_path):
        super().__init__()
        self.movie_key_identifier = movie_key_identifier
        self.watched_file_path = watched_file_path

        self._started_evt = threading.Event()
        self._ended_evt = threading.Event()
        self._total_time = 0.0
        self._played_max = 0.0
        self._lock = threading.Lock()

    # --- Player Events ---
    def onAVStarted(self):
        try:
            self._started_evt.set()
        except Exception:
            pass

    def onPlayBackEnded(self):
        try:
            self._ended_evt.set()
        except Exception:
            pass

    def onPlayBackStopped(self):
        try:
            self._ended_evt.set()
        except Exception:
            pass

    def _safe_refresh_container(self, monitor):
        """
        Refresh erst nach sauberem Rücksprung aus dem Player.
        Refresh nur dann, wenn die Liste NICHT schon NACH dem Save neu gebaut wurde.
        """
        try:
            # warten bis Kodi wirklich kein Media mehr hat
            for _ in range(40):  # ~10s max
                if not xbmc.getCondVisibility("Player.HasMedia"):
                    break
                if common.wait_or_abort(monitor, 0.25):
                    return

            if common.wait_or_abort(monitor, 0.3):
                return

            folder_path = xbmc.getInfoLabel("Container.FolderPath") or ""

            if not _is_movies_container_folderpath(folder_path):
                return

            if "action=play_movie" in folder_path:
                return

            # Wenn die Liste bereits nach unserem watched-save neu gebaut wurde -> kein extra Refresh
            win = xbmcgui.Window(10000)
            try:
                list_built_ts = float(win.getProperty(MOVIES_LISTING_TS_PROP) or "0")
            except Exception:
                list_built_ts = 0.0
            try:
                watched_ts = float(win.getProperty(MOVIES_WATCHED_UPDATED_TS_PROP) or "0")
            except Exception:
                watched_ts = 0.0

            # Wenn Liste jünger/gleich watched-save => UI ist schon aktuell
            if watched_ts > 0 and list_built_ts >= watched_ts:
                xbmc.log(
                    f"[{common.addon_id} movies.py] Monitor: Refresh skip (list already rebuilt after save). "
                    f"list_ts={list_built_ts:.3f} watched_ts={watched_ts:.3f}",
                    xbmc.LOGDEBUG
                )
                return

            # sonst: genau 1 Refresh (debounced)
            request_movies_container_refresh(reason="playback_end", delay_sec=1, min_interval_sec=2.0)

        except Exception as e:
            xbmc.log(f"[{common.addon_id} movies.py] Monitor: Safe-Refresh Fehler: {e}", xbmc.LOGDEBUG)

    def run(self):
        xbmc.log(
            f"[{common.addon_id} movies.py] monitor_playback_movie (event) gestartet für: {_mask_log_value(self.movie_key_identifier)}",
            xbmc.LOGINFO
        )
        monitor = xbmc.Monitor()

        try:
            if not self.watched_file_path:
                xbmc.log(f"[{common.addon_id} movies.py] Monitor: watched file path leer. Abbruch.", xbmc.LOGERROR)
                return
            if not self.movie_key_identifier:
                xbmc.log(f"[{common.addon_id} movies.py] Monitor: Kein Identifier. Abbruch.", xbmc.LOGERROR)
                return

            # 1) Warten bis Playback startet (Event oder Poll-Fallback)
            started = self._started_evt.wait(10.0)
            if not started:
                for _ in range(20):  # ~10s
                    if monitor.abortRequested():
                        return
                    if self.isPlayingVideo():
                        started = True
                        break
                    if common.wait_or_abort(monitor, 0.5):
                        return

            if not started:
                xbmc.log(f"[{common.addon_id} movies.py] Monitor: Player hat nicht gestartet. Abbruch.", xbmc.LOGWARNING)
                return

            # 2) TotalTime robust holen
            total_time = 0.0
            for _ in range(20):  # ~10s
                if monitor.abortRequested():
                    return
                if not self.isPlayingVideo():
                    xbmc.log(f"[{common.addon_id} movies.py] Monitor: Ende bevor TotalTime verfügbar.", xbmc.LOGDEBUG)
                    return
                try:
                    total_time = float(self.getTotalTime() or 0.0)
                except Exception:
                    total_time = 0.0
                if total_time > 0:
                    break
                if common.wait_or_abort(monitor, 0.5):
                    return

            if total_time <= 0:
                xbmc.log(f"[{common.addon_id} movies.py] Monitor: Konnte Gesamtdauer nicht ermitteln.", xbmc.LOGWARNING)
                return

            with self._lock:
                self._total_time = total_time

            # 3) Tracken bis Ende/Stop
            watched_flag_set = False

            while not monitor.abortRequested():
                if self._ended_evt.is_set():
                    break
                if not self.isPlayingVideo():
                    break
                try:
                    t = float(self.getTime() or 0.0)
                    with self._lock:
                        if t > self._played_max:
                            self._played_max = t
                except Exception:
                    pass

                # Reliability: sobald Schwelle erreicht ist, sofort speichern (falls Kodi abstürzt/Stecker -> Status bleibt)
                if not watched_flag_set:
                    try:
                        with self._lock:
                            played_max = float(self._played_max or 0.0)
                            total_time = float(self._total_time or 0.0)
                        if total_time > 0:
                            pct_now = (min(played_max, total_time) / total_time) * 100.0
                            if pct_now >= 84.0:
                                ok = False
                                try:
                                    with WATCHED_STATUS_LOCK:
                                        watched_status = common.load_watched_status_movies(self.watched_file_path)
                                        wid = common.movies_watched_id(self.movie_key_identifier)
                                        if wid:
                                            watched_status[wid] = {"playcount": 1}
                                            ok = common.save_watched_status_movies(self.watched_file_path, watched_status)
                                except Exception as e_save:
                                    xbmc.log(f"[{common.addon_id} movies.py] Monitor: Fehler beim Speichern (mid-playback): {e_save}", xbmc.LOGERROR)

                                if ok:
                                    watched_flag_set = True
                                    try:
                                        xbmcgui.Window(10000).setProperty(MOVIES_WATCHED_UPDATED_TS_PROP, str(time.time()))
                                    except Exception:
                                        pass
                                    xbmc.log(
                                        f"[{common.addon_id} movies.py] Monitor: Gesehen-Status während Playback gespeichert ({pct_now:.2f}%).",
                                        xbmc.LOGINFO
                                    )
                    except Exception:
                        pass

                if common.wait_or_abort(monitor, 1.0):
                    return

            # kein "letztes getTime()" nach Stop/Ende -> kann Exception werfen
            with self._lock:
                played_time = min(self._played_max, self._total_time)
                total_time = self._total_time

            percentage_watched = (played_time / total_time) * 100.0 if total_time > 0 else 0.0
            xbmc.log(
                f"[{common.addon_id} movies.py] Monitor: {_mask_log_value(self.movie_key_identifier)} zu {percentage_watched:.2f}% "
                f"(Played: {played_time:.2f}s / Total: {total_time:.2f}s).",
                xbmc.LOGDEBUG
            )

            if watched_flag_set:
                # Bereits während Playback gespeichert -> am Ende nur noch safe refresh (UI Update)
                self._safe_refresh_container(monitor)
                return

            if percentage_watched >=84.0:
                ok = False
                try:
                    with WATCHED_STATUS_LOCK:
                        watched_status = common.load_watched_status_movies(self.watched_file_path)
                        wid = common.movies_watched_id(self.movie_key_identifier)
                        if wid:
                            watched_status[wid] = {"playcount": 1}
                            ok = common.save_watched_status_movies(self.watched_file_path, watched_status)
                        else:
                            ok = False
                except Exception as e_save:
                    xbmc.log(f"[{common.addon_id} movies.py] Monitor: Fehler beim Speichern: {e_save}", xbmc.LOGERROR)

                if ok:
                    # Timestamp setzen, damit wir wissen "Save ist passiert"
                    try:
                        xbmcgui.Window(10000).setProperty(MOVIES_WATCHED_UPDATED_TS_PROP, str(time.time()))
                    except Exception:
                        pass

                    xbmc.log(
                        f"[{common.addon_id} movies.py] Monitor: Gesehen-Status für {_mask_log_value(self.movie_key_identifier)} gespeichert.",
                        xbmc.LOGINFO
                    )
                    self._safe_refresh_container(monitor)
                else:
                    xbmc.log(f"[{common.addon_id} movies.py] Monitor: FEHLER beim Speichern des Gesehen-Status.", xbmc.LOGERROR)

        except Exception as e_thread_main:
            xbmc.log(
                f"[{common.addon_id} movies.py] !!! FEHLER IM monitor_playback_movie Thread: {e_thread_main} !!!\n{traceback.format_exc()}",
                xbmc.LOGERROR
            )
        finally:
            xbmc.log(
                f"[{common.addon_id} movies.py] monitor_playback_movie Thread für Identifier: '{_mask_log_value(self.movie_key_identifier)}' BEENDET.",
                xbmc.LOGINFO
            )







def monitor_playback_movie(movie_key_identifier):
    tracker = _PlaybackTracker(movie_key_identifier, WATCHED_STATUS_FILE_MOVIES)
    tracker.run()



# -------------------------
# Online Liste: Meta (ETag/Last-Modified/CL) + Conditional GET
# -------------------------

def _load_movies_remote_meta(meta_path):
    meta = _safe_load_json_file(meta_path, {})
    if not isinstance(meta, dict):
        meta = {}
    out = {
        "content_length": None,
        "etag": None,
        "last_modified": None,
        "sha1": None,
        "ts": meta.get("ts", ""),
    }
    try:
        cl = meta.get("content_length")
        if cl not in (None, ""):
            out["content_length"] = int(str(cl))
    except Exception:
        out["content_length"] = None
    try:
        et = meta.get("etag")
        out["etag"] = str(et).strip() if et else None
    except Exception:
        out["etag"] = None
    try:
        lm = meta.get("last_modified")
        out["last_modified"] = str(lm).strip() if lm else None
    except Exception:
        out["last_modified"] = None
    try:
        sh = meta.get("sha1")
        out["sha1"] = str(sh).strip() if sh else None
    except Exception:
        out["sha1"] = None
    return out


def _save_movies_remote_meta(meta_path, content_length=None, etag=None, last_modified=None, sha1=None):
    payload = {
        "content_length": content_length if content_length is not None else "",
        "etag": etag if etag else "",
        "last_modified": last_modified if last_modified else "",
        "sha1": sha1 if sha1 else "",
        "ts": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
    }
    _atomic_write_json(meta_path, payload, indent=2)


def _get_merged_movie_list(force=False, fast=True):
    """
    - Online-Check via HEAD (ETag/Last-Modified/Content-Length wenn möglich)
    - Conditional GET (If-None-Match / If-Modified-Since)
    - movies.cache.json dient als lokaler Online-Cache
    - Merge lokal/online O(n)
    """
    # 1) Online-URL bestimmen
    ftp_username = (common.addon.getSetting("ftp_username") or "").strip()
    ftp_password = (common.addon.getSetting("ftp_password") or "").strip()
    ftp_host_raw = (common.addon.getSetting("ftp_host") or "").strip()

    if not (ftp_username and ftp_password) and not ftp_host_raw.startswith("http"):
        xbmc.log(
            f"[{common.addon_id} movies.py] FTP-Zugangsdaten fehlen und Host ist kein HTTP(S) Host. "
            f"Online-Liste könnte fehlschlagen.",
            xbmc.LOGWARNING
        )

    host_final, base_path, scheme_to_use = common.normalize_ftp_host_basepath()
    json_filename = "kodiMovie.json"

    url = f"{scheme_to_use}://{host_final}{base_path}{json_filename}"
    xbmc.log(f"[{common.addon_id} movies.py] Prüfe Online-Filmliste unter: {common.mask_url(url)}", xbmc.LOGDEBUG)

    meta_path = os.path.join(common.addon_data_dir, "movies_remote_meta.json")
    meta = _load_movies_remote_meta(meta_path)
    last_cl = meta.get("content_length")
    last_etag = meta.get("etag")
    last_lm = meta.get("last_modified")
    last_sha = meta.get("sha1")

    # NEW: tiny sidecar hash (kodiMovie.json.sha1) to detect changes cheaply
    remote_sha = None
    if scheme_to_use in ("http", "https") and not (remote_sha and last_sha and remote_sha == last_sha):
        try:
            remote_sha = common.fetch_remote_sha1(url, timeout_s=8)
        except Exception:
            remote_sha = None

    # Bootstrap: Wenn wir einen Remote-SHA haben, aber noch keinen lokal gespeicherten SHA (z.B. alte meta),
    # dann einmalig NICHT im Fast-Mode laufen lassen, damit meta/cache sauber initialisiert werden.
    if (not force) and fast and remote_sha and not last_sha:
        fast = False

    current_cl = None
    current_etag = None
    current_lm = None
    head_failed = False

    # 3) Online-Liste zunächst aus Cache laden
    movies_list_online, online_cache_usable = _load_online_movies_from_cache(with_status=True)

    # If we have a remote_sha and it matches the last stored sha1, we can skip HEAD altogether.
    # (We still keep the HEAD logic below as fallback when .sha1 is missing.)
    skip_head = bool((not force) and remote_sha and last_sha and remote_sha == last_sha and online_cache_usable)

    # 2) HEAD holen (nur HTTP/HTTPS)
    if scheme_to_use in ("http", "https") and not skip_head:
        try:
            req_head = common.get_authenticated_request(url)
            if not req_head:
                raise ValueError("Auth Request fehlgeschlagen für HEAD.")
            req_head.get_method = lambda: "HEAD"

            context = common.get_ssl_context()
            with common.retry_urlopen(req_head, context=context, timeout=10, retries=1, retry_delay=2.0) as response:
                if response.getcode() == 200:
                    info = response.info()
                    cl_header = info.get("Content-Length")
                    if cl_header:
                        try:
                            current_cl = int(cl_header)
                        except (ValueError, TypeError):
                            current_cl = None
                    et = info.get("ETag")
                    if et:
                        current_etag = str(et).strip()
                    lm = info.get("Last-Modified")
                    if lm:
                        current_lm = str(lm).strip()
                else:
                    xbmc.log(
                        f"[{common.addon_id} movies.py] HEAD-Request gab HTTP {response.getcode()} zurück.",
                        xbmc.LOGDEBUG
                    )
        except Exception as e_head:
            head_failed = True
            xbmc.log(
                f"[{common.addon_id} movies.py] HEAD-Request fehlgeschlagen ({common.mask_url(url)}): {e_head}",
                xbmc.LOGDEBUG
            )

    # 4) Entscheiden, ob wir neu laden
    need_refresh_from_remote = False
    if force:
        need_refresh_from_remote = True
    else:
        if scheme_to_use in ("http", "https"):
            # Prefer .sha1 sidecar if available (kodiMovie.json.sha1)
            if remote_sha and last_sha and remote_sha == last_sha and online_cache_usable:
                need_refresh_from_remote = False
            elif remote_sha and (not last_sha or remote_sha != last_sha):
                need_refresh_from_remote = True
            else:
                # Fallback: HEAD validators (ETag > Last-Modified > Content-Length)
                if current_etag and last_etag and current_etag == last_etag and online_cache_usable:
                    need_refresh_from_remote = False
                elif current_lm and last_lm and current_lm == last_lm and online_cache_usable:
                    need_refresh_from_remote = False
                elif current_cl is not None and last_cl is not None and current_cl == last_cl and online_cache_usable:
                    need_refresh_from_remote = False
                else:
                    # Wenn keine Validatoren UND keine .sha1 vorliegen oder HEAD komplett scheiterte -> lieber neu laden
                    if (current_etag is None and current_lm is None and current_cl is None and not remote_sha) or head_failed:
                        need_refresh_from_remote = True
                    else:
                        # wenn wir keinerlei HEAD-Infos haben, aber Cache existiert und .sha1 matcht: Cache behalten
                        if (current_etag is None and current_lm is None and current_cl is None) and online_cache_usable:
                            need_refresh_from_remote = False
                        else:
                            need_refresh_from_remote = True
        else:
            # Nicht-HTTP: ohne Metadaten nur refreshen, wenn kein Cache oder force
            if not online_cache_usable:
                need_refresh_from_remote = True

    # Movies: bei erkannter Remote-Änderung NICHT auf Cache hängen bleiben.
    # Der frühere Fast-Mode zeigte erst die alte Liste und hoffte auf einen späteren
    # UI-Refresh. Genau das führte dazu, dass neue Filme manchmal erst nach Kodi-Neustart
    # sichtbar wurden. Für Movies daher bewusst synchron neu laden, sobald Remote-Change
    # erkannt wurde.

    global _MOVIES_VIEW_STATE

    _MOVIES_VIEW_STATE['cache_used'] = bool(online_cache_usable and not need_refresh_from_remote)
    _MOVIES_VIEW_STATE['remote_changed_scheduled'] = False

    if need_refresh_from_remote and fast and online_cache_usable:
        xbmc.log(
            f"[{common.addon_id} movies.py] show_filme: remote_changed erkannt -> synchroner Refresh statt BG-Only.",
            xbmc.LOGINFO,
        )

    if need_refresh_from_remote:
        xbmc.log(f"[{common.addon_id} movies.py] Online-Filmliste: lade/prüfe Remote (force={force}).", xbmc.LOGINFO)

        try:
            req = common.get_authenticated_request(url)
            if not req:
                raise ValueError("Auth Request fehlgeschlagen für GET.")

            # Conditional headers nur nutzen, wenn ein nutzbarer Cache vorhanden ist.
            # Sonst erzwingen wir einen Body (kein 304), damit wir den Cache neu aufbauen.
            if (scheme_to_use in ("http", "https")) and not force and online_cache_usable:
                try:
                    if last_etag:
                        req.add_header("If-None-Match", last_etag)
                    if last_lm:
                        req.add_header("If-Modified-Since", last_lm)
                except Exception:
                    pass

            context = common.get_ssl_context()

            try:
                with common.retry_urlopen(req, context=context, timeout=30, retries=1, retry_delay=2.0) as response:
                    code = response.getcode()
                    if code == 200:
                        raw = response.read()
                        body_sha = None
                        try:
                            body_sha = common.sha1_hex(raw)
                        except Exception:
                            body_sha = None
                        json_data = raw.decode('utf-8-sig', errors='replace')
                        new_list = json.loads(json_data)
                        if isinstance(new_list, list):
                            movies_list_online = new_list
                            cache_ok = _save_online_movies_to_cache(movies_list_online)

                            if cache_ok:
                                info = response.info()
                                current_cl = None
                                current_etag = None
                                current_lm = None

                                clh = info.get("Content-Length")
                                if clh:
                                    try:
                                        current_cl = int(clh)
                                    except Exception:
                                        current_cl = None
                                eth = info.get("ETag")
                                if eth:
                                    current_etag = str(eth).strip()
                                lmh = info.get("Last-Modified")
                                if lmh:
                                    current_lm = str(lmh).strip()

                                _save_movies_remote_meta(
                                    meta_path,
                                    current_cl,
                                    current_etag,
                                    current_lm,
                                    sha1=(body_sha or remote_sha)
                                )
                            else:
                                xbmc.log(
                                    f"[{common.addon_id} movies.py] Cache-Write fehlgeschlagen – Meta bleibt unverändert.",
                                    xbmc.LOGWARNING
                                )
                        else:
                            xbmc.log(
                                f"[{common.addon_id} movies.py] Online-Daten sind keine Liste ({type(new_list)}). Behalte Cache.",
                                xbmc.LOGWARNING
                            )
                    else:
                        xbmc.log(
                            f"[{common.addon_id} movies.py] HTTP Fehler beim Laden: {code} von {common.mask_url(url)}",
                            xbmc.LOGWARNING
                        )
            except urllib.error.HTTPError as he:
                if he.code == 304:
                    xbmc.log(f"[{common.addon_id} movies.py] Online-Filmliste: 304 Not Modified – verwende Cache.", xbmc.LOGDEBUG)
                    # Meta-Timestamp aktualisieren (optional)
                    _save_movies_remote_meta(meta_path, last_cl, last_etag, last_lm, sha1=(last_sha or remote_sha))
                else:
                    xbmc.log(
                        f"[{common.addon_id} movies.py] HTTPError beim Laden der Online-Filmliste: {he.code} {he}",
                        xbmc.LOGWARNING
                    )
            except Exception as e_get:
                raise e_get

        except Exception as e_online:
            xbmc.log(
                f"[{common.addon_id} movies.py] Exception beim Laden der Online-Filmliste von {common.mask_url(url)}: {e_online}\n"
                f"{traceback.format_exc()}",
                xbmc.LOGWARNING
            )
    else:
        xbmc.log(f"[{common.addon_id} movies.py] Online-Filmliste unverändert – verwende lokalen Cache.", xbmc.LOGDEBUG)

    # Merge: nur Online-Movies (Local-Merge wurde mit dem Download-Feature entfernt).
    merged_movies = {}
    for movie_online in movies_list_online:
        if not isinstance(movie_online, dict):
            continue
        key_for_merge = f"movie_{movie_online.get('tmdbId')}" if movie_online.get('tmdbId') else movie_online.get('file_ort')

        if key_for_merge and isinstance(key_for_merge, str) and key_for_merge.strip():
            key_for_merge = key_for_merge.strip()
            movie_online['source'] = 'online'
            movie_online['original_online_key'] = movie_online.get('file_ort', key_for_merge)
            merged_movies[key_for_merge] = movie_online
        else:
            xbmc.log(
                f"[{common.addon_id} movies.py] Online-Film ohne gültigen Key übersprungen: {movie_online.get('name')}",
                xbmc.LOGWARNING
            )

    return list(merged_movies.values())


def play_random_movie():
    xbmc.log(f"[{common.addon_id} movies.py] play_random_movie: Funktion gestartet.", xbmc.LOGINFO)
    all_movies = _get_merged_movie_list()
    playable_movies = [movie for movie in all_movies if movie.get("file_ort") and isinstance(movie.get("file_ort"), str) and movie.get("file_ort").strip()]

    if not playable_movies:
        xbmcgui.Dialog().notification("Zufallsfilm", "Keine abspielbaren Filme gefunden.", common.addon.getAddonInfo('icon'))
        return

    chosen_movie = random.choice(playable_movies)
    movie_path_raw = (chosen_movie.get("file_ort") or "").strip()
    movie_title = chosen_movie.get("name", "Zufälliger Film").replace(" (lokal)", "").replace(" (local_only)", "").strip()

    if not movie_path_raw:
        xbmcgui.Dialog().notification("Zufallsfilm", "Keine gültige Film-URL gefunden.", common.addon.getAddonInfo('icon'))
        return

    status_identifier = chosen_movie.get("original_online_key", movie_path_raw)

    # Privacy: starte über plugin://play_movie statt direkter Stream-URL.
    # So landen keine sensitiven URL-Header im OpenFile-Log als erste Zeile.
    codec_hint = _resolve_movie_codec_hint(chosen_movie, movie_path_raw)

    plugin_play_url = common.build_url({
        'action': 'play_movie',
        'movie_key': movie_path_raw,
        'title': movie_title,
        'original_online_key': status_identifier,
        'video_codec': codec_hint,
    })

    # Für Random-Playback bereits hier ein vollständiges ListItem mitgeben,
    # damit Poster/Fanart/Plot auch im OSD sichtbar bleiben und nicht nur der Titel.
    random_play_li = xbmcgui.ListItem(label=movie_title, path=plugin_play_url, offscreen=True)
    random_play_li.setProperty("IsPlayable", "true")

    try:
        info_tag = random_play_li.getVideoInfoTag()
        info_tag.setMediaType('movie')
        info_tag.setTitle(movie_title or "Film")
        info_tag.setOriginalTitle(movie_title or "Film")

        if chosen_movie.get("overview"):
            info_tag.setPlot(str(chosen_movie.get("overview")))
        if chosen_movie.get("tagline"):
            info_tag.setTagLine(str(chosen_movie.get("tagline")))

        premiered_date_str = chosen_movie.get("release_date")
        release_year_val = 0
        if premiered_date_str and isinstance(premiered_date_str, str) and premiered_date_str.strip():
            info_tag.setPremiered(premiered_date_str)
            try:
                release_year_val = int(premiered_date_str.split("-")[0])
            except (ValueError, TypeError, IndexError):
                release_year_val = 0
        elif chosen_movie.get("release_year"):
            try:
                release_year_val = int(str(chosen_movie.get("release_year")))
                info_tag.setPremiered(f"{release_year_val}-01-01")
            except (ValueError, TypeError):
                release_year_val = 0
        if release_year_val > 0:
            info_tag.setYear(release_year_val)

        imdb_rating_str = chosen_movie.get("imdbRating")
        if imdb_rating_str is not None:
            try:
                info_tag.setRating(float(str(imdb_rating_str).replace(",", ".")))
            except (ValueError, TypeError):
                pass

        votes_int = _normalize_votes_value(chosen_movie.get("imdbVotes", None))
        _set_votes_on_info(info_tag, random_play_li, votes_int)

        runtime_val = chosen_movie.get("runtime")
        if runtime_val is not None:
            try:
                info_tag.setDuration(int(runtime_val) * 60)
            except (ValueError, TypeError):
                pass

        if chosen_movie.get("genre"):
            genres = [g.strip() for g in str(chosen_movie.get("genre")).split(',') if g.strip()]
            if genres:
                info_tag.setGenres(genres)

        studio_string = chosen_movie.get("studio")
        if studio_string and isinstance(studio_string, str):
            normalized_studio_string = re.sub(r'\s*[/;]\s*', ',', studio_string)
            cleaned_studios = [s.strip() for s in normalized_studio_string.split(',') if s.strip() and s.strip().lower() != 'about:blank' and 'unknown' not in s.strip().lower()]
            if cleaned_studios:
                try:
                    info_tag.setStudios(cleaned_studios)
                except Exception:
                    pass

        if chosen_movie.get("director"):
            directors = [d.strip() for d in str(chosen_movie.get("director")).split(',') if d.strip()]
            if directors:
                info_tag.setDirectors(directors)

        country_str = chosen_movie.get("country", chosen_movie.get("land", ""))
        if country_str:
            countries = [c.strip() for c in str(country_str).split(',') if c.strip()]
            if countries:
                info_tag.setCountries(countries)

        actor_names = chosen_movie.get("actors")
        if isinstance(actor_names, list) and actor_names:
            actor_info_list = []
            for i, actor_name in enumerate(actor_names):
                if isinstance(actor_name, str) and actor_name.strip():
                    try:
                        actor_info_list.append(xbmc.Actor(name=actor_name, role="", thumbnail="", order=i))
                    except Exception:
                        pass
            if actor_info_list:
                try:
                    info_tag.setCast(actor_info_list)
                except Exception:
                    pass

        if chosen_movie.get("added"):
            added_norm_m = str(chosen_movie.get("added", "")).strip().replace("T", " ").replace("Z", "").strip()
            if added_norm_m:
                info_tag.setDateAdded(added_norm_m)

        fsk_value = chosen_movie.get("fsk")
        if fsk_value is not None:
            fsk_str = str(fsk_value).strip()
            if fsk_str:
                try:
                    info_tag.setMpaa(f"FSK {int(fsk_str)}")
                except ValueError:
                    info_tag.setMpaa(fsk_str)

        trailer_url = str(chosen_movie.get("trailer", "")).strip()
        if trailer_url and trailer_url.startswith(('http', 'plugin://')):
            info_tag.setTrailer(trailer_url)

        unique_ids = {}
        imdb_id_val = chosen_movie.get("imdbId")
        tmdb_id_val = chosen_movie.get("tmdbId")
        if imdb_id_val:
            unique_ids['imdb'] = str(imdb_id_val)
        if tmdb_id_val:
            unique_ids['tmdb'] = str(tmdb_id_val)
        if unique_ids:
            info_tag.setUniqueIDs(unique_ids)
        if tmdb_id_val:
            random_play_li.setProperty("TMDbId", str(tmdb_id_val))
    except Exception as e:
        _log(f"play_random_movie: metadata prefill failed: {e}", xbmc.LOGDEBUG)

    try:
        def _c_fast_random(url):
            """Gibt die Remote-URL zurueck – Kodi Texture-Cache laedt das Bild."""
            if not url or not isinstance(url, str):
                return ""
            return url

        images_dict = chosen_movie.get("images", {})
        if not isinstance(images_dict, dict):
            images_dict = {}

        art = {
            "poster": _c_fast_random(chosen_movie.get("poster", "")),
            "fanart": _c_fast_random(chosen_movie.get("backcover", chosen_movie.get("fanart", ""))),
            "clearlogo": _c_fast_random(chosen_movie.get("logo", images_dict.get("logo", images_dict.get("clearlogo", "")))),
            "clearart": _c_fast_random(chosen_movie.get("clearart", images_dict.get("clearart", ""))),
            "discart": _c_fast_random(chosen_movie.get("disc", images_dict.get("discart", images_dict.get("disc", "")))),
            "banner": _c_fast_random(chosen_movie.get("banner", images_dict.get("banner", ""))),
            "thumb": _c_fast_random(chosen_movie.get("thumb", images_dict.get("thumb", chosen_movie.get("poster", "")))),
        }
        art = {k: v for k, v in art.items() if v and isinstance(v, str) and v.strip()}
        if not art.get("thumb") and art.get("poster"):
            art["thumb"] = art["poster"]
        if not art.get("icon"):
            art["icon"] = art.get("thumb", art.get("poster", ""))

        default_movie_icon_path = os.path.join(common.addon_path, "movies.png")
        if not art.get("icon"):
            art["icon"] = default_movie_icon_path if xbmcvfs.exists(default_movie_icon_path) else common.addon.getAddonInfo('icon')
        if not art.get("thumb"):
            art["thumb"] = art.get("icon")
        if not art.get("poster"):
            art["poster"] = art.get("icon")

        if art:
            random_play_li.setArt(art)
            if art.get("fanart"):
                random_play_li.setProperty("fanart_image", art["fanart"])
                random_play_li.setProperty("Fanart_Image", art["fanart"])
    except Exception as e:
        _log(f"play_random_movie: artwork prefill failed: {e}", xbmc.LOGDEBUG)

    xbmc.Player().play(item=plugin_play_url, listitem=random_play_li)


def check_movies_json(final_movie_list=None):
    _t_start = time.time()
    xbmc.log(f"[{common.addon_id} movies.py] DEBUG: check_movies_json (Modernisierte Metadaten V3.1 + sync-remote-refresh): Funktion gestartet.", xbmc.LOGINFO)

    addon_handle = _resolve_addon_handle()

    # Verbindungscheck: bei fehlender Server-/Internet-Verbindung kein
    # Cache-Listing zeigen (Abspielen würde eh fehlschlagen).
    if final_movie_list is None and not common.check_connectivity_or_notify():
        try:
            li = xbmcgui.ListItem(label="Keine Verbindung zum Server", offscreen=True)
            li.setProperty("IsPlayable", "false")
            icon = common.addon.getAddonInfo('icon') or ""
            if icon:
                li.setArt({'thumb': icon, 'icon': icon, 'poster': icon})
            xbmcplugin.addDirectoryItem(
                handle=common.addon_handle,
                url="",
                listitem=li,
                isFolder=False,
            )
        except Exception as e:
            xbmc.log(f"[{common.addon_id} movies.py] offline hint-item failed: {e}", xbmc.LOGDEBUG)
        _safe_end_of_directory(addon_handle, succeeded=True, cache_to_disc=False)
        return

    if final_movie_list is None:
        final_movie_list = _get_merged_movie_list()


    try:
        global _MOVIES_VIEW_STATE
        items = len(final_movie_list) if isinstance(final_movie_list, list) else 0
        _MOVIES_VIEW_STATE['items'] = items
        if _MOVIES_VIEW_STATE.get('remote_changed_scheduled'):
            xbmc.log(f"[{common.addon_id} movies.py] show_filme: remote_changed -> refresh scheduled", xbmc.LOGINFO)
        else:
            xbmc.log(f"[{common.addon_id} movies.py] show_filme: cache_used -> items={items}", xbmc.LOGINFO)
    except Exception as e:
        _log(f"show_filme: could not update/log _MOVIES_VIEW_STATE: {e}", xbmc.LOGDEBUG)
    # OPTIMIZED: Kopien bauen, um globale Strukturen nicht zu mutieren
    safe_list = []
    for m in final_movie_list:
        if isinstance(m, dict):
            mc = dict(m)
            if isinstance(mc.get("images"), dict):
                mc["images"] = dict(mc["images"])
            safe_list.append(mc)
        else:
            safe_list.append(m)
    final_movie_list = safe_list

    watched_status = common.load_watched_status_movies(WATCHED_STATUS_FILE_MOVIES)

    # Artwork wird via Kodis eigenem Texture-Cache verwaltet (Remote-URLs direkt).

    # Special Item: Random Movie
    if final_movie_list:
        random_movie_li = xbmcgui.ListItem(label="Zufälliger Film starten", offscreen=True)
        random_movie_li.setProperty("specialsort", "top")
        random_movie_info_tag = random_movie_li.getVideoInfoTag()
        random_movie_info_tag.setPlot("Startet die Wiedergabe eines zufälligen Films aus der aktuellen Liste.")
        random_movie_info_tag.setMediaType('video')

        # Icon/Thumb (Zufall-Icon) + Fanart (Addon-Fanart)
        random_icon_path = os.path.join(common.addon_path, "random.png")
        icon_final = random_icon_path if xbmcvfs.exists(random_icon_path) else common.addon.getAddonInfo('icon')

        addon_fanart = ""
        try:
            addon_fanart = common.addon.getAddonInfo('fanart') or ""
        except Exception:
            addon_fanart = ""

        # Falls fanart nicht in addon.xml gemappt ist, direkt im Addon-Ordner suchen
        if not addon_fanart or not xbmcvfs.exists(addon_fanart):
            cand = os.path.join(common.addon_path, "fanart.png")
            if xbmcvfs.exists(cand):
                addon_fanart = cand
            else:
                cand = os.path.join(common.addon_path, "fanart.jpg")
                if xbmcvfs.exists(cand):
                    addon_fanart = cand

        fanart_final = addon_fanart or icon_final

        art = {'icon': icon_final, 'thumb': icon_final, 'poster': icon_final, 'fanart': fanart_final}
        # ein paar Skins (z.B. MQ) greifen gern auf fanart1.. zu
        for i in range(1, 6):
            art[f'fanart{i}'] = fanart_final
        random_movie_li.setArt(art)

        # Extra: einige Skins erwarten zusätzlich Properties statt Art()
        try:
            random_movie_li.setProperty("fanart_image", fanart_final)
            random_movie_li.setProperty("Fanart_Image", fanart_final)
        except Exception:
            pass

        random_play_url = common.build_url({'action': 'play_random_movie'})
        xbmcplugin.addDirectoryItem(handle=addon_handle, url=random_play_url, listitem=random_movie_li, isFolder=False)
# -------------------------
    # Sorting logic
    # -------------------------
    # Hart verdrahtet: Filme immer nach "Hinzugefügt" sortieren, neueste zuerst.
    sort_method_param = "added"
    sort_source = "forced"
    try:
        def parse_date_robust_movie(m):
            d_str = str(m.get("added", "") or "").strip().replace("T", " ").replace("Z", "").strip()
            if not d_str:
                return datetime(1970, 1, 1)
            fmts = ["%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"]
            for f_format in fmts:
                try:
                    return datetime.strptime(d_str, f_format)
                except (ValueError, TypeError):
                    continue
            return datetime(1970, 1, 1)

        final_movie_list = sorted(final_movie_list, key=parse_date_robust_movie, reverse=True)
        xbmc.log(f"[{common.addon_id} movies.py] sort_method={sort_method_param} order=desc (source={sort_source})", xbmc.LOGINFO)
    except Exception as e_sort:
        xbmc.log(f"[{common.addon_id} movies.py] Fehler Datumssortierung ({sort_method_param}): {e_sort}. Sortiere alphabetisch.", xbmc.LOGERROR)
        final_movie_list = sorted(final_movie_list, key=lambda m: str(m.get("name", "")).lower())

    # Keine Filme -> Notification nur wenn nicht gefiltert
    if not final_movie_list and addon_handle != -1:
        is_filtered = False
        try:
            if len(sys.argv) > 2 and sys.argv[2].startswith('?'):
                plugin_params_filter_check = dict(urllib.parse.parse_qsl(sys.argv[2][1:]))
                if plugin_params_filter_check.get('filter_term') or plugin_params_filter_check.get('genre_filter'):
                    is_filtered = True
        except Exception:
            pass
        if not is_filtered:
            xbmcgui.Dialog().notification("Info", "Keine Filme gefunden.", common.addon.getAddonInfo('icon'))

    # -------------------------
    # Artwork: Remote-URLs direkt an Kodi uebergeben.
    # Kodi laedt die Bilder beim ersten Anzeigen herunter und cached sie
    # automatisch in seinem eigenen Texture-Cache (~/.kodi/userdata/Thumbnails/).
    # -------------------------
    def _c_fast(url):
        """Gibt die originale Remote-URL zurueck – immer.

        Kodi verwaltet seinen eigenen Textur-Cache anhand der URL als Schluessel.
        """
        if not url or not isinstance(url, str):
            return ""
        return url

    # -------------------------
    # Build items
    # -------------------------
    for movie in final_movie_list:
        if not isinstance(movie, dict):
            continue

        name = str(movie.get("name", "Unbekannt")).strip()
        file_ort = str(movie.get("file_ort", "")).strip()
        original_online_key = str(movie.get("original_online_key", file_ort))

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

        info_tag = li.getVideoInfoTag()
        info_tag.setMediaType('movie')
        info_tag.setTitle(name)

        cleaned_original_title = name.replace(" (lokal)", "").replace(" (local_only)", "").strip()
        info_tag.setOriginalTitle(cleaned_original_title)

        if movie.get("overview"):
            info_tag.setPlot(str(movie.get("overview")))
        if movie.get("tagline"):
            info_tag.setTagLine(str(movie.get("tagline")))

        release_year_val = 0
        premiered_date_str = movie.get("release_date")
        if premiered_date_str and isinstance(premiered_date_str, str) and premiered_date_str.strip():
            info_tag.setPremiered(premiered_date_str)
            try:
                release_year_val = int(premiered_date_str.split("-")[0])
            except (ValueError, TypeError, IndexError):
                pass
        elif movie.get("release_year"):
            try:
                release_year_val = int(str(movie.get("release_year")))
                info_tag.setPremiered(f"{release_year_val}-01-01")
            except (ValueError, TypeError):
                pass
        if release_year_val > 0:
            info_tag.setYear(release_year_val)

        imdb_rating_str = movie.get("imdbRating")
        if imdb_rating_str is not None:
            try:
                info_tag.setRating(float(str(imdb_rating_str).replace(",", ".")))
            except (ValueError, TypeError):
                pass

        votes_int = _normalize_votes_value(movie.get("imdbVotes", None))
        _set_votes_on_info(info_tag, li, votes_int)

        runtime_val = movie.get("runtime")
        if runtime_val is not None:
            try:
                info_tag.setDuration(int(runtime_val) * 60)
            except (ValueError, TypeError):
                pass

        if movie.get("genre"):
            genres = [g.strip() for g in str(movie.get("genre")).split(',') if g.strip()]
            if genres:
                info_tag.setGenres(genres)

        studio_string = movie.get("studio")
        if studio_string and isinstance(studio_string, str):
            normalized_studio_string = re.sub(r'\s*[/;]\s*', ',', studio_string)
            potential_studios = normalized_studio_string.split(',')
            cleaned_studios = []
            for s in potential_studios:
                s_stripped = s.strip()
                if s_stripped and s_stripped.lower() != "about:blank" and "unknown" not in s_stripped.lower():
                    cleaned_studios.append(s_stripped)
            if cleaned_studios:
                try:
                    info_tag.setStudios(cleaned_studios)
                except Exception as e_set_studios:
                    xbmc.log(f"[{common.addon_id} movies.py] Fehler bei info_tag.setStudios: {e_set_studios}. Studios: {cleaned_studios}", xbmc.LOGERROR)

        if movie.get("director"):
            directors = [d.strip() for d in str(movie.get("director")).split(',') if d.strip()]
            if directors:
                info_tag.setDirectors(directors)

        country_str = movie.get("country", movie.get("land", ""))
        if country_str:
            countries = [c.strip() for c in str(country_str).split(',') if c.strip()]
            if countries:
                info_tag.setCountries(countries)

        actor_names = movie.get("actors")
        if isinstance(actor_names, list) and actor_names:
            actor_info_list = []
            for i, actor_name in enumerate(actor_names):
                if isinstance(actor_name, str) and actor_name.strip():
                    try:
                        actor_obj = xbmc.Actor(name=actor_name, role="", thumbnail="", order=i)
                        actor_info_list.append(actor_obj)
                    except Exception as e_actor_create:
                        xbmc.log(f"[{common.addon_id} movies.py] Fehler beim Erstellen von xbmc.Actor für Schauspieler '{actor_name}': {e_actor_create}", xbmc.LOGWARNING)
            if actor_info_list:
                try:
                    info_tag.setCast(actor_info_list)
                except Exception as e_set_cast:
                    xbmc.log(f"[{common.addon_id} movies.py] Fehler bei info_tag.setCast (xbmc.Actor list): {e_set_cast}", xbmc.LOGERROR)

        if movie.get("added"):
            added_norm_m = str(movie.get("added", "")).strip().replace("T", " ").replace("Z", "").strip()
            info_tag.setDateAdded(added_norm_m)

        fsk_value = movie.get("fsk")
        if fsk_value is not None:
            fsk_str = str(fsk_value).strip()
            if fsk_str:
                try:
                    fsk_num = int(fsk_str)
                    info_tag.setMpaa(f"FSK {fsk_num}")
                except ValueError:
                    info_tag.setMpaa(fsk_str)

        wid = common.movies_watched_id(original_online_key)
        try:
            playcount = int((watched_status.get(wid) or {}).get('playcount', 0) or 0) if wid else 0
        except Exception:
            playcount = 0
        info_tag.setPlaycount(playcount)
        trailer_url = str(movie.get("trailer", "")).strip()
        if trailer_url and trailer_url.startswith(('http', 'plugin://')):
            info_tag.setTrailer(trailer_url)

        unique_ids = {}
        imdb_id_val = movie.get("imdbId")
        tmdb_id_val = movie.get("tmdbId")
        if imdb_id_val:
            unique_ids['imdb'] = str(imdb_id_val)
        if tmdb_id_val:
            unique_ids['tmdb'] = str(tmdb_id_val)
        if unique_ids:
            info_tag.setUniqueIDs(unique_ids)
        if tmdb_id_val:
            li.setProperty("TMDbId", str(tmdb_id_val))

        # --- Video Stream Info ---
        vs_codec_raw = movie.get("video_codec")
        vs_width_raw = movie.get("video_resolution_width")
        vs_height_raw = movie.get("video_resolution_height")
        vs_aspect_raw = movie.get("video_aspect_ratio")

        final_vs_codec = "h264"
        if isinstance(vs_codec_raw, str) and vs_codec_raw.strip():
            final_vs_codec = vs_codec_raw.strip()
        elif vs_codec_raw is not None:
            final_vs_codec = str(vs_codec_raw).strip()

        final_vs_width = 0
        if vs_width_raw is not None:
            try:
                final_vs_width = int(str(vs_width_raw).strip())
            except (ValueError, TypeError):
                xbmc.log(f"[{common.addon_id} movies.py] WARN: Konnte video_resolution_width '{vs_width_raw}' nicht zu int parsen.", xbmc.LOGWARNING)

        final_vs_height = 0
        if vs_height_raw is not None:
            try:
                final_vs_height = int(str(vs_height_raw).strip())
            except (ValueError, TypeError):
                xbmc.log(f"[{common.addon_id} movies.py] WARN: Konnte video_resolution_height '{vs_height_raw}' nicht zu int parsen.", xbmc.LOGWARNING)

        if final_vs_width > 0 and final_vs_height > 0:
            video_constructor_args = {'codec': final_vs_codec, 'width': final_vs_width, 'height': final_vs_height}

            if isinstance(vs_aspect_raw, str) and vs_aspect_raw.strip():
                aspect_val = 0.0
                try:
                    if ":" in vs_aspect_raw:
                        ar_parts = vs_aspect_raw.split(":")
                        if len(ar_parts) == 2:
                            num = float(ar_parts[0])
                            den = float(ar_parts[1])
                            if den != 0:
                                aspect_val = num / den
                    else:
                        aspect_val = float(vs_aspect_raw)
                    if aspect_val > 0:
                        video_constructor_args['aspect'] = aspect_val
                except (ValueError, TypeError):
                    xbmc.log(f"[{common.addon_id} movies.py] WARN: Konnte video_aspect_ratio '{vs_aspect_raw}' nicht zu float parsen.", xbmc.LOGWARNING)
            elif isinstance(vs_aspect_raw, (float, int)) and vs_aspect_raw > 0:
                video_constructor_args['aspect'] = float(vs_aspect_raw)

            try:
                vs_detail = xbmc.VideoStreamDetail(**video_constructor_args)
                info_tag.addVideoStream(vs_detail)
            except Exception as e_vs_call:
                xbmc.log(f"[{common.addon_id} movies.py] ERROR addVideoStream: {e_vs_call}. Data: {video_constructor_args}\n{traceback.format_exc()}", xbmc.LOGERROR)

        # --- Audio Stream Info ---
        as_codec_raw = movie.get("audio_codec")
        as_channels_raw = movie.get("audio_channels")
        as_lang_raw = movie.get("audio_language")

        final_as_codec = ""
        if isinstance(as_codec_raw, str) and as_codec_raw.strip():
            final_as_codec = as_codec_raw.strip()
        elif as_codec_raw is not None:
            final_as_codec = str(as_codec_raw).strip()

        final_as_channels = None
        if as_channels_raw is not None:
            try:
                ch = int(str(as_channels_raw).strip())
                if ch > 0:
                    final_as_channels = ch
            except (ValueError, TypeError):
                xbmc.log(f"[{common.addon_id} movies.py] WARN: Konnte audio_channels '{as_channels_raw}' nicht zu int parsen.", xbmc.LOGWARNING)

        audio_constructor_args = {}
        if final_as_codec:
            audio_constructor_args['codec'] = final_as_codec
        if final_as_channels is not None:
            audio_constructor_args['channels'] = final_as_channels
        if isinstance(as_lang_raw, str) and as_lang_raw.strip():
            audio_constructor_args['language'] = as_lang_raw.strip().lower()

        if audio_constructor_args:
            try:
                as_detail = xbmc.AudioStreamDetail(**audio_constructor_args)
                info_tag.addAudioStream(as_detail)
            except Exception as e_as_call:
                xbmc.log(f"[{common.addon_id} movies.py] ERROR addAudioStream: {e_as_call}. Data: {audio_constructor_args}\n{traceback.format_exc()}", xbmc.LOGERROR)

        # Media Flags als Properties
        source_type_prop = str(movie.get("source_type", "WEB")).upper()
        video_codec_label_prop = str(movie.get("video_codec_label", final_vs_codec)).upper()
        audio_codec_label_prop = str(movie.get("audio_codec_label", final_as_codec)).upper()
        resolution_label_prop = str(movie.get("resolution_label", "")).upper()
        hdr_format_prop = str(movie.get("hdr_format", "")).upper()

        li.setProperty("video_format", source_type_prop)
        li.setProperty("video_codec_label", video_codec_label_prop)
        li.setProperty("audio_codec_label", audio_codec_label_prop)
        if resolution_label_prop:
            li.setProperty("video_resolution_label", resolution_label_prop)
        if hdr_format_prop:
            li.setProperty("hdr_format_label", hdr_format_prop)

        # Artwork – bevorzugt lokaler Cache, Fallback auf Remote-URL
        art = {}
        art["poster"] = _c_fast(movie.get("poster", ""))
        art["fanart"] = _c_fast(movie.get("backcover", movie.get("fanart", "")))

        images_dict = movie.get("images", {})
        if not isinstance(images_dict, dict):
            images_dict = {}

        art["clearlogo"] = _c_fast(movie.get("logo", images_dict.get("logo", images_dict.get("clearlogo", ""))))
        art["clearart"] = _c_fast(movie.get("clearart", images_dict.get("clearart", "")))
        art["discart"] = _c_fast(movie.get("disc", images_dict.get("discart", images_dict.get("disc", ""))))
        art["banner"] = _c_fast(movie.get("banner", images_dict.get("banner", "")))
        art["thumb"] = _c_fast(movie.get("thumb", images_dict.get("thumb", art.get("poster", ""))))

        art = {k: v for k, v in art.items() if v and isinstance(v, str) and v.strip()}
        if not art.get("thumb") and art.get("poster"):
            art["thumb"] = art["poster"]
        if not art.get("icon"):
            art["icon"] = art.get("thumb", art.get("poster", ""))

        default_movie_icon_path = os.path.join(common.addon_path, "movies.png")
        if not art.get("icon"):
            art["icon"] = default_movie_icon_path if xbmcvfs.exists(default_movie_icon_path) else common.addon.getAddonInfo('icon')
        if not art.get("thumb"):
            art["thumb"] = art.get("icon")
        if not art.get("poster"):
            art["poster"] = art.get("icon")

        if art:
            li.setArt(art)

        # Context Menu
        context_items = []
        if trailer_url and trailer_url.startswith(('http', 'plugin://')):
            context_items.append(("Trailer abspielen", f'PlayMedia({trailer_url})'))

        if original_online_key:
            toggle_watched_url = common.build_url({'action': 'toggle_watched_status', 'movie_key': original_online_key})
            context_items.append(("Als gesehen/ungesehen markieren", f'RunPlugin({toggle_watched_url})'))

        if context_items:
            li.addContextMenuItems(context_items)

        if file_ort:
            codec_hint = _resolve_movie_codec_hint(movie, file_ort)
            play_url = common.build_url({
                'action': 'play_movie',
                'movie_key': file_ort,
                'title': cleaned_original_title,
                'original_online_key': original_online_key,
                'video_codec': codec_hint,
            })
            li.setProperty('IsPlayable', 'true')
            xbmcplugin.addDirectoryItem(handle=addon_handle, url=play_url, listitem=li, isFolder=False)
        else:
            xbmc.log(f"[{common.addon_id} movies.py] Kein gültiger 'file_ort' für '{name}'. Item wird nicht als abspielbar markiert.", xbmc.LOGWARNING)

    xbmcplugin.setContent(addon_handle, "movies")

    # Im Kodi-Menü sollen NUR diese zwei Sortierungen erscheinen:
    #  - Hinzugefügt am
    #  - Bewertung
    try:
        xbmcplugin.addSortMethod(addon_handle, xbmcplugin.SORT_METHOD_DATEADDED)
    except Exception as e:
        _log(f"addSortMethod(DATEADDED) nicht verfügbar: {e}", xbmc.LOGDEBUG)
    try:
        xbmcplugin.addSortMethod(addon_handle, xbmcplugin.SORT_METHOD_VIDEO_RATING)
    except Exception as e:
        _log(f"addSortMethod(VIDEO_RATING) nicht verfügbar: {e}", xbmc.LOGDEBUG)

    _safe_end_of_directory(addon_handle, succeeded=True, update_listing=False, cache_to_disc=False)
    _force_movies_container_dateadded_desc()

    # ✅ Timestamps und Datensignatur NACH dem Aufbau setzen.
    # Erst hier setzen, damit maybe_refresh_movies und background_refresh_movies_once
    # sehen, WANN das Listing fertig war und MIT WELCHEN DATEN es gebaut wurde.
    try:
        _win = xbmcgui.Window(10000)
        _win.setProperty(MOVIES_LISTING_TS_PROP, str(time.time()))
        # Signatur der angezeigten Daten speichern: BG-Refresh prüft dies, um
        # unnötige Container.Refresh-Aufrufe zu vermeiden, wenn das Listing
        # bereits die neuesten Daten enthält.
        _listing_data_sig = _compute_movies_signature(final_movie_list)
        _win.setProperty(MOVIES_LISTING_DATA_SIG_PROP, _listing_data_sig)
    except Exception as _e:
        _log(f"setProperty listing ts/sig failed: {_e}", xbmc.LOGDEBUG)

    xbmc.log(
        f"[{common.addon_id} movies.py] Kodi-native Sortmethoden registriert: DATEADDED, VIDEO_RATING. "
        f"Standard-Default aktiv erzwungen: DATEADDED descending (neueste zuerst). "
        f"Sichtbar im Kodi-Menü: Hinzugefügt am, Bewertung.",
        xbmc.LOGINFO
    )
    xbmc.log(f"[{common.addon_id} movies.py] Verzeichnis erfolgreich aufgebaut (Metadaten V3.1 + IMDbVotes).", xbmc.LOGINFO)
    xbmc.log(f"[{common.addon_id} movies.py] TIMING check_movies_json TOTAL: {time.time() - _t_start:.3f}s | movies={len(final_movie_list)}", xbmc.LOGINFO)




def play_movie(movie_key, title, original_online_key_param=None, codec_hint_param=None):
    addon_handle = _resolve_addon_handle()
    xbmc.log(
        f"[{common.addon_id} movies.py] play_movie: Aufgerufen. "
        f"Key='{_mask_log_value(movie_key)}', Title='{title}', "
        f"OriginalOnlineKey='{_mask_log_value(original_online_key_param)}', "
        f"CodecHint='{codec_hint_param}'",
        xbmc.LOGDEBUG
    )
    if not movie_key or not isinstance(movie_key, str) or not movie_key.strip():
        xbmc.log(f"[{common.addon_id} movies.py] play_movie: Ungültiger movie_key.", xbmc.LOGWARNING)
        xbmcgui.Dialog().notification("Fehler", "Keine gültige Filmdatei.", common.addon.getAddonInfo('icon'))
        if addon_handle != -1:
            xbmcplugin.setResolvedUrl(addon_handle, False, xbmcgui.ListItem(offscreen=True))
        return

    play_url = common.prepare_playback_path(movie_key, codec_hint=codec_hint_param)
    if not play_url:
        xbmc.log(
            f"[{common.addon_id} movies.py] play_movie: Wiedergabe abgebrochen "
            f"(blockiert/ungueltiger Playback-Pfad). Key='{_mask_log_value(movie_key)}', "
            f"CodecHint='{codec_hint_param or ''}'",
            xbmc.LOGINFO
        )
        try:
            if addon_handle != -1:
                xbmcplugin.setResolvedUrl(addon_handle, False, xbmcgui.ListItem(offscreen=True))
        except Exception:
            pass
        return

    play_item = xbmcgui.ListItem(path=play_url, offscreen=True)
    play_item.setProperty("IsPlayable", "true")
    info_tag = play_item.getVideoInfoTag()
    info_tag.setTitle(title if title else "Film")
    info_tag.setMediaType('movie')
    xbmcplugin.setResolvedUrl(handle=addon_handle, succeeded=True, listitem=play_item)
    xbmc.log(f"[{common.addon_id} movies.py] play_movie: setResolvedUrl aufgerufen.", xbmc.LOGINFO)

    status_identifier = original_online_key_param if original_online_key_param else movie_key
    if status_identifier:
        _start_daemon_task(
            monitor_playback_movie,
            status_identifier,
            task_name="MoviesPlaybackMonitor",
            use_executor=False,
        )


def play_trailer(trailer_url_param=None):
    addon_handle = _resolve_addon_handle()
    final_trailer_url = trailer_url_param
    if not final_trailer_url:
        try:
            if len(sys.argv) > 2 and sys.argv[2].startswith('?'):
                params_trailer = dict(urllib.parse.parse_qsl(sys.argv[2].lstrip('?')))
                final_trailer_url = params_trailer.get('trailer_url', params_trailer.get('trailer'))
        except Exception as e_parse_trailer:
            xbmc.log(f"[{common.addon_id} movies.py] play_trailer: Fehler beim Parsen der Plugin-Parameter: {e_parse_trailer}", xbmc.LOGWARNING)
            final_trailer_url = None

    if final_trailer_url and isinstance(final_trailer_url, str) and final_trailer_url.startswith(('http', 'plugin://')):
        play_item = xbmcgui.ListItem(path=final_trailer_url, offscreen=True)
        play_item.setProperty("IsPlayable", "true")
        info_tag = play_item.getVideoInfoTag()
        info_tag.setTitle("Trailer")
        info_tag.setMediaType('video')
        xbmcplugin.setResolvedUrl(handle=addon_handle, succeeded=True, listitem=play_item)
    else:
        xbmc.log(f"[{common.addon_id} movies.py] play_trailer: Keine gültige Trailer-URL gefunden: '{common.mask_url(final_trailer_url)}'", xbmc.LOGWARNING)
        xbmcgui.Dialog().notification("Fehler", "Kein Trailer gefunden.", common.addon.getAddonInfo('icon'))
        if addon_handle != -1:
            xbmcplugin.setResolvedUrl(handle=addon_handle, succeeded=False, listitem=xbmcgui.ListItem(offscreen=True))


def toggle_watched_status(movie_key_identifier_param=None):
    final_movie_key_identifier = movie_key_identifier_param
    if not final_movie_key_identifier:
        try:
            if len(sys.argv) > 2 and sys.argv[2].startswith('?'):
                params_toggle = dict(urllib.parse.parse_qsl(sys.argv[2].lstrip('?')))
                final_movie_key_identifier = params_toggle.get('movie_key')
        except Exception as e_parse_toggle:
            xbmc.log(f"[{common.addon_id} movies.py] toggle_watched_status: Fehler beim Parsen der Plugin-Parameter: {e_parse_toggle}", xbmc.LOGWARNING)
            final_movie_key_identifier = None

    if not final_movie_key_identifier:
        xbmc.log(f"[{common.addon_id} movies.py] toggle_watched_status: Kein Key zum Umschalten des Status bereitgestellt.", xbmc.LOGWARNING)
        xbmcgui.Dialog().notification("Fehler", "Kein Film-Identifikator für Statusumschaltung.", common.addon.getAddonInfo('icon'))
        return
    if not WATCHED_STATUS_FILE_MOVIES:
        xbmc.log(f"[{common.addon_id} movies.py] toggle_watched_status: Pfad zur Statusdatei nicht gesetzt.", xbmc.LOGERROR)
        xbmcgui.Dialog().notification("Fehler", "Statusdatei nicht konfiguriert.", common.addon.getAddonInfo('icon'))
        return

    try:
        with WATCHED_STATUS_LOCK:
            watched_status = common.load_watched_status_movies(WATCHED_STATUS_FILE_MOVIES)
            wid = common.movies_watched_id(final_movie_key_identifier)
            try:
                current_playcount = int((watched_status.get(wid) or {}).get('playcount', 0) or 0) if wid else 0
            except Exception:
                current_playcount = 0

            if current_playcount > 0:
                if wid in watched_status:
                    del watched_status[wid]
                action_msg = "Film als ungesehen markiert."
            else:
                if wid:
                    watched_status[wid] = {'playcount': 1}
                    action_msg = "Film als gesehen markiert."
                else:
                    action_msg = "Ungültiger Film-Identifier."

            ok = common.save_watched_status_movies(WATCHED_STATUS_FILE_MOVIES, watched_status) if wid else False
        if ok:
            xbmc.log(f"[{common.addon_id} movies.py] toggle_watched_status: {action_msg}", xbmc.LOGINFO)
            xbmcgui.Dialog().notification("Status", action_msg, common.addon.getAddonInfo('icon'), 2000, False)

            # statt direkt Container.Refresh:
            request_movies_container_refresh(reason="toggle_watched", delay_sec=0, min_interval_sec=1.5)

        else:
            xbmc.log(f"[{common.addon_id} movies.py] Fehler beim Speichern des Gesehen-Status nach Umschaltung.", xbmc.LOGERROR)
            xbmcgui.Dialog().notification("Fehler", "Status konnte nicht gespeichert werden.", common.addon.getAddonInfo('icon'))

    except Exception as e:
        xbmc.log(
            f"[{common.addon_id} movies.py] Fehler beim Umschalten des Gesehen-Status für "
            f"'{_mask_log_value(final_movie_key_identifier)}': {e}\n{traceback.format_exc()}",
            xbmc.LOGERROR
        )
        xbmcgui.Dialog().notification("Fehler", "Status konnte nicht umgeschaltet werden.", common.addon.getAddonInfo('icon'))


# --- Haupt-Router ---
# ---------------------------------------------------------------------------------------
# Legacy-Entry-Point: wird von RunPlugin/AlarmClock direkt aufgerufen (z.B. maybe_refresh_movies,
# init_bg_movies, play_movie). Neue Actions bitte in default.py DISPATCH registrieren.
# Dieser Block bleibt für Rückwärtskompatibilität bestehender AlarmClock-/RunPlugin-Calls.
# ---------------------------------------------------------------------------------------
if __name__ == '__main__':
    addon_handle = _resolve_addon_handle()
    params_main = {}
    if len(sys.argv) >= 2 and sys.argv[1] and isinstance(sys.argv[1], str):
        try:
            if len(sys.argv) >= 3 and sys.argv[2] and sys.argv[2].startswith('?'):
                params_main = dict(urllib.parse.parse_qsl(sys.argv[2][1:]))
        except Exception as e_parse_main_params:
            xbmc.log(
                f"[{common.addon_id} movies.py] Fehler beim Parsen der Hauptparameter: {e_parse_main_params}",
                xbmc.LOGERROR
            )

    action = params_main.get('action')
    movie_key_param = params_main.get('movie_key')
    title_param = params_main.get('title')
    data_param = params_main.get('data')
    trailer_url_param_main = params_main.get('trailer_url')
    original_online_key_main_param = params_main.get('original_online_key')
    video_codec_main_param = params_main.get('video_codec')

    xbmc.log(
        f"[{common.addon_id} movies.py] Aktion: '{action}', "
        f"MovieKey: '{_mask_log_value(movie_key_param)}', Title: '{title_param}', "
        f"OrigOnlineKey: '{_mask_log_value(original_online_key_main_param)}', "
        f"VideoCodec: '{video_codec_main_param}'",
        xbmc.LOGDEBUG
    )

    if action == 'play_movie':
        play_movie(
            movie_key_param,
            title_param,
            original_online_key_param=original_online_key_main_param,
            codec_hint_param=video_codec_main_param,
        )

    elif action == 'play_trailer':
        play_trailer(trailer_url_param=trailer_url_param_main)

    elif action == 'toggle_watched_status':
        toggle_watched_status(movie_key_identifier_param=movie_key_param)

    elif action == 'play_random_movie':
        play_random_movie()

    elif action == 'maybe_refresh_movies':
        # kommt von AlarmClock -> RunPlugin
        try:
            maybe_refresh_movies()
        except Exception as e:
            xbmc.log(f"[{common.addon_id} movies.py] maybe_refresh_movies failed: {e}", xbmc.LOGWARNING)
        _safe_end_of_directory(addon_handle, succeeded=True, cache_to_disc=False)

    elif action == 'init_bg_movies':
        try:
            background_refresh_movies_once(source="ui:init_bg_movies")
        except Exception as e:
            xbmc.log(
                f"[{common.addon_id} movies.py] init_bg_movies: Fehler beim einmaligen BG-Refresh: {e}",
                xbmc.LOGWARNING
            )
        _safe_end_of_directory(addon_handle, succeeded=True, cache_to_disc=False)

    else:
        try:
            final_movie_list = _get_merged_movie_list()
            check_movies_json(final_movie_list=final_movie_list)
        except Exception as e:
            xbmc.log(f"[{common.addon_id} movies.py] FATAL listing error: {e}\n{traceback.format_exc()}", xbmc.LOGERROR)
            xbmcgui.Dialog().notification("Fehler", "Filmliste konnte nicht geladen werden (siehe Log).", common.addon.getAddonInfo('icon'))
            _safe_end_of_directory(addon_handle, succeeded=False, cache_to_disc=False)