# coding: utf-8

import sys
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 None

import os
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

import common  # <-- NEU: dein externes common.py

WATCHED_STATUS_LOCK = threading.Lock()


# --- 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

# --- In-Memory Cache (Artwork) ---
_movie_image_path_cache = {}  # url -> cached_path oder None
_MOVIE_IMG_CACHE_LOCK = threading.Lock()
_MOVIE_IMG_CACHE_MAX = 5000  # simples Limit gegen unendliches Wachsen


# Eigene Exception für Download-Abbruch
class DownloadCancelledError(Exception):
    pass



def _fs_local(path):
    try:
        return xbmcvfs.translatePath(path) if path else path
    except Exception:
        return path


def _fs_exists(path):
    """xbmcvfs.exists + os.path.exists fallback (wichtig wegen vfs_exists=False)."""
    if not path:
        return False
    try:
        if xbmcvfs.exists(path):
            return True
    except Exception:
        pass
    try:
        return os.path.exists(_fs_local(path))
    except Exception:
        return False


def _fs_mkdirs(path):
    if not path:
        return False
    try:
        if _fs_exists(path):
            return True
        if xbmcvfs.mkdirs(path):
            return True
    except Exception:
        pass
    try:
        os.makedirs(_fs_local(path), exist_ok=True)
        return True
    except Exception:
        return False


def _fs_delete(path):
    if not path:
        return False
    ok = False
    try:
        if xbmcvfs.delete(path):
            return True
    except Exception:
        pass
    try:
        lp = _fs_local(path)
        if os.path.isfile(lp):
            os.remove(lp)
            ok = True
    except Exception:
        ok = False
    return ok


def _fs_rmdir(path):
    if not path:
        return False
    try:
        if xbmcvfs.rmdir(path):
            return True
    except Exception:
        pass
    try:
        os.rmdir(_fs_local(path))
        return True
    except Exception:
        return False


def _fs_listdir(path):
    """Gibt (dirs, files) zurück wie xbmcvfs.listdir."""
    try:
        return xbmcvfs.listdir(path)
    except Exception:
        pass
    try:
        lp = _fs_local(path)
        entries = os.listdir(lp)
        dirs = [e for e in entries if os.path.isdir(os.path.join(lp, e))]
        files = [e for e in entries if os.path.isfile(os.path.join(lp, e))]
        return dirs, files
    except Exception:
        return [], []


def _fs_rename(src, dst):
    """Versucht xbmcvfs.rename, fallback os.replace auf translatePath."""
    if not src or not dst:
        return False
    try:
        if xbmcvfs.rename(src, dst):
            return True
    except Exception:
        pass
    try:
        os.replace(_fs_local(src), _fs_local(dst))
        return True
    except Exception:
        return False



# -------------------------
# Atomic File Write Helpers
# -------------------------
def _atomic_write_bytes(dest_path, data_bytes, backup=False, durable=False):
    """
    Atomisch schreiben:
      - tmp schreiben
      - optional: fsync(tmp) + dir fsync (durable=True)
      - optional: Backup-Rotation (.bak -> .bak.prev, dest -> .bak)
      - tmp -> dest (os.replace)
    Fallback: wenn kein lokaler OS-Pfad möglich ist, nutzt es die alte VFS-Variante (ohne fsync).
    """
    if not dest_path or data_bytes is None:
        return False

    # Kodi/VFS -> echter Pfad fürs OS (wichtig für fsync/os.replace)
    try:
        real_dest = xbmcvfs.translatePath(dest_path)
    except Exception:
        real_dest = dest_path

    # Wenn es immer noch nach VFS aussieht, können wir nicht sauber fsyncen.
    # Dann nutzen wir best-effort die alte VFS-Atomic-Logik (ohne durability).
    if not isinstance(real_dest, str) or "://" in real_dest:
        try:
            tmp_path = dest_path + ".tmp"
            bak_path = dest_path + ".bak"
            bak_prev_path = dest_path + ".bak.prev"

            # Zielordner sicherstellen
            try:
                dest_dir = os.path.dirname(dest_path)
                if dest_dir:
                    _fs_mkdirs(dest_dir)
            except Exception:
                pass

            # tmp löschen
            try:
                if _fs_exists(tmp_path):
                    _fs_delete(tmp_path)
            except Exception:
                pass

            # tmp schreiben (VFS)
            with xbmcvfs.File(tmp_path, "wb") as f:
                f.write(data_bytes)

            # Backup
            if backup:
                try:
                    if _fs_exists(bak_path):
                        try:
                            if _fs_exists(bak_prev_path):
                                _fs_delete(bak_prev_path)
                        except Exception:
                            pass
                        _fs_rename(bak_path, bak_prev_path)

                    if _fs_exists(dest_path):
                        _fs_rename(dest_path, bak_path)
                except Exception:
                    raise

            # tmp -> dest
            if _fs_rename(tmp_path, dest_path):
                try:
                    if _fs_exists(bak_prev_path):
                        _fs_delete(bak_prev_path)
                except Exception:
                    pass
                return True

            # overwrite-fallback
            try:
                if _fs_exists(dest_path):
                    _fs_delete(dest_path)
            except Exception:
                pass

            if _fs_rename(tmp_path, dest_path):
                try:
                    if _fs_exists(bak_prev_path):
                        _fs_delete(bak_prev_path)
                except Exception:
                    pass
                return True

            # copy-fallback
            try:
                if xbmcvfs.copy(tmp_path, dest_path):
                    try:
                        _fs_delete(tmp_path)
                    except Exception:
                        pass
                    try:
                        if _fs_exists(bak_prev_path):
                            _fs_delete(bak_prev_path)
                    except Exception:
                        pass
                    return True
            except Exception:
                pass

            return False

        except Exception as e:
            xbmc.log(f"[{common.addon_id} movies.py] Atomic VFS write failed ({dest_path}): {e}", xbmc.LOGWARNING)
            return False

    # ----- OS-durable branch -----
    tmp = real_dest + ".tmp"
    bak = real_dest + ".bak"
    bak_prev = real_dest + ".bak.prev"

    try:
        # Zielordner sicherstellen
        try:
            d = os.path.dirname(real_dest)
            if d and not os.path.exists(d):
                os.makedirs(d, exist_ok=True)
        except Exception:
            pass

        # tmp aufräumen
        try:
            if os.path.exists(tmp):
                os.remove(tmp)
        except Exception:
            pass

        # 1) TMP schreiben (+ optional fsync)
        with open(tmp, "wb") as fh:
            fh.write(data_bytes)
            fh.flush()
            if durable:
                os.fsync(fh.fileno())

        # 2) Backup Rotation
        if backup:
            # .bak -> .bak.prev
            try:
                if os.path.exists(bak):
                    try:
                        if os.path.exists(bak_prev):
                            os.remove(bak_prev)
                    except Exception:
                        pass
                    os.replace(bak, bak_prev)
            except Exception:
                pass

            # dest -> .bak
            if os.path.exists(real_dest):
                os.replace(real_dest, bak)

        # 3) TMP -> dest (atomic replace)
        os.replace(tmp, real_dest)

        # 4) dir fsync (macht rename/replace “durable” unter Linux)
        if durable:
            _fsync_dir_for(real_dest)

        # 5) bak_prev weg (bak bleibt als Recovery)
        try:
            if os.path.exists(bak_prev):
                os.remove(bak_prev)
        except Exception:
            pass

        return True

    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] Atomic write bytes fehlgeschlagen ({dest_path}): {e}", xbmc.LOGWARNING)
        xbmc.log(traceback.format_exc(), xbmc.LOGDEBUG)

        # tmp NICHT aggressiv löschen (Recovery via _safe_load_json_file möglich)
        # rollback best-effort: wenn backup=True und dest fehlt, versuche .bak zurück
        if backup:
            try:
                if (not os.path.exists(real_dest)) and os.path.exists(bak):
                    os.replace(bak, real_dest)
            except Exception:
                pass
            try:
                if os.path.exists(bak_prev) and not os.path.exists(bak):
                    os.replace(bak_prev, bak)
            except Exception:
                pass

        return False

