
# -*- coding: utf-8 -*-
# Module: series_manager
# Author: user extension (+ curated streams/TMDb enrichment by mm)
# Updated: 2025-11-27 (refactor: clean_filename/path_exists/listdir_safe from ui_utils)
# License: AGPL v.3 https://www.gnu.org/licenses/agpl-3.0.html

import os
import re
import json
import sys
import xbmc
import xbmcgui
import xbmcplugin
import xbmcaddon
import xbmcvfs
from resources.lib.utils.log_utils import get_logger
from resources.lib.utils.ui_utils import clean_filename, path_exists, listdir_safe, format_date_cz, _light_clean, _normalize_and_slugify

# Nové moduly pro dekompozici
from . import series_helpers as helpers
from . import series_filter as s_filter
from . import series_logic as logic
from . import series_ui_builder as ui_builder

from resources.lib.series.series_curator import StreamCurator
from resources.lib.series.series_enricher import SeriesEnricher
from resources.lib.database.metadata_db import db

# Konstanty pro uložení stavu
PROP_SEASON_FILTER_BASE = '__YAWSP_SEASON_FILTER_{}__' 
log = get_logger()

def _default_build_url(**kwargs): return helpers._default_build_url(**kwargs)

   


def _derive_latest_episode_timestamp(series_data): return helpers._derive_latest_episode_timestamp(series_data)
def _features_to_stream_info(features, runtime_min=None): return helpers._features_to_stream_info(features, runtime_min)
       

def _norm(s):
    return _normalize_and_slugify(s) if s else ''

class SeriesManager:
    def __init__(self, addon, profile):
        self.addon = addon
        self.profile = profile
        self.series_db_path = os.path.join(profile, 'series_db')
        self.ensure_db_exists()
        self.page_size = 100
        self.curator = StreamCurator(addon)
        self.enricher = SeriesEnricher(addon)
        self.max_pages = 5  # dle dohody

    # --- NOVÉ/UPRAVENÉ METODY ROBUSTNÍHO NAČÍTÁNÍ ----------------------------

    # --- SAVE s doplněním _content_hash (vložit do třídy SeriesManager) ---
