# -*- coding: utf-8 -*-
"""
trakt_service.py — Core logika, API obálka a cache managery pro Trakt.tv.
Data-layer pro Kodi addon.
"""
import sys
import os
import json
import time
import gzip
import hashlib
from typing import Any, Dict, Optional, List
from datetime import datetime
import sqlite3
import threading

# Externí knihovny
import requests

# Kodi/XBMC moduly (Nutné pro cestu k souborům, dialogy, atd.)
import xbmc
import xbmcvfs
import xbmcaddon
import xbmcgui
from xbmcvfs import translatePath
from urllib.parse import urlencode


from resources.lib.tmdb.tmdb_module import IMAGE_BASE_URL_POSTER # Příklad (pokud ji používáte)
from resources.lib.utils.log_utils import get_logger

log = get_logger()

# ============================================================
# === AUTH HELPERS (QR kód, TinyURL, Clipboard) ===
# ============================================================

def _make_qrcode(url: str) -> str:
    """
    Vygeneruje QR kód jako PNG do profilu doplňku.
    Vrátí absolutní cestu k PNG souboru nebo '' pokud není dostupná knihovna segno.
    Pořadí importu: 1) resources/lib/segno (vendored), 2) systémová segno, 3) None
    """
    try:
        segno = None
        try:
            # 1. Vendored segno v doplňku
            import importlib, sys as _sys
            _addon_segno = os.path.join(
                os.path.dirname(__file__), '..', '..', 'lib', 'segno'
            )
            _addon_segno = os.path.normpath(_addon_segno)
            if os.path.isdir(_addon_segno):
                if os.path.dirname(_addon_segno) not in _sys.path:
                    _sys.path.insert(0, os.path.dirname(_addon_segno))
            import segno as segno
        except ImportError:
            pass
        if segno is None:
            return ''
        profile = translatePath(ADDON.getAddonInfo('profile'))
        art_path = os.path.join(profile, 'trakt_qr.png')
        qrcode = segno.make(url, micro=False)
        qrcode.save(art_path, scale=10)
        return art_path
    except Exception as e:
        log.warning(f"[TraktAuth] QR kód nelze vygenerovat: {e}")
        return ''


def _make_tinyurl(url: str) -> str:
    """
    Zkrátí URL přes TinyURL API. Vrátí zkrácenou URL nebo ''.
    Nepropaguje výjimky – při chybě vrátí prázdný řetězec.
    """
    try:
        resp = requests.get('http://tinyurl.com/api-create.php', params={'url': url}, timeout=5)
        if resp.status_code == 200 and resp.text.startswith('http'):
            return resp.text.strip()
    except Exception as e:
        log.debug(f"[TraktAuth] TinyURL selhalo: {e}")
    return ''


def _copy2clip(txt: str) -> None:
    """
    Zkopíruje txt do systémové schránky (Windows / macOS / Linux).
    Tiše ignoruje chyby.
    """
    import sys as _sys
    try:
        if _sys.platform == 'win32':
            from subprocess import check_call
            cmd = 'echo ' + txt.replace('&', '^&').strip() + '|clip'
            check_call(cmd, shell=True)
        elif _sys.platform == 'darwin':
            from subprocess import check_call
            check_call('echo ' + txt.strip() + '|pbcopy', shell=True)
        elif _sys.platform.startswith('linux'):
            from subprocess import Popen, PIPE
            p = Popen(['xsel', '-pi'], stdin=PIPE)
            p.communicate(input=txt.encode())
    except Exception as e:
        log.debug(f"[TraktAuth] Clipboard selhalo: {e}")
ADDON = xbmcaddon.Addon('plugin.video.mmirousek_v3')
TRAKT_CLIENT_SECRET = ADDON.getSetting('trakt_client_secret') or ""

# === KONFIGURACE TRAKT ===
TRAKT_CLIENT_ID = "4c2c45518d4accf713853b837c1251d787878434d76e71ca279db0aae40ae343"
TRAKT_API_URL = "https://api.trakt.tv"

# === TTL (sekundy) ===
TTL_MEDIUM = 10 * 60
TTL_LONG   = 24 * 60 * 60
TTL_FAST   = 60 * 60
TTL_TMDB_DETAILS = 14 * 24 * 60 * 60
TMDB_PREFETCH_WORKERS = 4

# === UTILITY PRO ČASOVÁNÍ SYNCHRONIZACE ===
def get_last_sync_time() -> float:
    """Načte čas poslední plné synchronizace (Unix timestamp)."""
    try:
        # 'trakt_last_sync_ts' je ID, které definujeme v settings.xml (interně se uloží)
        ts = float(ADDON.getSetting('trakt_last_sync_ts') or '0.0')
        return ts
    except Exception:
        return 0.0

def set_last_sync_time():
    """Uloží aktuální Unix timestamp jako čas poslední plné synchronizace."""
    # Ukládáme jako string, protože Kodi nastavení neukládá float.
    ADDON.setSetting('trakt_last_sync_ts', str(time.time()))

# === TRAKT HEADERS / TOKEN HELPERY ===
def _trakt_headers_base() -> Dict[str, str]:
    h = {
        'trakt-api-version': '2',
        'trakt-api-key': TRAKT_CLIENT_ID,
        'User-Agent': 'plugin.video.mmirousek_v3/1.0 (+Kodi)'
    }
    try:
        lang = ADDON.getSetting('trakt_lang') or ''
        if lang:
            h['Accept-Language'] = lang
    except Exception:
        pass
    return h

def _auth_headers(for_post: bool = False) -> Optional[Dict[str, str]]:
    token = ADDON.getSetting('trakt_access_token')
    if not token:
        return None
    h = _trakt_headers_base()
    h['Authorization'] = f'Bearer {token}'
    if for_post:
        h['Content-Type'] = 'application/json'
    return h

def get_trakt_headers(for_post: bool = False):
    """
    Vrátí hlavičky pro volání Trakt API.
    - for_post=True → vyžaduje platný access token, jinak None
    - for_post=False → pokud není token, vrátí veřejné hlavičky (api-key, UA)
    """
    h_auth = _auth_headers(for_post=for_post)
    if h_auth:
        return h_auth
    if for_post:
        return None
    return _trakt_headers_base()

def update_trakt_status_setting() -> None:
    """
    Zapíše do settings pole 'trakt_status' informaci o stavu přihlášení.
    Pokud je token, pokusí se načíst /users/me a ukáže username; jinak 'Nepřihlášen'.
    """
    try:
        addon = xbmcaddon.Addon('plugin.video.mmirousek_v3')
        token = addon.getSetting('trakt_access_token') or ''
        if not token:
            addon.setSetting('trakt_status', 'Nepřihlášen')
            return
        api = TraktAPI()
        me = api.get('users/me', params=None, auth=True, cache_ttl=0) or {}
        username = me.get('username') or (me.get('name') or '')
        if username:
            addon.setSetting('trakt_status', f'Přihlášen: {username}')
        else:
            addon.setSetting('trakt_status', 'Přihlášen (uživatel neidentifikován)')
    except Exception as e:
        log.warning(f"[Trakt] update_trakt_status_setting error: {e}")

