# coding: utf-8

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


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

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

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

import gb_common as common  # zentrales Addon-Shared-Modul

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


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

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


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


def _force_movies_container_dateadded_desc():
    """Delegiert an gemeinsamen Helper in gb_common."""
    common.force_container_sort_dateadded_desc("show_filme")

def _prepare_playback_path(path):
    """Prepare HTTP(S) playback URLs to avoid user:pass in Kodi core logs."""
    try:
        p = (path or "").strip()
    except Exception:
        p = ""
    if not p:
        return ""
    if p.startswith(("http://", "https://")):
        try:
            return common.kodi_prepare_play_url(p)
        except Exception as e:
            _log(f"playback path hardening fallback (raw URL used): {e}", xbmc.LOGDEBUG)
            return p
    return p


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

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


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

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

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

    return masked

# --- In-Memory 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):
    return common.fs_local(path)


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


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


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


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


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


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



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

def _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):
    """Delegiert auf common.atomic_write_json, nutzt dessen Backup- und Durability-Logik."""
    try:
        return common.atomic_write_json(dest_path, obj, indent=indent, durable=durable, backup=True)
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] atomic_write_json delegate failed ({dest_path}): {e}", xbmc.LOGWARNING)
        return False









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


# -------------------------
# 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:
        cache_exists = os.path.isdir(LOCAL_IMAGE_CACHE_DIR_MOVIES) or xbmcvfs.exists(LOCAL_IMAGE_CACHE_DIR_MOVIES)
        if not cache_exists:
            xbmcvfs.mkdirs(LOCAL_IMAGE_CACHE_DIR_MOVIES)
            cache_exists = os.path.isdir(LOCAL_IMAGE_CACHE_DIR_MOVIES) or xbmcvfs.exists(LOCAL_IMAGE_CACHE_DIR_MOVIES)
            if cache_exists:
                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 konnte nicht angelegt werden: {LOCAL_IMAGE_CACHE_DIR_MOVIES}", xbmc.LOGWARNING)
    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"
# Shared window-property name: movies.py sets it, service.py reads it.
# Must match MOVIES_IMAGE_PREFETCH_TRIGGER_PROP in service.py.
MOVIES_IMAGE_PREFETCH_TRIGGER_PROP = f"{common.addon_id}.movies.image.prefetch.trigger"


# 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.
        if _start_daemon_task(
            background_refresh_movies_once,
            task_name="MoviesRefresh",
            use_executor=False,  # avoid non-daemon executor workers blocking addon shutdown
            source=f"ui:{reason}",
        ):
            _log(f"show_filme: {reason} -> refresh scheduled")
            return True
        _log(f"show_filme: {reason} schedule failed: task start returned False", xbmc.LOGWARNING)
        return False
    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 _is_movies_container_folderpath(folder_path: str) -> bool:
    """True nur in der Filme-Ansicht dieses Addons."""
    try:
        fp = (folder_path or "").replace('"', "").strip().lower()
        addon_fp = f"plugin://{common.addon_id}".lower()
        if addon_fp not in fp:
            return False
        # Refresh ausschließlich in der Filme-Liste zulassen.
        return ("action=show_filme" in fp) or ("action=check_movies_json" in fp)
    except Exception:
        return False

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

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

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

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

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

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

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

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

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



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

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

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

        win.setProperty(MOVIES_REFRESH_REQ_TS_PROP, str(now))

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

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

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

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

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

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

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

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



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

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

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

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

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

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