# --- HASH helper (vložit do třídě SeriesManager) ---
    def _calculate_series_hash(self, data: dict) -> str:
        """
        Spočítá SHA1 hash JSON obsahu BEZ klíče '_content_hash'.
        Vrací hex string.
        """
        import hashlib, json
        # vyřadit hash z dat, ať se nevytváří nekonečná rekurze
        data_nohash = {k: v for k, v in (data or {}).items() if k != "_content_hash"}
        raw = json.dumps(data_nohash, ensure_ascii=False, sort_keys=True)
        return hashlib.sha1(raw.encode("utf-8")).hexdigest()




    def sync_series_with_sql(self, series_name=None):
        return logic.sync_series_with_sql(self, series_name)

    def validate_all_streams(self, series_name, token, api_fn):
        return logic.validate_all_streams(self, series_name, token, api_fn)

    def update_episode_streams(self, series_name, season, episode, streams):
        """Aktualizuje seznam streamů pro konkrétní epizodu a uloží do DB."""
        data = self.load_series_data(series_name)
        if not data: return False
        
        s_key, e_key = str(season), str(episode)
        seasons = data.get('seasons', {})
        if s_key not in seasons: seasons[s_key] = {}
        
        ep = seasons[s_key].get(e_key)
        if not ep:
            ep = {"streams": [], "tmdb": {}}
            seasons[s_key][e_key] = ep
            
        # Aktualizace streamů
        ep['streams'] = streams
        # Doplnění labelů a features (jako v migraci)
        for st in ep['streams']:
            st['features'] = self._extract_features(st)
            # label se sestaví z features nebo quality
            st['label'] = ', '.join(st['features']) if st['features'] else (st.get('quality') or 'Stream')
            st.setdefault('added_at', xbmc.getInfoLabel('System.Date'))
            st.setdefault('source', 'webshare')

        ep['updated_at'] = xbmc.getInfoLabel('System.Date')
        self._save_series_data(series_name, data)
        return True

    def get_next_episodes_list(self):
        return logic.get_next_episodes_list(self)





    def load_series_data(self, series_name_or_path):
        """
        Fáze 3: Načte data seriálu přímo z tabulky series_data ve SQLite (místo I/O souborů).
        Ořezává '.json' i celou cestu, pro fallback starých favorites/widgetů.
        """
        if not series_name_or_path: return None
        
        # Zpětná kompatibilita (pokud volání z jiného pluginu/widgetu posílá old .json filepath)
        clean_name = os.path.basename(str(series_name_or_path)).replace('.json', '')
        
        try:
            # 1. Přímý pokus (pokud byl clean_name uložen přesně takhle)
            series_data = db.get_series_data(clean_name)
            if series_data:
                return self._migrate_schema(series_data)
                
            # 2. Pokus pomocí vygenerovaného safe_name 
            safe_name = self._safe_filename(clean_name)
            series_data = db.get_series_data(safe_name)
            if series_data:
                return self._migrate_schema(series_data)
                
            # 3. Fallback: Projití všech seriálů a porovnání "name" atributu (kompatibilita se starými tmdb_ widgety a zástupnými URL z Kodi menu)
            if db.conn:
                cursor = db.conn.execute("SELECT json_data FROM series_data")
                c_clean = clean_name.strip().lower()
                for row in cursor.fetchall():
                    try:
                        d = json.loads(row[0])
                        nm = (d.get("name") or "").strip().lower()
                        if nm == c_clean:
                            return self._migrate_schema(d)
                    except Exception:
                        pass
        except Exception as e:
            log.error(f'[SeriesManager] Error loading series data pro {clean_name}: {e}')
            
        return None

    def _save_series_data(self, series_name: str, data: dict) -> None:
        """
        Interne uloží data do SQLite (replaces old atomic_json_write via file I/O).
        Sjednotíme safe název do čistého string parametru.
        """
        if not data: return
        safe_name = self._safe_filename(series_name, data.get("tmdb", {}).get("id"))
        
        # Přepočteme nový content hash a zapíšeme do sqlite blobs vrstvy
        data["_content_hash"] = self._calculate_series_hash(data)
        db.save_series_data(safe_name, data)

    def _get_tmdb_id(self, series_name: str) -> int:
        """
        Vrátí TMDb ID pro daný seriál — robustně.
        Zkusí načíst data přes _resolve_series_file_path a přečte tmdb.id.
        Pokud nenajde, vrátí None.
        """
        try:
            # 1) Zkus najít soubor podle názvu/safe name
            fp = self._resolve_series_file_path(series_name)
            if fp and os.path.exists(fp):
                with io.open(fp, 'r', encoding='utf8') as f:
                    d = json.load(f)
                if isinstance(d, dict):
                    tid = (d.get("tmdb") or {}).get("id")
                    if isinstance(tid, int) and tid > 0:
                        return tid

            # 2) Fallback: projdi tmdb_* soubory a porovnej name
            for fn in os.listdir(self.series_db_path):
                if not fn.startswith('tmdb_') or not fn.endswith('.json'):
                    continue
                try:
                    with io.open(os.path.join(self.series_db_path, fn), 'r', encoding='utf8') as f:
                        d = json.load(f)
                    if not isinstance(d, dict):
                        continue
                    nm = (d.get('name') or '').strip().lower()
                    if nm and nm == (series_name or '').strip().lower():
                        tid = (d.get("tmdb") or {}).get("id")
                        if isinstance(tid, int) and tid > 0:
                            return tid
                except Exception:
                    continue
            return None
        except Exception:
            return None

    def mark_episode_progress(self, series_name: str, season: int, episode: int, progress: float,
                              set_by: str = 'manual', watched_at: str = None, tmdb_id: int = None) -> None:
        """Označí jednu epizodu jako zhlédnutou/rozkoukanou nebo resetuje stav.

        `tmdb_id` (pokud je znám, např. z Trakt payloadu) je preferovaný způsob
        dohledání seriálu — je spolehlivější než porovnávání textového názvu,
        který se může lišit (diakritika, alternativní title, apod.).
        """
        log.info(f"{series_name} - Progress: {progress}, Watched_At Arg: {watched_at}, tmdb_id: {tmdb_id}")
        try:
            data = None
            save_key = series_name

            if tmdb_id:
                storage_key = self._safe_filename(series_name, tmdb_id)
                data = db.get_series_data(storage_key)
                if data:
                    save_key = storage_key
                    log.info(f"[mark_episode_progress] Seriál nalezen podle tmdb_id={tmdb_id} -> klíč {storage_key}")

            if not data:
                data = self.load_series_data(series_name)
                save_key = series_name

            if not data:
                try:
                    total_rows = db.conn.execute("SELECT COUNT(*) FROM series_data").fetchone()[0] if db.conn else -1
                except Exception:
                    total_rows = -1
                log.warning(f"Data nenalezena pro {series_name} (series_data řádků v DB: {total_rows}, tmdb_id: {tmdb_id})")
                return

            s_key, e_key = str(season), str(episode)
            ep = data.get('seasons', {}).get(s_key, {}).get(e_key)
            if not ep:
                log.warning(f"Epizoda {s_key}/{e_key} nenalezena")
                return

            ep['progress'] = round(progress, 1)
            ep['watched'] = progress >= 90.0
            if progress > 0.0 and watched_at:
                ep['watched_at'] = watched_at
                ep['set_by'] = set_by
            else:
                # Pokud je progress 0.0 (reset), smazat záznam
                ep.pop('watched_at', None)
                # set_by necháme, pokud by to bylo relevantní i pro 0%

            if ep['watched']:
                ep['play_count'] = ep.get('play_count', 0) + 1
                # ISO UTC — dřívější System.Date+System.Time (lokalizovaný formát)
                # neuměl parsovat web SPA ani _derive_latest_episode_timestamp
                ep['last_watched_at'] = watched_at or helpers.now_iso_utc()
                ep['set_by'] = set_by
                

            # Přepočítat stav sezóny
            self._recalc_season_state(data, s_key)
            if watched_at:
                data['last_watched_at_utc'] = watched_at
    
            # 🚨 ZDE MUSÍ BÝT VOLÁNÍ PRO ULOŽENÍ DAT NA DISK
            self._save_series_data(series_name, data)

            log.info(f"mark_episode_progress: {series_name} S{s_key}E{e_key} -> {progress}%")
        except Exception as e:
            log.error(f"CHYBA mark_episode_progress: {e}")

    def recalc_and_save_season_state(self, data: dict, season_key: str):
        """
        Přepočítá stav sezóny a uloží změny do SQLite přes _save_series_data
        (ten současně přepočte _content_hash pro CloudSync detekci).
        """
        try:
            self._recalc_season_state(data, season_key)
            self._save_series_data(data.get('name'), data)
            log.info(f"Season {season_key} state recalculated and saved")
        except Exception as e:
            log.error(f"CHYBA recalc_and_save_season_state: {e}")

    # --- Přepočet stavu sezóny ------------------------------------------------
    def _recalc_season_state(self, data: dict, season_key: str) -> None:
        """
        Spočítá watched_count, total a watched flag pro sezónu.
        Uloží do data['season_state'][season_key].
        """
        try:
            season = data.get('seasons', {}).get(season_key, {})
            total = len(season)
            # Pro sezóny 1+ preferuj TMDb episode_count; sezónu 0 (Specials) nech dle nalezených epizod.
            if str(season_key) != '0':
                tmdb_seasons = ((data.get('tmdb') or {}).get('seasons') or {})
                tmdb_total = (tmdb_seasons.get(str(season_key)) or {}).get('episode_count')
                try:
                    if tmdb_total is not None:
                        total = max(total, int(tmdb_total))
                except (ValueError, TypeError):
                    pass
            watched_count = sum(1 for ep in season.values() if ep.get('watched'))
            watched_flag = (watched_count == total and total > 0)

            season_state = data.setdefault('season_state', {})
            season_state[season_key] = {
                'watched': watched_flag,
                'watched_count': watched_count,
                'total': total,
                'last_updated': xbmc.getInfoLabel('System.Date')
            }
        except Exception as e:
            log.error(f'_recalc_season_state CHYBA: {e}')

    def _label_suffix_for_episode(self, ep: dict, color: bool) -> str:
        """
        Vrátí suffix pro stav epizody: [OK], [75%], [New] s/bez barvy.
        """
        try:
            progress = float(ep.get('progress', 0.0) or 0.0)
            if ep.get('watched'):
                return '[COLOR lime]ok[/COLOR]' if color else '[ok]'
            elif progress > 5.0:
                p = int(round(progress))
                return f"[COLOR lime]{p}%[/COLOR]" if color else f"[{p}%]"
            else:
                return '[COLOR dimgray]New[/COLOR]' if color else '[New]'
        except Exception:
            return ''

    def _label_prefix_for_series(self, data: dict, color: bool = True) -> str:
        """
        Vrátí prefix pro stav seriálu: [ok] nebo % na začátek labelu.
        """
        try:
            completed = is_series_completed(data)
            if completed:
                return '[COLOR lime][ok][/COLOR] ' if color else '[ok] '
            
            # Výpočet progressu (pokud máme data)
            seasons = data.get('seasons') or {}
            if not seasons:
                return ''
                
            total_eps = sum(len(v) for v in seasons.values() if isinstance(v, dict))
            if total_eps == 0:
                return ''
                
            season_state = data.get('season_state') or {}
            watched_eps = sum(st.get('watched_count', 0) for st in season_state.values())
            
            if watched_eps > 0:
                p = int((watched_eps / total_eps) * 100)
                if p >= 100 and not completed:
                    p = 99 # vizuální pojistka, pokud is_series_completed je přísnější
                return f"[COLOR orange][{p}%][/COLOR] " if color else f"[{p}%] "
            else:
                return '[COLOR dimgray][New][/COLOR] ' if color else '[New] '
        except Exception:
            return ''

    # --- FS zajištění ---------------------------------------------------------
    def ensure_db_exists(self):
        """Ensure that the series database directory exists"""
        try:
            if not os.path.exists(self.profile):
                os.makedirs(self.profile)
            if not os.path.exists(self.series_db_path):
                os.makedirs(self.series_db_path)
        except Exception as e:
            log.error(f'Error creating directories: {e}')

    # --- Vyhledávání + kurátorování + TMDb enrichment -------------------------

    def search_series(self, series_name, api_function, token, **kwargs):
        """
        Wrapper: deleguje na SearchEngine, aby zůstal kompatibilní zbytek kódu (UI).
        """
        from resources.lib.series.search_engine import SearchEngine  # lokální import, aby nevznikl cyklus
        engine = SearchEngine(self)
        return engine.search_series(series_name, api_function, token, **kwargs)

    def tmdb_filter(self, series_data, buckets, filter_mode='standard'):
        return s_filter.filter_invalid_seasons(self, series_data, buckets, filter_mode=filter_mode)

    def _is_likely_episode(self, filename, series_name):
        """Check if a filename is likely to be an episode of the series"""
        if not filename:
            return False
        # povolit i varianty názvu (diakritika/oddělovače)
        if series_name and re.search(re.escape(series_name), filename, re.IGNORECASE) is None:
            if re.sub(r'\W+', '', series_name.lower()) not in re.sub(r'\W+', '', filename.lower()):
                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, api_function, token, offset=0, limit=100):
        """Perform the actual search using the provided API function with paging"""
        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, series_name):
        """Detect S/E from filename (robust, CZ friendly)."""
        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:
            # groups: (sezona|série|řada) (S) (díl|epizoda|ep) (E)
            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

    def get_all_series(self):
        """Vrátí seznam všech seriálů rozkoukaných ve fázi 3 z V10 SQLite."""
        series_list = []
        try:
            if not db.conn:
                return series_list
                
            cur = db.conn.execute("SELECT series_name, json_data FROM series_data")
            for row in cur.fetchall():
                safe_name = row[0]
                json_raw = row[1]
                display_name = safe_name.replace('_', ' ')
                
                try:
                    d = json.loads(json_raw)
                    nm = (d.get('name') or '').strip()
                    if nm:
                        display_name = nm
                except Exception:
                    pass
                    
                series_list.append({
                    'name': display_name,
                    'filename': f"{safe_name}.json",  # zachovani zpetne kompatibility klice 
                    'safe_name': safe_name
                })
        except Exception as e:
            log.error(f'[SeriesManager] Error listovani serialu z DB: {e}')
            
        return series_list

    def _safe_filename(self, series_name: str, tmdb_id: int = None) -> str:
        """
        Vrátí bezpečný název souboru pro seriál.
        Preferuje TMDb ID, jinak fallback na očištěný název.
        """
        if tmdb_id:
            return f"tmdb_{tmdb_id}"
        return re.sub(r'[^a-z0-9]+', '_', (series_name or '').lower()).strip('_')

    # --- schéma v3 + migrace --------------------------------------------------
    def _init_series_data(self, series_name):
        data = self.load_series_data(series_name) or {}
        if not data:
            data = {
                "schema_version": 3,
                "name": series_name,
                "aliases": [],
                "last_updated": xbmc.getInfoLabel('System.Date'),
                "source": "webshare",
                "known_idents": [],
                "tmdb": {},
                "extras": {"season_packs": []},
                "seasons": {}
            }
        else:
            data.setdefault("schema_version", 3)
            data.setdefault("aliases", [])
            data.setdefault("source", "webshare")
            data.setdefault("known_idents", [])
            data.setdefault("tmdb", {})
            data.setdefault("extras", {"season_packs": []})
            data.setdefault("seasons", {})
            data["last_updated"] = xbmc.getInfoLabel('System.Date')
        return data

    def _migrate_schema(self, data):
        if not isinstance(data, dict):
            return data
        data.setdefault("schema_version", 3)
        data.setdefault("aliases", [])
        data.setdefault("source", "webshare")
        data.setdefault("known_idents", [])
        data.setdefault("tmdb", {})
        data.setdefault("extras", {"season_packs": []})
        seasons = data.setdefault("seasons", {})

        # migrace starého "ident" -> streams[0]
        for s in list(seasons.keys()):
            for e in list(seasons[s].keys()):
                ep = seasons[s][e]
                if isinstance(ep, dict) and "streams" not in ep:
                    if "ident" in ep:
                        streams = [{
                            "ident": ep.get("ident"),
                            "name": ep.get("name", ""),
                            "quality": ep.get("quality", ""),
                            "lang": ep.get("lang", ""),
                            "size": int(ep.get("size", "0") or 0),
                            "ainfo": ep.get("ainfo", ""),
                            "score": 0,
                            "added_at": xbmc.getInfoLabel('System.Date'),
                            "source": "webshare"
                        }]
                        # doplň features/label
                        for st in streams:
                            st['features'] = self._extract_features(st)
                            st['label'] = ', '.join(st['features']) if st['features'] else (_norm(st['quality']) or _norm(st['name']) or 'Stream')
                        ep["streams"] = streams
                        ep.setdefault("source_name", ep.get("name", ""))
                        ep.setdefault("tmdb", {})
                        ep.setdefault("curated", False)
                        ep.setdefault("locked", False)
                        ep.setdefault("updated_at", xbmc.getInfoLabel('System.Date'))
                        # ep['name'] bude později přepsán TMDb názvem
        return data

    # --- skórování a 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 = 50
    ):
        return self.curator.curate_streams(
            candidates,
            skip_high_quality=skip_high_quality,
            force_hd_only=force_hd_only,
            name_only=name_only,
            only_uhd=only_uhd,
            max_per_episode=max_per_episode
        )

    
    def _enrich_with_tmdb(self, series_data: dict, series_name: str):
        return self.enricher.enrich_with_tmdb(series_data, series_name)
    def _populate_tmdb_placeholders(self, series_data: dict, tv_id: int, language: str = 'cs'):
        return self.enricher.populate_tmdb_placeholders(
            series_data, tv_id, language, 
            recalc_callback=self._recalc_season_state
        )


    @staticmethod
    def _extract_features(st):
        return StreamCurator.extract_features(st)

    # --- public helper: smazání seriálu --------------------------------------
    def remove_series(self, series_name, tmdb_id=None):
        """
        Odstraní seriál z lokální SQLite databáze.
        Vrací bool: True pokud byl seriál smazán.
        """
        try:
            log.info(f"[SeriesManager.remove_series] Začínám mazat seriál: '{series_name}' (tmdb_id: {tmdb_id})")

            from resources.lib.database.metadata_db import db
            from resources.lib.utils.ui_utils import clean_filename
            clean_name = clean_filename(series_name)
            
            log.info(f"[SeriesManager.remove_series] Volám db.delete_series_data('{series_name}', tmdb_id='{tmdb_id}') a cz.clear_offline_cache('{clean_name}')")
            db_removed = db.delete_series_data(series_name, tmdb_id=tmdb_id)
            log.info(f"[SeriesManager.remove_series] db_removed: {db_removed}")
            
            db.clear_offline_cache_for_series(clean_name)

            return db_removed
        except Exception as e:
            log.error(f'remove_series error: {e}')
            return False
        