# === CACHE UTILS ===
def _build_cache_key(endpoint: str, params: Optional[Dict[str, Any]], auth: bool) -> str:
    p = params or {}
    try:
        p_ser = json.dumps(p, sort_keys=True, ensure_ascii=False)
    except Exception:
        p_ser = str(p)
    return f"{endpoint}\n{p_ser}\n{'A' if auth else 'N'}"

def _pick_pagination_headers(h: Dict[str, Any]) -> Dict[str, Any]:
    keep = {}
    for k, v in (h.items() if isinstance(h, dict) else []):
        if isinstance(k, str) and k.lower().startswith('x-pagination-'):
            keep[k] = v
    return keep

# === TMDb CACHE LOGIKA (Musí být zde, protože ji používá inkrementální update) ===
_TMDBCACHE = None

def _ensure_tmdb_cache():
    global _TMDBCACHE
    if _TMDBCACHE is None:
        # FileCache může být definována až níže → inicializujeme lhostejně až při prvním použití
        try:
            _TMDBCACHE = FileCache(namespace='tmdb')
        except NameError:
            # Pokud FileCache ještě není definována, necháme None (modul se dokončí a pak bude možné vytvořit)
            _TMDBCACHE = None

def _tmdb_cache_key(media_type: str, tmdb_id: int) -> str:
    return f"tmdb:{('movie' if media_type=='movie' else 'tv')}:{int(tmdb_id)}"

def get_tmdb_details_from_cache_only(tmdb_id: int, media_type: str):
    if not tmdb_id:
        return None
    _ensure_tmdb_cache()
    if _TMDBCACHE is None:
        return None
    key = _tmdb_cache_key(media_type, tmdb_id)
    obj = _TMDBCACHE.get(key)
    return (obj or {}).get('data') if obj else None

def store_tmdb_details(tmdb_id: int, media_type: str, details: dict, ttl_sec: int = TTL_TMDB_DETAILS):
    if not (tmdb_id and isinstance(details, dict) and details):
        return
    _ensure_tmdb_cache()
    if _TMDBCACHE is None:
        return
    key = _tmdb_cache_key(media_type, tmdb_id)
    _TMDBCACHE.set(key, details, hdr=None, ttl_sec=ttl_sec)

_TMDB_SEM = threading.Semaphore(TMDB_PREFETCH_WORKERS)
_PREFETCH_ACTIVE_COUNT = 0
_PREFETCH_LOCK = threading.Lock()
_REFRESH_TIMER = None

def _trigger_delayed_refresh(delay=2.5):
    """
    Spustí odložený refresh kontejneru v Kodi.
    Debounce mechanismus zajistí, že se refreshne až po dokončení celé dávky.
    """
    global _REFRESH_TIMER
    
    def _do_refresh():
        # Kontrola, zda jsme stále v doplňku (nebo v režimu, kde dává smysl refresh)
        # Refresh vyvoláme jen pokud je v popředí seznam, který by mohl těžit z nových metadat.
        try:
            # Kontrola, zda nebyla spuštěna další vlna stahování
            with _PREFETCH_LOCK:
                if _PREFETCH_ACTIVE_COUNT > 0:
                    log.info("[TMDb Prefetch] Refresh cancelled, another batch in progress")
                    return
            
            log.error("[TMDb Prefetch] !!! TRIGGERING CONTAINER REFRESH !!!")
            xbmc.executebuiltin("Container.Refresh")
        except Exception as e:
            log.error(f"[TMDb Prefetch] Refresh trigger failed: {e}")

    with _PREFETCH_LOCK:
        if _REFRESH_TIMER:
            _REFRESH_TIMER.cancel()
        _REFRESH_TIMER = threading.Timer(delay, _do_refresh)
        _REFRESH_TIMER.start()


# Pozdější (lazy) import get_tmdb_details_fallback — vyhneme se ImportError při importu modulu,
# pokud tmdb_module neobsahuje přesně toto jméno.
def _import_tmdb_fallback():
    try:
        from resources.lib.trakt.trakt_module import get_tmdb_details_fallback
        return get_tmdb_details_fallback
    except Exception as e:
        log.warning(f"[TMDb Prefetch] get_tmdb_details_fallback not available: {e}")
        return None

def _prefetch_tmdb_detail_async(tmdb_id: int, media_type: str):
    """
    Na pozadí stáhne TMDb detaily (CZ) a uloží do cache, pokud tam nejsou.
    Zároveň sleduje počet úloh a vyvolá refresh po dokončení dávky.
    """
    global _PREFETCH_ACTIVE_COUNT

    if not tmdb_id:
        return

    # Pokud už je v cache, nic nedělej
    existing = get_tmdb_details_from_cache_only(tmdb_id, media_type)
    if existing:
        return

    def _task():
        global _PREFETCH_ACTIVE_COUNT
        try:
            with _TMDB_SEM:
                try:
                    _get_fallback = _import_tmdb_fallback()
                    if not _get_fallback:
                        log.error("[TMDb Prefetch] ERROR: Fallback function not found")
                        return
                    det = _get_fallback(tmdb_id, 'movie' if media_type == 'movie' else 'tv')
                    if det:
                        store_tmdb_details(tmdb_id, media_type, det, ttl_sec=TTL_TMDB_DETAILS)
                except Exception as e:
                    log.error(f"[TMDb Prefetch] error tmdb_id={tmdb_id}: {e}")
        finally:
            with _PREFETCH_LOCK:
                _PREFETCH_ACTIVE_COUNT -= 1
                if _PREFETCH_ACTIVE_COUNT == 0:
                    _trigger_delayed_refresh()

    try:
        with _PREFETCH_LOCK:
            _PREFETCH_ACTIVE_COUNT += 1
        
        # Použijeme daemon=False, aby vlákna přežila konec hlavního skriptu a mohla vyvolat refresh
        t = threading.Thread(target=_task, name=f"tmdb_prefetch_{media_type}_{tmdb_id}", daemon=False)
        t.start()
    except Exception as e:
        with _PREFETCH_LOCK:
            _PREFETCH_ACTIVE_COUNT -= 1
        log.error(f"[TMDb Prefetch] FAILED to start thread: {e}")
    except Exception:
        # Fallback bez threadingu (neblokovat listing!)
        log.warning("[TMDb Prefetch] failed to start thread; skipping")


# ====================================================================
# --- TŘÍDY PRO PŘÍSTUP K DATŮM A CACHE ---
# ====================================================================
# === SQLITE CACHE (cache.db) =======================================================
import sqlite3
import json

