# -*- coding: utf-8 -*-
"""
Series UI Builder: tvorba Kodi menu (ListItem, DirectoryItems).
"""
import os
import datetime
import hashlib
import json
import xbmc
import xbmcgui
import xbmcplugin
import xbmcaddon
from resources.lib.utils.log_utils import get_logger
from resources.lib.utils.ui_utils import clean_filename
from resources.lib.utils.context_builder import build_context_menu, style_context_menu
from resources.lib.database.metadata_db import db
from resources.lib.cache.menu_cache_server import get_menu_cache_server
from resources.lib.cache.menu_cache_config import is_cache_enabled, get_cache_ttl, log_cache_event

# Importy z našich nových modulů
from resources.lib.series.series_helpers import (
    _default_build_url,
    format_date_cz,
    _features_to_stream_info,
    _norm,
    episode_probe_marker_suffix,
    season_probe_marker_suffix,
)
from resources.lib.series.series_logic import is_series_completed, is_series_started
from resources.lib.down.offline_cache_utils import (
    offline_cache_lookup_keys,
    get_merged_offline_cache,
    build_disk_folder_to_storage_key_index,
)

log = get_logger()


def set_video_info(li, info_labels: dict):
    """Lokální setter metadata přes InfoTagVideo (bez importu tmdb_kodi)."""
    if not isinstance(info_labels, dict):
        return
    try:
        tag = li.getVideoInfoTag()
    except Exception:
        return

    title = info_labels.get('title')
    plot = info_labels.get('plot')
    aired = info_labels.get('aired')
    mediatype = info_labels.get('mediatype')
    season = info_labels.get('season')
    episode = info_labels.get('episode')

    if title:
        tag.setTitle(str(title))
    if plot:
        tag.setPlot(str(plot))
    if aired:
        try:
            tag.setFirstAired(str(aired))
        except Exception:
            pass
    if mediatype:
        try:
            tag.setMediaType(str(mediatype))
        except Exception:
            pass
    if season is not None:
        try:
            tag.setSeason(int(season))
        except Exception:
            pass
    if episode is not None:
        try:
            tag.setEpisode(int(episode))
        except Exception:
            pass


def _art_snapshot(li):
    art = {}
    if hasattr(li, 'getArt'):
        for k in ('icon', 'thumb', 'poster', 'fanart'):
            try:
                v = li.getArt(k)
            except Exception:
                v = ''
            if v:
                art[k] = v
    return art


def _offline_signature(offline_items):
    try:
        tuples = []
        for i in offline_items or []:
            tuples.append((int(i.get('season', -1)), int(i.get('episode', -1)), str(i.get('path', ''))))
        tuples.sort()
        raw = json.dumps(tuples, ensure_ascii=False)
        return hashlib.md5(raw.encode('utf-8')).hexdigest()[:10]
    except Exception:
        return 'nosig'


def _restore_cached_directory_items(cached):
    directory_items = []
    for item in cached.get('items', []):
        li = xbmcgui.ListItem(item.get('label', ''))
        art = item.get('art') or {}
        info = item.get('info') or {}
        props = item.get('properties') or {}
        cm = item.get('context') or []

        if art:
            li.setArt(art)
        if info:
            set_video_info(li, info)
        for k, v in props.items():
            li.setProperty(str(k), str(v))
        if cm:
            cm_norm = []
            for c in cm:
                if isinstance(c, (list, tuple)) and len(c) >= 2:
                    cm_norm.append((str(c[0]), str(c[1])))
            if cm_norm:
                cm_norm = style_context_menu(cm_norm)
                li.addContextMenuItems(cm_norm, replaceItems=True)

        directory_items.append((item.get('url', ''), li, bool(item.get('isFolder', True))))
    return directory_items

def _cz_plural_episodes(n: int) -> str:
    """České skloňování slova epizoda."""
    if n == 1: return "1 epizoda"
    if 2 <= n <= 4: return f"{n} epizody"
    return f"{n} epizod"

def _count_offline_for_series(series_name: str, series_data: dict, offline_cache: dict = None, folder_index: dict = None) -> int:
    """Spočítá stažené epizody pro celý seriál (storage key + název složky na disku)."""
    if not offline_cache:
        return 0
    seen = set()
    total = 0
    for key in offline_cache_lookup_keys(series_name, series_data, cache_all=offline_cache, folder_index=folder_index):
        for item in offline_cache.get(key, []):
            sig = (item.get('season'), item.get('episode'))
            if sig in seen:
                continue
            seen.add(sig)
            total += 1
    return total


