
# -*- coding: utf-8 -*-
# Module: offline_manager
# Author: mmirousek
# License: AGPL v3

import os
import re
import xbmc
import xbmcgui
import xbmcaddon
import xbmcvfs
from resources.lib.down.download_manager import download_file
from resources.lib.down.offline_cache_utils import (
    offline_cache_lookup_keys,
    resolve_folder_display_name as _resolve_folder_display_name,
    resolve_dfolder_path,
    resolve_series_folder_label,
)
from resources.lib.series.series_manager import SeriesManager
from resources.lib.utils.ui_utils import clean_filename
from resources.lib.utils.ui_utils import reset_cancel_flags, is_cancel_requested
from resources.lib.database.metadata_db import db as _metadata_db
from resources.lib.utils.log_utils import get_logger

log = get_logger()

ADDON_ID = 'plugin.video.mmirousek_v3'
ADDON = xbmcaddon.Addon(ADDON_ID)

# --- cancel 'all' flag (Window property) ---
WIN_KEY_ALL = 'mm_cancel_download_all'

_EPISODE_TAG_RE = re.compile(r"(?i)\bS\d{1,2}E\d{1,2}\b")
_VALID_VIDEO_EXTS = {'.mp4', '.mkv', '.avi', '.ts'}


def _merge_season_files(primary_folder: str, legacy_folder: str):
    """Sjednotí názvy souborů ze dvou kandidátních sezónních složek (nová + legacy tmdb_*)."""
    names = []
    for folder in (primary_folder, legacy_folder):
        if folder and xbmcvfs.exists(folder):
            names.extend(_listdir_safe(folder))
    # dedup zachovat pořadí
    seen = set()
    out = []
    for f in names:
        if f not in seen:
            seen.add(f)
            out.append(f)
    return out


def _normalize_video_ext(ext: str) -> str:
    """Vrátí validní video příponu nebo prázdný string."""
    e = (ext or '').strip().lower()
    return e if e in _VALID_VIDEO_EXTS else ''


def _episode_prefix(season: str, episode: str) -> str:
    """Bezpečný prefix epizody ve tvaru SxxEyy."""
    try:
        return f"S{int(season):02d}E{int(episode):02d}"
    except (TypeError, ValueError):
        return f"S{season}E{episode}"


def _build_episode_filename(ep: dict, best_stream: dict, season: str, episode: str) -> tuple:
    """Složí konzistentní název souboru epizody + základ bez přípony.

    - pseudo-přípony (např. `.epizoda`) nebere jako finální ext
    - když chybí validní ext, vezme ext ze streamu, fallback `.mkv`
    - pokud v názvu chybí tag epizody, doplní prefix `SxxEyy`
    """
    raw_title = ep.get('name') or ep.get('title') or ep.get('source_name') or ''
    sanitized = clean_filename(raw_title) if raw_title else ''

    root, ext = os.path.splitext(sanitized)
    valid_title_ext = _normalize_video_ext(ext)
    if valid_title_ext:
        base_name = root
        final_ext = valid_title_ext
    else:
        base_name = sanitized
        _, stream_ext = os.path.splitext((best_stream or {}).get('name') or '')
        final_ext = _normalize_video_ext(stream_ext) or '.mkv'

    base_name = (base_name or '').strip()
    ep_tag = _episode_prefix(season, episode)
    if not base_name:
        base_name = ep_tag
    elif not _EPISODE_TAG_RE.search(base_name):
        base_name = f"{ep_tag} {base_name}"

    return base_name, f"{base_name}{final_ext}"