def _fsync_dir_for(path_):
    """Best-effort fsync auf das Verzeichnis (Linux rename-durability)."""
    try:
        d = os.path.dirname(path_) or "."
        if hasattr(os, "O_DIRECTORY"):
            fd = os.open(d, os.O_RDONLY | os.O_DIRECTORY)
        else:
            fd = os.open(d, os.O_RDONLY)
        try:
            os.fsync(fd)
        finally:
            os.close(fd)
    except Exception:
        pass


def _atomic_write_json(dest_path, obj, indent=None, encoding="utf-8", durable=True):
    """
    JSON atomisch + durability default ON:
      - schreibt bytes über _atomic_write_bytes(..., backup=True, durable=True)
    """
    try:
        s = json.dumps(obj, ensure_ascii=False, indent=indent)
        data = s.encode(encoding, errors="replace")
        return _atomic_write_bytes(dest_path, data, backup=True, durable=durable)
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] Atomic write json fehlgeschlagen ({dest_path}): {e}", xbmc.LOGWARNING)
        xbmc.log(traceback.format_exc(), xbmc.LOGDEBUG)
        return False









def _safe_load_json_file(path, default):
    """
    Robust JSON Load:
      1) path
      2) path.bak
      3) path.tmp

    Wenn .bak/.tmp gültig sind:
      - .bak -> final wird per COPY wiederhergestellt (bak bleibt!)
      - .tmp -> final wird per RENAME/COPY wiederhergestellt
    """
    if not path:
        return default

    def _read_json(p):
        try:
            if not p or not _fs_exists(p):
                return None
            with xbmcvfs.File(p, "r") as f:
                raw = f.read() or ""
            # BOM-safe + trim
            if isinstance(raw, str):
                raw = raw.lstrip("\ufeff").strip()
            else:
                # sehr selten: bytes
                raw = str(raw).lstrip("\ufeff").strip()

            if not raw:
                return None
            return json.loads(raw)
        except Exception:
            return None

    final_p = path
    bak_p = path + ".bak"
    tmp_p = path + ".tmp"

    # 1) final
    data = _read_json(final_p)
    if data is not None:
        return data

    # 2) bak
    data_bak = _read_json(bak_p)
    if data_bak is not None:
        xbmc.log(f"[{common.addon_id} movies.py] _safe_load_json_file: RECOVER from .bak -> {final_p}", xbmc.LOGWARNING)
        try:
            # restore final (bak bleibt bestehen)
            if not _fs_exists(final_p):
                # copy bevorzugt, damit .bak bleibt
                xbmcvfs.copy(bak_p, final_p)
        except Exception:
            # not fatal
            pass
        return data_bak

    # 3) tmp
    data_tmp = _read_json(tmp_p)
    if data_tmp is not None:
        xbmc.log(f"[{common.addon_id} movies.py] _safe_load_json_file: RECOVER from .tmp -> {final_p}", xbmc.LOGWARNING)
        try:
            # rename bevorzugt
            if _fs_rename(tmp_p, final_p):
                return data_tmp
            # fallback copy (tmp darf bleiben, stört nicht)
            try:
                xbmcvfs.copy(tmp_p, final_p)
            except Exception:
                pass
        except Exception:
            pass
        return data_tmp

    return default


# -------------------------
# Download Wrapper
# -------------------------

def _download_with_cancel(url, dest_path, title="Download", is_asset=False):
    """
    Wrapper um common.download_file().
    common.download_file wirft normale Exception bei Cancel -> hier in DownloadCancelledError umwandeln.
    """
    try:
        return common.download_file(url, dest_path, title=title, is_asset=is_asset)
    except Exception as e:
        if "abgebrochen" in str(e).lower():
            raise DownloadCancelledError()
        raise


# --- 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'
LOCAL_JSON_FILENAME_MOVIES = "downloaded_movies.json"
LOCAL_IMAGE_CACHE_SUBDIR_MOVIES = "cache_movies_images"
MOVIES_CACHE_FILENAME = "movies.cache.json"  # vom Service geschriebener Online-Cache

WATCHED_STATUS_FILE_MOVIES = ""
LOCAL_JSON_MOVIES_PATH = ""
LOCAL_IMAGE_CACHE_DIR_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)
    LOCAL_JSON_MOVIES_PATH = os.path.join(common.addon_data_dir, LOCAL_JSON_FILENAME_MOVIES)
    LOCAL_IMAGE_CACHE_DIR_MOVIES = os.path.join(common.addon_data_dir, LOCAL_IMAGE_CACHE_SUBDIR_MOVIES)
    MOVIES_CACHE_PATH = os.path.join(common.addon_data_dir, MOVIES_CACHE_FILENAME)

    if LOCAL_IMAGE_CACHE_DIR_MOVIES:
        if not xbmcvfs.exists(LOCAL_IMAGE_CACHE_DIR_MOVIES):
            xbmcvfs.mkdirs(LOCAL_IMAGE_CACHE_DIR_MOVIES)
            xbmc.log(f"[{common.addon_id} movies.py] Film-Cache-Verzeichnis erstellt: {LOCAL_IMAGE_CACHE_DIR_MOVIES}", xbmc.LOGINFO)
    else:
        xbmc.log(f"[{common.addon_id} movies.py] WARNUNG: Film-Cache-Verzeichnis-Pfad konnte nicht erstellt werden.", xbmc.LOGWARNING)

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


# --- Background Refresh (Movies) ---
BG_REFRESH_INTERVAL_HOURS_MOVIES = 1

# 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"

# --- 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 _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 + 60)

    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.
        t = threading.Thread(
            target=background_refresh_movies_once,
            kwargs={"source": f"ui:{reason}"},
            daemon=True,
        )
        t.start()
        _log(f"show_filme: {reason} -> refresh scheduled")
        return True
    except Exception as e:
        _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 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 f"plugin://{common.addon_id}" not in folder_path:
            xbmc.log(f"[{common.addon_id} movies.py] maybe_refresh_movies: SKIP (not our container): {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

        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 f"plugin://{common.addon_id}" not in folder_path:
            xbmc.log(f"[{common.addon_id} movies.py] Refresh skip (not our container): {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():
    """
    Lädt die vom Service oder vom Plugin geschriebene movies.cache.json robust.
    Erwartet eine Liste von Movies, gibt bei Problemen [] zurück.
    """
    if not MOVIES_CACHE_PATH:
        return []

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

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

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




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
    try:
        _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)



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):
    """
    Lädt fehlende Artworks in den lokalen Cache – ohne Progress-Dialoge.
    UI bleibt unberührt.
    """
    if not LOCAL_IMAGE_CACHE_DIR_MOVIES:
        return
    for m in movie_list:
        if not isinstance(m, dict):
            continue
        for _art_key, url, _in_images_dict in _gather_artwork_sources_from_movie(m):
            if not get_cached_movie_image(url):
                download_and_cache_movie_image(url)