def create_series_menu(series_manager, handle):
    """Hlavní seznam seriálů."""
    cache_server = get_menu_cache_server()
    cache_key = "series_main_menu_v1"
    if is_cache_enabled('series_main_menu'):
        cached = cache_server.get_cached_menu(cache_key)
        if cached:
            log_cache_event('HIT', 'series_main_menu', f"{cached.get('count', 0)} items")
            directory_items = _restore_cached_directory_items(cached)
            xbmcplugin.addDirectoryItems(handle, directory_items)
            xbmcplugin.setPluginCategory(handle, "Seriály")
            xbmcplugin.setContent(handle, "tvshows")
            xbmcplugin.endOfDirectory(handle, cacheToDisc=False)
            return
        else:
            log_cache_event('MISS', 'series_main_menu')

    _shared_offline_cache = db.get_offline_cache_all()
    _shared_folder_index = build_disk_folder_to_storage_key_index() if _shared_offline_cache else {}
    series_list = series_manager.get_all_series()
    directory_items = []
    serializable_items = []
    
    for s in series_list:
        series_name = s['safe_name']
        data = series_manager.load_series_data(series_name) or {}
        tm = data.get('tmdb', {})
        year = tm.get('year')
        
        seasons = data.get('seasons', {})
        seasons_count = len(seasons)
        episodes_count = sum(len(v) for v in seasons.values() if isinstance(v, dict))

        offline_count = _count_offline_for_series(series_name, data, _shared_offline_cache, _shared_folder_index)
        offline_tag = f" [COLOR green]offline: {_cz_plural_episodes(offline_count)}[/COLOR]" if offline_count > 0 else ""
        
        prefix = series_manager._label_prefix_for_series(data)
        label = f"{prefix}{data.get('name') or series_name}"
        if year: label += f" ({year})"
        label += f" [COLOR blue]{seasons_count} sez. / {episodes_count} ep.[/COLOR]{offline_tag}"

        li = xbmcgui.ListItem(label=label)
        li.setArt({
            'icon': 'DefaultFolder.png',
            'poster': tm.get('poster_url', ''),
            'fanart': tm.get('backdrop_url', ''),
            'thumb': 'DefaultHardDisk.png' if offline_count > 0 else ''
        })
        if tm.get('overview'):
            set_video_info(li, {'plot': tm['overview']})

        item_data = {
            'series_name': series_name,
            'tmdb_id': tm.get('id'),
            'title': data.get('name') or '',
        }
        cm = build_context_menu(_default_build_url, item_data, contexts=['series_manager'])
        cm.append(("Přidat do oblíbených", "Action(AddToFavorites)"))
        cm = style_context_menu(cm)
        li.addContextMenuItems(cm, replaceItems=True)

        detail_url = _default_build_url(action='series_detail', series_name=series_name)
        directory_items.append((detail_url, li, True))
        serializable_items.append((
            detail_url,
            label,
            True,
            {'plot': tm.get('overview', ''), 'title': data.get('name') or series_name},
            _art_snapshot(li),
            {},
            cm,
            False,
        ))

    if is_cache_enabled('series_main_menu') and serializable_items:
        cache_server.cache_menu(cache_key, serializable_items, get_cache_ttl('series_main_menu'))

    xbmcplugin.addDirectoryItems(handle, directory_items)

    xbmcplugin.setPluginCategory(handle, "Seriály")
    xbmcplugin.setContent(handle, "tvshows")
    xbmcplugin.endOfDirectory(handle, cacheToDisc=False)