def _episode_in_offline_cache(
    series_name: str, resolved_folder_label: str, season, ep_num,
    cache_all: dict = None, folder_index: dict = None,
) -> bool:
    """True pokud offline_cache už zná tuto epizodu pod klíčem safe_name nebo display klíčem."""
    try:
        s_int = int(season)
        e_int = int(ep_num)
    except (TypeError, ValueError):
        return False
    lookup_data = {'name': resolved_folder_label} if resolved_folder_label else None
    for key in offline_cache_lookup_keys(series_name, lookup_data, cache_all=cache_all, folder_index=folder_index):
        if not key:
            continue
        rows = (cache_all.get(key) if cache_all is not None else _metadata_db.get_offline_cache_for_series(key)) or []
        for it in rows:
            try:
                if int(it.get("season", -1)) == s_int and int(
                    it.get("episode", -1)
                ) == e_int:
                    return True
            except (TypeError, ValueError):
                continue
    return False


def _listdir_safe(path: str):
    """Bezpečně vrátí seznam souborů (pouze přes xbmcvfs – funguje i na SMB/NFS)."""
    if not path:
        return []
    try:
        dirs, files = xbmcvfs.listdir(path)
        return files  # vracíme jen soubory
    except Exception as e:
        log.debug(f"_listdir_safe error: {path} → {e}")
        return []
    
def _get_target_folder(
    series_name: str,
    season: str = None,
    display_name: str = None,
    series_data: dict = None,
):
    """Vrátí cílovou složku pro seriál/sezónu – plně kompatibilní s SMB/NFS.

    Preferuje existující složku na disku (lidský název), aby nové epizody
    neskončily v paralelní složce ``tmdb_<id>``.
    """
    base_real = resolve_dfolder_path(ADDON)
    if not base_real:
        xbmcgui.Dialog().notification("Offline", "Chybí složka pro stahování (nastavení).", xbmcgui.NOTIFICATION_ERROR)
        return None

    folder_name = resolve_series_folder_label(
        series_name,
        series_data=series_data,
        dfolder=base_real,
        hint_display_name=display_name,
    )
    series_folder = f"{base_real}{folder_name}/"
    if season:
        series_folder += f"Sezona {season}/"

    try:
        xbmcvfs.mkdirs(series_folder)
    except Exception as e:
        log.error(f"Nelze vytvořit složku {series_folder}: {e}")

    return series_folder


# ---------------------------------------------------------------------------
# Pomocné funkce pro výběr streamu (sdílené save_episode + save_season)
# ---------------------------------------------------------------------------

def _stream_has_quality(st: dict, tag: str) -> bool:
    """True pokud stream odpovídá požadované kvalitě.
    Kontroluje blob (quality + name + ainfo) i features list.
    tag: '2160' | '1080' | '720'
    """
    blob = " ".join([
        st.get('quality', ''),
        st.get('name', ''),
        st.get('ainfo', ''),
    ]).lower()
    feats = [f.lower() for f in st.get('features', []) if isinstance(f, str)]
    if tag == '2160':
        return (
            ('2160' in blob or '4k' in blob or 'uhd' in blob)
            or any('2160p' in f or '4k' in f or 'uhd' in f for f in feats)
        )
    return (tag in blob) or any(tag in f for f in feats)


def _tv_supports_dv() -> bool:
    """True pokud uživatel v nastavení označil, že jeho TV/přehrávač
    podporuje Dolby Vision. Default je False (konzervativní — většina
    TV umí jen HDR a DV-only stream by vypadal špatně).
    """
    try:
        return (ADDON.getSetting('download_tv_supports_dv') or 'false').lower() == 'true'
    except Exception:
        return False