def background_refresh_movies_once(source=None, **kwargs):
    """
    Einmaliger BG-Run (vom Service als Hook aufgerufen):
    - Liste (Online + lokal) mergen
    - Artworks still cachen
    - Signatur aktualisieren (nur für Logging)
    Hinweis: Das eigentliche Container.Refresh übernimmt der Service.
    """
    xbmc.log(f"[{common.addon_id} movies.py] BG: Movies-Refresh einmalig gestartet (Service-Hook).", xbmc.LOGINFO)

    movie_list = _get_merged_movie_list(force=True)

    try:
        _prefetch_movie_artwork_silently(movie_list)
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] BG: Artwork-Prefetch Fehler: {e}", xbmc.LOGWARNING)

    new_sig = _compute_movies_signature(movie_list)
    old_sig = _load_movies_signature()
    if new_sig and new_sig != old_sig:
        _save_movies_signature(new_sig)
        xbmc.log(
            f"[{common.addon_id} movies.py] BG: neue Movies-Signatur gespeichert (Änderung erkannt – Refresh wird über den Service ausgelöst).",
            xbmc.LOGINFO
        )
    else:
        xbmc.log(f"[{common.addon_id} movies.py] BG: Keine Movie-Änderung anhand Signatur erkannt.", xbmc.LOGDEBUG)


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()
        while not monitor.waitForAbort(BG_REFRESH_INTERVAL_HOURS_MOVIES * 3600):
            background_refresh_movies_once()
    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")
        t = threading.Thread(target=_movies_bg_loop, name="MoviesBGWorker", daemon=True)
        t.start()
        xbmc.log(f"[{common.addon_id} movies.py] BG: Movies-Worker gestartet.", xbmc.LOGINFO)
    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):
    try:
        if not isinstance(url, str) or not url:
            return "invalid_url_placeholder.jpg"
        hash_object = hashlib.md5(url.encode('utf-8'))
        try:
            path = urllib.parse.urlparse(url).path
            ext = os.path.splitext(path)[1] if path else ""
        except Exception:
            ext = ""
        return hash_object.hexdigest() + (ext if ext else ".jpg")
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] Fehler in get_movie_cache_filename für URL '{url}': {e}", xbmc.LOGERROR)
        return "error_filename_placeholder.jpg"


def _img_cache_set(url, value):
    try:
        with _MOVIE_IMG_CACHE_LOCK:
            _movie_image_path_cache[url] = value
            if len(_movie_image_path_cache) > _MOVIE_IMG_CACHE_MAX:
                _movie_image_path_cache.clear()
    except Exception:
        pass



def get_cached_movie_image(url):
    """Return local cached image path for a movie artwork URL (or None).

    Uses the shared cache logic in common.py:
      - Thread-safe RAM cache
      - Miss-TTL (kein permanentes 'None' Caching)
      - Robust fs_exists
    """
    if not LOCAL_IMAGE_CACHE_DIR_MOVIES or not url or not isinstance(url, str):
        return None
    try:
        return common.get_cached_image(url, cache_dir=LOCAL_IMAGE_CACHE_DIR_MOVIES)
    except Exception:
        # Fallback: simple exists() check (best-effort)
        try:
            filename = get_movie_cache_filename(url)
            file_path = os.path.join(LOCAL_IMAGE_CACHE_DIR_MOVIES, filename)
            return file_path if _fs_exists(file_path) else None
        except Exception:
            return None



def download_and_cache_movie_image(url):
    """Download movie artwork into the shared cache (or return existing cached path).

    Uses common.download_and_cache_image() to ensure atomic writes + miss TTL.
    """
    if not LOCAL_IMAGE_CACHE_DIR_MOVIES or not url:
        try:
            xbmc.log(f"[{common.addon_id} movies.py] Cache-Verzeichnis oder URL fehlt für download_and_cache_movie_image.", xbmc.LOGWARNING)
        except Exception:
            pass
        return None

    try:
        return common.download_and_cache_image(url, cache_dir=LOCAL_IMAGE_CACHE_DIR_MOVIES, timeout_s=15)
    except Exception as e:
        # Fallback: keep old behaviour best-effort
        try:
            xbmc.log(f"[{common.addon_id} movies.py] WARN: common.download_and_cache_image failed: {e}", xbmc.LOGDEBUG)
        except Exception:
            pass
        try:
            filename = get_movie_cache_filename(url)
            file_path = os.path.join(LOCAL_IMAGE_CACHE_DIR_MOVIES, filename)
            req = common.get_authenticated_request(url)
            if not req:
                return None
            _fs_mkdirs(LOCAL_IMAGE_CACHE_DIR_MOVIES)
            context = ssl.create_default_context()
            with urllib.request.urlopen(req, context=context, timeout=15) as response:
                if response.getcode() != 200:
                    return None
                data = response.read()
                if not data:
                    return None
            if _atomic_write_bytes(file_path, data, backup=False):
                return file_path
        except Exception:
            pass
        return None



def _async_cache_missing_movie_images(missing_urls):
    """
    OPTIMIZED: Lädt fehlende Bilder im Hintergrund und refresht nur wenn nötig.
    Refresh jetzt debounced + delayed, damit Kodi nicht doppelt listet.
    """
    if not missing_urls:
        return
    changed = False
    for url in missing_urls:
        try:
            if url and isinstance(url, str) and url.startswith("http"):
                if not get_cached_movie_image(url):
                    if download_and_cache_movie_image(url):
                        changed = True
        except Exception:
            pass

    if changed:
        try:
            request_movies_container_refresh(reason="artwork_cached", delay_sec=2, min_interval_sec=3)
        except Exception:
            pass