def create_series_menu_filtered(series_manager, handle, status: str):
    """Filtrovaný seznam seriálů (Rozkoukané, Nové, Dosledované)."""
    cache_server = get_menu_cache_server()
    cache_key = f"series_filtered_menu_v1_{status}"
    if is_cache_enabled('series_filtered_menu'):
        cached = cache_server.get_cached_menu(cache_key)
        if cached:
            log_cache_event('HIT', 'series_filtered_menu', f"status={status}, {cached.get('count', 0)} items")
            directory_items = _restore_cached_directory_items(cached)
            category_titles = {'completed': "Seriály · Dosledované", 'new': "Seriály · Nově zařazené"}
            xbmcplugin.addDirectoryItems(handle, directory_items)
            xbmcplugin.setPluginCategory(handle, category_titles.get(status, "Seriály · Rozkoukané"))
            xbmcplugin.setContent(handle, 'tvshows')
            xbmc.executebuiltin('Container.SetViewMode(50)')
            xbmcplugin.endOfDirectory(handle, cacheToDisc=False)
            return
        else:
            log_cache_event('MISS', 'series_filtered_menu', f"status={status}")

    _shared_offline_cache = db.get_offline_cache_all()
    _shared_folder_index = build_disk_folder_to_storage_key_index() if _shared_offline_cache else {}
    series_list = series_manager.get_all_series()
    filtered = []
    directory_items = []
    serializable_items = []
    
    for s in series_list:
        data = series_manager.load_series_data(s['safe_name']) or {}
        completed = is_series_completed(data)
        started = is_series_started(data)

        if status == 'completed' and completed:
            filtered.append((s['safe_name'], data, s['name']))
        elif status == 'in_progress' and not completed and started:
            filtered.append((s['safe_name'], data, s['name']))
        elif status == 'new' and not completed and not started:
            filtered.append((s['safe_name'], data, s['name']))

    if status == 'in_progress':
        filtered.sort(key=lambda x: x[1].get('last_watched_at_utc') or '1970', reverse=True)

    category_titles = {'completed': "Seriály · Dosledované", 'new': "Seriály · Nově zařazené"}
    xbmcplugin.setPluginCategory(handle, category_titles.get(status, "Seriály · Rozkoukané"))

    for series_name, data, display_name in filtered:
        tm = data.get('tmdb', {})
        seasons = data.get('seasons', {})
        seasons_count = len(seasons)
        episodes_count = sum(len(v) for v in seasons.values() if isinstance(v, dict))

        offline_count = _count_offline_for_series(series_name, data, _shared_offline_cache, _shared_folder_index)
        offline_tag = f" [COLOR green]offline: {_cz_plural_episodes(offline_count)}[/COLOR]" if offline_count > 0 else ""
        
        prefix = series_manager._label_prefix_for_series(data)
        label = f"{prefix}{data.get('name') or series_name}"
        if tm.get('year'): label += f" ({tm['year']})"
        label += f" [COLOR blue]{seasons_count} sez. / {episodes_count} ep.[/COLOR]{offline_tag}"

        li = xbmcgui.ListItem(label=label)
        li.setArt({
            'icon': 'DefaultFolder.png',
            'poster': tm.get('poster_url', ''),
            'fanart': tm.get('backdrop_url', ''),
            'thumb': 'DefaultHardDisk.png' if offline_count > 0 else ''
        })
        if tm.get('overview'):
            set_video_info(li, {'plot': tm['overview']})

        cm = build_context_menu(
            _default_build_url,
            {
                'series_name': series_name,
                'tmdb_id': tm.get('id'),
                'title': data.get('name') or display_name or '',
            },
            contexts=['series_manager'],
        )
        # Override pro Odebrat seriál s parametrem status
        remove_idx = next((i for i, (name, _) in enumerate(cm) if "Odebrat seriál" in name), -1)
        if remove_idx != -1:
            url = _default_build_url(action='series_remove', series_name=series_name, status=status, tmdb_id=tm.get('id') or '')
            cm[remove_idx] = ("Odebrat seriál", f"RunPlugin({url})")
        cm.append(("Přidat do oblíbených", "Action(AddToFavorites)"))
        cm = style_context_menu(cm)
        li.addContextMenuItems(cm, replaceItems=True)

        detail_url = _default_build_url(action='series_detail', series_name=series_name)
        directory_items.append((detail_url, li, True))
        serializable_items.append((
            detail_url,
            label,
            True,
            {'plot': tm.get('overview', ''), 'title': data.get('name') or series_name},
            _art_snapshot(li),
            {},
            cm,
            False,
        ))

    if is_cache_enabled('series_filtered_menu') and serializable_items:
        cache_server.cache_menu(cache_key, serializable_items, get_cache_ttl('series_filtered_menu'))

    xbmcplugin.addDirectoryItems(handle, directory_items)

    xbmcplugin.setContent(handle, 'tvshows')
    xbmc.executebuiltin('Container.SetViewMode(50)')
    xbmcplugin.endOfDirectory(handle, cacheToDisc=False)