def _save_online_movies_to_cache(movies_list):
    """
    Speichert eine Online-Movieliste in movies.cache.json (atomisch).
    """
    if not MOVIES_CACHE_PATH or not isinstance(movies_list, list):
        return
    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.
    Gibt (downloaded, failed) zurück damit der Aufrufer den Trigger gezielt setzen kann.
    """
    if not LOCAL_IMAGE_CACHE_DIR_MOVIES:
        return 0, 0
    downloaded = skipped = failed = 0
    downloaded_titles = []
    for m in movie_list:
        if not isinstance(m, dict):
            continue
        movie_downloaded = 0
        for _art_key, url, _in_images_dict in _gather_artwork_sources_from_movie(m):
            if get_cached_movie_image(url):
                skipped += 1
            else:
                result = download_and_cache_movie_image(url)
                if result:
                    downloaded += 1
                    movie_downloaded += 1
                else:
                    failed += 1
                    title = m.get("title") or m.get("name") or "?"
                    xbmc.log(f"[{common.addon_id} movies.py] _prefetch_artwork: FAILED url={common.mask_url(url)} movie='{title}'", xbmc.LOGWARNING)
        if movie_downloaded:
            title = m.get("title") or m.get("name") or "?"
            downloaded_titles.append(f"'{title}' ({movie_downloaded} Bilder)")
    if downloaded_titles:
        xbmc.log(f"[{common.addon_id} movies.py] _prefetch_artwork: Neue Bilder fuer: {', '.join(downloaded_titles)}", xbmc.LOGINFO)
    xbmc.log(f"[{common.addon_id} movies.py] _prefetch_artwork: downloaded={downloaded} skipped(cached)={skipped} failed={failed} total={len(movie_list)}", xbmc.LOGINFO)
    return downloaded, failed


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.

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

    movie_list = _get_merged_movie_list(force=True)

    prefetch_downloaded = 0
    prefetch_failed = 0
    try:
        prefetch_downloaded, prefetch_failed = _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 (source={_src}, "
            f"prefetch: downloaded={prefetch_downloaded} failed={prefetch_failed}).",
            xbmc.LOGINFO
        )
        # Trigger service.py mid-cycle image prefetch ONLY when the silent prefetch above
        # missed something (had failures or genuinely downloaded 0 despite a signature change).
        # This avoids a double-run when service.py's own startup prefetch already covers it.
        _needs_service_prefetch = (prefetch_failed > 0) or (prefetch_downloaded == 0)
        if _needs_service_prefetch:
            try:
                import xbmcgui as _xbmcgui
                _xbmcgui.Window(10000).setProperty(MOVIES_IMAGE_PREFETCH_TRIGGER_PROP, "1")
                xbmc.log(
                    f"[{common.addon_id} movies.py] BG: {MOVIES_IMAGE_PREFETCH_TRIGGER_PROP} gesetzt "
                    f"(failed={prefetch_failed} downloaded={prefetch_downloaded}).",
                    xbmc.LOGDEBUG
                )
            except Exception:
                pass
        else:
            xbmc.log(
                f"[{common.addon_id} movies.py] BG: Kein Service-Trigger noetig "
                f"(prefetch hat alle Bilder geladen: downloaded={prefetch_downloaded}).",
                xbmc.LOGDEBUG
            )
    else:
        xbmc.log(f"[{common.addon_id} movies.py] BG: Keine Movie-Änderung anhand Signatur erkannt (source={_src}).", 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(source="startup")
        while not monitor.waitForAbort(BG_REFRESH_INTERVAL_HOURS_MOVIES * 3600):
            background_refresh_movies_once(source="bg_loop")
    except Exception as e:
        xbmc.log(f"[{common.addon_id} movies.py] BG: Worker-Fehler: {e}\n{traceback.format_exc()}", xbmc.LOGERROR)
    finally:
        try:
            if win.getProperty(MOVIES_BG_PROP) == "1":
                win.setProperty(MOVIES_BG_PROP, "")
        except Exception:
            pass
        xbmc.log(f"[{common.addon_id} movies.py] BG: Movies-Worker beendet.", xbmc.LOGINFO)


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


def get_movie_cache_filename(url):
    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 '{common.mask_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 not xbmc.getCondVisibility("Player.HasMedia"):
                    break
                if common.wait_or_abort(monitor, 0.25):
                    return

            if common.wait_or_abort(monitor, 0.3):
                return

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

            if not _is_movies_container_folderpath(folder_path):
                return

            if "action=play_movie" in folder_path:
                return

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

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

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

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

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

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

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

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

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

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

            with self._lock:
                self._total_time = total_time

            # 3) Tracken bis Ende/Stop
            watched_flag_set = False

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

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

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

                if common.wait_or_abort(monitor, 1.0):
                    return

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

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

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

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

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

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

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







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



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

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


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


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

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

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

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

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

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

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

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

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

    # 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:
            head_failed = True
            xbmc.log(
                f"[{common.addon_id} movies.py] HEAD-Request fehlgeschlagen ({common.mask_url(url)}): {e_head}",
                xbmc.LOGDEBUG
            )

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

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

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

    global _MOVIES_VIEW_STATE

    _MOVIES_VIEW_STATE['cache_used'] = bool(online_cache_usable)

    _MOVIES_VIEW_STATE['remote_changed_scheduled'] = False

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

            context = ssl.create_default_context()

            try:
                with urllib.request.urlopen(req, context=context, timeout=30) as response:
                    code = response.getcode()
                    if code == 200:
                        raw = response.read()
                        body_sha = None
                        try:
                            body_sha = common.sha1_hex(raw)
                        except Exception:
                            body_sha = None
                        json_data = raw.decode('utf-8-sig', errors='replace')
                        new_list = json.loads(json_data)
                        if isinstance(new_list, list):
                            movies_list_online = new_list
                            _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,
                                sha1=(body_sha or remote_sha)
                            )
                        else:
                            xbmc.log(
                                f"[{common.addon_id} movies.py] Online-Daten sind keine Liste ({type(new_list)}). Behalte Cache.",
                                xbmc.LOGWARNING
                            )
                    else:
                        xbmc.log(
                            f"[{common.addon_id} movies.py] HTTP Fehler beim Laden: {code} von {common.mask_url(url)}",
                            xbmc.LOGWARNING
                        )
            except urllib.error.HTTPError as he:
                if he.code == 304:
                    xbmc.log(f"[{common.addon_id} movies.py] Online-Filmliste: 304 Not Modified – verwende Cache.", xbmc.LOGDEBUG)
                    # Meta-Timestamp aktualisieren (optional)
                    _save_movies_remote_meta(meta_path, last_cl, last_etag, last_lm, sha1=(last_sha or remote_sha))
                else:
                    xbmc.log(
                        f"[{common.addon_id} movies.py] HTTPError beim Laden der Online-Filmliste: {he.code} {he}",
                        xbmc.LOGWARNING
                    )
            except Exception as e_get:
                raise e_get

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

    # 5) Lokale DB laden
    local_movies_db = {}
    if LOCAL_JSON_MOVIES_PATH and _fs_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():
    xbmc.log(f"[{common.addon_id} movies.py] play_random_movie: Funktion gestartet.", xbmc.LOGINFO)
    all_movies = _get_merged_movie_list()
    playable_movies = [movie for movie in all_movies if movie.get("file_ort") and isinstance(movie.get("file_ort"), str) and movie.get("file_ort").strip()]

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

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

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

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

    # Privacy: starte über plugin://play_movie statt direkter Stream-URL.
    # So landen keine sensitiven URL-Header im OpenFile-Log als erste Zeile.
    plugin_play_url = common.build_url({
        'action': 'play_movie',
        'movie_key': movie_path_raw,
        'title': movie_title,
        'original_online_key': status_identifier,
    })
    xbmc.Player().play(item=plugin_play_url)


def check_movies_json(final_movie_list=None):
    _t_start = time.time()
    xbmc.log(f"[{common.addon_id} movies.py] DEBUG: check_movies_json (Modernisierte Metadaten V3.1): 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 as e:
        _log(f"setProperty({MOVIES_LISTING_TS_PROP}) failed: {e}", xbmc.LOGDEBUG)

    if final_movie_list is None:
        final_movie_list = _get_merged_movie_list()


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

    watched_status = common.load_watched_status_movies(WATCHED_STATUS_FILE_MOVIES)

    # 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))
        _start_daemon_task(
            _async_cache_missing_movie_images,
            unique_missing,
            task_name="MoviesImageCacheFill",
            use_executor=False,  # daemon thread is safer for Kodi stop/restart flow
        )

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

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

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

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

        fanart_final = addon_fanart or icon_final

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

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

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

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

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

    # -------------------------
    # Schnell-Preload: alle Dateinamen im Image-Cache einmalig einlesen (ein os.scandir statt 2850 fs_exists)
    # -------------------------
    _cached_movie_filenames = set()
    try:
        if LOCAL_IMAGE_CACHE_DIR_MOVIES and os.path.isdir(LOCAL_IMAGE_CACHE_DIR_MOVIES):
            _cached_movie_filenames = {e.name for e in os.scandir(LOCAL_IMAGE_CACHE_DIR_MOVIES) if e.is_file()}
    except Exception:
        pass

    def _c_fast(url):
        """Gibt lokalen Cache-Pfad zurück (via scandir-Set, kein fs_exists pro URL)."""
        if not url or not isinstance(url, str) or not url.startswith("http"):
            return url or ""
        try:
            fname = os.path.basename(common.get_cache_filename(url, cache_dir=LOCAL_IMAGE_CACHE_DIR_MOVIES) or "")
            if fname and fname in _cached_movie_filenames:
                return os.path.join(LOCAL_IMAGE_CACHE_DIR_MOVIES, fname)
        except Exception:
            pass
        return url

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        if art:
            li.setArt(art)

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

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

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

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

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




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

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

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


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

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


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

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

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

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

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

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

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

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




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: "
                f"{_mask_log_value(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: {_mask_log_value(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}': {common.mask_url(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}' ({common.mask_url(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:
                    # Hauptdatei entfernen, wenn der gesamte Prozess nicht erfolgreich war (auch wenn der Download selbst klappte)
                    if downloaded_local_movie_path and xbmcvfs.exists(downloaded_local_movie_path):
                        xbmcvfs.delete(downloaded_local_movie_path)
                        xbmc.log(f"[{addon_id_log}] Hauptdatei nach fehlgeschlagenem Prozess 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 '{_mask_log_value(online_key_of_deleted_movie)}' "
                    f"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(offscreen=True))



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

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

    xbmc.log(
        f"[{common.addon_id} movies.py] Aktion: '{action}', "
        f"MovieKey: '{_mask_log_value(movie_key_param)}', Title: '{title_param}', "
        f"OrigOnlineKey: '{_mask_log_value(original_online_key_main_param)}'",
        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)
        _safe_end_of_directory(addon_handle, succeeded=True, cache_to_disc=False)

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

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

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