def _stream_hdr_rank(st: dict) -> int:
    """Numerický rank HDR kvality streamu (vyšší = lepší dynamic range).

    Chování závisí na settings ``download_tv_supports_dv``:

    - **TV podporuje DV** (toggle ON): DV je nejvyšší priorita bez ohledu
      na to, jestli má fallback. Pořadí: ``DV=4 > HDR10+=3 > HDR10=2 > HDR=1 > 0``.
    - **TV nepodporuje DV** (default, toggle OFF): DV samotné nedává bonus,
      protože HDR-only TV DV layer nepřehraje. Stream se rank-uje podle
      playable HDR fallback vrstvy:
        * ``DV + HDR10+`` → 3 (HDR10+ layer se přehraje)
        * ``DV + HDR10`` → 2
        * ``DV + HDR``   → 1
        * ``DV only`` (bez HDR fallback) → **-1** (auto-pick se mu vyhne)
        * samotné HDR10+/HDR10/HDR → 3/2/1
        * SDR → 0

    DV-only stream s rank -1 v preview zůstane viditelný (půjde na konec
    seznamu alternativ přes stable sort), takže ho uživatel může ručně
    vybrat, ale auto-výběr ho nezvolí přes cokoli jiného.

    Detekce kombinuje ``features`` (``DV``, ``HDR10+``, ``HDR10``, ``HDR``)
    a blob (``name``/``quality``/``ainfo``).
    """
    feats_upper = set()
    for f in (st.get('features') or []):
        if isinstance(f, str):
            feats_upper.add(f.upper().strip())

    blob = " ".join([
        st.get('quality', '') or '',
        st.get('name', '') or '',
        st.get('ainfo', '') or '',
    ]).lower()

    has_dv = (
        'DV' in feats_upper
        or 'dolby vision' in blob
        or 'dolbyvision' in blob
        or bool(re.search(r'\bdv\b', blob))
    )
    has_hdr10plus = (
        'HDR10+' in feats_upper
        or 'hdr10+' in blob
        or 'hdr10plus' in blob
        or 'hdr10 plus' in blob
    )
    has_hdr10 = 'HDR10' in feats_upper or 'hdr10' in blob
    has_hdr = 'HDR' in feats_upper or bool(re.search(r'\bhdr\b', blob))

    if _tv_supports_dv():
        if has_dv:
            return 4
        if has_hdr10plus:
            return 3
        if has_hdr10:
            return 2
        if has_hdr:
            return 1
        return 0

    if has_dv:
        if has_hdr10plus:
            return 3
        if has_hdr10:
            return 2
        if has_hdr:
            return 1
        return -1

    if has_hdr10plus:
        return 3
    if has_hdr10:
        return 2
    if has_hdr:
        return 1
    return 0


def _stream_has_cz_audio(st: dict) -> bool:
    """True pokud stream obsahuje českou audio stopu.

    Priorita zdrojů:
      1) Pokud byl stream proskenován (ffprobe / http_range_probe →
         `subcaps_cache.is_probed(ident)`), je cache **autoritativní**.
         Vrátí přesně to, co `subcaps_cache.has_cz_audio(ident)`.
      2) Jinak fallback na nový/legacy CZdab feature tag uložený ve
         `st['features']` (`CZ`/`CZDAB`/`CZAUDIO`).
      3) Posledně filename heuristika sdílená s
         `series_curator.extract_features`
         (`series_helpers.detect_cz_dub_from_blob`) — zohledňuje subs
         kontext, aby `EN.cz.titulky.mkv` nebyl falešně označen jako CZ-audio.

    Pozn.: legacy kombinované tokeny typu `CZ-SK` / `CZ-EN-Atmos-5.1` se zde
    už **nepovažují** za CZ-audio bez další konfirmace (vznikaly stejnou
    regex chybou a propagovaly false-positives). Pokud jsou v DB ještě
    staré záznamy, je třeba spustit „Přepočítat features" v
    experimentálním menu, aby se features přepočítaly novou logikou.
    """
    ident = st.get('ident')
    if ident:
        try:
            from resources.lib.series import subcaps_cache
            if subcaps_cache.is_probed(ident):
                return bool(subcaps_cache.has_cz_audio(ident))
        except Exception:
            pass

    feats_upper = [f.upper() for f in st.get('features', []) if isinstance(f, str)]
    if any(f in ('CZ', 'CZDAB', 'CZAUDIO') for f in feats_upper):
        return True
    blob = " ".join([
        st.get('quality', ''),
        st.get('name', ''),
        st.get('ainfo', ''),
    ])
    try:
        from resources.lib.series.series_helpers import detect_cz_dub_from_blob
    except Exception:
        return bool(re.search(r'\bcz\b|\bczech\b|\bcs\b', blob.lower()))
    return detect_cz_dub_from_blob(blob, st.get('lang', ''))


