
# -*- coding: utf-8 -*-
# Module: download_preview
# Author: mmirousek
# License: AGPL v3
#
# Interaktivní preview editor pro hromadné zařazení epizod sezóny / celého
# seriálu do fronty stahování. Návazné moduly:
#   - offline_manager.save_season / save_whole_show (volá build_queue_plan + show_interactive_plan)
#   - offline_manager.save_episode(..., selected_stream=...) (finální zařazení)

import os
from typing import List, Optional

import xbmcgui

from resources.lib.utils.log_utils import get_logger
from resources.lib.utils.ui_utils import clean_filename
from resources.lib.down.offline_manager import (
    _pick_stream_by_quality,
    _stream_has_cz_audio,
    _stream_has_czsub,
    _stream_has_quality,
    _stream_hdr_rank,
    _resolve_folder_display_name,
    _get_target_folder,
    _merge_season_files,
    _episode_in_offline_cache,
)

log = get_logger()


_SIZE_UNITS = ('B', 'KB', 'MB', 'GB', 'TB')


def _format_size(size_bytes) -> str:
    """Lidsky čitelná velikost; None/0/invalid → prázdný řetězec."""
    try:
        n = float(size_bytes or 0)
    except (TypeError, ValueError):
        return ''
    if n <= 0:
        return ''
    idx = 0
    while n >= 1024.0 and idx < len(_SIZE_UNITS) - 1:
        n /= 1024.0
        idx += 1
    if idx == 0:
        return f"{int(n)} {_SIZE_UNITS[idx]}"
    return f"{n:.1f} {_SIZE_UNITS[idx]}"


def _stream_badge_label(st: dict) -> str:
    """Kompaktní prefix [CZdab][1080p]... z compact_stream_badges."""
    try:
        from resources.lib.series.series_helpers import compact_stream_badges
        badges = compact_stream_badges(st) or []
    except Exception:
        badges = []
    return ''.join(f"[{b}]" for b in badges)


def _quality_rank(st: dict) -> int:
    """Numerický rank pro řazení (vyšší = lepší). 2160→4, 1080→3, 720→2, jinak 1."""
    if _stream_has_quality(st, '2160'):
        return 4
    if _stream_has_quality(st, '1080'):
        return 3
    if _stream_has_quality(st, '720'):
        return 2
    return 1


def _lang_group_rank(st: dict) -> int:
    """0 = CZdab, 1 = CZtit, 2 = ostatní (pro řazení v pool listu)."""
    if _stream_has_cz_audio(st):
        return 0
    if _stream_has_czsub(st):
        return 1
    return 2


def _sort_streams_for_display(streams: list) -> list:
    """Setřídí streamy: CZdab → CZtit → rest, uvnitř podle kvality sestupně,
    při shodné kvalitě HDR > SDR (DV > HDR10+ > HDR10 > HDR > nic), při shodě
    podle velikosti sestupně. Pořadí odpovídá auto-výběru v
    offline_manager._best_in_quality_chain.
    """
    if not streams:
        return []
    return sorted(
        streams,
        key=lambda s: (
            _lang_group_rank(s),
            -_quality_rank(s),
            -_stream_hdr_rank(s),
            -(int(s.get('size') or 0)),
        ),
    )


class PlanItem:
    """Jeden řádek plánu — jedna epizoda se zvoleným streamem a všemi alternativami."""

    __slots__ = (
        'season', 'episode', 'ep_title',
        'all_streams', 'chosen_idx', 'skip_reason',
    )

    def __init__(self, season: str, episode: str, ep_title: str,
                 all_streams: list, chosen_idx: int, skip_reason):
        self.season = str(season)
        self.episode = str(episode)
        self.ep_title = ep_title or ''
        self.all_streams = all_streams or []
        self.chosen_idx = chosen_idx if chosen_idx is not None else -1
        self.skip_reason = skip_reason  # None | 'offline' | 'in_queue' | 'no_stream' | 'invalid'

    @property
    def chosen_stream(self):
        if 0 <= self.chosen_idx < len(self.all_streams):
            return self.all_streams[self.chosen_idx]
        return None

    @property
    def se_label(self) -> str:
        try:
            return f"S{int(self.season):02d}E{int(self.episode):02d}"
        except (TypeError, ValueError):
            return f"S{self.season}E{self.episode}"