class FileCache:
    """Lokální SQLite cache nahrazující původní gzip soubory."""
    def __init__(self, namespace: str = 'trakt'):
        self.namespace = namespace
        try:
            profile = xbmcvfs.translatePath(ADDON.getAddonInfo('profile'))
        except NameError:
            # fallback pokud neni xbmcvfs naprimo
            import xbmc
            profile = xbmc.translatePath(ADDON.getAddonInfo('profile'))
            
        os.makedirs(profile, exist_ok=True)
        self.db_path = os.path.join(profile, 'cache.db')
        
        self.conn = sqlite3.connect(self.db_path, timeout=30.0, check_same_thread=False)
        self.conn.execute("PRAGMA journal_mode = WAL")
        self.conn.execute("PRAGMA synchronous = NORMAL")
        self._create_table()

    def _create_table(self):
        try:
            self.conn.execute("""
            CREATE TABLE IF NOT EXISTS api_cache (
                namespace TEXT,
                key_hash TEXT,
                ts INTEGER,
                ttl INTEGER,
                hdr TEXT,
                data TEXT,
                PRIMARY KEY (namespace, key_hash)
            )
            """)
            self.conn.commit()
        except Exception as e:
            log.error(f"[SQLiteCache] Create table error: {e}")

    @staticmethod
    def _hash_key(key: str) -> str:
        return hashlib.md5(key.encode('utf-8')).hexdigest()

    def get(self, key: str) -> Optional[Dict[str, Any]]:
        key_hash = self._hash_key(key)
        try:
            cur = self.conn.execute(
                "SELECT ts, ttl, hdr, data FROM api_cache WHERE namespace=? AND key_hash=?",
                (self.namespace, key_hash)
            )
            row = cur.fetchone()
            if not row:
                return None
                
            ts, ttl, hdr_str, data_str = row
            if ttl > 0 and (time.time() - ts) > ttl:
                self.conn.execute(
                    "DELETE FROM api_cache WHERE namespace=? AND key_hash=?",
                    (self.namespace, key_hash)
                )
                self.conn.commit()
                return None
                
            obj = {
                'ts': ts,
                'ttl': ttl,
                'hdr': json.loads(hdr_str) if hdr_str else {},
                'data': json.loads(data_str) if data_str else None
            }
            return obj
        except Exception as e:
            log.warning(f"[SQLiteCache] Read fail: {e}")
            return None

    def set(self, key: str, data: Any, hdr: Optional[Dict[str, Any]], ttl_sec: int):
        key_hash = self._hash_key(key)
        ts = int(time.time())
        ttl = int(ttl_sec)
        hdr_str = json.dumps(hdr or {}, ensure_ascii=False)
        data_str = json.dumps(data, ensure_ascii=False)
        
        try:
            self.conn.execute("""
                INSERT OR REPLACE INTO api_cache (namespace, key_hash, ts, ttl, hdr, data)
                VALUES (?, ?, ?, ?, ?, ?)
            """, (self.namespace, key_hash, ts, ttl, hdr_str, data_str))
            self.conn.commit()
            
            # Autoclean expirace (volitelně, každých X zápisů?)
            if ts % 50 == 0:  # Jen náhodně občas smažeme prexpirované
                self.conn.execute("DELETE FROM api_cache WHERE ttl > 0 AND (? - ts) > ttl", (ts,))
                self.conn.commit()
                
        except Exception as e:
            log.warning(f"[SQLiteCache] Write fail: {e}")

    def clear_namespace(self):
        try:
            self.conn.execute("DELETE FROM api_cache WHERE namespace=?", (self.namespace,))
            self.conn.commit()
        except Exception as e:
            log.error(f"[SQLiteCache] Clear namespace error: {e}")

    def __del__(self):
        try:
            self.conn.close()
        except:
            pass