def create_seasons_menu(series_manager, handle, series_name, build_url_fn=None):
    """Seznam sezón seriálu."""
    builder = build_url_fn or _default_build_url
    ADDON = xbmcaddon.Addon()
    # Barevné tagy pro stav epizod (původně 'series_progress_color_tags')
    color_tags = True
    series_data = series_manager.load_series_data(series_name)
    if not series_data:
        xbmcgui.Dialog().notification('YaWSP', 'Data seriálu nenalezena', xbmcgui.NOTIFICATION_WARNING)
        xbmcplugin.endOfDirectory(handle, succeeded=False)
        return

    _offline_cache = get_merged_offline_cache(series_name, series_data)

    cache_server = get_menu_cache_server()
    series_hash = (series_data.get('_content_hash') or '')[:12]
    offline_sig = _offline_signature(_offline_cache)
    cache_key = f"series_seasons_v3_{clean_filename(series_name)}_{series_hash}_{offline_sig}"

    if is_cache_enabled('series_seasons'):
        cached = cache_server.get_cached_menu(cache_key)
        if cached:
            log_cache_event('HIT', 'series_seasons', f"{series_name}, {cached.get('count', 0)} items")
            directory_items = _restore_cached_directory_items(cached)
            display_name = (series_data.get('name') or series_name)
            xbmcplugin.addDirectoryItems(handle, directory_items)
            xbmcplugin.setPluginCategory(handle, f"Sezóny: {display_name}")
            xbmcplugin.setContent(handle, "seasons")
            xbmc.executebuiltin("Container.SetViewMode(50)")
            xbmcplugin.endOfDirectory(handle, cacheToDisc=False)
            return
        else:
            log_cache_event('MISS', 'series_seasons', f"{series_name}")

    tm = series_data.get('tmdb', {})
    poster_url, fanart_url = tm.get('poster_url', ''), tm.get('backdrop_url', '')
    display_name = series_data.get('name') or series_name
    directory_items = []
    serializable_items = []
    
    # Hlavička
    li_info = xbmcgui.ListItem(label=display_name)
    li_info.setArt({'icon': 'DefaultFolder.png', 'poster': poster_url, 'fanart': fanart_url})
    if tm.get('overview'): set_video_info(li_info, {'plot': tm['overview']})
    cm_info = [
        ("Aktualizovat seriál", f"RunPlugin({builder(action='series_refresh_smart', series_name=series_name)})"),
        ("Nastavit limit streamů", f"RunPlugin({builder(action='series_set_max_streams', series_name=series_name)})"),
        ("Odebrat seriál", f"RunPlugin({builder(action='series_remove', series_name=series_name, tmdb_id=tm.get('id') or '')})"),
    ]
    cm_info = style_context_menu(cm_info)
    li_info.addContextMenuItems(cm_info, replaceItems=False)
    info_url = builder(action='noop')
    directory_items.append((info_url, li_info, False))
    serializable_items.append((
        info_url,
        display_name,
        False,
        {'plot': tm.get('overview') or '', 'title': display_name},
        _art_snapshot(li_info),
        {},
        cm_info,
        False,
    ))

    # Akce — isFolder=True pro folder akce, isFolder=False pro RunPlugin akce
    # POZOR: nikdy nepoužívat RunPlugin(...) wrapper jako URL v addDirectoryItem!

    # Spočítej aktuální platný limit pro suffix dlaždice "Aktualizovat streamy"
    _max_override = int((series_data.get('extras') or {}).get('max_streams_override') or 0)
    if _max_override > 0:
        _effective_limit = _max_override
    else:
        try:
            _effective_limit = int(ADDON.getSetting('series_max_streams_per_episode') or 50)
        except (TypeError, ValueError):
            _effective_limit = 50
    _refresh_label = f"Aktualizovat streamy  [limit: {_effective_limit}]"

    for label, action, icon in [
        (_refresh_label, 'series_refresh_smart', 'DefaultAddonsSearch.png'),
        ("Ověřit dostupnost streamů", 'series_validate_streams', 'DefaultAddonService.png'),
        ("Zobrazit uložené streamy", 'series_streams_list', 'DefaultHardDisk.png'),
        ("Odebrat seriál", 'series_remove', 'DefaultIconError.png'),
    ]:
        li = xbmcgui.ListItem(label=label)
        li.setArt({'icon': icon, 'poster': poster_url, 'fanart': fanart_url})
        args = {'action': action, 'series_name': series_name}

        if action == 'series_remove':
            args['from_detail'] = '1'
            if tm.get('id'): args['tmdb_id'] = tm['id']

        # is_folder=False pro akce, které Kodi pouze spustí na pozadí a neočekává adresář
        is_folder = action == 'series_streams_list'
        action_url = builder(**args)
        directory_items.append((action_url, li, is_folder))
        serializable_items.append((
            action_url,
            label,
            bool(is_folder),
            {'title': label},
            _art_snapshot(li),
            {},
            [],
            False,
        ))


    today = datetime.date.today()
    for s_key in sorted(series_data.get('seasons', {}).keys(), key=int):
        season_data = series_data['seasons'][s_key]
        state = series_data.get('season_state', {}).get(s_key, {})
        w_count = sum(1 for ep in season_data.values() if (ep or {}).get('watched'))
        try:
            total = int(state.get('total', len(season_data)))
        except (ValueError, TypeError):
            total = len(season_data)
        if str(s_key) != '0':
            tmdb_total = (((series_data.get('tmdb') or {}).get('seasons') or {}).get(str(s_key)) or {}).get('episode_count')
            try:
                if tmdb_total is not None:
                    total = max(len(season_data), int(tmdb_total))
            except (ValueError, TypeError):
                total = max(len(season_data), total)
        elif total is None:
            total = len(season_data)
        w_flag = bool(total > 0 and w_count == total)
        
        prefix = ("[COLOR lime][ok][/COLOR] " if color_tags else "[ok] ") if w_flag else ""
        offline_count = sum(1 for item in _offline_cache if item.get('season') == int(s_key))
        offline_tag = f" [COLOR green]offline[/COLOR] {_cz_plural_episodes(offline_count)}" if offline_count > 0 else ""
        
        # Spočítej chybějící
        missing = 0
        for ep_k, ep in season_data.items():
            if ep.get('streams'): continue
            air = (ep.get('tmdb') or {}).get('air_date') or ep.get('air_date')
            if air and len(air) >= 10:
                try:
                    if datetime.date.fromisoformat(air[:10]) > today: continue
                except: pass
            if not any(i.get('season') == int(s_key) and i.get('episode') == int(ep_k) for i in _offline_cache):
                missing += 1
        
        miss_suffix = f" [COLOR red]chybí: {missing}[/COLOR]" if missing > 0 else ""
        probe_suffix = season_probe_marker_suffix(season_data)
        label = f"{prefix}Sezona {s_key} [{w_count}/{total}]{offline_tag}{miss_suffix}{probe_suffix}"

        li = xbmcgui.ListItem(label=label)
        li.setArt({'icon': 'DefaultFolder.png', 'poster': poster_url, 'fanart': fanart_url, 'thumb': 'DefaultHardDisk.png' if offline_count > 0 else ''})
        if tm.get('overview'): set_video_info(li, {'plot': tm['overview']})

        cm = build_context_menu(
            builder,
            {
                'series_name': series_name,
                'tmdb_id': tm.get('id'),
                'season': s_key,
                'title': series_data.get('name') or '',
            },
            contexts=['series_manager'],
        )
        # Override pro sezónní akce
        for i, (name, _) in enumerate(cm):
            if "Aktualizovat seriál" in name: cm[i] = ("Aktualizovat tuto sezónu", f"RunPlugin({builder(action='series_refresh_smart', series_name=series_name, season=s_key)})")
            elif "Odebrat seriál" in name: cm[i] = ("Odebrat tuto sezónu", f"RunPlugin({builder(action='series_remove_season', series_name=series_name, season=s_key)})")
            elif "Stáhnout všechny sezóny" in name: cm[i] = ("Stáhnout celou sezónu", f"RunPlugin({builder(action='series_download_season', series_name=series_name, season=s_key)})")
            elif "Trakt" in name and tm.get('id'):
                try: url = builder(action='series_trakt_mark_season_prompt', tmdb_id=int(tm['id']), season=int(s_key))
                except: url = builder(action='series_trakt_mark_season_prompt', tmdb_id=tm['id'], season=s_key)
                cm[i] = ("Odeslat SEZÓNU na Trakt…", f"RunPlugin({url})")

        cm.append(("Označit sezónu jako zhlédnutou", f"RunPlugin({builder(action='series_mark_season', series_name=series_name, tmdb_id=tm.get('id'), season=s_key, progress=100)})"))
        # Sezónní sken: ffprobe z nastavení, jinak http_range_probe (Android/CoreELEC/Linux/Windows).
        cm.append(("Detekovat CZ titulky (sezóna)", f"RunPlugin({builder(action='series_detect_czsubs_season', series_name=series_name, season=s_key)})"))

        cm = style_context_menu(cm)
        li.addContextMenuItems(cm, replaceItems=True)
        season_url = builder(action='series_season', series_name=series_name, season=s_key)
        directory_items.append((season_url, li, True))
        serializable_items.append((
            season_url,
            label,
            True,
            {'plot': tm.get('overview') or '', 'title': f'Sezona {s_key}'},
            _art_snapshot(li),
            {},
            cm,
            False,
        ))

    if is_cache_enabled('series_seasons') and serializable_items:
        cache_server.cache_menu(cache_key, serializable_items, get_cache_ttl('series_seasons'))

    xbmcplugin.addDirectoryItems(handle, directory_items)

    xbmcplugin.setPluginCategory(handle, f"Sezóny: {display_name}")
    xbmcplugin.setContent(handle, "seasons")
    xbmc.executebuiltin("Container.SetViewMode(50)")
    xbmcplugin.endOfDirectory(handle, cacheToDisc=False)