def _find_chosen_idx(sorted_streams: list, best_stream) -> int:
    """Najde index 'best_stream' v setříděném listu (fallback 0)."""
    if not best_stream or not sorted_streams:
        return 0 if sorted_streams else -1
    best_ident = best_stream.get('ident')
    if best_ident:
        for i, st in enumerate(sorted_streams):
            if st.get('ident') == best_ident:
                return i
    # fallback identity
    for i, st in enumerate(sorted_streams):
        if st is best_stream:
            return i
    return 0


def build_queue_plan(series_name: str, data: dict, seasons: list,
                     prefer_quality: str) -> List[PlanItem]:
    """Sestaví seznam PlanItem pro zadané sezóny s vypočítaným skip_reason.

    - `seasons`: list of season keys (str), pořadí = pořadí v preview.
    - `prefer_quality`: '2160' | '1080' | '720' | '' (prázdné = bez preference).
    """
    result: List[PlanItem] = []
    if not isinstance(data, dict):
        return result

    all_seasons = data.get('seasons') or {}
    display_name = _resolve_folder_display_name(series_name, data)

    # Queue snapshot (1x) pro in_queue check
    try:
        from resources.lib.down.download_queue import load_queue
        q = load_queue() or []
    except Exception:
        q = []

    # Offline cache snapshot (1x) pro celý plán - vyhne se opakovanému SQL scanu per epizoda
    try:
        from resources.lib.database.metadata_db import db as _md
        from resources.lib.down.offline_cache_utils import build_disk_folder_to_storage_key_index
        _cache_all = _md.get_offline_cache_all() or {}
        _folder_index = build_disk_folder_to_storage_key_index() if _cache_all else {}
    except Exception:
        _cache_all, _folder_index = {}, {}

    for s_key in seasons:
        season = all_seasons.get(str(s_key)) or {}
        if not season:
            continue

        # Fyzické soubory v cílové složce (primary + legacy) — jednou per sezóna
        try:
            season_primary = _get_target_folder(
                series_name, str(s_key), display_name=display_name, series_data=data
            )
            season_legacy = _get_target_folder(
                series_name, str(s_key), display_name=series_name, series_data=data
            )
            files = _merge_season_files(season_primary or '', season_legacy or '')
        except Exception:
            files = []

        ep_keys = sorted(season.keys(), key=lambda k: int(k) if str(k).isdigit() else 0)
        for ep_num in ep_keys:
            ep = season.get(ep_num) or {}
            ep_title = ep.get('name') or ep.get('title') or ep.get('source_name') or ''
            streams = ep.get('streams') or []

            sorted_streams = _sort_streams_for_display(streams)

            # Skip reason detection
            skip_reason = None
            if not streams:
                skip_reason = 'no_stream'
            else:
                # offline file check
                expected_bases = []
                if ep_title:
                    expected_bases.append(clean_filename(ep_title).lower())
                try:
                    expected_bases.append(
                        f"S{int(s_key):02d}E{int(ep_num):02d}".lower()
                    )
                except (TypeError, ValueError):
                    pass
                on_disk = False
                for fname in files:
                    try:
                        base_noext = os.path.splitext(fname)[0].lower()
                        if any(base_noext.startswith(b) or base_noext == b for b in expected_bases if b):
                            on_disk = True
                            break
                    except Exception:
                        continue
                if on_disk:
                    skip_reason = 'offline'
                elif _episode_in_offline_cache(series_name, display_name, s_key, ep_num, cache_all=_cache_all, folder_index=_folder_index):
                    skip_reason = 'offline'
                else:
                    in_q = any(
                        i.get('series_name') == series_name
                        and str(i.get('season')) == str(s_key)
                        and str(i.get('episode')) == str(ep_num)
                        for i in q
                    )
                    if in_q:
                        skip_reason = 'in_queue'

            # Výběr streamu (i pro skip epizody ať uživatel vidí, co by se jinak vybralo)
            if streams:
                best = _pick_stream_by_quality(streams, prefer_quality or '')
                chosen_idx = _find_chosen_idx(sorted_streams, best)
            else:
                chosen_idx = -1

            result.append(PlanItem(
                season=str(s_key),
                episode=str(ep_num),
                ep_title=ep_title,
                all_streams=sorted_streams,
                chosen_idx=chosen_idx,
                skip_reason=skip_reason,
            ))

    return result