def _stream_has_czsub(st: dict) -> bool:
    """True pokud stream obsahuje české titulky.

    Detekuje nový sjednocený tag `CZtit` i legacy `CZsub`.
    """
    feats_upper = [f.upper() for f in st.get('features', []) if isinstance(f, str)]
    if 'CZSUB' in feats_upper or 'CZTIT' in feats_upper:
        return True
    blob = " ".join([
        st.get('quality', ''),
        st.get('name', ''),
        st.get('ainfo', ''),
    ]).lower()
    return 'czsub' in blob or 'cztit' in blob or bool(re.search(r'czech.*sub|cz.*titulk', blob))


def _best_in_quality_chain(pool: list, prefer: str):
    """Z poolu streamů vybere nejlepší podle quality fallback řetězce.
    prefer: '2160' | '1080' | '720' | ''
    Fallback: prefer -> nižší kvality -> první existující v poolu.

    Uvnitř každého tier se preferuje vyšší HDR varianta
    (DV > HDR10+ > HDR10 > HDR > SDR) — např. `2160p + DV` bije `2160p` SDR,
    `1080p + HDR10` bije `1080p` SDR. Při shodném HDR rank se zachovává
    původní pořadí v listu (stabilita).
    """
    if not pool:
        return None

    def _pick_from_tier(tier_streams):
        if not tier_streams:
            return None
        indexed = list(enumerate(tier_streams))
        indexed.sort(key=lambda kv: (-_stream_hdr_rank(kv[1]), kv[0]))
        return indexed[0][1]

    if prefer:
        tier_match = [st for st in pool if _stream_has_quality(st, prefer)]
        best = _pick_from_tier(tier_match)
        if best is not None:
            return best

    fallback_chain = {
        '2160': ('1080', '720'),
        '1080': ('720',),
        '720': (),
        '': ('2160', '1080', '720'),
    }
    for tag in fallback_chain.get(prefer or '', ()):
        tier_match = [st for st in pool if _stream_has_quality(st, tag)]
        best = _pick_from_tier(tier_match)
        if best is not None:
            return best

    return _pick_from_tier(pool) or pool[0]


def _pick_stream_by_quality(streams: list, prefer: str):
    """Vybere nejlepší stream s prioritou CZdab > CZtit > ostatní NAPŘÍČ všemi
    kvalitami. Tj. CZdab 720p bije EN 1080p; CZtit 1080p bije EN 2160p.
    V rámci každé jazykové skupiny se pak uplatní quality fallback řetězec
    (prefer -> nižší -> první existující).
    """
    if not streams:
        return None

    cz_dub_pool = [st for st in streams if _stream_has_cz_audio(st)]
    if cz_dub_pool:
        return _best_in_quality_chain(cz_dub_pool, prefer)

    cz_tit_pool = [st for st in streams if _stream_has_czsub(st) and not _stream_has_cz_audio(st)]
    if cz_tit_pool:
        return _best_in_quality_chain(cz_tit_pool, prefer)

    rest_pool = [st for st in streams if not _stream_has_cz_audio(st) and not _stream_has_czsub(st)]
    if rest_pool:
        return _best_in_quality_chain(rest_pool, prefer)

    return _best_in_quality_chain(streams, prefer)