def create_episodes_menu(series_manager, handle, series_name, season_num, build_url_fn=None):
    """Seznam epizod sezóny."""
    from resources.lib.series.subcaps_cache import has_cz_subs, audio_badges, video_badges
    builder = build_url_fn or _default_build_url
    ADDON = xbmcaddon.Addon()
    # Barevné tagy pro stav epizod (původně 'series_progress_color_tags')
    color_tags = True
    series_data = series_manager.load_series_data(series_name)

    if not series_data or str(season_num) not in series_data.get('seasons', {}):
        xbmcgui.Dialog().notification('YaWSP', 'Data sezóny nenalezena', xbmcgui.NOTIFICATION_WARNING)
        xbmcplugin.endOfDirectory(handle, succeeded=False)
        return

    _offline_cache = get_merged_offline_cache(series_name, series_data)

    cache_server = get_menu_cache_server()
    series_hash = (series_data.get('_content_hash') or '')[:12]
    offline_sig = _offline_signature(_offline_cache)
    cache_key = f"series_season_v3_{clean_filename(series_name)}_{season_num}_{series_hash}_{offline_sig}"
    
    if is_cache_enabled('series_season'):
        cached = cache_server.get_cached_menu(cache_key)
        if cached:
            log_cache_event('HIT', 'series_season', f"{series_name} S{season_num}, {cached['count']} items")
            directory_items = _restore_cached_directory_items(cached)
            xbmcplugin.addDirectoryItems(handle, directory_items)
            xbmcplugin.setContent(handle, "tvshows")
            xbmc.executebuiltin("Container.SetViewMode(50)")
            xbmcplugin.endOfDirectory(handle, cacheToDisc=False)
            return
        else:
            log_cache_event('MISS', 'series_season', f"{series_name} S{season_num}")

    tm_s = series_data.get('tmdb', {})
    season_key = str(season_num)
    season = series_data['seasons'][season_key]
    today = datetime.date.today()

    # === Collect items for caching ===
    directory_items = []
    serializable_items = []
    
    for e_num in sorted(season.keys(), key=int):
        ep = season[e_num]
        tm_e = ep.get('tmdb', {})
        ep_title = ep.get('name') or ep.get('title') or ep.get('source_name') or f"Epizoda {e_num}"
        
        # Badges (sjednocené jazykové tagy: CZdab / CZtit)
        idents = [st.get('ident') for st in ep.get('streams', []) if st.get('ident')]
        from resources.lib.series.series_helpers import normalize_language_features
        feats_norm_per_stream = [
            normalize_language_features(st.get('features', []))
            for st in ep.get('streams', [])
        ]

        badges = []
        if any('CZdab' in fs for fs in feats_norm_per_stream) or any(
            any(b == 'CZdab' for b in audio_badges(i)) for i in idents
        ):
            badges.append('CZdab')
        if any('CZtit' in fs for fs in feats_norm_per_stream) or any(
            has_cz_subs(i) for i in idents
        ):
            badges.append('CZtit')

        try:
            log.debug(
                f"season_badges series={series_name} S{int(season_num):02d}E{int(e_num):02d} "
                f"idents={len(idents)} badges={badges} features_lang={feats_norm_per_stream}"
            )
        except Exception:
            pass
        
        b_tail = f"  [{', '.join(badges)}]" if badges else ""
        probe_tail = episode_probe_marker_suffix(ep)
        suffix = series_manager._label_suffix_for_episode(ep, color_tags)
        
        local_path = next((i.get('path') for i in _offline_cache if i.get('season') == int(season_key) and i.get('episode') == int(e_num)), None)
        offline_tag = " [COLOR limegreen]offline[/COLOR]" if local_path else ""

        air = tm_e.get('air_date') or ep.get('air_date')
        is_future = False
        if isinstance(air, str) and len(air) >= 10:
            try: is_future = datetime.date.fromisoformat(air[:10]) > today
            except: pass

        color = "orange" if is_future else ("red" if not ep.get('streams') and not local_path else None)
        d_pref = f"[{format_date_cz(air)}] " if (is_future and air) else ""
        
        base_label = f"S{int(season_num):02d}E{int(e_num):02d} - {ep_title} {suffix}{offline_tag}{b_tail}{probe_tail}"
        label = f"{d_pref}[COLOR {color}]{base_label}[/COLOR]" if color else f"{d_pref}{base_label}"
        
        li = xbmcgui.ListItem(label=label)
        li.setArt({
            'icon': tm_e.get('still_url', 'DefaultVideo.png'),
            'thumb': 'DefaultHardDisk.png' if local_path else tm_e.get('still_url', ''),
            'poster': tm_s.get('poster_url', ''),
            'fanart': tm_s.get('backdrop_url', '')
        })

        tag = li.getVideoInfoTag()
        tag.setTitle(ep_title)
        if tm_e.get('overview'): tag.setPlot(tm_e['overview'])
        if air: tag.setFirstAired(air)
        tag.setSeason(int(season_num)); tag.setEpisode(int(e_num))
        tag.setMediaType('episode')
        if tm_s.get('year'):
            try: tag.setYear(int(tm_s['year']))
            except (ValueError, TypeError): pass

        # Context menu
        cm = build_context_menu(
            builder,
            {
                'series_name': series_name,
                'season': season_num,
                'episode': e_num,
                'tmdb_id': tm_s.get('id'),
                'show_title': tm_s.get('name') or '',
            },
            contexts=['series_episode_item', 'titulky_episode'],
        )
        li.addContextMenuItems(cm, replaceItems=True)

        li.setProperty('IsPlayable', 'false')
        url_kwargs = {
            'action': 'play',
            'series_name': series_name,
            'season': season_key,
            'episode': e_num,
            'select': '1',
            'mediatype': 'episode',
            'offline_choice': '1',
        }
        if tm_s.get('id'):
            url_kwargs['tmdb_id'] = tm_s.get('id')
        url = builder(**url_kwargs)
        directory_items.append((url, li, False))
        serializable_items.append((
            url,
            label,
            False,
            {
                'title': ep_title,
                'plot': tm_e.get('overview', ''),
                'aired': air,
                'season': int(season_num),
                'episode': int(e_num),
                'mediatype': 'episode'
            },
            _art_snapshot(li),
            {'IsPlayable': 'false'},
            cm,
            False,
        ))

    # === MENU CACHE: Save to cache and render ===
    if is_cache_enabled('series_season') and serializable_items:
        cache_server.cache_menu(cache_key, serializable_items, get_cache_ttl('series_season'))
    
    xbmcplugin.addDirectoryItems(handle, directory_items)
    xbmcplugin.setContent(handle, "tvshows")
    xbmc.executebuiltin("Container.SetViewMode(50)")
    xbmcplugin.endOfDirectory(handle, cacheToDisc=False)