def validate_plan_streams(plan: List[PlanItem], series_name: str,
                          series_manager, prefer_quality: str = '') -> bool:
    """Ověří existenci streamů na Webshare (file_info) pro epizody k zařazení.

    Mutuje plán in-place: neplatné streamy vyhodí z ``all_streams``, případně
    přepne ``chosen_idx`` / nastaví ``skip_reason='invalid'``. Neplatné idents
    odstraní i z dat seriálu (persist).

    Vrací False při zrušení progress dialogu (caller má abortovat flow), jinak True.
    """
    import time
    from xml.etree import ElementTree as ET

    if not plan:
        return True

    to_check = [it for it in plan if it.skip_reason is None and it.all_streams]
    if not to_check:
        return True

    try:
        from resources.lib.auth.webshare_auth import WebshareClient
        ws = WebshareClient('plugin.video.mmirousek_v3')
        token = ws.revalidate()
    except Exception as e:
        log.error(f"validate_plan_streams: Webshare init failed: {e}")
        xbmcgui.Dialog().notification(
            'Offline', 'Nelze ověřit streamy (Webshare).', xbmcgui.NOTIFICATION_WARNING, 3000
        )
        return True  # neblokovat stahování při výpadku auth

    if not token:
        xbmcgui.Dialog().notification(
            'Offline', 'Nejste přihlášeni k Webshare — streamy neověřeny.',
            xbmcgui.NOTIFICATION_WARNING, 3000,
        )
        return True

    # Unikátní idents napříč plánem (jen enqueueovatelné epizody)
    idents_ordered = []
    seen = set()
    for it in to_check:
        for st in it.all_streams:
            ident = st.get('ident')
            if ident and ident not in seen:
                seen.add(ident)
                idents_ordered.append(ident)

    if not idents_ordered:
        return True

    validity = {}  # ident -> bool
    dlg = xbmcgui.DialogProgress()
    dlg.create('Kontrola streamů', 'Ověřuji dostupnost na Webshare…')
    total = len(idents_ordered)
    cancelled = False

    for i, ident in enumerate(idents_ordered):
        if dlg.iscanceled():
            cancelled = True
            break
        dlg.update(
            int((i / total) * 100),
            f"Kontrola {i + 1}/{total}",
        )
        try:
            resp = ws.api('file_info', {'ident': ident, 'wst': token})
            xml = ET.fromstring(resp.content)
            validity[ident] = (xml.findtext('status') == 'OK')
        except Exception as e:
            log.warning(f"file_info failed ident={ident}: {e}")
            validity[ident] = False
        time.sleep(0.17)

    dlg.close()
    if cancelled:
        log.info('validate_plan_streams: zrušeno uživatelem')
        return False

    # Aplikuj výsledky na plán + sběr mrtvých identů pro persist
    dead_idents = {ident for ident, ok in validity.items() if not ok}
    if not dead_idents:
        log.info(f'validate_plan_streams: všech {total} streamů OK')
        return True

    for it in to_check:
        old_chosen = it.chosen_stream
        old_ident = (old_chosen or {}).get('ident')
        valid_streams = [
            st for st in it.all_streams
            if st.get('ident') and validity.get(st.get('ident'), True)
        ]
        # ident bez výsledku validity (nemělo nastat) ponech; chybějící = True výše
        it.all_streams = valid_streams
        if not valid_streams:
            it.chosen_idx = -1
            it.skip_reason = 'invalid'
            continue

        # Zachovej původní výběr, pokud ještě žije; jinak auto-pick
        if old_ident and any(st.get('ident') == old_ident for st in valid_streams):
            it.chosen_idx = _find_chosen_idx(valid_streams, old_chosen)
        else:
            best = _pick_stream_by_quality(valid_streams, prefer_quality or '')
            it.chosen_idx = _find_chosen_idx(valid_streams, best)

    # Persist: odstraň mrtvé streamy z dat seriálu
    try:
        data = series_manager.load_series_data(series_name)
        if data:
            removed = 0
            for s_k, season in (data.get('seasons') or {}).items():
                for e_k, ep in (season or {}).items():
                    streams = ep.get('streams') or []
                    kept = [st for st in streams if st.get('ident') not in dead_idents]
                    if len(kept) != len(streams):
                        removed += len(streams) - len(kept)
                        ep['streams'] = kept
            if removed:
                series_manager._save_series_data(series_name, data)
                log.info(
                    f'validate_plan_streams: odstraněno {removed} neplatných streamů '
                    f'({len(dead_idents)} unikátních identů)'
                )
    except Exception as e:
        log.error(f'validate_plan_streams: persist failed: {e}')

    return True