def save_episode(series_name: str, season: str, episode: str, series_manager: SeriesManager, prefer_quality=None, selected_stream=None, alt_streams=None):
    """
    Přidá epizodu do persistentní fronty stahování.
    Pokud je předán `selected_stream` (dict), použije ho přímo a přeskočí autovýběr
    (tuto cestu používá preview editor v download_preview.py).
    `alt_streams` — další streamy pro noční fallback ve workeru (jen z season/show plánu).
    """

    if is_cancel_requested():
        log.info(f"Cancel active before queue add S{season}E{episode}")
        return

    # Načti data epizody
    data = series_manager.load_series_data(series_name)
    if not data:
        xbmcgui.Dialog().notification("Offline", "Data seriálu nenalezena.", xbmcgui.NOTIFICATION_ERROR)
        return

    ep = data.get('seasons', {}).get(str(season), {}).get(str(episode))
    if not ep or not ep.get('streams'):
        xbmcgui.Dialog().notification("Offline", "Streamy pro epizodu nenalezeny.", xbmcgui.NOTIFICATION_WARNING)
        return

    streams = ep['streams']

    if selected_stream is not None:
        best_stream = selected_stream
    else:
        if prefer_quality is None:
            if any(_stream_has_quality(st, '2160') for st in streams):
                prefer_quality = '2160'
            elif any(_stream_has_quality(st, '1080') for st in streams):
                prefer_quality = '1080'
            elif any(_stream_has_quality(st, '720') for st in streams):
                prefer_quality = '720'

        best_stream = _pick_stream_by_quality(streams, prefer_quality or '')

    if not best_stream or not best_stream.get('ident'):
        xbmcgui.Dialog().notification("Offline", "Nepodařilo se vybrat stream.", xbmcgui.NOTIFICATION_ERROR)
        return

    ws_ident = best_stream.get('ident')
    expected_size = int(best_stream.get('size') or 0) if best_stream.get('size') else None

    # Cílová složka (lidský název z TMDb, ne zástupný tmdb_* v poli name)
    display_name = _resolve_folder_display_name(series_name, data)
    folder = _get_target_folder(
        series_name, str(season), display_name=display_name, series_data=data
    )
    if not folder:
        return

    # Název souboru — odolný vůči pseudo-příponám typu ".epizoda".
    base_name, filename = _build_episode_filename(ep, best_stream, season, episode)
    target_path = os.path.join(folder, filename)

    # Alternativy pro worker fallback (pořadí = preference z plánu, bez primárního)
    alt_idents = []
    for st in (alt_streams or []):
        alt_ident = (st or {}).get('ident')
        if not alt_ident or alt_ident == ws_ident:
            continue
        try:
            alt_size = int(st.get('size') or 0) or None
        except (TypeError, ValueError):
            alt_size = None
        alt_base, alt_filename = _build_episode_filename(ep, st, season, episode)
        alt_idents.append({
            'ident': alt_ident,
            'size': alt_size,
            'name': alt_base,
            'target_path': os.path.join(folder, alt_filename),
        })

    # ►► PŘIDÁNÍ DO FRONTY — URL se fetchuje Worker těsně před stažením (link_raw prázdný záměrně) ◄◄
    from resources.lib.down.download_queue import add_to_queue
    payload = {
        'ident': ws_ident,
        'name': base_name,
        'series_name': series_name,
        'display_name': display_name,
        'season': str(season),
        'episode': str(episode),
        'size': expected_size,
        'target_path': target_path,
        'url': '',
        'link_raw': '',
        'prefer_quality': prefer_quality,
    }
    if alt_idents:
        payload['alt_idents'] = alt_idents
    add_to_queue(payload)

    log.info(f"Přidáno do fronty: {series_name} S{season}E{episode}"
             + (f" (+{len(alt_idents)} alt)" if alt_idents else ""))