_STREAMS_LIST_HUB_THRESHOLD = 100


def _stream_counts_by_season(data):
    """Počty uložených streamů podle čísla sezóny."""
    from resources.lib.series import series_helpers as helpers

    counts = {}
    for s_i, _e_i, _ep, _st in helpers._iter_saved_streams(data):
        counts[s_i] = counts.get(s_i, 0) + 1
    return counts


_LANG_BADGES = ('CZdab', 'CZtit')


def _format_badges_prefix(badges):
    """Sestaví barevný prefix labelu z badge listu.

    Jazyk (CZdab/CZtit) zlatě, technické (rozlišení/HDR/Atmos) modře.
    Skupiny jsou obaleny jednou ``[COLOR ...]`` značkou (méně tagů → rychlejší
    rendering Kodi listu).
    """
    if not badges:
        return ''
    lang = [b for b in badges if b in _LANG_BADGES]
    tech = [b for b in badges if b not in _LANG_BADGES]
    parts = []
    if lang:
        parts.append('[COLOR gold]' + ''.join(f'[{b}]' for b in lang) + '[/COLOR]')
    if tech:
        parts.append('[COLOR deepskyblue]' + ''.join(f'[{b}]' for b in tech) + '[/COLOR]')
    return ''.join(parts) + ' '