def _stream_line(st: dict) -> str:
    """Textový label pro jeden stream v sub-dialogu i hlavním listu."""
    prefix = _stream_badge_label(st) or '[?]'
    size = _format_size(st.get('size'))
    if size:
        return f"{prefix} {size}"
    return prefix


def _plan_row_label(item: PlanItem) -> str:
    """Label pro řádek v hlavním select dialogu."""
    title = item.ep_title or ''
    left = f"{item.se_label}"
    if title:
        left += f"  {title}"

    if item.skip_reason == 'offline':
        return f"[COLOR gray]{left}  [skip: už stažena][/COLOR]"
    if item.skip_reason == 'in_queue':
        return f"[COLOR gray]{left}  [skip: ve frontě][/COLOR]"
    if item.skip_reason == 'no_stream':
        return f"[COLOR gray]{left}  [bez streamu][/COLOR]"
    if item.skip_reason == 'invalid':
        return f"[COLOR gray]{left}  [neplatný stream][/COLOR]"

    chosen = item.chosen_stream
    stream_lbl = _stream_line(chosen) if chosen else '[---]'
    return f"{left}  {stream_lbl}"


def _summary_counts(plan: List[PlanItem]):
    """Vrátí (enqueue_count, skip_offline, skip_in_queue, skip_no_stream, skip_invalid, total_bytes)."""
    enq = 0
    skip_off = 0
    skip_q = 0
    skip_ns = 0
    skip_inv = 0
    total_bytes = 0
    for it in plan:
        if it.skip_reason == 'offline':
            skip_off += 1
            continue
        if it.skip_reason == 'in_queue':
            skip_q += 1
            continue
        if it.skip_reason == 'no_stream':
            skip_ns += 1
            continue
        if it.skip_reason == 'invalid':
            skip_inv += 1
            continue
        enq += 1
        st = it.chosen_stream
        try:
            total_bytes += int((st or {}).get('size') or 0)
        except (TypeError, ValueError):
            pass
    return enq, skip_off, skip_q, skip_ns, skip_inv, total_bytes