def _ask_quality_dialog(episodes_iter, title: str):
    """Zobrazí dialog výběru kvality podle dostupných streamů napříč epizodami.

    `episodes_iter` = iterable dict epizod (hodnot, ne klíčů).
    Vrátí ``(prefer_quality, was_offered)``:
      - `prefer_quality`: '2160' | '1080' | '720' | '' (prázdné když nic nebylo)
      - `was_offered`: True pokud byl dialog zobrazen, False pokud žádná kvalita nebyla
      - None když uživatel dialog zrušil.
    """
    has_2160, has_1080, has_720 = False, False, False
    for ep in episodes_iter:
        for st in (ep or {}).get('streams', []) or []:
            if not has_2160 and _stream_has_quality(st, '2160'):
                has_2160 = True
            if not has_1080 and _stream_has_quality(st, '1080'):
                has_1080 = True
            if not has_720 and _stream_has_quality(st, '720'):
                has_720 = True
            if has_2160 and has_1080 and has_720:
                break
        if has_2160 and has_1080 and has_720:
            break

    options, mapping = [], []
    if has_2160:
        options.append("4K (2160p) – nejvyšší kvalita")
        mapping.append('2160')
    if has_1080:
        options.append("1080p – vyvážená kvalita")
        mapping.append('1080')
    if has_720:
        options.append("720p – menší soubor")
        mapping.append('720')

    if not options:
        return ('', False)

    idx = xbmcgui.Dialog().select(title, options)
    if idx is None or idx < 0:
        return None
    return (mapping[idx], True)


def _bulk_enqueue_from_plan(series_name, plan, series_manager, progress_title="Offline"):
    """Projde plán z download_preview a pro každou ne-skip epizodu volá save_episode
    s vybraným streamem. Zobrazuje DialogProgress s cancel tlačítkem.
    """
    dlg = xbmcgui.DialogProgress()
    dlg.create(progress_title, "Přidávám do fronty...")
    total = max(1, len(plan))
    count = 0
    added = 0

    for item in plan:
        if dlg.iscanceled():
            log.info("Uživatel zrušil přidávání plánu do fronty.")
            break

        if is_cancel_requested():
            log.info("Global cancel ALL hit — stopping bulk enqueue")
            reset_cancel_flags()
            break

        count += 1
        pct = int(count * 100 / total)

        if item.skip_reason:
            dlg.update(pct, f"Přeskakuji {item.se_label} ({item.skip_reason})")
            continue

        chosen = item.chosen_stream
        if not chosen:
            dlg.update(pct, f"Přeskakuji {item.se_label} (bez streamu)")
            continue

        try:
            alts = [
                st for i, st in enumerate(item.all_streams)
                if i != item.chosen_idx and (st or {}).get('ident')
            ]
            save_episode(
                series_name,
                str(item.season),
                str(item.episode),
                series_manager,
                selected_stream=chosen,
                alt_streams=alts,
            )
            added += 1
            dlg.update(pct, f"Přidáno {added}/{total} · {item.se_label}")
        except Exception as e:
            log.error(f"CHYBA při přidávání {item.se_label}: {e}")
            dlg.update(pct, f"Chyba u {item.se_label}")

    dlg.close()
    return added