# === Trakt API obálka ========================================================
class TraktAPI:
    def __init__(self) -> None:
        self.base = TRAKT_API_URL
        self.last_headers: Dict[str, str] = {}
        self.cache = FileCache('trakt')

    def authorize_trakt(self) -> None:
        """
        Device Code flow – interaktivní progress dialog s QR kódem, TinyURL a clipboardem.
        Uživatel může kdykoli zrušit stiskem tlačítka Zrušit.
        """
        client_id = TRAKT_CLIENT_ID
        client_secret = TRAKT_CLIENT_SECRET
        if not client_id or not client_secret:
            xbmcgui.Dialog().notification('Trakt', 'Chybí client_id / client_secret.', xbmcgui.NOTIFICATION_ERROR, 3000)
            return

        # ── Fáze 1: Získání device kódu ─────────────────────────────────
        try:
            r = requests.post(f"{self.base}/oauth/device/code",
                              json={'client_id': client_id}, timeout=10)
            r.raise_for_status()
            device      = r.json()
            user_code   = str(device.get('user_code', ''))
            verify_url  = str(device.get('verification_url', 'https://trakt.tv/activate'))
            device_code = str(device.get('device_code', ''))
            interval    = max(int(device.get('interval', 5)), 1)
            expires_in  = int(device.get('expires_in', 600))
        except Exception as e:
            log.error(f"[TraktAuth] Nelze získat device code: {e}")
            xbmcgui.Dialog().notification('Trakt', 'Chyba při získávání kódu.', xbmcgui.NOTIFICATION_ERROR, 3000)
            return

        # ── Fáze 2: Příprava UI prvků (QR, TinyURL, clipboard) ──────────
        auth_url  = f"{verify_url}?code={user_code}"

        # QR kód (nevyzažduje síť, rychlé; selže tiše pokud segno není k dispozici)
        qr_path   = _make_qrcode(auth_url)   # '' pokud segno není

        # TinyURL a clipboard – spouštíme paralelně, aby nezpomalily dialog
        short_url = ''
        try:
            import threading as _t
            _su_result = ['']
            def _fetch_short():
                _su_result[0] = _make_tinyurl(auth_url)
            _t.Thread(target=_fetch_short, daemon=True).start()
        except Exception:
            pass
        _copy2clip(auth_url)   # ihned zkopírovat do schránky

        # ── Text obsahu dialogu ─────────────────────────────────────────
        def _build_content(su: str) -> str:
            lines = [
                f"Zadej kód [B]{user_code}[/B] na webu:[CR]",
                f"[B]{verify_url}[/B]",
            ]
            if qr_path:
                lines.append("[CR]nebo naskenuj QR kód →")
            if su:
                lines.append(f"[CR][CR]Zkrácená URL:[CR][B]{su}[/B]")
            lines.append("[CR][CR][COLOR grey]URL zkopírována do schránky[/COLOR]")
            return ''.join(lines)

        # ── Fáze 3: Custom okno + polling ve vlákně ──────────────────────
        # ARCHITEKTURA: doModal() MUSÍ být na hlavním vlákně.
        #               Polling loop běží v background vláknu a volá win.request_close().
        import threading as _th

        # Sdílená data mezi vlákny
        _token_result = {}   # naplní polling vlákno po úspěchu

        try:
            from resources.lib.trakt.trakt_progress_window import TraktProgressWindow
            win = TraktProgressWindow.create(
                heading='Trakt – Přihlášení',
                icon=qr_path,
                content=_build_content(short_url)
            )
        except Exception as e:
            log.warning(f"[TraktAuth] Import TraktProgressWindow selhal: {e}")
            win = None

        # ── Polling loop (background vlákno) ────────────────────────────
        def _poll_loop():
            nonlocal short_url
            # Počkat na onInit() pokud používáme custom okno
            if win is not None:
                win.wait_ready(timeout=5.0)

            start        = time.time()
            poll_elapsed = 0.0

            while True:
                # Zkontrolovat zrušení
                if win is not None and win.is_canceled():
                    break

                xbmc.sleep(500)
                elapsed      = time.time() - start
                poll_elapsed += 0.5

                if elapsed >= expires_in:
                    break

                progress_pct = int(100 * elapsed / expires_in)

                # Aktualizovat UI
                if not short_url and _su_result[0]:
                    short_url = _su_result[0]
                content_now = _build_content(short_url)
                if win is not None:
                    win.update(content_now, progress_pct)

                # HTTP polling každých `interval` sekund
                if poll_elapsed < interval:
                    continue
                poll_elapsed = 0.0

                try:
                    rr = requests.post(
                        f"{self.base}/oauth/device/token",
                        json={'client_id': client_id,
                              'client_secret': client_secret,
                              'code': device_code},
                        timeout=10
                    )
                except Exception as e:
                    log.warning(f"[TraktAuth] Polling error: {e}")
                    continue

                if rr.status_code == 200:
                    td = rr.json()
                    if td.get('access_token'):
                        _token_result.update(td)
                        win.auth_success = True
                    break
                elif rr.status_code == 400:
                    continue
                elif rr.status_code in (404, 410):
                    log.warning(f"[TraktAuth] Device code expired ({rr.status_code})")
                    break

            # Zavřít okno z vlákna
            if win is not None:
                win.request_close()

        # Spustit polling v background vlákně
        poll_thread = _th.Thread(target=_poll_loop, name='TraktPoll', daemon=True)
        poll_thread.start()

        # ── doModal() na hlavním vlákně (blokuje) ───────────────────────
        if win is not None:
            win.doModal()
        else:
            # Fallback: DialogProgress na hlavním vlákně + čekat na polling
            p_dialog = xbmcgui.DialogProgress()
            p_dialog.create('Trakt – Přihlášení', _build_content(short_url))
            p_dialog.update(0)
            poll_thread.join()
            p_dialog.close()

        # ── Výsledek (po zavření okna) ───────────────────────────────────
        if _token_result.get('access_token'):
            ADDON.setSetting('trakt_access_token',    _token_result['access_token'])
            ADDON.setSetting('trakt_refresh_token',   _token_result.get('refresh_token', ''))
            ADDON.setSetting('trakt_token_issued_at', str(int(time.time())))
            ADDON.setSetting('trakt_token_expires_in', str(_token_result.get('expires_in', 7776000)))
            xbmcgui.Dialog().notification(
                'Trakt', 'Přihlášení proběhlo úspěšně! ✓',
                xbmcgui.NOTIFICATION_INFO, 3000
            )
            try:
                update_trakt_status_setting()
            except Exception:
                pass
        elif win is not None and win.is_canceled():
            log.info("[TraktAuth] Uživatel zrušil autorizaci")
        else:
            xbmcgui.Dialog().notification(
                'Trakt', 'Přihlášení vypršelo nebo bylo odmítnuto.',
                xbmcgui.NOTIFICATION_WARNING, 3000
            )

    def refresh_token_if_needed(self, force: bool = False, notify_on_success: bool = True) -> bool:
        LOG = "mm.trakt_refresh"
        import time, requests, xbmc, xbmcgui

        # --- Anti-spam: max 1 pokus / 60 s (401 / explicitní force obchází)
        now = time.time()
        if not force:
            last_ok = float(ADDON.getSetting('trakt_last_refresh_success') or '0')
            if (now - last_ok) < 60:
                log.info(f"[{LOG}] Refresh skipped – úspěšný refresh před <60s")
                return False

        client_secret = ADDON.getSetting('trakt_client_secret') or ""
        refresh_token = ADDON.getSetting('trakt_refresh_token') or ""
        if not (client_secret and refresh_token):
            log.info(f"[{LOG}] missing refresh_token or client_secret → skip")
            return False

        url = f"{self.base}/oauth/token"
        payload = {
            "refresh_token": refresh_token,
            "client_id": TRAKT_CLIENT_ID,
            "client_secret": client_secret,
            "redirect_uri": "urn:ietf:wg:oauth:2.0:oob",
            "grant_type": "refresh_token",
        }

        try:
            r = requests.post(url, json=payload, timeout=10)

            # --- 200 OK → uložit a hotovo
            if r.status_code == 200:
                data = r.json() or {}
                new_access = (data.get('access_token') or '').strip()
                if not new_access:
                    log.error(f"[{LOG}] 200 OK ale chybí access_token v těle – starý token ponechávám")
                    return False
                ADDON.setSetting('trakt_access_token', new_access)
                if data.get('refresh_token'):
                    ADDON.setSetting('trakt_refresh_token', data.get('refresh_token'))
                ADDON.setSetting('trakt_token_issued_at', str(int(time.time())))
                ADDON.setSetting('trakt_token_expires_in', str(data.get('expires_in', 86400)))
                ADDON.setSetting('trakt_last_refresh_success', str(now))
                log.info(f"[{LOG}] token refreshed OK")
                if notify_on_success:
                    xbmcgui.Dialog().notification('Trakt', 'Token byl automaticky obnoven', xbmcgui.NOTIFICATION_INFO, 3000)
                try:
                    update_trakt_status_setting()
                except Exception:
                    pass
                return True

            # --- 400 invalid_grant → token je neplatný/odvolaný/expir.
            if r.status_code == 400:
                try:
                    j = r.json() or {}
                except Exception:
                    j = {}
                if (j.get('error') == 'invalid_grant'):
                    # 1) Vyčistit staré tokeny
                    for k in ('trakt_access_token', 'trakt_refresh_token', 'trakt_token_issued_at', 'trakt_token_expires_in'):
                        try:
                            ADDON.setSetting(k, '')
                        except Exception:
                            pass
                    log.warning(f"[{LOG}] invalid_grant → clearing tokens & forcing re-auth (detail: {r.text})")

                    # 2) Spustit interaktivní autorizaci (Device Code flow)
                    try:
                        self.authorize_trakt()
                        try:
                            update_trakt_status_setting()
                        except Exception:
                            pass
                        # Úmyslně vracíme False → volající ať případně zopakuje původní volání po re-auth
                        return False
                    except Exception as e:
                        log.error(f"[{LOG}] re-auth failed after invalid_grant: {e}")
                        xbmcgui.Dialog().notification('Trakt', 'Nutná nová autorizace v Nastavení.', xbmcgui.NOTIFICATION_WARNING, 3500)
                        return False

            # --- ostatní HTTP kódy
            log.error(f"[{LOG}] refresh failed status={r.status_code} response={r.text}")
            return False

        except Exception as e:
            log.error(f"[{LOG}] refresh exception: {e}")
            return False
      
    # --- GET wrapper ---

    def get(self, endpoint: str, params=None, auth: bool = True, timeout: int = 12,
            cache_ttl: int = 0, cache_key: Optional[str] = None):
        import time, requests, xbmc, xbmcgui

        url = f"{self.base}/{endpoint.lstrip('/')}"
        headers = _auth_headers(False) if auth else _trakt_headers_base()
        if auth and not headers:
            return None  # Bez tokenu → žádná chyba, jen None

        # Cache-first
        key = cache_key or _build_cache_key(endpoint, params, auth)
        if cache_ttl > 0:
            cached = self.cache.get(key)
            if cached is not None:
                self.last_headers = cached.get('hdr', {}) or {}
                log.info(f"[TraktCache] HIT {endpoint} {params}")
                return cached.get('data')

        def _request_once(hdrs):
            return requests.get(url, headers=hdrs, params=params, timeout=timeout)

        try:
            r = _request_once(headers)

            # Rate-limit retry
            if r.status_code == 429:
                time.sleep(int(r.headers.get('Retry-After', '3')))
                r = _request_once(headers)

            # 401 → pokus o refresh + retry
            if r.status_code == 401 and auth:
                if self.refresh_token_if_needed(force=True, notify_on_success=False):
                    hdrs_retry = _auth_headers(False)
                    if hdrs_retry:
                        r = _request_once(hdrs_retry)

            r.raise_for_status()
            self.last_headers = dict(r.headers) if hasattr(r, 'headers') else {}
            data = r.json()
            if cache_ttl > 0:
                self.cache.set(key, data, _pick_pagination_headers(self.last_headers), cache_ttl)
            return data

        except requests.HTTPError as e:
            if getattr(e.response, "status_code", None) == 401:
                xbmcgui.Dialog().notification('Trakt', 'Neplatný token – přihlaste se znovu.', xbmcgui.NOTIFICATION_ERROR, 3000)
            log.error(f"[TraktAPI] HTTP ERROR: {e} [{endpoint}]")
            return None
        except Exception as e:
            log.error(f"[TraktAPI] ERROR: {e} [{endpoint}]")
            return None

    def search_by_id(self, id_value: Any, id_type: str = 'tmdb', type: str = 'show', cache_ttl: int = TTL_LONG) -> Optional[List[Dict[str, Any]]]:
        """
        Vyhledá položku na Traktu podle externího ID (tmdb, imdb, tvdb).
        id_type: 'tmdb', 'imdb', 'tvdb'
        type: 'show', 'movie', 'episode', 'person'
        """
        endpoint = f"search/{id_type}/{id_value}"
        params = {'type': type}
        return self.get(endpoint, params=params, auth=False, cache_ttl=cache_ttl)

    def search_by_tmdb(self, tmdb_id: Any, type: str = 'show') -> Optional[List[Dict[str, Any]]]:
        """Alias pro search_by_id s výchozím id_type='tmdb'"""
        return self.search_by_id(tmdb_id, id_type='tmdb', type=type)

    # --- Public endpoints / helpers ---
    def genres(self, media_type: str) -> List[Dict[str, Any]]:
        return self.get(f'genres/{media_type}', auth=False, cache_ttl=TTL_LONG) or []

    def movies(self, mode: str, page: int, limit: int,
            genres: Optional[str], years: Optional[str],
            extended: Optional[str] = 'min'): # <--- ZMĚNA 1: Přidání argumentu s výchozí hodnotou
            
        endpoint = f"movies/{mode}"
        
        # ZMĚNA 2: Použití hodnoty z argumentu (buď 'min' nebo 'full')
        params = {'extended': extended, 'page': page, 'limit': limit} 
        
        if genres: params['genres'] = genres
        if years:  params['years']  = years
        
        return self.get(endpoint, params=params, auth=False, cache_ttl=TTL_FAST)

    def shows(self, mode: str, page: int, limit: int, 
            genres: Optional[str], years: Optional[str],
            extended: Optional[str] = 'min'): # <--- PŘIDANÝ ARGUMENT
            
        endpoint = f"shows/{mode}"
        
        params = {'extended': extended, 'page': page, 'limit': limit} # Použití 'extended'
        
        if genres: params['genres'] = genres
        if years:  params['years']  = years
        
        return self.get(endpoint, params=params, auth=False, cache_ttl=TTL_FAST)

    def list_popular_or_trending(self, mode: str, page: int, limit: int):
        endpoint = 'lists/popular' if mode == 'popular' else 'lists/trending'
        params = {'extended': 'min', 'page': page, 'limit': limit}
        return self.get(endpoint, params=params, auth=False, cache_ttl=TTL_FAST)

    def list_items(self, list_id: str, page: int, limit: int, auth: bool = False):
        endpoint = f'lists/{list_id}/items'
        params = {'extended': 'min', 'page': page, 'limit': limit}
        return self.get(endpoint, params=params, auth=auth, cache_ttl=TTL_FAST)

    def people(self, media_type: str, trakt_id_or_slug: str, cache_ttl: int = TTL_LONG, auth: bool = False):
        endpoint = f'{media_type}/{trakt_id_or_slug}/people'
        log.info(f"[TraktAPI.people] endpoint={endpoint} auth={'A' if auth else 'N'} ttl={cache_ttl}")
        return self.get(endpoint, params=None, auth=auth, cache_ttl=cache_ttl)

    def related(self, media_type: str, trakt_id_or_slug: str, page: int, limit: int):
        endpoint = f'{media_type}/{trakt_id_or_slug}/related'
        params = {'extended': 'full', 'page': page, 'limit': limit}
        return self.get(endpoint, params=params, auth=True, cache_ttl=TTL_MEDIUM)

    def users_watched_shows(self, page: int = 1, limit: int = 200,
                            extended: bool = False, extended_str: Optional[str] = 'min'):
        endpoint = 'users/me/watched/shows'
        params = {'page': page, 'limit': limit}
        params['extended'] = extended_str or ('full' if extended else 'min')
        return self.get(endpoint, params=params, auth=True, cache_ttl=TTL_MEDIUM) or []

    def users_watched_movies(self, page: int = 1, limit: int = 200, extended: bool = False, extended_str: str = 'min', updated_since: Optional[str] = None, cache_ttl: int = 0):
            """
            Historie filmů uživatele (watched movies) — vrací i plays/last_watched_at.
            """
            endpoint = 'users/me/watched/movies'
            
            # Sestavení základních parametrů
            params = {'page': page, 'limit': limit}
            params['extended'] = extended_str or ('full' if extended else 'min')
            
            # Podmíněné přidání 'updated_since' pro inkrementální update
            if updated_since:
                params['updated_since'] = updated_since
                
            # Nyní voláme API se správně sestavenými parametry a TTL
            return self.get(endpoint, params=params, auth=True, cache_ttl=cache_ttl) or []

    def watched_movies(self, extended: str = 'full', cache_ttl: int = TTL_LONG) -> List[Dict[str, Any]]:
            """
            Získá zhlédnuté filmy ze /sync/watched/movies.
            Toto je standardní endpoint, který vrací 'plays' a 'last_watched_at' na nejvyšší úrovni
            (ideální pro synchronizaci historie).
            """
            path = '/sync/watched/movies' # <--- Tento endpoint vrací data, která potřebujeme
            params = {'extended': extended}
            
            # Klíčová změna: Používáme tvou metodu pro volání API.
            return self.get(path, params=params, auth=True, cache_ttl=cache_ttl) or []
    
    def users_watched_shows_history(self, page: int = 1, limit: int = 200,
                                    extended: bool = True, extended_str: Optional[str] = 'full'):
        """
        Historie seriálů uživatele (watched shows) — vrací i plays/last_watched_at.
        """
        endpoint = 'users/me/watched/shows'
        params = {'page': page, 'limit': limit}
        params['extended'] = extended_str or ('full' if extended else 'min')
        return self.get(endpoint, params=params, auth=True, cache_ttl=0) or []

    def users_history(self, type_: str = 'episodes',
                      start_at: Optional[str] = None, end_at: Optional[str] = None,
                      page: int = 1, limit: int = 200, extended: str = 'min',
                      cache_ttl: int = TTL_MEDIUM):
        endpoint = f'users/me/history/{type_}'
        params = {'page': page, 'limit': limit}
        if start_at: params['start_at'] = start_at
        if end_at:   params['end_at'] = end_at
        if extended: params['extended'] = extended
        return self.get(endpoint, params=params, auth=True, cache_ttl=cache_ttl) or []

    def shows_progress_watched(self, trakt_id_or_slug: str,
                                    hidden: bool = False, specials: bool = False,
                                    count_specials: bool = False, last_activity: bool = True, extended: str = None,
                                    cache_ttl: int = 0):
            if not trakt_id_or_slug:
                return None
            endpoint = f"shows/{trakt_id_or_slug}/progress/watched"
            
            # 📢 OPRAVA: Sestavujeme parametry POUZE pokud je hodnota True (Trakt standard)
            params = {}
            
            # if hidden:
            #     params['hidden'] = 'true'
            # if specials:
            #     params['specials'] = 'true'
            # if count_specials:
            #     params['count_specials'] = 'true'
            # if last_activity:
            #     params['last_activity'] = 'true'
                
            return self.get(endpoint, params=params, auth=True, cache_ttl=cache_ttl)
    def shows_stats(self, trakt_id_or_slug: str, cache_ttl: int = 0):
        """Získá detailní statistiky seriálu, včetně počtu odvysílaných epizod."""
        if not trakt_id_or_slug:
            return None
        endpoint = f"shows/{trakt_id_or_slug}/stats"
        # Endpoint stats nevyžaduje autorizaci
        return self.get(endpoint, auth=False, cache_ttl=cache_ttl)

    def last_activities(self) -> Optional[Dict[str, Any]]:
        return self.get('sync/last_activities', auth=True, cache_ttl=0)

    # NEW — show summary (public endpoint) — doplnění titulu/roku/ids/overview
    def show_summary(self, trakt_id_or_slug: str, extended: str = 'min') -> Dict[str, Any]:
        if not trakt_id_or_slug:
            return {}
        endpoint = f'shows/{trakt_id_or_slug}'
        params = {'extended': extended} if extended else None
        data = self.get(endpoint, params=params, auth=False, cache_ttl=TTL_LONG) or {}
        if isinstance(data, dict):
            return {
                'title': data.get('title') or '',
                'year': data.get('year'),
                'ids': data.get('ids') or {},
                'overview': data.get('overview') or ''
            }
        return {}

    # --- Collections (Movies/Shows) ---
    def collection_movies(self, sort_desc: bool = True):
        data = self.get('users/me/collection/movies', params={'extended': 'full'}, auth=True, cache_ttl=0) or []
        def _sort_key(row): return (row.get('listed_at') or row.get('collected_at') or '')
        data.sort(key=_sort_key, reverse=sort_desc)
        return data

    def collection_shows(self, sort_desc: bool = True):
        data = self.get('users/me/collection/shows', params={'extended': 'full'}, auth=True, cache_ttl=0) or []
        def _sort_key(row): return (row.get('listed_at') or row.get('collected_at') or '')
        data.sort(key=_sort_key, reverse=sort_desc)
        return data