def _stream_matches_filter(st, filter_mode):
    """True pokud stream projde filtrem (jazykové tagy přes cache-aware lookup).

    `cz_any` je union (CZdab **nebo** CZtit), `cz_both` ponecháno jako alias
    pro zpětnou kompatibilitu URL — semanticky se nyní chová identicky jako
    `cz_any` (tj. OR).
    """
    if not filter_mode or filter_mode == 'all':
        return True
    from resources.lib.series.series_helpers import get_stream_language_tags
    tags = set(get_stream_language_tags(st) or [])
    if filter_mode == 'cz_dab':
        return 'CZdab' in tags
    if filter_mode == 'cz_tit':
        return 'CZtit' in tags
    if filter_mode in ('cz_any', 'cz_both'):
        return 'CZdab' in tags or 'CZtit' in tags
    return True


def _add_saved_stream_directory_items(
    handle, data, series_name, builder, tm, season_only, filter_mode='all'
):
    """Vykreslí položky streamů (season_only=None = všechny sezóny)."""
    from resources.lib.series.series_manager import _iter_saved_streams
    from resources.lib.series.series_helpers import compact_stream_badges

    display_name = data.get('name') or series_name

    for s_i, e_i, ep, st in _iter_saved_streams(data, season_only=season_only):
        if not _stream_matches_filter(st, filter_mode):
            continue
        badges = compact_stream_badges(st)
        prefix = _format_badges_prefix(badges)
        size_gb = f" [{float(st.get('size', 0))/(1024**3):.1f} GB]" if st.get('size') else ""
        label = f"{prefix}S{int(s_i):02d}E{int(e_i):02d} · {st.get('name', 'Stream')}{size_gb}"

        li = xbmcgui.ListItem(label=label)
        li.setArt({'icon': 'DefaultVideo.png', 'poster': tm.get('poster_url', ''), 'fanart': tm.get('backdrop_url', '')})
        set_video_info(li, {'title': ep.get('name', f'Epizoda {e_i}'), 'season': int(s_i), 'episode': int(e_i)})

        vinfo, ainfo = _features_to_stream_info(st.get('features', []))
        li.addStreamInfo('video', vinfo)
        li.addStreamInfo('audio', ainfo)

        cm = build_context_menu(builder, {}, contexts=['series_saved_stream_item'])
        li.addContextMenuItems(cm, replaceItems=True)

        url = builder(
            action='queue_add',
            ident=st.get('ident'),
            name=st.get('name'),
            series_name=series_name,
            display_name=display_name,
            season=str(s_i),
            episode=str(e_i),
            size=str(st.get('size', 0)),
        )
        xbmcplugin.addDirectoryItem(handle, url, li, isFolder=False)


_FILTER_LABELS = {
    'all': 'Vše',
    'cz_dab': 'jen CZdab',
    'cz_tit': 'jen CZtit',
    'cz_any': 'CZdab nebo CZtit',
    'cz_both': 'CZdab nebo CZtit',
}


def _add_filter_item(handle, builder, series_name, season, filter_mode, tm):
    """Přidá nahoru seznamu klikací položku „Filtr: <stav> (změnit…)“."""
    fm = filter_mode if filter_mode in _FILTER_LABELS else 'all'
    label = _FILTER_LABELS[fm]
    li = xbmcgui.ListItem(
        label=f"[COLOR yellow]Filtr:[/COLOR] {label}  [COLOR grey](změnit…)[/COLOR]"
    )
    li.setArt(
        {
            'icon': 'DefaultAddonsSearch.png',
            'poster': tm.get('poster_url', ''),
            'fanart': tm.get('backdrop_url', ''),
        }
    )
    url = builder(
        action='series_streams_filter_select',
        series_name=series_name,
        season=(season or ''),
        filter_mode=fm,
    )
    xbmcplugin.addDirectoryItem(handle, url, li, isFolder=False)