def save_season(series_name: str, season: str, series_manager: SeriesManager):
    """
    Přidá všechny epizody sezóny do fronty s interaktivním preview editorem.

    Flow:
      1) Načte data seriálu.
      2) Dialog výběru kvality (2160/1080/720) podle dostupných streamů v sezóně.
      3) Sestaví plán přes download_preview.build_queue_plan.
      4) Ověří streamy na Webshare (file_info) — neplatné vyhodí / nahradí.
      5) Zobrazí interaktivní editor (plán lze per epizodu upravit).
      6) Bulk zařazení přes save_episode(..., selected_stream=..., alt_streams=...).
    """

    if is_cancel_requested():
        log.info(f"Cancel active before queue add season S{season}")
        reset_cancel_flags()
        return

    data = series_manager.load_series_data(series_name)
    if not data:
        xbmcgui.Dialog().notification("Offline", "Data seriálu nenalezena.", xbmcgui.NOTIFICATION_ERROR)
        return

    episodes = data.get('seasons', {}).get(str(season), {})
    if not episodes:
        xbmcgui.Dialog().notification("Offline", "Sezóna nenalezena.", xbmcgui.NOTIFICATION_WARNING)
        return

    ask_result = _ask_quality_dialog(episodes.values(), "Vyber kvalitu pro sezónu")
    if ask_result is None:
        log.info("Uživatel zrušil volbu kvality sezóny.")
        return
    prefer_quality, _ = ask_result

    from resources.lib.down.download_preview import (
        build_queue_plan,
        show_interactive_plan,
        validate_plan_streams,
    )

    plan = build_queue_plan(series_name, data, [str(season)], prefer_quality)
    if not plan:
        xbmcgui.Dialog().notification("Offline", "Žádné epizody k zařazení.", xbmcgui.NOTIFICATION_INFO, 2500)
        return

    if not validate_plan_streams(plan, series_name, series_manager, prefer_quality):
        log.info(f"Uživatel zrušil kontrolu streamů sezóny S{season}")
        return

    confirmed = show_interactive_plan(plan, f"Sezóna {season} — přehled výběru")
    if confirmed is None:
        log.info(f"Uživatel zrušil preview sezóny S{season}")
        return

    added = _bulk_enqueue_from_plan(series_name, confirmed, series_manager, "Offline")
    log.info(f"save_season S{season}: zařazeno {added} epizod do fronty")


def save_whole_show(series_name: str, series_manager: SeriesManager):
    """Zařadí všechny epizody všech sezón seriálu do fronty.

    Flow:
      1) Načte data seriálu, seřadí sezóny.
      2) Jeden dialog výběru kvality napříč všemi sezónami.
      3) Jeden velký preview editor (řádky po sezónách).
      4) Bulk zařazení.
    """
    if is_cancel_requested():
        log.info("Cancel active before queue add whole show")
        reset_cancel_flags()
        return

    data = series_manager.load_series_data(series_name)
    if not data:
        xbmcgui.Dialog().notification("Offline", "Data seriálu nenalezena.", xbmcgui.NOTIFICATION_ERROR)
        return

    seasons_dict = data.get('seasons') or {}
    if not seasons_dict:
        xbmcgui.Dialog().notification("Offline", "Seriál nemá žádné sezóny.", xbmcgui.NOTIFICATION_WARNING)
        return

    try:
        season_keys = sorted(seasons_dict.keys(), key=int)
    except (TypeError, ValueError):
        season_keys = sorted(seasons_dict.keys())

    all_episodes = []
    for s_key in season_keys:
        for ep in (seasons_dict.get(s_key) or {}).values():
            all_episodes.append(ep)

    ask_result = _ask_quality_dialog(all_episodes, "Vyber kvalitu pro celý seriál")
    if ask_result is None:
        log.info("Uživatel zrušil volbu kvality celého seriálu.")
        return
    prefer_quality, _ = ask_result

    from resources.lib.down.download_preview import (
        build_queue_plan,
        show_interactive_plan,
        validate_plan_streams,
    )

    plan = build_queue_plan(series_name, data, season_keys, prefer_quality)
    if not plan:
        xbmcgui.Dialog().notification("Offline", "Žádné epizody k zařazení.", xbmcgui.NOTIFICATION_INFO, 2500)
        return

    if not validate_plan_streams(plan, series_name, series_manager, prefer_quality):
        log.info("Uživatel zrušil kontrolu streamů celého seriálu")
        return

    confirmed = show_interactive_plan(plan, "Celý seriál — přehled výběru")
    if confirmed is None:
        log.info("Uživatel zrušil preview celého seriálu")
        return

    added = _bulk_enqueue_from_plan(series_name, confirmed, series_manager, "Offline")
    log.info(f"save_whole_show: zařazeno {added} epizod do fronty")