# === Progress cache manager (cache-first) ====================================
class ProgressCacheManager:
    def __init__(self):
        from resources.lib.database.metadata_db import db
        self.db = db

    def load_cache(self):
        """Načte progress všech seriálů z centrální DB"""
        cache = {"shows": {}, "last_activity": ""}
        try:
            rows = self.db.conn.execute("""
                SELECT tmdb_id, trakt_watched, trakt_collected FROM media 
                WHERE media_type = 'tv' AND (trakt_watched IS NOT NULL OR trakt_collected IS NOT NULL)
            """).fetchall()
            for tmdb_id, watched, collected in rows:
                cache["shows"][str(tmdb_id)] = {
                    "watched": watched or 0,
                    "collected": collected or 0
                }
            log.info(f"[Trakt] ProgressCacheManager: načteno {len(cache['shows'])} seriálů z DB")
        except Exception as e:
            log.error(f"[Trakt] ProgressCacheManager load error: {e}")
        return cache

    def save_cache(self, cache):
        """Uloží progress všech seriálů do centrální DB"""
        try:
            for tmdb_id_str, data in cache.get("shows", {}).items():
                tmdb_id = int(tmdb_id_str)
                self.db.update(tmdb_id, {
                    'media_type': 'tv',
                    'trakt_episodes_completed': data.get("watched", 0)
                }, source='trakt_progress')
            log.info(f"[Trakt] ProgressCacheManager: uloženo {len(cache.get('shows', {}))} seriálů do DB")
        except Exception as e:
            log.error(f"[Trakt] ProgressCacheManager save error: {e}")

    def ensure_cache(self):
            """
            [DEPRECATED] Dříve zajišťovalo načtení JSON cache.
            Nyní už není potřeba, protože data se načítají a aktualizují on-demand z metadata.db.
            Metoda je ponechána jen pro zpětnou kompatibilitu, aby se nerozbilo volání z trakt_module/handlerů.
            """
            pass            