def _pick_alt_stream(item: PlanItem) -> Optional[int]:
    """Sub-dialog výběru alternativního streamu pro jednu epizodu.
    Vrací nový chosen_idx (může být stejný) nebo None při zrušení."""
    if not item.all_streams:
        xbmcgui.Dialog().notification('Offline', 'Epizoda nemá žádné streamy.', xbmcgui.NOTIFICATION_WARNING, 2500)
        return None

    options = []
    for i, st in enumerate(item.all_streams):
        marker = '▶ ' if i == item.chosen_idx else '   '
        options.append(f"{marker}{_stream_line(st)}")

    title = f"{item.se_label} — vyber stream"
    preselect = max(0, item.chosen_idx)
    try:
        idx = xbmcgui.Dialog().select(title, options, preselect=preselect)
    except Exception:
        idx = xbmcgui.Dialog().select(title, options)
    if idx is None or idx < 0:
        return None
    return idx


def show_interactive_plan(plan: List[PlanItem], title: str) -> Optional[List[PlanItem]]:
    """Interaktivní editor. Vrací upravený plán (in-place) nebo None při zrušení.

    Hlavní položky:
      0) POKRAČOVAT (zařadit X epizod, ~Y)
      1) ZRUŠIT
      2+) jednotlivé epizody (řazené podle plan list pořadí)
    """
    if not plan:
        xbmcgui.Dialog().notification('Offline', 'Nic k zařazení.', xbmcgui.NOTIFICATION_INFO, 2500)
        return None

    dlg = xbmcgui.Dialog()
    while True:
        enq, skip_off, skip_q, skip_ns, skip_inv, total_bytes = _summary_counts(plan)
        size_str = _format_size(total_bytes) or '?'

        continue_label = f"[COLOR lime]POKRAČOVAT — zařadit {enq} epizod (~{size_str})[/COLOR]"
        if enq == 0:
            continue_label = "[COLOR gray]POKRAČOVAT — nic k zařazení[/COLOR]"
        cancel_label = "[COLOR red]ZRUŠIT[/COLOR]"

        options = [continue_label, cancel_label]
        for it in plan:
            options.append(_plan_row_label(it))

        # Info řádek v titulku
        info_parts = []
        if skip_off:
            info_parts.append(f"offline:{skip_off}")
        if skip_q:
            info_parts.append(f"ve frontě:{skip_q}")
        if skip_ns:
            info_parts.append(f"bez streamu:{skip_ns}")
        if skip_inv:
            info_parts.append(f"neplatné:{skip_inv}")
        info = (' · ' + ', '.join(info_parts)) if info_parts else ''
        full_title = f"{title}{info}"

        try:
            sel = dlg.select(full_title, options)
        except Exception as e:
            log.error(f"show_interactive_plan select error: {e}")
            return None

        if sel is None or sel < 0:
            return None
        if sel == 0:
            if enq == 0:
                xbmcgui.Dialog().notification('Offline', 'Nic k zařazení.', xbmcgui.NOTIFICATION_INFO, 2500)
                continue
            return plan
        if sel == 1:
            return None

        # Epizoda (index posunutý o 2 hlavní položky)
        ep_idx = sel - 2
        if ep_idx < 0 or ep_idx >= len(plan):
            continue

        item = plan[ep_idx]
        if item.skip_reason == 'offline':
            xbmcgui.Dialog().notification('Offline', 'Epizoda je už stažená.', xbmcgui.NOTIFICATION_INFO, 2500)
            continue
        if item.skip_reason == 'in_queue':
            xbmcgui.Dialog().notification('Offline', 'Epizoda je už ve frontě.', xbmcgui.NOTIFICATION_INFO, 2500)
            continue
        if item.skip_reason == 'no_stream':
            xbmcgui.Dialog().notification('Offline', 'Epizoda nemá žádné streamy.', xbmcgui.NOTIFICATION_WARNING, 2500)
            continue
        if item.skip_reason == 'invalid':
            xbmcgui.Dialog().notification(
                'Offline', 'Žádný platný stream na Webshare.', xbmcgui.NOTIFICATION_WARNING, 2500
            )
            continue

        new_idx = _pick_alt_stream(item)
        if new_idx is not None:
            item.chosen_idx = new_idx