# === Utility functions for the UI layer =======================================



def filter_invalid_seasons(sm, data, buckets):
    return s_filter.filter_invalid_seasons(sm, data, buckets)


def get_url(**kwargs):
    """Create a URL for calling the plugin recursively"""
    from sm3_entry import _url
    return '{0}?{1}'.format(_url, urlencode(kwargs, encoding='utf-8'))


def create_series_menu(self, handle):
    return ui_builder.create_series_menu(self, handle)

def is_series_completed(series_data): return logic.is_series_completed(series_data)
def is_series_started(series_data): return logic.is_series_started(series_data)


def create_series_menu_filtered(self, handle, status):
    return ui_builder.create_series_menu_filtered(self, handle, status)

def create_seasons_menu(self, handle, series_name, build_url_fn=None):
    return ui_builder.create_seasons_menu(self, handle, series_name, build_url_fn)



def create_episodes_menu(self, handle, series_name, season_num, build_url_fn=None):
    return ui_builder.create_episodes_menu(self, handle, series_name, season_num, build_url_fn)

# --- Nové pomocné API pro sm3_entry.py -------------------------------------------
def _format_stream_label(st):
    return ui_builder._format_stream_label(st)


def get_episode_streams(series_manager_obj, series_name, season, episode):
    return helpers.get_episode_streams(series_manager_obj, series_name, season, episode)


def format_stream_two_lines(st, runtime=None):
    return ui_builder.format_stream_two_lines(st, runtime)

def _iter_saved_streams(series_data, season_only=None):
    # Pro zachování kompatibility s voláním s season_only
    if season_only is not None:
        for s_i, e_i, ep, st in helpers._iter_saved_streams(series_data):
            if str(s_i) == str(season_only):
                yield s_i, e_i, ep, st
    else:
        yield from helpers._iter_saved_streams(series_data)

def _ask_season_filter():
    # Původní verze v SM neměla argument
    return ui_builder._ask_season_filter(None)

def list_saved_streams(
    series_manager, handle, series_name, build_url_fn=None, season=None,
    filter_mode=None
):
    return ui_builder.list_saved_streams(
        series_manager, handle, series_name, build_url_fn,
        season=season, filter_mode=filter_mode,
    )