class WatchedMoviesCacheManager:
    def __init__(self):
        from resources.lib.database.metadata_db import db
        self.db = db

    def load_cache(self):
        try:
            rows = self.db.conn.execute("""
                SELECT tmdb_id FROM media 
                WHERE media_type = 'movie' AND trakt_is_fully_watched = 1
            """).fetchall()
            cache = [str(row[0]) for row in rows]
            log.info(f"[Trakt] WatchedMovies: načteno {len(cache)} filmů z DB")
            return cache
        except Exception as e:
            log.error(f"[Trakt] WatchedMovies load error: {e}")
            return []

    def save_cache(self, tmdb_ids: list):
            """Ukládá seznam TMDb ID filmů do centrální metadata.db."""
            try:
                from resources.lib.database.metadata_db import db
                count = 0
                
                # tmdb_ids je seznam TMDb ID
                for tid in tmdb_ids:
                    # Nastavíme film jako zhlédnutý (1) přesměrovaně do is_fully_watched
                    db.update(int(tid), {'media_type': 'movie', 'trakt_is_fully_watched': 1}, source='trakt_movie')
                    count += 1
                
                log.info(f"[Trakt] WatchedMovies: uloženo {count} filmů do DB")
                return True
                
            except Exception as e:
                log.error(f"[Trakt] WatchedMoviesCacheManager save_cache error: {e}")
                return False
        