# -------------------------
# 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 monitor.abortRequested():
                    return
                if not xbmc.getCondVisibility("Player.HasMedia"):
                    break
                xbmc.sleep(250)

            xbmc.sleep(300)

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

            if f"plugin://{common.addon_id}" not in 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: {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
                    xbmc.sleep(500)

            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
                xbmc.sleep(500)

            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
            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
                xbmc.sleep(1000)

            # 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: {self.movie_key_identifier} zu {percentage_watched:.2f}% "
                f"(Played: {played_time:.2f}s / Total: {total_time:.2f}s).",
                xbmc.LOGDEBUG
            )

            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 {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: '{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,
        "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
    return out


def _save_movies_remote_meta(meta_path, content_length=None, etag=None, last_modified=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 "",
        "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: {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")

    current_cl = None
    current_etag = None
    current_lm = None

    # 2) HEAD holen (nur HTTP/HTTPS)
    if scheme_to_use in ("http", "https"):
        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 = ssl.create_default_context()
            with urllib.request.urlopen(req_head, context=context, timeout=10) 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:
            xbmc.log(
                f"[{common.addon_id} movies.py] HEAD-Request fehlgeschlagen ({url}): {e_head}",
                xbmc.LOGDEBUG
            )

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

    # 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"):
            # Stabil: ETag > Last-Modified > Content-Length
            if current_etag and last_etag and current_etag == last_etag and movies_list_online:
                need_refresh_from_remote = False
            elif current_lm and last_lm and current_lm == last_lm and movies_list_online:
                need_refresh_from_remote = False
            elif current_cl is not None and last_cl is not None and current_cl == last_cl and movies_list_online:
                need_refresh_from_remote = False
            else:
                # wenn wir keinerlei HEAD-Infos haben, aber Cache existiert: lieber Cache behalten
                if (current_etag is None and current_lm is None and current_cl is None) and movies_list_online:
                    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 movies_list_online:
                need_refresh_from_remote = True

    # Fast-mode: wenn Cache vorhanden -> direkt anzeigen und Refresh im Hintergrund

    global _MOVIES_VIEW_STATE

    _MOVIES_VIEW_STATE['cache_used'] = bool(movies_list_online)

    _MOVIES_VIEW_STATE['remote_changed_scheduled'] = False

    if need_refresh_from_remote and fast and movies_list_online:
        # Show the cached list immediately and refresh in the background.
        req_ts = time.time()
        reason = "remote_changed"
        _schedule_movies_refresh(req_ts=req_ts, reason=reason)
        _MOVIES_VIEW_STATE['remote_changed_scheduled'] = True
        need_refresh_from_remote = False
        merged = movies_list_online


    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 wenn nicht force)
            if (scheme_to_use in ("http", "https")) and not force:
                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 = ssl.create_default_context()

            try:
                with urllib.request.urlopen(req, context=context, timeout=30) as response:
                    code = response.getcode()
                    if code == 200:
                        json_data = response.read().decode('utf-8')
                        new_list = json.loads(json_data)
                        if isinstance(new_list, list):
                            movies_list_online = new_list
                            _save_online_movies_to_cache(movies_list_online)

                            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)
                        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 {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)
                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 {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)

    # 5) Lokale DB laden
    local_movies_db = {}
    if LOCAL_JSON_MOVIES_PATH and xbmcvfs.exists(LOCAL_JSON_MOVIES_PATH):
        try:
            with xbmcvfs.File(LOCAL_JSON_MOVIES_PATH, 'r') as f:
                content = f.read()
                if content:
                    local_movies_db = json.loads(content)
                    if not isinstance(local_movies_db, dict):
                        xbmc.log(
                            f"[{common.addon_id} movies.py] Lokale Film-DB ist kein Dictionary. Initialisiere neu.",
                            xbmc.LOGWARNING
                        )
                        local_movies_db = {}
        except Exception as e_local_json:
            xbmc.log(
                f"[{common.addon_id} movies.py] Fehler beim Laden der lokalen Film-DB ({LOCAL_JSON_MOVIES_PATH}): {e_local_json}",
                xbmc.LOGWARNING
            )
            local_movies_db = {}

    # 6) Merge Online + Lokal
    merged_movies = {}

    # Online 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
            )

    # Map für O(n)-Lookup lokal->online
    online_key_to_merged_key = {
        v.get('original_online_key'): k
        for k, v in merged_movies.items()
        if isinstance(v, dict) and v.get('original_online_key')
    }

    # Lokale Movies dazumergen
    for online_key_from_local_db, local_info in local_movies_db.items():
        if not isinstance(local_info, dict):
            continue

        target_key_merged = online_key_to_merged_key.get(online_key_from_local_db)
        local_path = local_info.get("local_path")
        if not local_path or not isinstance(local_path, str):
            continue

        if target_key_merged and target_key_merged in merged_movies:
            entry = merged_movies[target_key_merged]
            entry["file_ort"] = local_path
            if "(lokal)" not in entry.get("name", ""):
                entry["name"] = f'{entry.get("name", local_info.get("title", "Unbekannt"))} (lokal)'
            entry["source"] = "local"

            # Assets: JSON-Keys konsistent ("poster", "backcover", "logo", "disc", "thumb", "banner")
            if isinstance(local_info.get("assets"), dict):
                if local_info["assets"].get("poster"):
                    entry["poster"] = local_info["assets"]["poster"]
                if local_info["assets"].get("backcover"):
                    entry["backcover"] = local_info["assets"]["backcover"]
                if local_info["assets"].get("logo"):
                    entry["logo"] = local_info["assets"]["logo"]
                if local_info["assets"].get("disc"):
                    entry["disc"] = local_info["assets"]["disc"]
                if local_info["assets"].get("thumb"):
                    entry["thumb"] = local_info["assets"]["thumb"]
                if local_info["assets"].get("banner"):
                    entry["banner"] = local_info["assets"]["banner"]

            if local_info.get("download_date"):
                entry["added"] = local_info.get("download_date")
            if entry.get("imdbVotes") in (None, "", 0) and local_info.get("imdbVotes") not in (None, ""):
                entry["imdbVotes"] = local_info.get("imdbVotes")
        else:
            new_entry_key = online_key_from_local_db
            new_entry = {
                "name": local_info.get("title", "Unbekannt") + " (lokal)",
                "file_ort": local_path,
                "original_online_key": online_key_from_local_db,
                "source": "local_only",
                "release_date": local_info.get("release_date", ""),
                "overview": local_info.get("overview", ""),
                "added": local_info.get("download_date", ""),
                "poster": local_info.get("assets", {}).get("poster", ""),
                "backcover": local_info.get("assets", {}).get("backcover", ""),
                "imdbRating": local_info.get("imdbRating", ""),
                "imdbVotes": local_info.get("imdbVotes", None),
                "tmdbId": local_info.get("tmdbId"),
                "genre": local_info.get("genre", ""),
                "studio": local_info.get("studio", ""),
                "country": local_info.get("country", local_info.get("land", "")),
                "tagline": local_info.get("tagline", ""),
                "runtime": local_info.get("runtime", 0),
                "director": local_info.get("director", ""),
                "images": local_info.get("assets", {}),
                "fsk": local_info.get("fsk"),
                "video_codec": local_info.get('video_codec'),
                "video_codec_label": local_info.get('video_codec_label'),
                "video_resolution_width": local_info.get('video_resolution_width'),
                "video_resolution_height": local_info.get('video_resolution_height'),
                "resolution_label": local_info.get('resolution_label'),
                "audio_codec": local_info.get('audio_codec'),
                "audio_codec_label": local_info.get('audio_codec_label'),
                "audio_channels": local_info.get('audio_channels'),
                "source_type": local_info.get('source_type'),
                "hdr_format": local_info.get('hdr_format'),
                "actors": local_info.get('actors')
            }
            constructed_local_key = f"movie_{new_entry['tmdbId']}" if new_entry.get('tmdbId') else common.sanitize_filename(new_entry['name'])
            if constructed_local_key not in ("movie_", "invalid_slug", "") and constructed_local_key not in merged_movies:
                merged_movies[constructed_local_key] = new_entry
            elif new_entry_key not in merged_movies:
                merged_movies[new_entry_key] = new_entry
            elif constructed_local_key in merged_movies:
                xbmc.log(
                    f"[{common.addon_id} movies.py] Schlüssel '{constructed_local_key}' für lokalen Film '{new_entry['name']}' existiert bereits oder ist ungültig. Überspringe Duplikat.",
                    xbmc.LOGWARNING
                )

    return list(merged_movies.values())


