# -*- coding: utf-8 -*-
"""
SearchEngine: oddělená logika vyhledávání a kurátorování streamů (Webshare)
- paging přes API
- detekce S/E (CZ-friendly)
- slug validace
- kurátorování (kvalita + CZ titulky + fallback)
- využití CZ titulkové cache (ident -> CZtit)
- NEDSTRUKTIVNÍ MERGE (nové dopředu, staré zachovat, dedupe podle ident, ořez)
- zápis přes SeriesManager + TMDb enrichment

Zachovává rozhraní původního SeriesManager.search_series(...)
"""

from typing import Dict, Any, List, Tuple
import re
import xbmc
import xml.etree.ElementTree as ET
from resources.lib.utils.log_utils import get_logger
from resources.lib.utils.ui_utils import _light_clean, _normalize_and_slugify
from resources.lib.series import series_filter as s_filter

log = get_logger()


# ========================= Regex & helpers (lokální kopie) ====================

RE_SxxEyy  = re.compile(r'(?i)\bS(\d{1,2})\s*E(\d{1,3})\b')
RE_1x02    = re.compile(r'(?i)\b(\d{1,2})\s*[x×]\s*(\d{1,3})\b')
RE_CZ      = re.compile(r'(?i)\b(sezona|série|řada)\s*(\d{1,2}).*\b(díl|epizoda|ep)\s*(\d{1,3})\b')
RE_EPONLY  = re.compile(r'(?i)\b(ep|epizoda)\s*(\d{1,3})\b')
SEASON_PACK_HINTS = re.compile(r'(?i)\b(pack|complete|season\s*pack|s\d{1,2}-s\d{1,2})\b')
_RE_TMDB_STORAGE_KEY = re.compile(r'^tmdb_\d+$', re.IGNORECASE)


def is_storable_search_query(s) -> bool:
    """
    True pokud řetězec dává smysl jako Webshare dotaz na název seriálu (ne úložný klíč tmdb_* ani čisté číslo).
    Používá se pro search_query_name a pro sestavení kandidátů v UI.
    """
    if s is None or not isinstance(s, str):
        return False
    t = s.strip()
    if len(t) < 2:
        return False
    if _RE_TMDB_STORAGE_KEY.match(t):
        return False
    if t.isdigit():
        return False
    # URL / špatná serializace často dodá literál "None" / "null"
    if t.lower() in ('none', 'null'):
        return False
    return True



def _promote_cz_features(feats: list) -> list:
    """
    Vrátí nový seznam features, kde jsou jazykové tagy `CZdab` / `CZtit`
    na začátku (v tomto pořadí). Vstup nejdřív znormalizuje, takže
    legacy tokeny (`CZ`, `CZsub`, `CZaudio`, `CZ-SK`, …) se převedou.
    """
    if not isinstance(feats, list):
        return feats
    from resources.lib.series.series_helpers import normalize_language_features
    norm = normalize_language_features(feats)
    cz_dab = [f for f in norm if f == 'CZdab']
    cz_tit = [f for f in norm if f == 'CZtit']
    other  = [f for f in norm if f not in ('CZdab', 'CZtit')]
    return cz_dab + cz_tit + other