class WatchedShowsCacheManager:
    def __init__(self):
        from resources.lib.database.metadata_db import db
        self.db = db

    def load_cache(self):
            """Načte seznam seriálů z DB jako plné slovníky (pro listování a řazení)."""
            try:
                # 💡 NOVÁ LOGIKA: Použijeme cursor a sqlite3.Row pro získání slovníků 
                # (když db.query není dostupné)
                
                # Vytvoříme dočasný cursor a nastavíme row_factory pro přístup k datům jménem sloupce
                cursor = self.db.conn.cursor()
                cursor.row_factory = sqlite3.Row 
                
                # Select VŠECH sloupců (SELECT *) a filtrujeme + řadíme přímo v DB (nejefektivnější)
                cursor.execute("""
                SELECT 
                    tmdb_id, 
                    title_en, 
                    title_cs, 
                    title_original, 
                    trakt_last_watched_at, 
                    -- NOVÉ/DOPLNĚNÉ SLUPCE: Nutné pro X/Y progres a UI
                    overview_en,
                    overview_cs,
                    trakt_total_episodes,
                    trakt_episodes_aired,
                    trakt_episodes_completed,
                    trakt_is_fully_watched,
                    trakt_next_episode_title
                FROM media 
                WHERE media_type = 'tv' 
                    AND (trakt_is_fully_watched = 1 OR trakt_episodes_completed > 0)
                    -- last_watched_at je klíčový pro určení, že seriál byl sledován.
                    AND trakt_last_watched_at IS NOT NULL
                ORDER BY trakt_last_watched_at DESC
                """)

                rows = cursor.fetchall()
               
                # Převedeme sqlite3.Row objekty na skutečné Python slovníky (dict)
                # To zaručí maximální kompatibilitu s Python logikou v list_watched_shows.
                cache = [dict(row) for row in rows] 
                
                log.info(f"[Trakt] WatchedShowsCacheManager: načteno {len(cache)} seriálů z DB (plné slovníky)")
                return cache

            except Exception as e:
                log.error(f"[Trakt] WatchedShowsCacheManager load error: {e}")
                return []
                

    def save_cache(self, tmdb_ids: list):
            """Ukládá seznam TMDb ID kompletně zhlédnutých seriálů do centrální metadata.db."""
            try:
                from resources.lib.database.metadata_db import db
                count = 0
                
                # tmdb_ids je seznam TMDb ID
                for tid in tmdb_ids:
                    # Nastavíme seriál jako dokončený (1 místo -1 z v2 a přemapováno do is_fully_watched)
                    db.update(int(tid), {'media_type': 'tv', 'trakt_is_fully_watched': 1}, source='trakt_show_complete')
                    count += 1
                
                log.info(f"[Trakt] WatchedShowsCacheManager: uloženo {count} dokončených seriálů do DB")
                return True
                
            except Exception as e:
                log.error(f"[Trakt] WatchedShowsCacheManager save_cache error: {e}")
                return False
        