def play_random_movie():
    addon_handle = _resolve_addon_handle()

    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 = chosen_movie.get("file_ort")
    movie_title = chosen_movie.get("name", "Zufälliger Film").replace(" (lokal)", "").replace(" (local_only)", "").strip()

    li = xbmcgui.ListItem(label=movie_title, path=movie_path)
    li.setProperty("IsPlayable", "true")

    info_tag = li.getVideoInfoTag()
    info_tag.setTitle(movie_title)
    info_tag.setMediaType('movie')
    if chosen_movie.get("overview"):
        info_tag.setPlot(chosen_movie.get("overview"))

    if chosen_movie.get("release_date"):
        try:
            year_val = int(str(chosen_movie.get("release_date")).split("-")[0])
            info_tag.setYear(year_val)
            info_tag.setPremiered(str(chosen_movie.get("release_date")))
        except (ValueError, TypeError, IndexError):
            pass

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

    votes_norm = _normalize_votes_value(chosen_movie.get("imdbVotes", None))
    _set_votes_on_info(info_tag, li, votes_norm)

    if chosen_movie.get("runtime"):
        try:
            info_tag.setDuration(int(chosen_movie.get("runtime", 0)) * 60)
        except (ValueError, TypeError):
            pass

    actor_names_random = chosen_movie.get("actors")
    if isinstance(actor_names_random, list) and actor_names_random:
        actor_info_list_random = []
        for i, actor_name_random in enumerate(actor_names_random):
            if isinstance(actor_name_random, str) and actor_name_random.strip():
                try:
                    actor_obj = xbmc.Actor(name=actor_name_random, role="", thumbnail="", order=i)
                    actor_info_list_random.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 Zufallsfilm-Schauspieler '{actor_name_random}': {e_actor_create}", xbmc.LOGWARNING)
        if actor_info_list_random:
            try:
                info_tag.setCast(actor_info_list_random)
            except Exception as e_set_cast_random:
                xbmc.log(f"[{common.addon_id} movies.py] Fehler bei info_tag.setCast (xbmc.Actor list) für Zufallsfilm: {e_set_cast_random}", xbmc.LOGERROR)

    art_random = {"poster": chosen_movie.get("poster", ""), "fanart": chosen_movie.get("backcover", "")}
    art_random = {k: v for k, v in art_random.items() if v and isinstance(v, str) and v.strip()}
    if art_random:
        li.setArt(art_random)

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

    status_identifier = chosen_movie.get("original_online_key", movie_path)
    if status_identifier and 'monitor_playback_movie' in globals() and callable(globals()['monitor_playback_movie']):
        threading.Thread(target=monitor_playback_movie, args=(status_identifier,), daemon=True).start()


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

    addon_handle = _resolve_addon_handle()


    # ✅ Listing-Timestamp setzen (damit maybe_refresh_movies Doppel-Refresh erkennt)
    try:
        xbmcgui.Window(10000).setProperty(MOVIES_LISTING_TS_PROP, str(time.time()))
    except Exception:
        pass

    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:
        pass
    # 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)

    # OPTIMIZED: Cache ersetzen + Missing sammeln, Downloads async
    missing_urls = []
    if LOCAL_IMAGE_CACHE_DIR_MOVIES:
        for movie_item_idx, movie_item_val in enumerate(final_movie_list):
            if not isinstance(movie_item_val, dict):
                continue

            for art_key, art_url_original, is_images_dict_flag in _gather_artwork_sources_from_movie(movie_item_val):
                if art_url_original and isinstance(art_url_original, str) and art_url_original.startswith("http"):
                    cached_path = get_cached_movie_image(art_url_original)
                    if cached_path:
                        target_dict_for_update = final_movie_list[movie_item_idx]
                        if is_images_dict_flag:
                            if "images" not in target_dict_for_update or not isinstance(target_dict_for_update.get("images"), dict):
                                target_dict_for_update["images"] = {}
                            target_dict_for_update["images"][art_key] = cached_path
                        else:
                            target_dict_for_update[art_key] = cached_path
                    else:
                        missing_urls.append(art_url_original)

    if missing_urls and LOCAL_IMAGE_CACHE_DIR_MOVIES:
        unique_missing = list(set(missing_urls))
        threading.Thread(
            target=_async_cache_missing_movie_images,
            args=(unique_missing,),
            daemon=True
        ).start()

    # Special Item: Random Movie
    if final_movie_list:
        random_movie_li = xbmcgui.ListItem(label="Zufälliger Film starten")
        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')

        random_icon_path = os.path.join(common.addon_path, "random.png")
        if xbmcvfs.exists(random_icon_path):
            random_movie_li.setArt({'icon': random_icon_path, 'thumb': random_icon_path})
        else:
            random_movie_li.setArt({'icon': common.addon.getAddonInfo('icon')})

        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
    # -------------------------
    sort_method_param = "alpha"
    try:
        default_sort_setting = common.addon.getSetting("default_movie_sort")
        if default_sort_setting is not None and isinstance(default_sort_setting, str) and default_sort_setting.strip():
            default_sort = default_sort_setting.strip().lower()
        else:
            default_sort = "alpha"
    except Exception:
        default_sort = "alpha"

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

    if not sort_method_param:
        sort_method_param = "alpha"

    if sort_method_param in ["date", "added"]:
        try:
            def parse_date_robust_movie(m):
                d_str = m.get("added", "")
                if not isinstance(d_str, 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)
        except Exception as e_sort:
            xbmc.log(f"[{common.addon_id} movies.py] Fehler Datumssortierung: {e_sort}. Sortiere alphabetisch.", xbmc.LOGERROR)
            final_movie_list = sorted(final_movie_list, key=lambda m: str(m.get("name", "")).lower())

    elif sort_method_param == "year":
        try:
            def get_year_movie(m):
                year_str = str(m.get("release_date", "0")).split("-")[0]
                return int(year_str) if year_str.isdigit() else 0

            final_movie_list = sorted(final_movie_list, key=get_year_movie, reverse=True)
        except Exception as e_sort:
            xbmc.log(f"[{common.addon_id} movies.py] Fehler Jahressortierung: {e_sort}. Sortiere alphabetisch.", xbmc.LOGERROR)
            final_movie_list = sorted(final_movie_list, key=lambda m: str(m.get("name", "")).lower())

    else:
        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'))

    # -------------------------
    # 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)
        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"):
            info_tag.setDateAdded(str(movie.get("added")))

        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
        art = {}
        art["poster"] = movie.get("poster", "")
        art["fanart"] = movie.get("backcover", movie.get("fanart", ""))

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

        art["clearlogo"] = movie.get("logo", images_dict.get("logo", images_dict.get("clearlogo", "")))
        art["clearart"] = movie.get("clearart", images_dict.get("clearart", ""))
        art["discart"] = movie.get("disc", images_dict.get("discart", images_dict.get("disc", "")))
        art["banner"] = movie.get("banner", images_dict.get("banner", ""))
        art["thumb"] = 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})'))

        current_source = movie.get("source", "")
        if current_source in ['local', 'local_only'] and file_ort:
            delete_url = common.build_url({"action": "delete_local_movie", "movie_key": file_ort, "title": cleaned_original_title})
            context_items.append(("Lokal geladenen Film löschen", f'RunPlugin({delete_url})'))
        elif current_source == 'online' and original_online_key:
            download_url = common.build_url({"action": "download_movie", "movie_key": original_online_key, "title": cleaned_original_title, "data": json.dumps(movie)})
            context_items.append(("Film herunterladen", f'RunPlugin({download_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:
            play_url = common.build_url({'action': 'play_movie', 'movie_key': file_ort, 'title': cleaned_original_title, 'original_online_key': original_online_key})
            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")
    xbmcplugin.addSortMethod(addon_handle, xbmcplugin.SORT_METHOD_LABEL)
    xbmcplugin.addSortMethod(addon_handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR)
    xbmcplugin.addSortMethod(addon_handle, xbmcplugin.SORT_METHOD_DATEADDED)
    xbmcplugin.addSortMethod(addon_handle, xbmcplugin.SORT_METHOD_VIDEO_RATING)
    try:
        xbmcplugin.addSortMethod(addon_handle, xbmcplugin.SORT_METHOD_VOTES)
    except Exception:
        pass

    xbmcplugin.endOfDirectory(addon_handle, succeeded=True, updateListing=False, cacheToDisc=False)
    xbmc.log(f"[{common.addon_id} movies.py] Verzeichnis erfolgreich aufgebaut (Metadaten V3.1 + IMDbVotes).", xbmc.LOGINFO)




def play_movie(movie_key, title, original_online_key_param=None):
    addon_handle = _resolve_addon_handle()
    xbmc.log(f"[{common.addon_id} movies.py] play_movie: Aufgerufen. Key='{movie_key}', Title='{title}', OriginalOnlineKey='{original_online_key_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())
        return

    play_item = xbmcgui.ListItem(path=movie_key)
    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:
        threading.Thread(target=monitor_playback_movie, args=(status_identifier,), daemon=True).start()


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)
        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: '{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())


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 '{final_movie_key_identifier}': {e}\n{traceback.format_exc()}", xbmc.LOGERROR)
        xbmcgui.Dialog().notification("Fehler", "Status konnte nicht umgeschaltet werden.", common.addon.getAddonInfo('icon'))




def update_local_movies_json(online_movie_key, title, local_movie_path, local_assets, movie_data_for_json=None):
    if not LOCAL_JSON_MOVIES_PATH:
        xbmc.log(f"[{common.addon_id} movies.py] LOCAL_JSON_MOVIES_PATH nicht gesetzt. Kann lokale DB nicht aktualisieren.", xbmc.LOGERROR)
        return False

    if not online_movie_key or not isinstance(online_movie_key, str) or not online_movie_key.strip():
        xbmc.log(f"[{common.addon_id} movies.py] update_local_movies_json: online_movie_key ungültig.", xbmc.LOGERROR)
        return False

    safe_title = (title or "").strip() or "Unbenannt"
    safe_local_path = (local_movie_path or "").strip()
    if not safe_local_path:
        xbmc.log(f"[{common.addon_id} movies.py] update_local_movies_json: local_movie_path leer.", xbmc.LOGERROR)
        return False

    # Lokale DB robust laden (+ .bak)
    local_movies = _safe_load_json_file(LOCAL_JSON_MOVIES_PATH, {})
    if not isinstance(local_movies, dict):
        local_movies = {}

    # movie_data_for_json absichern + verhindern, dass es "assets" überschreibt
    safe_movie_data = dict(movie_data_for_json) if isinstance(movie_data_for_json, dict) else {}
    safe_movie_data.pop("assets", None)
    safe_movie_data.pop("local_path", None)
    safe_movie_data.pop("download_date", None)
    safe_movie_data.pop("title", None)

    entry_data = {
        **safe_movie_data,
        "local_path": safe_local_path,
        "title": safe_title,
        "download_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "assets": local_assets if isinstance(local_assets, dict) else {},
    }

    # unnötige Keys entfernen
    entry_data.pop('file_ort', None)
    entry_data.pop('name', None)
    entry_data.pop('images', None)

    local_movies[online_movie_key] = entry_data

    try:
        data_dir = os.path.dirname(LOCAL_JSON_MOVIES_PATH)
        if data_dir:
            _fs_mkdirs(data_dir)

        ok = _atomic_write_json(LOCAL_JSON_MOVIES_PATH, local_movies, indent=4)
        if ok:
            xbmc.log(f"[{common.addon_id} movies.py] Lokale Film-DB erfolgreich aktualisiert für Key: {online_movie_key}.", xbmc.LOGINFO)
            return True

        xbmc.log(f"[{common.addon_id} movies.py] FEHLER: Atomic write lokale Film-DB fehlgeschlagen.", xbmc.LOGERROR)
        return False

    except Exception as e_save:
        xbmc.log(f"[{common.addon_id} movies.py] FEHLER beim Speichern der lokalen Film-DB: {e_save}\n{traceback.format_exc()}", xbmc.LOGERROR)
        return False





def download_movie(online_movie_key, title, data_str):
    addon_id_log = common.addon_id

    # title kann None sein -> safe machen
    clean_title_for_folder_and_file = (title or "").replace(" (lokal)", "").replace(" (online)", "").strip()
    if not clean_title_for_folder_and_file:
        clean_title_for_folder_and_file = "Unbenannt"

    try:
        download_path_setting = common.addon.getSetting("download_path")
        default_value = "Drück mich"
        current_path_stripped = download_path_setting.strip() if isinstance(download_path_setting, str) else ""
        if not current_path_stripped or current_path_stripped == default_value:
            xbmcgui.Dialog().notification(
                "Download nicht möglich",
                "Downloadpfad zuerst in den Addon-Einstellungen festlegen!",
                common.addon.getAddonInfo('icon'),
                5000
            )
            return
    except Exception as e_setting:
        xbmc.log(f"[{addon_id_log} movies.py] Fehler beim Lesen der Download-Pfad Einstellung: {e_setting}", xbmc.LOGERROR)
        xbmcgui.Dialog().notification("Fehler", "Downloadpfad konnte nicht gelesen werden.", common.addon.getAddonInfo('icon'))
        return

    cancelled_by_user = False
    main_file_download_success = False
    film_folder_path = None
    download_process_successful = False
    downloaded_local_movie_path = None

    # Assets werden jetzt unter JSON-Keys gespeichert (fix für backcover/fanart mismatch)
    local_assets_paths = {}  # z.B. {"poster": "...", "backcover": "...", "logo": "...", ...}

    try:
        movie_data_from_param = json.loads(data_str) if data_str and isinstance(data_str, str) else {}
        if not isinstance(movie_data_from_param, dict):
            movie_data_from_param = {}

        if not online_movie_key:
            xbmcgui.Dialog().notification("Fehler", "Keine Film-URL zum Download angegeben.", common.addon.getAddonInfo('icon'))
            return

        download_base_path = current_path_stripped
        if not xbmcvfs.exists(download_base_path):
            xbmcvfs.mkdirs(download_base_path)

        safe_title_for_folder = common.sanitize_filename(clean_title_for_folder_and_file)
        film_folder_path = os.path.join(download_base_path, safe_title_for_folder)
        if not xbmcvfs.exists(film_folder_path):
            xbmcvfs.mkdirs(film_folder_path)

        main_filename_base = common.get_filename_from_url(online_movie_key, fallback_name=safe_title_for_folder)
        main_file_destination_path = os.path.join(film_folder_path, main_filename_base)

        xbmc.log(f"[{addon_id_log}] Starte Download Hauptfilm: {online_movie_key} -> {main_file_destination_path}", xbmc.LOGINFO)
        downloaded_local_movie_path = _download_with_cancel(
            online_movie_key,
            main_file_destination_path,
            title=f"Lade Film: {clean_title_for_folder_and_file}"
        )

        if downloaded_local_movie_path and xbmcvfs.exists(downloaded_local_movie_path):
            main_file_download_success = True
            xbmc.log(f"[{addon_id_log}] Hauptfilm erfolgreich heruntergeladen: {downloaded_local_movie_path}", xbmc.LOGINFO)
        else:
            xbmc.log(f"[{addon_id_log}] Download des Hauptfilms fehlgeschlagen (nicht durch Benutzerabbruch).", xbmc.LOGERROR)
            main_file_download_success = False

        if main_file_download_success:
            # Mapping: Kodi-Art -> JSON-Key
            asset_key_mapping = {
                "poster": "poster",
                "fanart": "backcover",   # IMPORTANT: fanart -> backcover (JSON)
                "banner": "banner",
                "clearlogo": "logo",
                "discart": "disc",
                "thumb": "thumb",
            }

            images_sub_dict_from_data = movie_data_from_param.get("images", {})
            if not isinstance(images_sub_dict_from_data, dict):
                images_sub_dict_from_data = {}

            for _kodi_art_type, json_key in asset_key_mapping.items():
                if cancelled_by_user:
                    break

                url_asset_to_download = images_sub_dict_from_data.get(
                    json_key,
                    movie_data_from_param.get(json_key, "")
                )
                url_asset_to_download = url_asset_to_download.strip() if isinstance(url_asset_to_download, str) else ""

                if url_asset_to_download and url_asset_to_download.startswith('http'):
                    try:
                        # Kollisionsfrei: f"{json_key}{ext}"
                        try:
                            p = urllib.parse.urlparse(url_asset_to_download).path
                            ext = os.path.splitext(p)[1] if p else ""
                        except Exception:
                            ext = ""
                        if not ext or len(ext) > 8:
                            ext = ".jpg"

                        asset_filename_base = f"{json_key}{ext}"
                        asset_destination_path = os.path.join(film_folder_path, asset_filename_base)

                        xbmc.log(f"[{addon_id_log}] Starte Download Asset '{json_key}': {url_asset_to_download} -> {asset_destination_path}", xbmc.LOGDEBUG)
                        downloaded_asset_path = _download_with_cancel(
                            url_asset_to_download,
                            asset_destination_path,
                            title=f"Lade {json_key} für {clean_title_for_folder_and_file}",
                            is_asset=True
                        )

                        if downloaded_asset_path and xbmcvfs.exists(downloaded_asset_path):
                            local_assets_paths[json_key] = downloaded_asset_path
                            xbmc.log(f"[{addon_id_log}] Asset '{json_key}' erfolgreich heruntergeladen: {downloaded_asset_path}", xbmc.LOGDEBUG)

                    except DownloadCancelledError:
                        cancelled_by_user = True
                        break
                    except Exception as e_asset_dl:
                        xbmc.log(f"[{addon_id_log}] Fehler beim Download von Asset '{json_key}' ({url_asset_to_download}): {e_asset_dl}", xbmc.LOGWARNING)

        if not cancelled_by_user and main_file_download_success and downloaded_local_movie_path:
            if update_local_movies_json(
                online_movie_key,
                clean_title_for_folder_and_file,
                downloaded_local_movie_path,
                local_assets_paths,
                movie_data_for_json=movie_data_from_param
            ):
                download_process_successful = True
            else:
                xbmc.log(f"[{addon_id_log}] Fehler beim Aktualisieren der lokalen JSON-DB für '{clean_title_for_folder_and_file}'.", xbmc.LOGERROR)
                download_process_successful = False

    except DownloadCancelledError:
        cancelled_by_user = True
        xbmc.log(f"[{addon_id_log}] Download-Prozess für '{clean_title_for_folder_and_file}' wurde vom Benutzer abgebrochen.", xbmc.LOGINFO)
    except Exception as e_download_main_logic:
        download_process_successful = False
        xbmc.log(f"[{addon_id_log}] Schwerwiegender Fehler im Download-Prozess für '{clean_title_for_folder_and_file}': {e_download_main_logic}\n{traceback.format_exc()}", xbmc.LOGERROR)
        xbmcgui.Dialog().notification("Download Fehler", f"Unerwarteter Fehler: {e_download_main_logic}", common.addon.getAddonInfo('icon'))
    finally:
        if cancelled_by_user or not download_process_successful:
            xbmc.log(
                f"[{addon_id_log}] Aufräumen nach fehlgeschlagenem/abgebrochenem Download für '{clean_title_for_folder_and_file}'. "
                f"Cancelled: {cancelled_by_user}, Successful: {download_process_successful}",
                xbmc.LOGINFO
            )
            if film_folder_path and xbmcvfs.exists(film_folder_path):
                try:
                    if downloaded_local_movie_path and xbmcvfs.exists(downloaded_local_movie_path) and not main_file_download_success:
                        xbmcvfs.delete(downloaded_local_movie_path)
                        xbmc.log(f"[{addon_id_log}] Unvollständige Hauptdatei gelöscht: {downloaded_local_movie_path}", xbmc.LOGDEBUG)

                    for asset_p_cleanup in local_assets_paths.values():
                        if asset_p_cleanup and xbmcvfs.exists(asset_p_cleanup):
                            xbmcvfs.delete(asset_p_cleanup)
                            xbmc.log(f"[{addon_id_log}] Asset-Datei gelöscht: {asset_p_cleanup}", xbmc.LOGDEBUG)

                    dirs, files = xbmcvfs.listdir(film_folder_path)
                    if not dirs and not files:
                        xbmcvfs.rmdir(film_folder_path)
                        xbmc.log(f"[{addon_id_log}] Leerer Filmordner gelöscht: {film_folder_path}", xbmc.LOGDEBUG)
                except Exception as e_cleanup:
                    xbmc.log(f"[{addon_id_log}] Fehler beim Aufräumen des Filmordners '{film_folder_path}': {e_cleanup}", xbmc.LOGWARNING)

        if cancelled_by_user:
            xbmcgui.Dialog().notification(
                "Download Abgebrochen",
                f"Download für '{clean_title_for_folder_and_file}' wurde abgebrochen.",
                common.addon.getAddonInfo('icon')
            )
        elif download_process_successful:
            xbmcgui.Dialog().notification(
                "Download Abgeschlossen",
                f"'{clean_title_for_folder_and_file}' erfolgreich heruntergeladen.",
                common.addon.getAddonInfo('icon')
            )

            # ✅ FIX: statt direkt Container.Refresh -> debounced/delayed
            request_movies_container_refresh(reason="download_done", delay_sec=1, min_interval_sec=2)

        else:
            xbmcgui.Dialog().notification(
                "Download Fehlgeschlagen",
                f"Download für '{clean_title_for_folder_and_file}' konnte nicht abgeschlossen werden.",
                common.addon.getAddonInfo('icon')
            )




def delete_local_movie(local_movie_path_key, title_param):
    addon_id_log = common.addon_id

    if not local_movie_path_key or not isinstance(local_movie_path_key, str):
        xbmcgui.Dialog().notification("Fehler", "Kein gültiger Filmpfad zum Löschen angegeben.", common.addon.getAddonInfo('icon'))
        return

    confirm_title = (title_param or "").replace(" (lokal)", "").replace(" (local_only)", "").strip() or "Unbenannt"

    # Lokale DB robust laden
    local_movies_db = _safe_load_json_file(LOCAL_JSON_MOVIES_PATH, {})
    if not isinstance(local_movies_db, dict):
        xbmc.log(f"[{common.addon_id} movies.py] Lokale Film-DB ist kein Dictionary. Initialisiere neu.", xbmc.LOGWARNING)
        local_movies_db = {}

    
    online_key_of_deleted_movie = None
    entry_data_of_deleted_movie = None

    try:
        for online_key, data_val_del in local_movies_db.items():
            if isinstance(data_val_del, dict) and (data_val_del.get("local_path", "") or "").strip() == local_movie_path_key.strip():
                online_key_of_deleted_movie = online_key
                entry_data_of_deleted_movie = data_val_del
                break
    except Exception:
        pass

    if not xbmcgui.Dialog().yesno(
        "Lokalen Film löschen",
        f"Soll '{confirm_title}' wirklich lokal gelöscht werden?\nPfad: {local_movie_path_key}",
        yeslabel="Ja, Löschen",
        nolabel="Abbrechen"
    ):
        return

    files_and_assets_deleted_successfully = True
    movie_folder_path = None

    try:
        if _fs_exists(local_movie_path_key):
            movie_folder_path = os.path.dirname(local_movie_path_key)
    except Exception as e_getpath:
        xbmc.log(f"[{addon_id_log} movies.py] Fehler beim Ermitteln des Ordnerpfads für '{local_movie_path_key}': {e_getpath}", xbmc.LOGWARNING)

    # Hauptdatei löschen
    if _fs_exists(local_movie_path_key):
        try:
            if not _fs_delete(local_movie_path_key) and _fs_exists(local_movie_path_key):
                files_and_assets_deleted_successfully = False
                xbmc.log(f"[{addon_id_log} movies.py] FEHLER beim Löschen der Hauptdatei: {local_movie_path_key}", xbmc.LOGERROR)
        except Exception as e_del_main:
            files_and_assets_deleted_successfully = False
            xbmc.log(f"[{addon_id_log} movies.py] EXCEPTION beim Löschen der Hauptdatei '{local_movie_path_key}': {e_del_main}", xbmc.LOGERROR)

    # Assets löschen
    if entry_data_of_deleted_movie and isinstance(entry_data_of_deleted_movie.get("assets"), dict):
        for asset_type, asset_path_val in entry_data_of_deleted_movie["assets"].items():
            if asset_path_val and isinstance(asset_path_val, str) and _fs_exists(asset_path_val):
                try:
                    if not _fs_delete(asset_path_val) and _fs_exists(asset_path_val):
                        files_and_assets_deleted_successfully = False
                        xbmc.log(f"[{addon_id_log} movies.py] FEHLER beim Löschen der Asset-Datei ({asset_type}): {asset_path_val}", xbmc.LOGERROR)
                except Exception as e_del_asset:
                    files_and_assets_deleted_successfully = False
                    xbmc.log(f"[{addon_id_log} movies.py] EXCEPTION beim Löschen der Asset-Datei ({asset_type}) '{asset_path_val}': {e_del_asset}", xbmc.LOGERROR)

    # Ordner löschen falls leer
    folder_successfully_deleted = False
    if files_and_assets_deleted_successfully and movie_folder_path and _fs_exists(movie_folder_path):
        try:
            dirs, files = _fs_listdir(movie_folder_path)
            if not dirs and not files:
                if _fs_rmdir(movie_folder_path) or not _fs_exists(movie_folder_path):
                    folder_successfully_deleted = True
                    xbmc.log(f"[{addon_id_log} movies.py] Filmordner erfolgreich gelöscht: {movie_folder_path}", xbmc.LOGINFO)
        except Exception as e_rmdir:
            xbmc.log(f"[{addon_id_log} movies.py] EXCEPTION beim Versuch, Filmordner zu löschen '{movie_folder_path}': {e_rmdir}", xbmc.LOGWARNING)
    else:
        folder_successfully_deleted = True

    # JSON updaten
    json_updated_successfully = True
    if online_key_of_deleted_movie and online_key_of_deleted_movie in local_movies_db:
        try:
            del local_movies_db[online_key_of_deleted_movie]
            json_updated_successfully = _atomic_write_json(LOCAL_JSON_MOVIES_PATH, local_movies_db, indent=4)
            if json_updated_successfully:
                xbmc.log(f"[{addon_id_log} movies.py] Eintrag für '{online_key_of_deleted_movie}' aus lokaler DB entfernt.", xbmc.LOGINFO)
        except Exception as e_save_db:
            json_updated_successfully = False
            xbmc.log(f"[{addon_id_log} movies.py] FEHLER beim Speichern der lokalen DB nach Löschen: {e_save_db}", xbmc.LOGERROR)

    if files_and_assets_deleted_successfully and json_updated_successfully:
        msg = f"'{confirm_title}' lokal entfernt."
        if not folder_successfully_deleted and movie_folder_path and _fs_exists(movie_folder_path):
            msg += "\nDer Ordner konnte nicht (oder nicht vollständig) entfernt werden."

        xbmcgui.Dialog().notification("Löschen erfolgreich", msg, common.addon.getAddonInfo('icon'))
        request_movies_container_refresh(reason="delete_local", delay_sec=1, min_interval_sec=2)
    else:
        xbmcgui.Dialog().notification("Fehler beim Löschen", f"'{confirm_title}' konnte nicht vollständig entfernt werden. Überprüfe das Log.", common.addon.getAddonInfo('icon'))




def play_downloaded_movie():  # Veraltet
    addon_handle = _resolve_addon_handle()
    xbmc.log(f"[{common.addon_id} movies.py] play_downloaded_movie aufgerufen (veraltet).", xbmc.LOGWARNING)
    xbmcgui.Dialog().notification("Info", "Diese Funktion ist veraltet.", common.addon.getAddonInfo('icon'))
    if addon_handle != -1:
        xbmcplugin.setResolvedUrl(addon_handle, False, xbmcgui.ListItem())



# --- Haupt-Router ---
if __name__ == '__main__':
    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')

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

    if action == 'play_movie':
        play_movie(movie_key_param, title_param, original_online_key_param=original_online_key_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 == 'download_movie':
        download_movie(movie_key_param, title_param, data_param)

    elif action == 'delete_local_movie':
        delete_local_movie(movie_key_param, title_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)
        if addon_handle != -1:
            xbmcplugin.endOfDirectory(addon_handle, succeeded=True, cacheToDisc=False)

    elif action == 'play_downloaded_movie':  # Veraltet
        play_downloaded_movie()

    elif action == 'init_bg_movies':
        try:
            background_refresh_movies_once()
        except Exception as e:
            xbmc.log(
                f"[{common.addon_id} movies.py] init_bg_movies: Fehler beim einmaligen BG-Refresh: {e}",
                xbmc.LOGWARNING
            )
        if addon_handle != -1:
            xbmcplugin.endOfDirectory(addon_handle, succeeded=True, cacheToDisc=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'))
            try:
                if addon_handle != -1:
                    xbmcplugin.endOfDirectory(addon_handle, succeeded=False, cacheToDisc=False)
            except Exception:
                pass