class SearchEngine:
    """
    Přijímá instanci SeriesManager (kvůli uložení, TMDb enrich a helperům).
    Neimportuje series_manager modul, aby se předešlo cyklům.
    """

    def __init__(self, series_manager):
        self.sm = series_manager  # instance SeriesManager

    # -------------------------- veřejné rozhraní ------------------------------

    def search_series(self, series_name: str, api_function, token, **kwargs) -> Dict[str, Any]:
        """
        Implementace přesunutá z původního SeriesManager.search_series(...):
        - vytvoří/init data
        - sestaví dotazy (standard vs name_only režim)
        - zavolá Webshare search (paging), odfiltruje season-packy
        - detekce S/E a bucketizace
        - TMDb validace rozsahu + slug sada (self.sm.tmdb_filter)
        - kurátorování streamů
        - NEDSTRUKTIVNÍ MERGE (nové dopředu, staré zachovat, dedupe podle ident, ořez)
        - TMDb enrichment a uložení
        - vrátí migrovaná data (stejně jako dříve)

        Podporované kwargs:
          refresh, skip_high_quality, force_hd_only, name_only, only_uhd,
          max_streams, update_policy ('smart'|'replace'|'append'),
          storage_key — SQLite/DB klíč (např. tmdb_123); series_name zůstává textem Webshare dotazu.
        """
        storage_key = kwargs.pop('storage_key', None)
        persistence_key = (storage_key or '').strip() or series_name

        # --- přepínače režimů ---
        refresh           = bool(kwargs.get('refresh', False))
        skip_high_quality = bool(kwargs.get('skip_high_quality', False))
        force_hd_only     = bool(kwargs.get('force_hd_only', False))
        name_only         = bool(kwargs.get('name_only', False))
        only_uhd          = bool(kwargs.get('only_uhd', False))
        max_streams       = int(kwargs.get('max_streams', 50))
        update_policy     = (kwargs.get('update_policy') or 'smart').lower()
        tmdb_id_override  = kwargs.get('tmdb_id')
        filter_mode       = (kwargs.get('filter_mode') or 'standard').lower()
        target_season     = kwargs.get('target_season')
        target_episode    = kwargs.get('target_episode')

        target_key = None
        try:
            if target_season is not None and target_episode is not None:
                target_key = (int(target_season), int(target_episode))
        except (TypeError, ValueError):
            target_key = None

        if filter_mode not in ('standard', 'aggressive'):
            filter_mode = 'standard'

        if skip_high_quality:
            log.info("skip_high_quality=True → vynechám 2160p")
        if force_hd_only:
            log.info("force_hd_only=True → jen 1080/720p")
        if name_only:
            log.info("name_only=True → SxxExx režim bez striktní slug shody")
        if only_uhd:
            log.info("only_uhd=True → vybírám pouze 2160p/4K")
        log.info(
            f"update_policy={update_policy}, tmdb_id_override={tmdb_id_override}, "
            f"filter_mode={filter_mode}, persistence_key={persistence_key!r}, webshare_query={series_name!r}, "
            f"target={target_key}"
        )

        # --- init dat seriálu (vždy stejný záznam při smart refresh i když se mění Webshare dotaz) ---
        series_data = self.sm._init_series_data(persistence_key)
        prev_sqn = series_data.get('search_query_name')
        if is_storable_search_query(series_name):
            series_data['search_query_name'] = series_name
        elif prev_sqn and is_storable_search_query(prev_sqn):
            pass
        else:
            if prev_sqn and not is_storable_search_query(prev_sqn):
                series_data.pop('search_query_name', None)
        saved_slug = (series_data.get('tmdb') or {}).get('slug')

        if tmdb_id_override:
            series_data.setdefault('tmdb', {})
            try:
                series_data['tmdb']['id'] = int(tmdb_id_override)
                log.info(f"Ručně nastaveno TMDb ID: {tmdb_id_override}")
            except ValueError:
                pass

        # pokus o brzký TMDb enrich (pro filtr sezón/epizod)
        try:
            self.sm._enrich_with_tmdb(series_data, series_name)
        except Exception as e:
            log.warning(f"TMDb enrich (early) skipped: {e}")

        if filter_mode == 'aggressive' and not (series_data.get('tmdb') or {}).get('id'):
            log.warning("Agresivní filtrace vyžaduje TMDb ID. Přepínám na standardní režim.")
            filter_mode = 'standard'


        slug_series = _normalize_and_slugify(series_name)
        if series_data.get('tmdb'):
            if refresh and saved_slug:
                series_data['tmdb']['slug'] = saved_slug
            else:
                series_data['tmdb']['slug'] = slug_series

        known_idents       = set(series_data.get("known_idents", []))
        tmdb_seasons_meta  = (series_data.get('tmdb') or {}).get('seasons')
        existing_streams   = series_data.get('seasons', {})
        title_gate         = s_filter.build_title_gate(series_data, fallback_name=series_name)

        # --- tvorba dotazů ---
        if target_key:
            s_num, e_num = target_key
            queries = [
                f"{series_name} S{s_num:02d}E{e_num:02d}",
                f"{series_name} {s_num}x{e_num:02d}",
                f"{series_name} epizoda {e_num}",
            ]
        elif refresh and name_only:
            # cílené doplňování chybějících epizod podle TMDb počtů
            missing_queries = set()
            if tmdb_seasons_meta:
                for s_num_str, s_meta in tmdb_seasons_meta.items():
                    num_eps = s_meta.get('episode_count')
                    if not num_eps:
                        continue
                    for e_num in range(1, int(num_eps) + 1):
                        s_str, e_str = str(s_num_str), str(e_num)
                        ep_streams = existing_streams.get(s_str, {}).get(e_str, {}).get('streams', [])
                        if not ep_streams:
                            missing_queries.add(f"{series_name} S{int(s_num_str):02d}E{e_num:02d}")
            if missing_queries:
                queries = list(missing_queries)
            else:
                queries = [f"{series_name} S", f"{series_name} E", f"{series_name} S01"]
        else:
            queries = [series_name, f"{series_name} s01", f"{series_name} episode", f"{series_name} season"]

        # --- Webshare search (paging) ---
        raw_items: List[dict] = []
        seen_idents = set()
        for q in queries:
            for page in range(self.sm.max_pages):
                results = self._perform_search(
                    q, api_function, token,
                    offset=page * self.sm.page_size,
                    limit=self.sm.page_size
                )
                if not results:
                    break

                for r in results:
                    name = r.get('name') or ''
                    ident = r.get('ident') or ''
                    if not ident or ident in seen_idents:
                        continue

                    # season packy pryč
                    if SEASON_PACK_HINTS.search(name or ''):
                        continue

                    # režimy matchování
                    is_match = False
                    if name_only:
                        s, e = self._detect_episode_info(name, series_name)
                        if s is not None and e is not None:
                            if target_key and (int(s), int(e)) != target_key:
                                continue
                            if filter_mode == 'aggressive' and not s_filter.strict_title_match(name, title_gate):
                                continue
                            is_match = True
                    else:
                        detected_s, detected_e = self._detect_episode_info(name, series_name)
                        if target_key and (
                            detected_s is None
                            or detected_e is None
                            or (int(detected_s), int(detected_e)) != target_key
                        ):
                            continue
                        if filter_mode == 'aggressive':
                            is_match = self._is_likely_episode(name, series_name) and s_filter.strict_title_match(name, title_gate)
                        else:
                            # slug + S/E
                            file_slug = _normalize_and_slugify(name)

                            # valid slugs z dat
                            valid_slugs = set()
                            tm = series_data.get('tmdb', {}) or {}
                            if tm.get('slug'): valid_slugs.add(tm['slug'])
                            if series_data.get('name'): valid_slugs.add(_normalize_and_slugify(series_data['name']))
                            if tm.get('original_title'): valid_slugs.add(_normalize_and_slugify(tm['original_title']))
                            for alias in series_data.get('aliases', []):
                                valid_slugs.add(_normalize_and_slugify(alias))

                            is_slug_match = any(vs in file_slug for vs in valid_slugs) if valid_slugs else file_slug.startswith(slug_series)
                            is_match = self._is_likely_episode(name, series_name) and is_slug_match

                    if not is_match:
                        continue

                    candidate = dict(r)
                    candidate['expected_tmdb_id'] = (series_data.get('tmdb') or {}).get('id')
                    candidate['filter_mode'] = filter_mode
                    raw_items.append(candidate)
                    seen_idents.add(ident)

        log.info(f"Po filtru kandidátů nalezeno: {len(raw_items)}")
        families = s_filter.detect_title_families(raw_items)
        if len(families) > 1:
            log.warning(f"[SearchEngine] Nalezen mix title family ({len(families)}): {families[:5]}")

        # --- bucket (S,E) -> [items]

        buckets = {}
        for item in raw_items:
            s, e = self._detect_episode_info(item.get('name', ''), series_name)
            if s is None or e is None:
                continue
            if target_key and (int(s), int(e)) != target_key:
                continue
            buckets.setdefault((s, e), []).append(item)

                # --- TMDb validace (rozsahy + slug sada)
        try:
            buckets = self.sm.tmdb_filter(series_data, buckets, filter_mode=filter_mode)  # tenký wrapper v SeriesManager
        except Exception as ex:
            log.warning(f"Filtr TMDb selhal: {ex}")

        if target_key:
            buckets = {target_key: buckets.get(target_key, [])} if buckets.get(target_key) else {}

        log.info(f"Po TMDb filtru bucketů: {len(buckets)}")

        # --- kurátorování + NEDSTRUKTIVNÍ MERGE pro každou epizodu -------------

        def _union_by_ident(primary: list, secondary: list, limit: int) -> list:
            """
            Nedestruktivní sloučení: nejdřív NOVÉ (primary), pak STARÉ (secondary),
            deduplikace podle 'ident', ořez na limit.
            """
            out, seen = [], set()
            for s in (primary or []) + (secondary or []):
                ident = (s or {}).get('ident')
                if not ident or ident in seen:
                    continue
                seen.add(ident)
                out.append(s)
                if limit and limit > 0 and len(out) >= limit:
                    break
            return out

        for (s, e), cand_list in buckets.items():
            if not cand_list:
                continue

            curated = self.sm._curate_streams(
                cand_list,
                skip_high_quality=skip_high_quality,
                force_hd_only=force_hd_only,
                name_only=name_only,
                only_uhd=only_uhd,
                max_per_episode=max_streams
            )
            if not curated:
                continue

            s_str, e_str = str(s), str(e)
            if s_str not in series_data['seasons']:
                series_data['seasons'][s_str] = {}
            entry = series_data['seasons'][s_str].get(e_str, {})
            locked = bool(entry.get('locked', False))
            if locked:
                log.info(f"LOCKED → ponechávám S{s_str}E{e_str} beze změny")
                continue

            old_streams = entry.get('streams', [])


            # 1) Pokud dříve nic nebylo → inicializace (ořez na max_streams)
            if not old_streams:
                entry.setdefault('source_name', curated[0].get('name') or '')
                entry.setdefault('name', entry.get('name') or '')
                entry.setdefault('tmdb', entry.get('tmdb') or {})
                entry['streams'] = (curated or [])[:max_streams]
                entry['curated'] = True
            else:
                # 2) Jinak VŽDY NEDSTRUKTIVNĚ MERGNI: nové dopředu, staré zachovat
                merged = _union_by_ident(curated, old_streams, max_streams)
                if len(merged) > len(old_streams):
                    log.info(f"  [+] S{s_str}E{e_str}: přidáno {len(merged) - len(old_streams)} nových streamů (celkem {len(merged)})")
                entry['streams'] = merged
                entry['curated'] = True

            # zapiš zpět
            series_data['seasons'][s_str][e_str] = entry

            # aktualizuj known_idents
            for st in entry.get('streams', []):
                if st.get('ident'):
                    known_idents.add(st['ident'])
            series_data['known_idents'] = sorted(list(known_idents))

                # --- TMDb doplnění epizodních metadat + zachování slug při refresh -----
        try:
            self.sm._enrich_with_tmdb(series_data, series_name)
        except Exception as e:
            log.warning(f"TMDb enrich skipped: {e}")

        if refresh and saved_slug and series_data.get('tmdb'):

            if series_data['tmdb'].get('slug') != saved_slug:
                series_data['tmdb']['slug'] = saved_slug

        # Doplnění placeholder epizod z TMDb (včetně air_date) jen pokud série má alespoň jeden stream.
        has_any_streams = any(
            bool((ep or {}).get('streams'))
            for season in (series_data.get('seasons') or {}).values()
            for ep in (season or {}).values()
        )
        tv_id = (series_data.get('tmdb') or {}).get('id')
        if has_any_streams and tv_id:
            try:
                self.sm._populate_tmdb_placeholders(series_data, int(tv_id), language='cs')
            except Exception as ex:
                log.warning(f"TMDb placeholders skipped: {ex}")

        # finální uložení – jen pokud jsme něco našli, abychom neukládali prázdné schránky
        if series_data.get('seasons'):
            self.sm._save_series_data(persistence_key, series_data)
            return self.sm._migrate_schema(series_data)
        else:
            log.info(f"[SearchEngine] Žádné streamy nalezeny pro '{series_name}', neukládám do DB.")
            return None


    # ----------------------- privátní pomocné metody ---------------------------

    def _is_likely_episode(self, filename: str, series_name: str) -> bool:
        """Heuristika na rozpoznání epizody (přenesené z manageru)."""
        if not filename:
            return False
        if series_name:
            series_slug = _normalize_and_slugify(series_name)
            filename_slug = _normalize_and_slugify(filename)
            if series_slug and series_slug not in filename_slug:
                return False
        t = _light_clean(filename)
        if RE_SxxEyy.search(t) or RE_1x02.search(t) or RE_CZ.search(t) or RE_EPONLY.search(t):
            return True
        return False

    def _perform_search(self, search_query: str, api_function, token, offset=0, limit=100) -> List[dict]:
        """Volání Webshare API + XML parse (přenesené z manageru)."""
        results = []
        try:
            response = api_function('search', {
                'what': search_query,
                'category': 'video',
                'sort': 'recent',
                'limit': int(limit),
                'offset': int(offset),
                'wst': token,
                'maybe_removed': 'true'
            })
            try:
                xml = ET.fromstring(response.content)
            except Exception as ex:
                log.warning(f'XML parse error: {ex}')
                return []
            status = xml.find('status')

            if status is not None and (status.text or '').upper() == 'OK':
                for file in xml.iter('file'):
                    item = {}
                    for elem in file:
                        item[elem.tag] = elem.text
                    results.append(item)
            return results
        except Exception as e:
            log.error(f'search error: {e}')
            return []


    def _detect_episode_info(self, filename: str, series_name: str) -> Tuple[int, int]:
        """Detekce S/E z názvu souboru (CZ-friendly, přeneseno)."""
        if not filename:
            return (None, None)
        t = _light_clean(filename)
        m = RE_SxxEyy.search(t)
        if m:
            return int(m.group(1)), int(m.group(2))
        m = RE_1x02.search(t)
        if m:
            return int(m.group(1)), int(m.group(2))
        m = RE_CZ.search(t)
        if m:
            try:
                return int(m.group(2)), int(m.group(4))
            except Exception:
                return (None, None)
        m = RE_EPONLY.search(t)
        if m:
            return (1, int(m.group(2)))
        return (None, None)

    # === kurátorování ==========================================================
    # def _curate_streams(
    #     self,
    #     candidates,
    #     skip_high_quality: bool = False,
    #     force_hd_only: bool = False,
    #     name_only: bool = False,
    #     only_uhd: bool = False,
    #     max_per_episode: int = 6
    # ):
    #     """
    #     FULL-PASS varianta kurátora: uloží VŠECHNY streamy, které přišly z bucketu
    #     (po TMDb filtru). Nedělá žádné další ořezávání ani filtrování kvality.
    #     Zachová původní pořadí z Webshare, provede pouze:
    #     - deduplikaci podle ident
    #     - enrichment: features + label
    #     """
    #     def _dedup_by_ident(items):
    #         out, seen = [], set()
    #         for s in (items or []):
    #             ident = s.get('ident')
    #             if not ident or ident in seen:
    #                 continue
    #             seen.add(ident)
    #             out.append(s)
    #         return out

    #     # 1) Převést položky do jednotné podoby + features/label
    #     normalized = []
    #     seen = set()
    #     for c in (candidates or []):
    #         ident = c.get('ident')
    #         if not ident or ident in seen:
    #             continue
    #         seen.add(ident)

    #         item = {
    #             'ident': ident,
    #             'name'  : c.get('name')    or '',
    #             'quality': c.get('quality') or c.get('ainfo') or '',
    #             'lang'  : c.get('lang')    or '',
    #             'size'  : int(c.get('size', '0') or 0),
    #             'ainfo' : c.get('ainfo')   or '',
    #             'score' : 0,
    #             'added_at': xbmc.getInfoLabel('System.Date'),
    #             'source': 'webshare'
    #         }

    #         # Features + label (stejně jako dřív)
    #         feats = item.get('features')
    #         if not feats:
    #             feats = self._extract_features(item)
    #         item['features'] = feats
    #         item['label'] = ', '.join(feats) if feats else (
    #             (item['quality'] or '').strip() or (item['name'] or '').strip() or 'Stream'
    #         )

    #         normalized.append(item)

    #     # 2) Deduplikace podle ident (pro jistotu) a návrat BEZ LIMITU
    #     normalized = _dedup_by_ident(normalized)
    #     return normalized  # ← žádné [:max_per_episode], žádné filtry navíc



    # def _curate_streams(self,
    #                     candidates: List[dict],
    #                     *,
    #                     skip_high_quality: bool = False,
    #                     force_hd_only: bool = False,
    #                     name_only: bool = False,
    #                     only_uhd: bool = False,
    #                     max_per_episode: int = 50
    #                     ) -> List[dict]:
    #     """
    #     Zachována logika:
    #     - filtry kvality dle režimu
    #     - dedupe podle ident
    #     - doplnění features+label (používá sm._extract_features)
    #     - doplnění CZsub z cache (pokud je k dispozici)
    #     - CZ fallback (když výsledný seznam nemá žádný CZ/CZsub)
    #     - LABEL má CZ/CZsub prefix (přes _promote_cz_features)
    #     """
    #     import re
    #     from resources.lib.series.subcaps_cache import has_cz_subs

    #     def _is_cz_stream(st: dict) -> bool:
    #         """Detekce „CZ“: nejprve audio/features/lang, pak názvová heuristika, pak CZsub (dle nastavení)."""
    #         try:
    #             feats = st.get('features') or []
    #             # 1) CZ audio v features
    #             if any(str(f).upper().startswith('CZ') and 'sub' not in str(f).lower() for f in feats):
    #                 return True
    #             # 2) lang
    #             lang = (st.get('lang') or '').lower()
    #             if 'cz' in lang or 'cs' in lang:
    #                 return True
    #             # 3) titulky: CZsub (výchozí: povolit i bez nastavení)
    #             try:
    #                 import xbmcaddon
    #                 treat = (xbmcaddon.Addon().getSetting('treat_cz_subs_as_cz') == 'true')
    #             except Exception:
    #                 treat = True
    #             if treat and any(str(f).lower() == 'czsub' for f in feats):
    #                 return True
    #             # 4) heuristika z názvu
    #             nm = (st.get('name') or '').lower()
    #             blob = f" {nm} "
    #             return any(tok in blob for tok in [' cz ', ' cz-', ' cz_', ' cz.', '(cz)', '[cz]'])
    #         except Exception:
    #             return False

    #     def _dedup_by_ident(items):
    #         out, seen = [], set()
    #         for s in (items or []):
    #             ident = s.get('ident')
    #             if not ident or ident in seen:
    #                 continue
    #             seen.add(ident)
    #             out.append(s)
    #         return out

    #     # primární kandidáti (možné filtry kvality)
    #     norm: List[dict] = []
    #     raw_candidates = candidates[:] if isinstance(candidates, list) else []
    #     filtered_candidates = candidates

    #     if not name_only:
    #         if only_uhd:
    #             filtered_candidates = [
    #                 c for c in filtered_candidates
    #                 if re.search(r'(?i)(2160|4k)', (c.get('quality', '') + ' ' + c.get('name', '')))
    #             ]
    #         if force_hd_only:
    #             filtered_candidates = [
    #                 c for c in filtered_candidates
    #                 if re.search(r'(?i)(1080|720)', (c.get('quality', '') + ' ' + c.get('name', '')))
    #             ]
    #         if skip_high_quality:
    #             filtered_candidates = [
    #                 c for c in filtered_candidates
    #                 if not re.search(r'(?i)(2160|4k)', (c.get('quality', '') + ' ' + c.get('name', '')))
    #             ]

    #     seen = set()
    #     for c in filtered_candidates:
    #         ident = c.get('ident') or ''
    #         if not ident or ident in seen:
    #             continue
    #         seen.add(ident)
    #         item = {
    #             'ident': ident,
    #             'name': c.get('name') or '',
    #             'quality': c.get('quality') or c.get('ainfo') or '',
    #             'lang': c.get('lang') or '',
    #             'size': int(c.get('size', '0') or 0),
    #             'ainfo': c.get('ainfo') or '',
    #             'score': 0,
    #             'added_at': xbmc.getInfoLabel('System.Date'),
    #             'source': 'webshare'
    #         }

    #         # features (z názvu/ainfo) + CZsub z cache
    #         feats = item.get('features')
    #         if not feats:
    #             feats = self.sm._extract_features(item)
    #         try:
    #             if has_cz_subs(ident) and not any(str(f).lower() == 'czsub' for f in (feats or [])):
    #                 feats = list(feats or [])
    #                 feats.append('CZsub')
    #                 log.debug(f"ident={ident} → CZsub via cache")
    #         except Exception:
    #             pass

    #         promoted = _promote_cz_features(feats)
    #         item['features'] = promoted
    #         item['label'] = ', '.join(promoted) if promoted else (
    #             (item['quality'] or '').strip() or (item['name'] or '').strip() or 'Stream'
    #         )
    #         norm.append(item)

    #     # dedupe + ořez
    #     norm = _dedup_by_ident(norm)[:max_per_episode]

    #     def _list_has_cz(lst) -> bool:
    #         return any(_is_cz_stream(s) for s in (lst or []))

    #     # CZ fallback – pokud zatím není žádný CZ/CZsub, zkus doplnit z původního poolu
    #     if not _list_has_cz(norm):
    #         cz_pool = []
    #         seen_raw = set()
    #         for c in raw_candidates:
    #             ident2 = c.get('ident')
    #             if not ident2 or ident2 in seen_raw:
    #                 continue
    #             seen_raw.add(ident2)
    #             st = {
    #                 'ident': ident2,
    #                 'name': c.get('name') or '',
    #                 'quality': c.get('quality') or c.get('ainfo') or '',
    #                 'lang': c.get('lang') or '',
    #                 'size': int(c.get('size', '0') or 0),
    #                 'ainfo': c.get('ainfo') or '',
    #                 'score': 0,
    #                 'added_at': xbmc.getInfoLabel('System.Date'),
    #                 'source': 'webshare'
    #             }
    #             feats2 = self.sm._extract_features(st)
    #             # doplnění CZsub i při fallbacku, pokud cache už ví
    #             try:
    #                 if has_cz_subs(ident2) and not any(str(f).lower() == 'czsub' for f in (feats2 or [])):
    #                     feats2 = list(feats2 or [])
    #                     feats2.append('CZsub')
    #             except Exception:
    #                 pass

    #             promoted2 = _promote_cz_features(feats2)
    #             st['features'] = promoted2
    #             st['label'] = ', '.join(promoted2) if promoted2 else (
    #                 (st['quality'] or '').strip() or (st['name'] or '').strip() or 'Stream'
    #             )
    #             if _is_cz_stream(st):
    #                 cz_pool.append(st)

    #         if cz_pool:
    #             # vyber CZ s největší velikostí (bajty)
    #             cz_best = max(cz_pool, key=lambda s: (s.get('size') or 0))
    #             norm = _dedup_by_ident([cz_best] + norm)[:max_per_episode]

    #     return norm

    # ---- drobné pomocné (lokální) --------------------------------------------

    def _best_quality_rank(self, lst) -> int:
        """Hrubý rank kvality pro porovnání setů (2160 > 1080 > 720 > SD)."""
        ranks = {'2160': 100, '4k': 100, '1080': 80, '720': 60, '540': 30, 'sd': 30}
        best = 0
        for s in (lst or []):
            blob = f"{s.get('quality','')} {s.get('name','')} {s.get('ainfo','')}".lower()
            if '2160' in blob or '4k' in blob:
                best = max(best, 100); continue
            if '1080' in blob:
                best = max(best, 80); continue
            if '720' in blob:
                best = max(best, 60); continue
            if '540' in blob or 'sd' in blob:
                best = max(best, 30); continue
        return best