# ====================================================================
# --- SAMOSTATNÉ FUNKCE (CACHE REFRESH) ---
# ====================================================================
def trakt_progress_cache_refresh_full_ui():
    """
    Aktualizuje všechny Trakt cache: pokrok seriálů + historie filmů.
    Používá inkrementální logiku: nevolá drahé API ani neaktualizuje DB, pokud se čas poslední aktivity nezměnil.
    Zobrazuje progress dialog.
    """
    LOG = "mm.trakt_cache_refresh"
    try:
        from resources.lib.database.metadata_db import db
        from resources.lib.trakt.trakt_module import get_tmdb_details_fallback
        
        api = TraktAPI() 
        dlg = xbmcgui.DialogProgress()
        dlg.create("Trakt • Aktualizace cache", "Inicializace...")

        # --- 1) Pokrok seriálů (rozpracované seriály) ---
        dlg.update(10, "Stahuji seznam sledovaných seriálů (Pokrok)...")

        # api.users_watched_shows_history() je rychlé, protože vrací jen základní data a last_watched_at
        watched_shows = api.users_watched_shows_history() or []
        total_shows = len(watched_shows)
        
        shows_to_update = 0
        shows_skipped = 0

        for idx, item in enumerate(watched_shows):
            if dlg.iscanceled(): dlg.close(); return
            if not isinstance(item, dict): continue

            last_watched = item.get('last_watched_at')
            pct = 10 + int((idx / total_shows) * 40) if total_shows else 10
            dlg.update(pct, f"Zpracovávám seriál {idx + 1}/{total_shows} (Aktualizováno: {shows_to_update}, Přeskočeno: {shows_skipped})")

            show = item.get('show', {})
            tmdb_id = show.get('ids', {}).get('tmdb')
            trakt_id = show.get('ids', {}).get('trakt')

            if tmdb_id and trakt_id:
                title_log = show.get('title', 'Neznámý seriál')
                
                aired_count = 0
                completed_count = 0
                is_fully_watched = 0
                next_episode_id = None
                next_episode_title = ""
                total_episodes = 0 # <--- NOVÁ INICIALIZACE
                
                # --- INKREMENTÁLNÍ KONTROLA: Seriály (Klíčové pro rychlost) ---
                cached_data = db.get(tmdb_id) or {}
                last_watched_cached = cached_data.get('trakt_last_watched_at')
                total_episodes_cached = cached_data.get('trakt_total_episodes') # <--- NAČTENÍ NOVÉHO POLE Z CACHE

                # OPRAVENÁ LOGIKA PŘESKOČENÍ: PŘESKOČÍME POUZE, POKUD JSOU DATA AKTUÁLNÍ A MÁME ULOŽENÉ total_episodes
                is_fully_synced = (last_watched == last_watched_cached) and (total_episodes_cached is not None and total_episodes_cached > 0)
                
                if is_fully_synced:
                    # 1. Čas se neshoduje a metadata jsou kompletní, přeskočíme volání API a použijeme lokální DB
                    shows_skipped += 1
                    log.debug(f"[TraktSync] Pokrok seriálu {title_log} ({trakt_id}) je aktuální a kompletní, přeskočeno API volání.")
                    
                    # Načteme existující progress data z DB
                    is_fully_watched = cached_data.get('trakt_is_fully_watched', 0)
                    next_episode_title = cached_data.get('trakt_next_episode_title', "")
                    next_episode_id = cached_data.get('trakt_next_episode_id')
                    aired_count = cached_data.get('trakt_episodes_aired')
                    completed_count = cached_data.get('trakt_episodes_completed')
                    total_episodes = total_episodes_cached # <--- NAČTENÍ HODNOTY total_episodes
                else:
                    # 2. Čas se změnil NEBO chybí metadata, musíme provést drahé API volání pro aktuální stav
                    shows_to_update += 1
                    log.debug(f"[TraktSync] Aktualizuji pokrok seriálu {title_log} ({trakt_id}). Volám shows_progress_watched.")

                    # --- ZÍSKÁNÍ CELKOVÉHO POČTU EPIZOD Z TMDb CACHE/FALLBACKU ---
                    try:
                        tmdb_details = get_tmdb_details_fallback(tmdb_id, 'tv') 
                        if tmdb_details:
                            total_episodes = tmdb_details.get('number_of_episodes', 0)
                        log.debug(f"[TMDb] Získáno celkem epizod: {total_episodes} pro {tmdb_id}")
                    except Exception as e:
                        log.warning(f"[TMDbSync] Chyba při získávání detailů TMDb pro {tmdb_id}: {e}")
                    # -----------------------------------------------------------
                    
                    try:
                        show_progress = api.shows_progress_watched(
                            str(trakt_id),
                            extended="full",
                            cache_ttl=0 # Požadujeme aktuální data
                        )

                        if show_progress:
                            aired_count = show_progress.get('aired_episodes') or show_progress.get('aired', 0)
                            completed_count = show_progress.get('completed', 0)
                            next_ep = show_progress.get('next_episode') # <--- KLÍČOVÝ ÚDAJ PRO OPRAVU

                            # OPRAVENÁ LOGIKA fully_watched: Zahrnuje kontrolu, zda existuje další epizoda
                            if aired_count > 0 and aired_count == completed_count and next_ep is None:
                                is_fully_watched = 1
                            else:
                                is_fully_watched = 0
                            # -------------------------------------------------------------
                            
                            if next_ep:
                                next_episode_id = next_ep.get('ids', {}).get('trakt')
                                season = next_ep.get('season')
                                episode = next_ep.get('number')
                                ep_title = next_ep.get('title') or ""

                                if season is not None and episode is not None:
                                    next_episode_title = f"S{season:02d}E{episode:02d} – {ep_title}"
                                else:
                                    next_episode_title = ep_title

                            # Logování zde zůstává pro diagnostiku
                            log.info(
                                f"[DEBUG PROGRESS] {title_log} ({trakt_id}): "
                                f"Aired={aired_count}, Completed={completed_count}, FullyWatched={is_fully_watched}"
                            )

                    except Exception as e:
                        log.warning(f"[TraktSync] Chyba progressu pro {tmdb_id} / {trakt_id}: {e}")
                        # Při chybě použijeme data z cache jako fallback
                        is_fully_watched = cached_data.get('trakt_is_fully_watched', 0)
                        next_episode_title = cached_data.get('trakt_next_episode_title', "")
                        next_episode_id = cached_data.get('trakt_next_episode_id')
                        aired_count = cached_data.get('trakt_episodes_aired')
                        completed_count = cached_data.get('trakt_episodes_completed')
                        # total_episodes je v tuto chvíli již nastaven z TMDb fallbacku (nebo 0)

                db.update(tmdb_id, {
                    'media_type': 'tv',
                    'title_en': show.get('title'),
                    'trakt_full_data': json.dumps(item),
                    'trakt_episodes_aired': aired_count,
                    'trakt_episodes_completed': completed_count,
                    'trakt_is_fully_watched': is_fully_watched,
                    'trakt_next_episode_title': next_episode_title,
                    'trakt_last_watched_at': last_watched,
                    'trakt_next_episode_id': next_episode_id,
                    'trakt_total_episodes': total_episodes # <--- NOVÉ POLE DO DB
                }, source='trakt_refresh')

        # --- 2) Historie filmů ---
        # ... zbytek funkce pro Filmy
        
        dlg.update(50, "Stahuji historii filmů (Dokončené)...")
        if dlg.iscanceled(): dlg.close(); return

        # ... kód pro aktualizaci filmů ...
        movies_data = api.watched_movies(cache_ttl=0) or []
        total_movies = len(movies_data)

        movie_tmdb_ids = []
        movies_to_update = 0
        movies_skipped = 0
        
        for idx, m in enumerate(movies_data):
            if dlg.iscanceled(): dlg.close(); return
            
            pct = 50 + int((idx / total_movies) * 30) if total_movies else 50
            dlg.update(pct, f"Zpracovávám filmy: {idx + 1}/{total_movies} (Aktualizováno: {movies_to_update}, Přeskočeno: {movies_skipped})")
            
            # if isinstance(m, dict) and m.get('movie'):
            #     tid = m['movie'].get('ids', {}).get('tmdb')
            if isinstance(m, dict) and m.get('movie'):
                movie_item = m['movie']
                tid = movie_item.get('ids', {}).get('tmdb')
                title_log = movie_item.get('title', 'Neznámý film')
                
                # 🚨 KLÍČOVÁ DIAGNOSTIKA 🚨
                # log.info(f"[TraktSync] Film '{title_log}' má TMDb ID: {tid}")
                # ------------------------
                
                if tid:
                    # --- INKREMENTÁLNÍ KONTROLA: Filmy ---
                    last_watched_new = m.get('last_watched_at')
                    
                    cached_data = db.get(tid) or {}
                    last_watched_cached = cached_data.get('trakt_last_watched_at')
                    
                    # Kontrola, zda je Last Watched stejný
                    if last_watched_new == last_watched_cached:
                        movies_skipped += 1
                        log.debug(f"[TraktSync] Film {tid} je aktuální (time). Přeskočeno UPDATE DB.")
                    else:
                        movies_to_update += 1
                        log.debug(f"[TraktSync] Aktualizuji film {tid} v DB. Time: {last_watched_cached} -> {last_watched_new}.")
                        
                        db.update(tid, {
                            'media_type': 'movie',
                            'trakt_last_watched_at': last_watched_new,
                            'trakt_is_fully_watched': 1
                        }, source='trakt_refresh')

                    movie_tmdb_ids.append(tid)
                else:
                    # 🚨 KLÍČOVÉ LOGOVÁNÍ PRO DIAGNOSTIKU 🚨
                    trakt_id = movie_item.get('ids', {}).get('trakt')
                    log.error(f"[TraktSync] Chyba: Film '{title_log}' (Trakt ID: {trakt_id or 'N/A'}) NEMA TMDb ID! Přeskočeno uložení v DB.")

        WatchedMoviesCacheManager().save_cache(movie_tmdb_ids)

        dlg.update(100, f"Hotovo. Seriály aktualizovány: {shows_to_update}, Filmy aktualizovány: {movies_to_update}.")
        dlg.close()
        xbmcgui.Dialog().notification('Trakt', 'Cache pokroku a historie byla úspěšně aktualizována.', xbmcgui.NOTIFICATION_INFO, 3000)
        xbmc.executebuiltin('Container.Refresh')

    except Exception as e:
        log.error(f"[{LOG}] error: {e}")
        try: dlg.close()
        except: pass
        xbmcgui.Dialog().notification('Trakt', 'Chyba při aktualizaci cache (viz log).', xbmcgui.NOTIFICATION_ERROR, 3000)