def list_saved_streams(
    series_manager, handle, series_name: str, build_url_fn=None, season=None,
    filter_mode=None
):
    """Seznam uložených streamů; při více sezónách / mnoha položkách nejdřív rozcestí podle sezóny."""
    builder = build_url_fn or _default_build_url
    data = series_manager.load_series_data(series_name)
    if not data:
        return

    fm = (filter_mode or 'all').strip().lower()
    if fm not in _FILTER_LABELS:
        fm = 'all'

    tm = data.get('tmdb', {})
    display = data.get('name') or series_name
    counts = _stream_counts_by_season(data)
    total_streams = sum(counts.values())
    seasons_nonzero = sorted(
        [s for s, c in counts.items() if c > 0],
        key=lambda x: int(x) if str(x).isdigit() else 0,
    )
    use_hub = len(seasons_nonzero) > 1 or total_streams > _STREAMS_LIST_HUB_THRESHOLD

    season_norm = (season or '').strip()
    if season_norm.lower() == 'all':
        season_filter = None
    elif season_norm:
        season_filter = season_norm
    else:
        season_filter = None

    if not season_norm and use_hub:
        header = xbmcgui.ListItem(
            label=(
                f"[COLOR lime]Uložené streamy[/COLOR] · [COLOR deepskyblue]{display}[/COLOR]"
                " — vyberte sezónu"
            )
        )
        header.setArt(
            {
                'icon': 'DefaultHardDisk.png',
                'poster': tm.get('poster_url', ''),
                'fanart': tm.get('backdrop_url', ''),
            }
        )
        xbmcplugin.addDirectoryItem(handle, builder(action='noop'), header, isFolder=False)

        _add_filter_item(handle, builder, series_name, season_norm, fm, tm)

        for s_key in seasons_nonzero:
            c = counts[s_key]
            li = xbmcgui.ListItem(label=f"Sezóna {s_key} · {c} streamů")
            li.setArt(
                {
                    'icon': 'DefaultFolder.png',
                    'poster': tm.get('poster_url', ''),
                    'fanart': tm.get('backdrop_url', ''),
                }
            )
            url = builder(
                action='series_streams_list',
                series_name=series_name,
                season=str(s_key),
                filter_mode=fm,
            )
            xbmcplugin.addDirectoryItem(handle, url, li, isFolder=True)

        li_all = xbmcgui.ListItem(
            label="Všechny sezóny (dlouhý výpis)"
        )
        li_all.setArt(
            {
                'icon': 'DefaultHardDisk.png',
                'poster': tm.get('poster_url', ''),
                'fanart': tm.get('backdrop_url', ''),
            }
        )
        url_all = builder(
            action='series_streams_list',
            series_name=series_name,
            season='all',
            filter_mode=fm,
        )
        xbmcplugin.addDirectoryItem(handle, url_all, li_all, isFolder=True)

        xbmcplugin.setContent(handle, 'files')
        xbmcplugin.endOfDirectory(handle, cacheToDisc=False)
        try:
            xbmc.executebuiltin("Container.SetViewMode(55)")
        except Exception:
            pass
        return

    if season_filter is not None:
        sub = f" — sezóna {season_filter}"
    else:
        sub = ""
    header = xbmcgui.ListItem(
        label=(
            f"[COLOR lime]Uložené streamy[/COLOR] · [COLOR deepskyblue]{display}[/COLOR]"
            f"{sub}"
        )
    )
    header.setArt(
        {
            'icon': 'DefaultHardDisk.png',
            'poster': tm.get('poster_url', ''),
            'fanart': tm.get('backdrop_url', ''),
        }
    )
    xbmcplugin.addDirectoryItem(handle, builder(action='noop'), header, isFolder=False)

    _add_filter_item(handle, builder, series_name, season_norm, fm, tm)

    _add_saved_stream_directory_items(
        handle,
        data,
        series_name,
        builder,
        tm,
        season_filter,
        filter_mode=fm,
    )

    xbmcplugin.setContent(handle, 'files')
    xbmcplugin.endOfDirectory(handle, cacheToDisc=False)
    try:
        xbmc.executebuiltin("Container.SetViewMode(55)")
    except Exception:
        pass

def _format_stream_label(st):
    """Krátký label pro stream v UI."""
    from resources.lib.series.series_helpers import _norm
    feats = st.get('features')
    if isinstance(feats, list) and feats:
        return ', '.join(feats)
    return _norm(st.get('label')) or _norm(st.get('quality')) or 'Stream'

def format_stream_two_lines(st, runtime_min=None):
    """Formátování pro dvouřádkový dialog výběru streamu.

    Druhý řádek používá sjednocené jazykové tagy `CZdab` / `CZtit` z
    `get_stream_language_tags`, ostatní featury jsou normalizované přes
    `normalize_language_features` (legacy CZ/CZsub/czaudio se převedou).
    """
    from resources.lib.series.series_helpers import (
        normalize_language_features,
        get_stream_language_tags,
    )
    line1 = (st.get('name') or '').strip()

    feats = normalize_language_features(st.get('features') or [])
    for l in get_stream_language_tags(st):
        if l not in feats:
            feats.insert(0, l)

    size_gb = f" | {float(st.get('size', 0))/(1024**3):.1f} GB" if st.get('size') else ""
    rt = f" | {runtime_min} min" if runtime_min else ""
    line2 = f"{', '.join(feats)}{size_gb}{rt}"
    return line1, line2

def _ask_season_filter(series_data):
    """Dialog pro výběr sezóny při filtraci streamů."""
    seasons = sorted(series_data.get('seasons', {}).keys(), key=int)
    if not seasons: return None
    labels = [f"Sezona {s}" for s in seasons]
    idx = xbmcgui.Dialog().select("Vyberte sezónu pro filtraci", labels)
    return seasons[idx] if idx >= 0 else None
