# wcs/ai/DialogMyTV.py
import xbmc
import xbmcgui
import xbmcaddon
import xbmcvfs
import os
from urllib.parse import quote_plus
from wcs.ai.DialogAIChatRecommendation import AIChatRecommendationDialog, AIRecommendationPromptBuilder
from wcs.ai.MediaChannelManager import MediaChannelManager as MyTVChannelManager
from wcs.ai.DialogAddMedia import AddMediaDialog, create_add_media_controller
from wcs import user_data, utils
from wcs.caching import get_cache_manager

# PIL import with fallback
try:
    from PIL import Image, ImageFilter, ImageDraw
    PIL_AVAILABLE = True
except ImportError:
    PIL_AVAILABLE = False
    xbmc.log("[MyTV] PIL not available - collage/effects disabled", xbmc.LOGWARNING)

from wcs.ai import ChannelHistory
from wcs.ai import ProgressManager
from wcs.ai import PlaylistBuilder


class MyTVDialog(AIChatRecommendationDialog):
    # Background mode constants
    BG_STATIC = 0
    BG_FANART = 1
    BG_STILL = 2
    BG_COLLAGE_POSTERS = 3
    BG_COLLAGE_STILLS = 4
    BG_COLLAGE_MIX = 5
    
    def __init__(self, xml_filename, addon_path, *args, **kwargs):
        # skin.milionar opens the dialog focused on one channel (docs/plan/skin-milionar/060 §5)
        self._initial_channel_id = kwargs.pop('initial_channel_id', None)
        self._context_uid = kwargs.pop('context_uid', '')
        self._context_row = kwargs.pop('context_row', '')
        self._initial_edit = kwargs.pop('initial_edit', False)
        super(MyTVDialog, self).__init__(xml_filename, addon_path, *args, **kwargs)
        self._xml_filename = xml_filename
        self._nav_section_id = 'my_tv'
        self.channel_manager = MyTVChannelManager(self.addon)
        self.current_channel_index = 0
        
        # Use centralized cache manager
        self._cache = get_cache_manager()
        
        # Run cache maintenance on startup (background, throttled)
        self._cache.maintenance()
        
        # Toast notification state
        self._toast_timer = None

    def _show_toast(self, message, toast_type='info', duration=3.0):
        """
        Zobrazí vlastní toast notifikaci v pravém horním rohu.
        
        Args:
            message: Text zprávy
            toast_type: 'info', 'success', 'warning', 'error'
            duration: Doba zobrazení v sekundách (default 3.0)
        """
        import threading
        
        # Cancel existing timer
        if self._toast_timer:
            self._toast_timer.cancel()
        
        # Set properties for XML
        self.setProperty('WCS.Toast.Message', message)
        self.setProperty('WCS.Toast.Type', toast_type)
        self.setProperty('WCS.Toast.Visible', 'true')
        
        # Auto-hide after duration
        self._toast_timer = threading.Timer(duration, self._hide_toast)
        self._toast_timer.daemon = True
        self._toast_timer.start()
    
    def _hide_toast(self):
        """Skryje toast notifikaci."""
        self.clearProperty('WCS.Toast.Visible')
        # Keep message/type for fade-out animation, clear after animation completes
        import threading
        def clear_toast_data():
            self.clearProperty('WCS.Toast.Message')
            self.clearProperty('WCS.Toast.Type')
        threading.Timer(0.3, clear_toast_data).start()


    def onInit(self):
        self._reset_state()
        self.setProperty('WCS.AIChat.Title', 'MyTV - televize budoucnosti')
        self.setProperty('WCS.AIChat.Status', 'Připraveno')
        
        # Default button labels for TV mode
        self.setProperty('WCS.Button.Play', 'Živé vysílání')
        self.setProperty('WCS.Button.PlayEp', 'Přehrát epizodu')
        self.setProperty('WCS.Button.PlaySeries', 'Přehrát seriál')
        self.setProperty('WCS.Button.Delete', 'Odebrat kanál')
        self.setProperty('WCS.Button.DeleteConfirm', 'Opravdu smazat kanál?')
        
        # Default row labels for TV mode
        self.setProperty('WCS.Row1.Label', 'Moje kanály')
        self.setProperty('WCS.Row3.Label', 'Moje seriály')
        
        # Start with the first channel (or the one the skin asked for)
        self.current_channel_index = 0
        if self._initial_channel_id:
            for index, channel in enumerate(self.channel_manager.get_all_channels()):
                if channel.id == self._initial_channel_id:
                    self.current_channel_index = index
                    break
        
        # CLEAR ALL BACKGROUND PROPERTIES - Start fresh with matte layer
        self.clearProperty('WCS.MyTV.Background.Custom')
        self.clearProperty('WCS.MyTV.Background.Ready')
        
        # Clear any leftover confirmation dialogs from previous session
        self.clearProperty('WCS.MyTV.DeleteSeriesConfirm')
        self.clearProperty('WCS.MyTV.DeleteConfirm')
        
        # RESET PROGRAM LIST IMMEDIATELY - Clear cached items that cause Fanart/Still flash
        try:
            program_list = self.getControl(9000)
            program_list.reset()
        except:
            pass
        
        # Set background mode and animation properties
        self._setup_background_properties()
        
        # Static mode: Only set Ready if no processing needed
        # If effects enabled, wait for PIL to generate blurred version
        if self._bg_mode == self.BG_STATIC and not self._is_background_processing_needed():
            self.setProperty('WCS.MyTV.Background.Ready', 'true')
        
        # Poster collage: Start background generation IMMEDIATELY (uses channel data, not program)
        if self._bg_mode == self.BG_COLLAGE_POSTERS:
            self._update_background()
        
        # Static mode with effects: Generate blurred version
        if self._bg_mode == self.BG_STATIC and self._is_background_processing_needed():
            self._update_background()
        
        # Queue update: remove watched episodes from cached programs
        # (keeps remaining episodes in order, they'll be filled up in refresh_program_view)
        self._update_program_queues()
        
        # Load View
        self.refresh_channels_view(set_focus=True)
        
        # Trigger background generation for other modes (after program loads)
        if self._bg_mode not in (self.BG_STATIC, self.BG_COLLAGE_POSTERS):
            self._update_background()
        
        # Ensure sidebar is hidden initially
        if self._show_nav_on_init:
            self._show_nav_sidebar(animate=False)
        else:
            self.clearProperty('WCS.AIChat.Visible')
        
        # Apple TV layout: Focus on "Živé vysílání" button (9004)
        # Other layouts use XML defaultcontrol (2000)
        if 'appletv' in getattr(self, '_xml_filename', ''):
            self.setFocusId(9004)

        # Auto-open Add Series dialog when no channels exist
        if not self.channel_manager.get_all_channels():
            self._create_new_channel()
        elif self._initial_edit:
            self._show_series_grid_dialog()


    def refresh_channels_view(self, set_focus=False):
        """Populate the bottom list with channels."""
        channels = self.channel_manager.get_all_channels()
        list_control = self.getControl(2000)
        list_control.reset()
        
        for ch in channels:
            item = xbmcgui.ListItem(label=ch.name)
            # Use channel icon or fallback
            icon = ch.icon if ch.icon else 'special://home/addons/plugin.video.milionar/resources/media/placeholder_tv_card.jpg'
            item.setArt({'poster': icon})
            item.setProperty('channel_id', ch.id)
            list_control.addItem(item)
            
        # Add "Create New Channel" item
        create_item = xbmcgui.ListItem(label="")
        create_item.setArt({'poster': 'special://home/addons/plugin.video.milionar/resources/media/plus_card.png'}) # Fallback to placeholder if not exists
        create_item.setProperty('is_create_button', 'true')
        list_control.addItem(create_item)
            
        # Select first if available (and reset index if out of bounds)
        if self.current_channel_index >= list_control.size():
            self.current_channel_index = 0
            
        if list_control.size() > 0:
            list_control.selectItem(self.current_channel_index)
            if self._initial_channel_id and list_control.getSelectedPosition() != self.current_channel_index:
                # selectItem is ignored while the window is still initialising; SetFocus by position sticks
                xbmc.executebuiltin(f'SetFocus(2000,{self.current_channel_index},absolute)', True)
            self.update_editor_view()
            self.refresh_program_view()
            # Let XML defaultcontrol handle initial focus (9004 for Apple TV, 2000 for others)

        # Populate My Series row (Apple TV layout)
        self._populate_my_series_list()

    def update_editor_view(self):
        """Update grid 3000 based on selected channel."""
        list_control = self.getControl(2000)
        idx = list_control.getSelectedPosition()
        if idx < 0:
            return
            
        item = list_control.getListItem(idx)
        
        # If "Create New" is selected, clear editor and return
        if item.getProperty('is_create_button') == 'true':
            self.getControl(9100).reset()
            # self.getControl(9000).reset() # Necháme program list být, ať to nebliká
            return
            
        channel_id = item.getProperty('channel_id')
        channel = self.channel_manager.get_channel_by_id(channel_id)
        
        grid_control = self.getControl(9100)
        grid_control.reset()
        
        if channel:
            # Add existing series
            for series in channel.series_list:
                s_item = xbmcgui.ListItem(label=series.get('name'))
                poster = series.get('poster_path') or series.get('poster')
                if poster and not poster.startswith('http'):
                     poster = f"https://image.tmdb.org/t/p/w500{poster}"
                elif not poster:
                     poster = 'special://home/addons/plugin.video.milionar/resources/media/placeholder_tv_card.jpg'
                
                s_item.setArt({'poster': poster})
                s_item.setProperty('series_id', str(series.get('id')))
                s_item.setProperty('channel_id', channel_id)
                grid_control.addItem(s_item)

            # Always add "Add Series" card at the end of the grid
            add_item = xbmcgui.ListItem(label="Přidat seriál")
            add_item.setArt({'poster': 'special://home/addons/plugin.video.milionar/resources/media/plus_card.png'})
            add_item.setProperty('is_add_button', 'true')
            add_item.setProperty('channel_id', channel_id)
            grid_control.addItem(add_item)

    def _update_program_queues(self):
        """Queue-based program update: remove watched episodes, keep rest, append new.
        
        For each channel with cached program:
        1. Remove episodes where progress has advanced past them (watched)
        2. Keep remaining episodes in their current order
        3. If count < upcoming_count, mark for async append
        """
        channels = self.channel_manager.get_all_channels()
        
        for channel in channels:
            channel_id = channel.id
            series_list = channel.series_list if hasattr(channel, 'series_list') else []
            if not series_list:
                continue
            
            cache_key = f"program_list_{channel_id}"
            cached_items = self._cache.get(cache_key)
            if not cached_items:
                continue  # No cache -- will be generated fresh by refresh_program_view
            
            # Filter out watched episodes
            remaining = []
            removed_count = 0
            for item_data in cached_items:
                props = item_data.get('properties', {})
                tmdb_id = props.get('tmdb_id', '')
                ep_season = int(props.get('season', 0) or 0)
                ep_episode = int(props.get('episode', 0) or 0)
                
                if not tmdb_id or not ep_season:
                    remaining.append(item_data)
                    continue
                
                # Get current progress for this series in this channel
                prog_season, prog_episode = ProgressManager.get_progress(channel_id, tmdb_id)
                
                # Episode is watched if progress is at or past it
                is_watched = (prog_season > ep_season) or \
                             (prog_season == ep_season and prog_episode >= ep_episode)
                
                if is_watched:
                    removed_count += 1
                else:
                    remaining.append(item_data)
            
            if removed_count > 0:
                # Save filtered list back to cache
                self._cache.set(cache_key, remaining, ttl_seconds=24 * 3600)
                xbmc.log(
                    f"[MyTV] Queue update channel {channel_id}: removed {removed_count} watched, "
                    f"{len(remaining)} remaining",
                    xbmc.LOGINFO
                )
            
            # Save updated fingerprint
            ProgressManager.save_progress_fingerprint(channel_id, series_list, self._cache)

    def refresh_program_view(self, shuffle=False):
        """Debounced trigger - schedules async load after 300ms delay.
        
        Args:
            shuffle: If True, generate new shuffled program order.
                     If False, use cached order (no re-shuffle on channel switch).
        """
        import threading
        
        # Initialize state if needed
        if not hasattr(self, '_program_load_token'):
            self._program_load_token = 0
        if not hasattr(self, '_program_load_timer'):
            self._program_load_timer = None
        
        # Increment token (cancels any pending/running loads)
        self._program_load_token += 1
        current_token = self._program_load_token
        
        # Get channel ID now (sync, fast)
        list_control = self.getControl(2000)
        idx = list_control.getSelectedPosition()
        if idx < 0:
            return
        
        item = list_control.getListItem(idx)
        channel_id = item.getProperty('channel_id')
        if not channel_id:
            return
        
        # Store for stale play prevention
        self._current_loaded_channel_id = channel_id
        
        # Update channel statistics display
        self._update_channel_stats(channel_id)
        
        # INSTANT LOAD: Try to load from program list cache immediately
        cache_loaded = self._instant_load_from_cache(channel_id)
        
        # If no shuffle requested and cache was loaded, skip async reload
        if not shuffle and cache_loaded:
            xbmc.log(f"[MyTV] Using cached program order for channel {channel_id} (no shuffle)", xbmc.LOGINFO)
            # Apply saved reorder on top of cached data (safety: ensures order is correct)
            self._apply_program_reorder(channel_id)
            # Still update background and hero for current items
            self._update_background(channel_id)
            self._update_hero_zone()
            
            # Check if cache needs filling (watched episodes were removed)
            try:
                upcoming_count_val = self.addon.getSetting('mytv_upcoming_count')
                upcoming_count = int(upcoming_count_val) if upcoming_count_val else 12
            except Exception:
                upcoming_count = 12
            
            cache_key = f"program_list_{channel_id}"
            cached_items = self._cache.get(cache_key)
            current_count = len(cached_items) if cached_items else 0
            
            if current_count < upcoming_count:
                # Need to append more episodes -- async
                xbmc.log(
                    f"[MyTV] Cache has {current_count}/{upcoming_count} items, "
                    f"appending {upcoming_count - current_count} more",
                    xbmc.LOGINFO
                )
                self._program_load_timer = threading.Timer(
                    0.3,
                    self._async_program_append,
                    args=(current_token, channel_id, upcoming_count - current_count)
                )
                self._program_load_timer.daemon = True
                self._program_load_timer.start()
            return
        
        # Invalidate cache when shuffle requested
        if shuffle:
            cache_key = f"program_list_{channel_id}"
            self._cache.delete(cache_key)
            # Also clear saved reorder (user wants fresh shuffle)
            reorder_key = f"program_reorder_{channel_id}"
            self._cache.delete(reorder_key)
        
        # Cancel existing timer
        if self._program_load_timer:
            self._program_load_timer.cancel()
        
        # Schedule debounced async refresh (300ms delay)
        self._program_load_timer = threading.Timer(
            0.3,
            self._async_program_load,
            args=(current_token, channel_id, shuffle)
        )
        self._program_load_timer.daemon = True
        self._program_load_timer.start()
    
    def _update_channel_stats(self, channel_id):
        """Update channel statistics display."""
        channel = self.channel_manager.get_channel_by_id(channel_id)
        if not channel:
            self.clearProperty('WCS.Channel.Stats')
            return
        
        # Count all channels
        all_channels = self.channel_manager.get_all_channels()
        channel_count = len(all_channels)
        
        # Count series/movies in current channel
        series_list = channel.series_list if hasattr(channel, 'series_list') else []
        media_list = channel.media_list if hasattr(channel, 'media_list') else []
        
        # Use appropriate list based on channel type
        items = media_list if media_list else series_list
        item_count = len(items)
        
        # Build stats string
        channel_type = getattr(self, '_channel_type', 'tv')
        
        if channel_type == 'cinema':
            # Cinema: X kin  •  Y filmů  •  Zh Mmin
            channel_label = f"{channel_count} {'kino' if channel_count == 1 else 'kina' if channel_count < 5 else 'kin'}"
            item_label = f"{item_count} {'film' if item_count == 1 else 'filmy' if item_count < 5 else 'filmů'}"
            
            # Calculate total runtime
            total_runtime = sum(m.get('runtime', 0) for m in items if m.get('runtime'))
            if total_runtime > 60:
                hours = total_runtime // 60
                mins = total_runtime % 60
                runtime_str = f"{hours}h {mins}min" if mins else f"{hours}h"
            else:
                runtime_str = f"{total_runtime} min" if total_runtime else ""
            
            stats = f"{channel_label}  •  {item_label}"
            if runtime_str:
                stats += f"  •  {runtime_str}"
        else:
            # TV: X kanálů  •  Y seriálů
            channel_label = f"{channel_count} {'kanál' if channel_count == 1 else 'kanály' if channel_count < 5 else 'kanálů'}"
            item_label = f"{item_count} {'seriál' if item_count == 1 else 'seriály' if item_count < 5 else 'seriálů'}"
            stats = f"{channel_label}  •  {item_label}"
        
        self.setProperty('WCS.Channel.Stats', stats)
        
        # Dynamic title: MyTV - {channel name}
        self.setProperty('WCS.AIChat.Title', f"MyTV - {channel.name}")
        
        # Global stats: total channels + total unique series across all channels
        total_series = set()
        for ch in all_channels:
            ch_series = ch.series_list if hasattr(ch, 'series_list') else []
            for s in ch_series:
                sid = str(s.get('id', ''))
                if sid:
                    total_series.add(sid)
        total_series_count = len(total_series)
        
        if channel_type == 'cinema':
            global_ch_label = f"{channel_count} {'kino' if channel_count == 1 else 'kina' if channel_count < 5 else 'kin'}"
            global_item_label = f"{total_series_count} {'film' if total_series_count == 1 else 'filmy' if total_series_count < 5 else 'filmů'}"
        else:
            global_ch_label = f"{channel_count} {'kanál' if channel_count == 1 else 'kanály' if channel_count < 5 else 'kanálů'}"
            global_item_label = f"{total_series_count} {'seriál' if total_series_count == 1 else 'seriály' if total_series_count < 5 else 'seriálů'}"
        
        self.setProperty('WCS.Channel.GlobalStats', f"{global_ch_label}  •  {global_item_label}")

    def _update_hero_zone(self):
        """Update hero zone properties from the first item in program list."""
        try:
            program_list = self.getControl(9000)
            if program_list.size() == 0:
                self._clear_hero_zone()
                return

            item = program_list.getListItem(0)
            series_name = item.getProperty('series_name') or item.getLabel() or ''
            
            self.setProperty('WCS.MyTV.Hero.SeriesName', series_name)
            self.setProperty('WCS.MyTV.Hero.SeriesLogo', item.getProperty('series_logo') or '')
            self.setProperty('WCS.MyTV.Hero.Fanart', item.getArt('fanart') or '')
            # Reformat ep_code from S01E01 to "Sezona 1 Epizoda 1" for hero display
            raw_code = item.getProperty('ep_code') or ''
            if raw_code and raw_code.startswith('S') and 'E' in raw_code:
                try:
                    parts = raw_code[1:].split('E')
                    season_num = int(parts[0])
                    episode_num = int(parts[1])
                    hero_code = f"Sez\u00f3na {season_num} Epizoda {episode_num}"
                except (ValueError, IndexError):
                    hero_code = raw_code
            else:
                hero_code = raw_code
            self.setProperty('WCS.MyTV.Hero.EpCode', hero_code)
            self.setProperty('WCS.MyTV.Hero.EpTitle', item.getProperty('episode_title') or '')
            plot = item.getProperty('plot') or ''
            if len(plot) > 300:
                plot = plot[:298].rstrip() + '..'
            self.setProperty('WCS.MyTV.Hero.Plot', plot)
            
            # Build meta line: genre | runtime | rating
            parts = []
            genre = item.getProperty('genre')
            if genre:
                parts.append(genre)
            runtime = item.getProperty('runtime')
            if runtime and runtime != '0':
                # Strip existing " min" suffix to avoid duplicates
                runtime_clean = runtime.replace(' min', '').strip()
                if runtime_clean and runtime_clean != '0':
                    parts.append(f"{runtime_clean} min")
            year = item.getProperty('year')
            if year and year != '0':
                parts.append(year)
            rating = item.getProperty('rating')
            if rating and rating != '0' and rating != '0.0':
                parts.append(f"{rating}/10")
            
            self.setProperty('WCS.MyTV.Hero.MetaLine', '  |  '.join(parts))
            
            # Resume check -- zmena labelu buttonu 9040
            channel_list = self.getControl(2000)
            current_channel = channel_list.getSelectedItem()
            channel_id = current_channel.getProperty('channel_id') if current_channel else ''
            tmdb_id = item.getProperty('tmdb_id') or ''
            if channel_id and tmdb_id:
                try:
                    from wcs.ai import ChannelHistory
                    resume = ChannelHistory.get_resume_point(channel_id, tmdb_id)
                    if resume and resume['position'] > 10:
                        self.setProperty('WCS.Button.PlayEp', 'Pokračovat')
                        self.setProperty('WCS.MyTV.Hero.HasResume', 'true')
                    else:
                        self.setProperty('WCS.Button.PlayEp', 'Přehrát epizodu')
                        self.clearProperty('WCS.MyTV.Hero.HasResume')
                except Exception:
                    self.setProperty('WCS.Button.PlayEp', 'Přehrát epizodu')
                    self.clearProperty('WCS.MyTV.Hero.HasResume')
            else:
                self.setProperty('WCS.Button.PlayEp', 'Přehrát epizodu')
                self.clearProperty('WCS.MyTV.Hero.HasResume')
            
        except Exception as e:
            xbmc.log(f"[MyTV] Error updating hero zone: {e}", xbmc.LOGERROR)
            self._clear_hero_zone()

    def _clear_hero_zone(self):
        """Clear all hero zone properties."""
        for prop in ['SeriesName', 'SeriesLogo', 'Fanart', 'EpCode', 'EpTitle',
                     'Plot', 'MetaLine']:
            self.clearProperty(f'WCS.MyTV.Hero.{prop}')

    def _populate_my_series_list(self):
        """Populate My Series list (9060) with unique series from all channels."""
        try:
            series_list = self.getControl(9060)
        except Exception:
            return  # Control 9060 not present in this layout

        series_list.reset()
        seen_ids = set()
        all_series = []

        for channel in self.channel_manager.get_all_channels():
            if not hasattr(channel, 'series_list'):
                continue
            for series in channel.series_list:
                sid = str(series.get('id', ''))
                if sid and sid not in seen_ids:
                    seen_ids.add(sid)
                    all_series.append((series, channel.id))

        for series, channel_id in all_series:
            name = series.get('name', '')
            item = xbmcgui.ListItem(label=name)

            poster = series.get('poster_path') or series.get('poster')
            if poster and not poster.startswith('http'):
                poster = f"https://image.tmdb.org/t/p/w500{poster}"
            elif not poster:
                poster = 'special://home/addons/plugin.video.milionar/resources/media/placeholder_tv_card.jpg'

            item.setArt({'poster': poster})
            item.setProperty('series_id', str(series.get('id')))
            item.setProperty('channel_id', channel_id)
            series_list.addItem(item)

        self.setProperty('WCS.MyTV.MySeriesCount', str(len(all_series)))
        self.setProperty('WCS.Row3.Label', f"Moje seriály  ·  {len(all_series)}")
    
    def _instant_load_from_cache(self, channel_id):
        """Instantly populate program list from cache (no API calls).
        
        Returns:
            True if cache was loaded, False if no cache available.
        """
        cache_key = f"program_list_{channel_id}"
        
        cached_items = self._cache.get(cache_key)
        if not cached_items:
            return False  # No cache, will load async
        
        try:
            program_list = self.getControl(9000)
            program_list.reset()
            
            for item_data in cached_items:
                item = xbmcgui.ListItem(label=item_data.get('label', ''))
                item.setLabel2(item_data.get('label2', ''))
                
                # Set all properties
                for key, value in item_data.get('properties', {}).items():
                    item.setProperty(key, str(value))
                
                # Set all art
                item.setArt(item_data.get('art', {}))
                
                program_list.addItem(item)
            
            # Resume check pro kazdy item
            try:
                ch_list = self.getControl(2000)
                ch_item = ch_list.getSelectedItem()
                ch_id = ch_item.getProperty('channel_id') if ch_item else ''
                if ch_id:
                    from wcs.ai import ChannelHistory
                    for idx in range(program_list.size()):
                        it = program_list.getListItem(idx)
                        tmdb = it.getProperty('tmdb_id') or ''
                        if tmdb:
                            resume = ChannelHistory.get_resume_point(ch_id, tmdb)
                            if resume and resume['position'] > 10 and resume['total'] > 0:
                                pct = min(resume['position'] / resume['total'], 1.0)
                                it.setProperty('is_resume', 'true')
                                it.setProperty('resume_percent', str(int(pct * 100)))
                                it.setProperty('resume_width', str(int(pct * 260)))
            except Exception:
                pass
            
            xbmc.log(f"[MyTV] Instantly loaded {len(cached_items)} items from cache for channel {channel_id}", xbmc.LOGINFO)
            
            # For Fanart/Still: Only set Ready if no processing needed
            # If effects enabled, wait for PIL to generate processed version
            if self._bg_mode in (self.BG_FANART, self.BG_STILL) and not self._is_background_processing_needed():
                self.setProperty('WCS.MyTV.Background.Ready', 'true')
            # Static already has Ready set in onInit (if no processing)
            # Collages will set Ready after background generation
            return True
        except Exception as e:
            xbmc.log(f"[MyTV] Error loading from cache: {e}", xbmc.LOGWARNING)
            return False
    
    def _async_program_load(self, token, channel_id, shuffle=True):
        """Background thread - loads program data without blocking UI."""
        import random
        from wcs.metadata import TMDbClient
        
        # Check if still valid
        if token != self._program_load_token:
            return  # Cancelled - user navigated away
        
        channel = self.channel_manager.get_channel_by_id(channel_id)
        if not channel or not channel.series_list:
            # Update UI with empty message
            self._apply_empty_program()
            return
        
        # Settings
        try:
            upcoming_count_val = self.addon.getSetting('mytv_upcoming_count')
            upcoming_count = int(upcoming_count_val) if upcoming_count_val else 12
        except:
            upcoming_count = 12
        
        block_size_setting = self.addon.getSetting('mytv_block_size') or "2"
        
        # Parse block size
        min_block, max_block = 1, 1
        if '-' in block_size_setting:
            try:
                parts = block_size_setting.split('-')
                min_block, max_block = int(parts[0]), int(parts[1])
            except:
                pass
        else:
            try:
                val = int(block_size_setting)
                min_block = max_block = val
            except:
                pass
        
        
        
        series_candidates = list(channel.series_list)
        if shuffle:
            random.shuffle(series_candidates)
        
        # Local progress simulation (loaded from channel history)
        simulated_progress = {}
        items_count = 0
        candidate_idx = 0
        max_loops = upcoming_count * 2
        loops = 0
        
        # Reset list at start
        try:
            program_list = self.getControl(9000)
            program_list.reset()
        except:
            return
        
        while items_count < upcoming_count and series_candidates and loops < max_loops:
            loops += 1
            
            # Token check - exit early if cancelled
            if token != self._program_load_token:
                return
            
            series = series_candidates[candidate_idx % len(series_candidates)]
            candidate_idx += 1
            
            series_id = str(series.get('id'))
            series_name = series.get('name')
            
            # Initialize progress from channel history (NOT global)
            if series_id not in simulated_progress:
                last_season, last_episode = ProgressManager.get_progress(channel_id, series_id)
                simulated_progress[series_id] = (last_season, last_episode)
            
            block_size = random.randint(min_block, max_block)
            
            for _ in range(block_size):
                if items_count >= upcoming_count:
                    break
                
                # Token check
                if token != self._program_load_token:
                    return
                
                curr_s, curr_e = simulated_progress[series_id]
                
                # Check cache first (using CacheManager)
                cache_key = f"episode_{series_id}_{curr_s}_{curr_e + 1}"
                next_meta = self._cache.get(cache_key)
                if not next_meta:
                    # Fetch from TMDb (slow)
                    next_meta = TMDbClient.get_next_episode_from_tmdb(series_id, curr_s, curr_e, self.addon)
                    if next_meta:
                        # Cache with 7-day TTL
                        self._cache.set(cache_key, next_meta, ttl_seconds=7 * 24 * 3600)
                
                if next_meta:
                    # Token check before UI update
                    if token != self._program_load_token:
                        return
                    
                    # Add item to UI immediately (progressive loading)
                    self._add_single_program_item(next_meta, series_name, series_id)
                    items_count += 1
                    simulated_progress[series_id] = (next_meta['season'], next_meta['episode'])
                    
                    # For Fanart/Still modes: Only set Ready after FIRST item if no processing needed
                    # If effects enabled, wait for PIL to generate processed version
                    if items_count == 1 and self._bg_mode in (self.BG_FANART, self.BG_STILL) and not self._is_background_processing_needed():
                        self.setProperty('WCS.MyTV.Background.Ready', 'true')
                else:
                    break
        
        # After loading program, update background for collage modes
        if items_count > 0:
            # For Still/Mix collage: generate background now (needs full program data)
            if self._bg_mode in (self.BG_COLLAGE_STILLS, self.BG_COLLAGE_MIX):
                self._update_background(channel_id)
                # Ready will be set by _generate_background_async after completion
            else:
                # For Fanart/Still with effects: trigger background update
                self._update_background(channel_id)
        
        # Save program list cache for instant reload
        self._save_program_list_cache(channel_id)
        
        # Save progress fingerprint for smart cache invalidation
        ProgressManager.save_progress_fingerprint(channel_id, channel.series_list, self._cache)
        
        # Apply saved reorder if exists (user previously moved episodes)
        self._apply_program_reorder(channel_id)
        
        # Update hero zone from first program item
        self._update_hero_zone()

        
        # Flush cache to disk after load completes
        self._cache.flush()
    
    def _async_program_append(self, token, channel_id, count_needed):
        """Background thread - append new episodes to existing program list.
        
        Used when watched episodes were removed and the list needs filling.
        Respects block size settings and round-robin series selection.
        
        Args:
            token: Cancellation token
            count_needed: How many episodes to add
        """
        import random
        from wcs.metadata import TMDbClient
        
        if token != self._program_load_token:
            return
        
        channel = self.channel_manager.get_channel_by_id(channel_id)
        if not channel or not channel.series_list:
            return
        
        # Load existing cache to know what's already there
        cache_key = f"program_list_{channel_id}"
        cached_items = self._cache.get(cache_key) or []
        
        # Build set of existing episodes (tmdb_id + ep_code) to avoid duplicates
        existing_keys = set()
        # Track last episode per series from existing program
        last_episode_per_series = {}
        for item_data in cached_items:
            props = item_data.get('properties', {})
            tmdb_id = props.get('tmdb_id', '')
            ep_code = props.get('ep_code', '')
            season = int(props.get('season', 0) or 0)
            episode = int(props.get('episode', 0) or 0)
            if tmdb_id and ep_code:
                existing_keys.add(f"{tmdb_id}_{ep_code}")
            if tmdb_id and season:
                prev = last_episode_per_series.get(tmdb_id, (0, 0))
                if (season, episode) > prev:
                    last_episode_per_series[tmdb_id] = (season, episode)
        
        # Block size settings
        block_size_setting = self.addon.getSetting('mytv_block_size') or "2"
        min_block, max_block = 1, 1
        if '-' in block_size_setting:
            try:
                parts = block_size_setting.split('-')
                min_block, max_block = int(parts[0]), int(parts[1])
            except Exception:
                pass
        else:
            try:
                val = int(block_size_setting)
                min_block = max_block = val
            except Exception:
                pass
        
        # Round-robin through series
        series_candidates = list(channel.series_list)
        random.shuffle(series_candidates)
        
        items_added = 0
        candidate_idx = 0
        max_loops = count_needed * 3
        loops = 0
        
        # Simulated progress: start from last episode in existing program or channel progress
        simulated_progress = {}
        for series in series_candidates:
            sid = str(series.get('id'))
            if sid in last_episode_per_series:
                simulated_progress[sid] = last_episode_per_series[sid]
            else:
                simulated_progress[sid] = ProgressManager.get_progress(channel_id, sid)
        
        while items_added < count_needed and series_candidates and loops < max_loops:
            loops += 1
            
            if token != self._program_load_token:
                return
            
            series = series_candidates[candidate_idx % len(series_candidates)]
            candidate_idx += 1
            
            series_id = str(series.get('id'))
            series_name = series.get('name')
            
            block_size = random.randint(min_block, max_block)
            
            for _ in range(block_size):
                if items_added >= count_needed:
                    break
                
                if token != self._program_load_token:
                    return
                
                curr_s, curr_e = simulated_progress.get(series_id, (1, 0))
                
                # Check episode cache first
                ep_cache_key = f"episode_{series_id}_{curr_s}_{curr_e + 1}"
                next_meta = self._cache.get(ep_cache_key)
                if not next_meta:
                    next_meta = TMDbClient.get_next_episode_from_tmdb(
                        series_id, curr_s, curr_e, self.addon
                    )
                    if next_meta:
                        self._cache.set(ep_cache_key, next_meta, ttl_seconds=7 * 24 * 3600)
                
                if next_meta:
                    ep_code = f"S{next_meta['season']:02d}E{next_meta['episode']:02d}"
                    ep_key = f"{series_id}_{ep_code}"
                    
                    # Skip if already in program
                    if ep_key in existing_keys:
                        simulated_progress[series_id] = (next_meta['season'], next_meta['episode'])
                        continue
                    
                    self._add_single_program_item(next_meta, series_name, series_id)
                    items_added += 1
                    existing_keys.add(ep_key)
                    simulated_progress[series_id] = (next_meta['season'], next_meta['episode'])
                else:
                    break
        
        if items_added > 0:
            xbmc.log(
                f"[MyTV] Appended {items_added} episodes to program for channel {channel_id}",
                xbmc.LOGINFO
            )
            # Save updated program list cache
            self._save_program_list_cache(channel_id)
            # Update hero zone (first item may have changed)
            self._update_hero_zone()
        
        self._cache.flush()
    
    def _save_program_list_cache(self, channel_id):
        """Save current program list to cache for instant reload."""
        try:
            program_list = self.getControl(9000)
            items_data = []
            
            for i in range(program_list.size()):
                item = program_list.getListItem(i)
                item_data = {
                    'label': item.getLabel(),
                    'label2': item.getLabel2(),
                    'properties': {
                        'tmdb_id': item.getProperty('tmdb_id'),
                        'series_name': item.getProperty('series_name'),
                        'season': item.getProperty('season'),
                        'episode': item.getProperty('episode'),
                        'ep_code': item.getProperty('ep_code'),
                        'episode_title': item.getProperty('episode_title'),
                        'plot': item.getProperty('plot'),
                        'plot_short': item.getProperty('plot_short'),
                        'runtime': item.getProperty('runtime'),
                        'rating': item.getProperty('rating'),
                        'series_logo': item.getProperty('series_logo'),
                        'poster': item.getProperty('poster'),
                        'fanart': item.getProperty('fanart'),
                        'year': item.getProperty('year'),
                        'genre': item.getProperty('genre'),
                        'air_date': item.getProperty('air_date')
                    },
                    'art': {
                        'thumb': item.getArt('thumb'),
                        'fanart': item.getArt('fanart'),
                        'clearlogo': item.getArt('clearlogo'),
                        'poster': item.getArt('poster')
                    }
                }
                items_data.append(item_data)
            
            cache_key = f"program_list_{channel_id}"
            # Save to CacheManager with 1-day TTL (program lists change frequently)
            self._cache.set(cache_key, items_data, ttl_seconds=24 * 3600)
            xbmc.log(f"[MyTV] Saved {len(items_data)} items to program list cache for channel {channel_id}", xbmc.LOGINFO)
            # Home shows what is on the channel now (first entry of this queue): its row reads the cache file
            self._cache.flush()
            from wcs.skin.refresh import changed
            changed({'widget_mytv_channels'})
        except Exception as e:
            xbmc.log(f"[MyTV] Error saving program list cache: {e}", xbmc.LOGWARNING)
    
    def _add_single_program_item(self, next_meta, series_name, series_id):
        """Add a single program item to the list (progressive loading)."""
        try:
            program_list = self.getControl(9000)
            
            ep_code = f"S{next_meta['season']:02d}E{next_meta['episode']:02d}"
            full_plot = next_meta.get('plot', '')
            plot_short = full_plot[:77] + '...' if len(full_plot) > 80 else full_plot
            
            item = xbmcgui.ListItem(label=series_name)
            item.setLabel2(str(next_meta.get('runtime', '')))
            
            # Set properties
            item.setProperty('tmdb_id', series_id)
            item.setProperty('series_name', series_name)
            item.setProperty('season', str(next_meta['season']))
            item.setProperty('episode', str(next_meta['episode']))
            item.setProperty('episode_title', next_meta.get('episode_title', ''))
            item.setProperty('plot', full_plot)
            item.setProperty('plot_short', plot_short)
            item.setProperty('rating', str(next_meta.get('rating', '')))
            item.setProperty('ep_code', ep_code)
            item.setProperty('series_logo', next_meta.get('series_logo', ''))
            item.setProperty('poster', next_meta.get('poster', ''))
            item.setProperty('fanart', next_meta.get('fanart', ''))
            item.setProperty('year', str(next_meta.get('year', '')))
            item.setProperty('genre', next_meta.get('genre', ''))
            item.setProperty('runtime', str(next_meta.get('runtime', '')))
            
            # Art
            thumb = next_meta.get('episode_thumb') or next_meta.get('poster') or ''
            item.setArt({
                'thumb': thumb,
                'icon': next_meta.get('poster', ''),
                'poster': next_meta.get('poster', ''),
                'fanart': next_meta.get('fanart', '')
            })
            
            # InfoTag
            info_tag = {
                'title': next_meta.get('episode_title', ''),
                'tvshowtitle': series_name,
                'plot': full_plot,
                'season': next_meta['season'],
                'episode': next_meta['episode'],
                'mediatype': 'episode'
            }
            try:
                dur_min = int(str(next_meta.get('runtime', '0')).replace(' min', '').strip() or '0')
                info_tag['duration'] = dur_min * 60
            except:
                pass
            
            utils.set_video_info_tag(item, info_tag)
            program_list.addItem(item)
        except Exception as e:
            xbmc.log(f"[MyTV] Error adding program item: {e}", xbmc.LOGERROR)
    
    def _apply_empty_program(self):
        """Apply empty state to program list."""
        try:
            program_list = self.getControl(9000)
            program_list.reset()
            item = xbmcgui.ListItem(label="Žádný program. Přidejte seriály.")
            program_list.addItem(item)
        except:
            pass
    
    def _apply_program_items(self, items_data):
        """Apply loaded program items to UI. Called from background thread."""
        try:
            program_list = self.getControl(9000)
            program_list.reset()
            
            for data in items_data:
                item = xbmcgui.ListItem(label=data['label'])
                item.setLabel2(data['label2'])
                
                # Set all properties
                for key in ['tmdb_id', 'series_name', 'season', 'episode', 'episode_title',
                            'plot', 'plot_short', 'rating', 'ep_code', 'series_logo',
                            'poster', 'fanart', 'year', 'genre', 'runtime', 'thumb']:
                    item.setProperty(key, data.get(key, ''))
                
                # Art
                item.setArt({
                    'thumb': data.get('thumb', ''),
                    'icon': data.get('poster', ''),
                    'poster': data.get('poster', ''),
                    'fanart': data.get('fanart', '')
                })
                
                # InfoTag
                info_tag = {
                    'title': data.get('episode_title', ''),
                    'tvshowtitle': data.get('series_name', ''),
                    'plot': data.get('plot', ''),
                    'season': int(data.get('season', 1)),
                    'episode': int(data.get('episode', 1)),
                    'mediatype': 'episode'
                }
                try:
                    runtime = data.get('runtime', 0)
                    if isinstance(runtime, str):
                        runtime = int(runtime.replace(' min', '').strip() or '0')
                    info_tag['duration'] = runtime * 60
                except:
                    pass
                
                utils.set_video_info_tag(item, info_tag)
                program_list.addItem(item)
            
            # Resume check pro kazdy item
            try:
                ch_list = self.getControl(2000)
                ch_item = ch_list.getSelectedItem()
                ch_id = ch_item.getProperty('channel_id') if ch_item else ''
                if ch_id:
                    from wcs.ai import ChannelHistory
                    for idx in range(program_list.size()):
                        it = program_list.getListItem(idx)
                        tmdb = it.getProperty('tmdb_id') or ''
                        if tmdb:
                            resume = ChannelHistory.get_resume_point(ch_id, tmdb)
                            if resume and resume['position'] > 10 and resume['total'] > 0:
                                pct = min(resume['position'] / resume['total'], 1.0)
                                it.setProperty('is_resume', 'true')
                                it.setProperty('resume_percent', str(int(pct * 100)))
                                it.setProperty('resume_width', str(int(pct * 260)))
            except Exception:
                pass
        except Exception as e:
            xbmc.log(f"[MyTV] Error applying program items: {e}", xbmc.LOGERROR)

    def onAction(self, action):
        # Delegate to Edit Progress dialog first if active
        if self._handle_edit_progress_action(action):
            return
        
        # Delegate to Series Detail dialog first if active
        if self._handle_series_detail_action(action):
            return
        
        # Delegate to Add Series controller if active
        if self._handle_add_series_action(action):
            return
        
        action_id = action.getId()
        focused_id = self.getFocusId()
        if (action_id == xbmcgui.ACTION_CONTEXT_MENU and
                self.getProperty('WCS.MyTV.ReorderMode') != 'true' and
                self.getProperty('WCS.MyTV.ProgramReorderMode') != 'true'):
            from wcs.skin.context import channel_dialog_context
            if channel_dialog_context(self):
                return
        
        # Hero zone (9042): DOWN returns to last action button
        if focused_id == 9042 and action_id == 4:  # ACTION_MOVE_DOWN
            last_btn = getattr(self, '_last_action_button', 9004)
            self.setFocusId(last_btn)
            return
        
        # Hero zone (9042): UP is noop -- block super from processing
        if focused_id == 9042 and action_id == 3:
            return
        
        # Handle channel reorder mode
        # LEFT/RIGHT are handled by the list control natively (navigate-then-place)
        # OK/SELECT goes through onClick(2000)
        if self.getProperty('WCS.MyTV.ReorderMode') == 'true':
            if action_id in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK):
                self._exit_reorder_mode(confirm=False)
                return
            if action_id in (3, 4):  # UP, DOWN -- block in reorder mode
                return
        
        # Handle program reorder mode
        # LEFT/RIGHT are handled by the list control natively (navigate-then-place, horizontal list)
        # OK/SELECT goes through onClick(9000)
        if self.getProperty('WCS.MyTV.ProgramReorderMode') == 'true':
            if action_id in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK):
                self._exit_program_reorder_mode(confirm=False)
                return
            if action_id in (3, 4):  # UP, DOWN -- block in program reorder mode (horizontal list)
                return
        
        # Handle delete confirmation cancel with Back action
        if action_id in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK):
            if self.getProperty('WCS.MyTV.DeleteConfirm') == 'true':
                self._cancel_delete_confirmation()
                return
            if self.getProperty('WCS.MyTV.SeriesGrid') == 'true':
                self._close_series_grid_dialog()
                return
            if self.getProperty('WCS.MyTV.DeleteSeriesConfirm') == 'true':
                self._cancel_delete_series_confirmation()
                return
            if self.getProperty('WCS.MyTV.PlaySelection') == 'true':
                self._hide_play_selection_dialog()
                return
        
        # Get last known focus (before this action)
        last_focus = getattr(self, '_last_known_focus', None)
        
        # Handle Channel List (2000) actions locally - DO NOT pass to base class!
        # Base class has its own sidebar logic for 2000 that conflicts with MyTV
        if focused_id == 2000:
            # Get current position for tracking
            try:
                list_control = self.getControl(2000)
                current_pos = list_control.getSelectedPosition()
            except:
                current_pos = 0
            
            if action_id == xbmcgui.ACTION_CONTEXT_MENU:  # Long press OK
                self._enter_reorder_mode()
                return
            if action_id in [1, 2]:  # LEFT/RIGHT
                # Check if LEFT on first position - should open nav sidebar
                # Use _last_list_position (like base class) to check PREVIOUS position
                last_pos = getattr(self, '_last_list_position', 0)
                if action_id == 1 and last_pos == 0:  # LEFT at position 0
                    # Open nav sidebar
                    self._show_nav_sidebar()
                    return
                
                # Update position tracking BEFORE processing
                self._last_list_position = current_pos
                
                # Allow UI to update selection then refresh
                xbmc.sleep(50)
                self.update_editor_view()
                self.refresh_program_view()
                self._last_known_focus = 2000
                # Let XML handle focus movement
                xbmcgui.WindowXMLDialog.onAction(self, action)
                return
            elif action_id == 3:  # UP
                last_focus = getattr(self, '_last_known_focus', None)
                if last_focus == 2000:
                    # User was already on channel list and pressed UP -> go to action buttons
                    last_btn = getattr(self, '_last_action_button', 9004)
                    self.setFocusId(last_btn)
                else:
                    # Arrived from program list (or other) via XML onup=2000 -> stay on channel list
                    self._last_known_focus = 2000
                return
            elif action_id == 4:  # DOWN - Let XML handle navigation to program list
                self._last_known_focus = 2000
                xbmcgui.WindowXMLDialog.onAction(self, action)
                return
            else:
                # Other actions on 2000 - handle with base Window, not our parent
                self._last_known_focus = 2000
                xbmcgui.WindowXMLDialog.onAction(self, action)
                return
        
        # Long press OK on program list (9000) -- enter program reorder mode directly
        if focused_id == 9000 and action_id == xbmcgui.ACTION_CONTEXT_MENU:
            self._enter_program_reorder_mode_direct()
            return
        
        # Update last known focus for non-2000 controls
        self._last_known_focus = focused_id
        
        super(MyTVDialog, self).onAction(action)
    
    def onFocus(self, controlId):
        # Reorder mode: force focus back to channel list
        if self.getProperty('WCS.MyTV.ReorderMode') == 'true' and controlId != 2000:
            self.setFocusId(2000)
            return
        
        # Program reorder mode: force focus back to program list
        if self.getProperty('WCS.MyTV.ProgramReorderMode') == 'true' and controlId != 9000:
            self.setFocusId(9000)
            return
        
        # Track last action button for hero zone return navigation
        if controlId in (9004, 9040, 9041, 9043):
            self._last_action_button = controlId
        
        # Edit Progress: update visible seasons when navigating between season lists
        if self.getProperty('WCS.MyTV.EditProgress') == 'true' and 9401 <= controlId <= 9410:
            season_idx = controlId - 9401
            self._update_visible_seasons(season_idx)
        
        # Delegate to Add Series controller first if active
        self._handle_add_series_focus(controlId)
        
        # Close chat sidebar when focusing channel list
        if controlId == 2000:
            self.clearProperty('WCS.AIChat.Visible')
        else:
            # For all other controls (including 4001 for sidebar close), call base class
            super(MyTVDialog, self).onFocus(controlId)

    def onClick(self, controlId):
        # Delegate to Edit Progress dialog first if active
        if self._handle_edit_progress_click(controlId):
            return
        
        # Delegate to Series Detail dialog first if active
        if self._handle_series_detail_click(controlId):
            return
        
        # Delegate to Add Series controller if active
        if self._handle_add_series_click(controlId):
            return
        
        if controlId == 2000:
            # Handle reorder mode confirmation
            if self.getProperty('WCS.MyTV.ReorderMode') == 'true':
                self._confirm_reorder()
                return
            
            # Check if "Create New"
            list_control = self.getControl(2000)
            item = list_control.getSelectedItem()
            if item.getProperty('is_create_button') == 'true':
                self._create_new_channel()
                return  # Don't call super().onClick - it may reset focus
            else:
                # Open series grid sub-dialog for this channel
                self._show_series_grid_dialog()
                return
        elif controlId == 9100:
            # Context Menu for Grid Item (Remove)
            self._handle_grid_click()
        elif controlId == 9110:
            # Play Broadcast from Series Grid
            self._close_series_grid_dialog()
            self._play_broadcast()
        elif controlId == 9111:
            # Shuffle from Series Grid
            self.refresh_program_view(shuffle=True)
            self._show_toast("Program zamíchán", "success")
        elif controlId == 9112:
            # Move Channel (reorder) from Series Grid
            self._enter_reorder_mode()
        elif controlId == 9113:
            # Regenerate Channel Name via AI from Series Grid
            self._regenerate_channel_name_from_grid()
        elif controlId == 9114:
            # Delete Channel from Series Grid (show confirmation inside grid)
            self._show_delete_confirmation()
        elif controlId == 9115:
            # Play game (channel mix mode) from Series Grid
            list_control = self.getControl(2000)
            item = list_control.getSelectedItem()
            if item:
                channel_id = item.getProperty('channel_id')
                channel = self.channel_manager.get_channel_by_id(channel_id)
                if channel and channel.series_list:
                    channel_name = channel.name or ''
                    # Build combined plot from all series
                    series_parts = []
                    for s in channel.series_list:
                        name = s.get('name', '')
                        overview = s.get('overview', '')
                        if name:
                            if overview:
                                series_parts.append(f'"{name}": {overview}')
                            else:
                                series_parts.append(f'"{name}"')
                    combined_plot = ' | '.join(series_parts)
                    # Use channel icon as poster
                    poster = channel.icon or ''
                    fanart = ''
                    logo = ''
                    self._show_game_dialog(
                        series_id='',
                        title=channel_name,
                        plot=combined_plot,
                        poster=poster,
                        fanart=fanart,
                        logo=logo,
                        media_type='channel'
                    )
        elif controlId == 9060:
            # My Series list item clicked - open Series Detail
            try:
                my_series_list = self.getControl(9060)
                selected = my_series_list.getSelectedItem()
                if selected:
                    series_id = selected.getProperty('series_id')
                    channel_id = selected.getProperty('channel_id')
                    if series_id and channel_id:
                        self._show_series_detail_dialog(series_id, channel_id, return_focus_id=9060)
            except Exception as e:
                xbmc.log(f"[MyTV] Error opening series detail from My Series: {e}", xbmc.LOGERROR)
        elif controlId == 9001:
            # Add from My Series (legacy - button removed)
            self._add_from_my_series()
        elif controlId == 9002:
            # Add New (Search) (legacy - button removed)
            self._add_new_search()
        elif controlId == 9003:
            # Delete Channel - show inline confirmation
            self._show_delete_confirmation()
        elif controlId == 9006:
            # Confirm Delete Channel
            self._confirm_delete_channel()
        elif controlId == 9007:
            # Cancel Delete Channel
            self._cancel_delete_confirmation()
        elif controlId == 9004:
            # Play Broadcast (First item in upcoming)
            self._play_broadcast()
        elif controlId == 9040:
            # Play Episode directly (respects autoplay setting)
            self._play_hero_episode()
        elif controlId == 9042:
            # Hero zone clicked - show play selection dialog
            self._show_play_selection_dialog()
        elif controlId == 9041:
            # Series Only - plays only current series
            self._play_series_only()
        elif controlId == 9043:
            # Milionář from Home - episode mode for current episode
            list_control = self.getControl(9000)
            item = list_control.getSelectedItem()
            if item:
                series_name = item.getProperty('series_name') or item.getLabel()
                plot = item.getProperty('plot') or ''
                poster = item.getProperty('poster') or ''
                fanart = item.getProperty('fanart') or ''
                logo = item.getProperty('series_logo') or ''
                episode_title = item.getProperty('episode_title') or ''
                series_id = item.getProperty('series_id') or ''
                season_number = None
                episode_number = None
                raw_code = item.getProperty('ep_code') or ''
                if raw_code and raw_code.startswith('S') and 'E' in raw_code:
                    try:
                        parts = raw_code[1:].split('E')
                        season_number = int(parts[0])
                        episode_number = int(parts[1])
                    except (ValueError, IndexError):
                        pass
                self._show_game_dialog(
                    series_id, series_name, plot, poster, fanart, logo,
                    episode_title=episode_title,
                    season_number=season_number,
                    episode_number=episode_number
                )
        elif controlId == 9000:
            # Handle program reorder mode confirmation
            if self.getProperty('WCS.MyTV.ProgramReorderMode') == 'true':
                self._confirm_program_reorder()
                return
            # Show Play Selection Dialog
            self._show_play_selection_dialog()
        elif controlId == 9026:
            # Edit Progress from Play Selection
            self._show_edit_progress_dialog()
        elif controlId == 9025:
            # Reorder program (move episode) from Play Selection
            self._enter_program_reorder_mode()
        elif controlId == 9024:
            # Info box clicked - show Play Selection Dialog
            self._show_play_selection_dialog()
        elif controlId == 9023:
            # Play single episode only
            self._play_single_episode()
            self._hide_play_selection_dialog()
        elif controlId == 9020:
            # Play Channel from selected position - builds playlist with autoplay
            self._play_channel_from_selected()
            self._hide_play_selection_dialog()
        elif controlId == 9021:
            # Play Series Only - IMPORTANT: call play first, hide clears the item!
            self._play_series_only()
            self._hide_play_selection_dialog()
        elif controlId == 9027:
            # Play game (episode mode) from Play Selection
            item = getattr(self, '_play_selection_item', None)
            if item:
                series_name = self.getProperty('WCS.PlaySelect.SeriesName') or ''
                plot = self.getProperty('WCS.PlaySelect.Plot') or ''
                poster = self.getProperty('WCS.PlaySelect.Poster') or ''
                fanart = self.getProperty('WCS.PlaySelect.Fanart') or ''
                logo = self.getProperty('WCS.PlaySelect.SeriesLogo') or ''
                episode_title = self.getProperty('WCS.PlaySelect.EpTitle') or ''
                series_id = item.getProperty('series_id') or ''
                # Parse season/episode from ep_code (S01E01)
                season_number = None
                episode_number = None
                raw_code = item.getProperty('ep_code') or ''
                if raw_code and raw_code.startswith('S') and 'E' in raw_code:
                    try:
                        parts = raw_code[1:].split('E')
                        season_number = int(parts[0])
                        episode_number = int(parts[1])
                    except (ValueError, IndexError):
                        pass
                self._show_game_dialog(
                    series_id, series_name, plot, poster, fanart, logo,
                    episode_title=episode_title,
                    season_number=season_number,
                    episode_number=episode_number
                )
        elif controlId == 9022:
            # Cancel
            self._hide_play_selection_dialog()
        
        super(MyTVDialog, self).onClick(controlId)

    def _create_new_channel(self):
        """Create a new channel with automatic naming based on added series."""
        # Block sidebar during this operation
        self._block_sidebar = True
        
        # Create channel with temporary name
        temp_name = f"Nový kanál {len(self.channel_manager.channels) + 1}"
        new_channel = self.channel_manager.create_channel(temp_name)
        
        # Store the new channel ID for auto-naming after dialog closes
        self._pending_new_channel_id = new_channel.id
        
        # Select the new channel (first one - new channels are prepended)
        self.current_channel_index = 0
        
        # Only refresh channel list - NO program refresh needed for empty channel
        list_control = self.getControl(2000)
        list_control.reset()
        for ch in self.channel_manager.get_all_channels():
            item = xbmcgui.ListItem(label=ch.name)
            icon = ch.icon if ch.icon else 'special://home/addons/plugin.video.milionar/resources/media/placeholder_tv_card.jpg'
            item.setArt({'poster': icon})
            item.setProperty('channel_id', ch.id)
            list_control.addItem(item)
        # Add "Create New Channel" item
        create_item = xbmcgui.ListItem(label="")
        create_item.setArt({'poster': 'special://home/addons/plugin.video.milionar/resources/media/plus_card.png'})
        create_item.setProperty('is_create_button', 'true')
        list_control.addItem(create_item)
        # NOTE: Don't call selectItem() here - it can cause Kodi to "remember" 
        # focus on this control and restore it later, breaking AddSeries focus
        
        # Clear editor grid (empty channel)
        self.getControl(9100).reset()
        
        # Try to move focus away from 2000 (helps with focus transfer)
        # Note: 9100 is empty so this may fail, but that's OK
        self.setFocusId(9100)
        xbmc.sleep(50)
        
        # Unblock sidebar before showing dialog
        self._block_sidebar = False
        
        # Open AddSeriesDialog
        self._show_add_series_dialog()


    def _show_delete_confirmation(self):
        """Show inline delete confirmation dialog."""
        list_control = self.getControl(2000)
        item = list_control.getSelectedItem()
        if not item or item.getProperty('is_create_button') == 'true': 
            return
        
        # Store channel info for deletion
        self._pending_delete_channel_id = item.getProperty('channel_id')
        self._pending_delete_channel_name = item.getLabel()
        
        # Show confirmation dialog
        self.setProperty('WCS.MyTV.DeleteConfirm', 'true')
        self.setFocusId(9006)
    
    def _cancel_delete_confirmation(self):
        """Cancel delete confirmation and return to normal state."""
        self.clearProperty('WCS.MyTV.DeleteConfirm')
        self._pending_delete_channel_id = None
        self._pending_delete_channel_name = None
        # Return focus to appropriate control
        if self.getProperty('WCS.MyTV.SeriesGrid') == 'true':
            self.setFocusId(9114)  # Grid is open (appletv) - back to delete button
        else:
            self.setFocusId(9003)  # No grid (other layouts) - back to delete button
    
    def _confirm_delete_channel(self):
        """Actually delete the channel after confirmation."""
        channel_id = getattr(self, '_pending_delete_channel_id', None)
        if not channel_id:
            return
        
        # Hide confirmation dialog
        self.clearProperty('WCS.MyTV.DeleteConfirm')
        
        # Close grid if open (appletv layout)
        if self.getProperty('WCS.MyTV.SeriesGrid') == 'true':
            self.clearProperty('WCS.MyTV.SeriesGrid')
            self.clearProperty('WCS.MyTV.SeriesGrid.Title')
        
        # Delete channel and its history
        ChannelHistory.delete_channel_history(channel_id)
        self.channel_manager.delete_channel(channel_id)
        
        # Clear pending data
        self._pending_delete_channel_id = None
        self._pending_delete_channel_name = None
        
        # Refresh
        self.current_channel_index = 0
        self.refresh_channels_view()
        
        # Focus on first channel
        self._delayed_focus(2000, delay=0.1)
        
        self._show_toast("Kanal smazan", "success")

    def _show_delete_series_confirmation(self):
        """Show inline delete series confirmation dialog."""
        self.setProperty('WCS.MyTV.DeleteSeriesConfirm', 'true')
        self.setFocusId(9305)
    
    def _cancel_delete_series_confirmation(self):
        """Cancel delete series confirmation and return to normal state."""
        self.clearProperty('WCS.MyTV.DeleteSeriesConfirm')
        self.setFocusId(9300)
    
    def _confirm_delete_series(self):
        """Actually delete the series after confirmation."""
        series_id = self._series_detail_series_id
        channel_id = self._series_detail_channel_id
        
        # Hide confirmation dialog
        self.clearProperty('WCS.MyTV.DeleteSeriesConfirm')
        
        # Get series name for notification
        channel = self.channel_manager.get_channel_by_id(channel_id)
        series_name = ''
        if channel:
            for s in channel.series_list:
                if str(s.get('id')) == str(series_id):
                    series_name = s.get('name', '')
                    break
        
        # Remove series from channel
        self.channel_manager.toggle_series_in_channel(channel_id, {'id': series_id})
        
        # Close dialog and refresh
        self._close_series_detail_dialog(removed=True)
        
        self._show_toast(f"'{series_name}' odebráno", "success")

    def _generate_ai_channel_name_async(self, channel_id, series_list, generator, current_name=None, update_grid_title=False):
        """Generate AI channel name in background thread.
        
        Args:
            channel_id: Channel ID to rename
            series_list: List of series dicts for context
            generator: ChannelNameGenerator instance
            current_name: Optional current name (AI generates different name)
            update_grid_title: If True, also updates SeriesGrid.Title property
        """
        import threading
        
        def worker():
            try:
                # Call AI to generate name
                ai_name = generator.generate_from_ai(series_list, current_name=current_name)
                
                # Apply the name change in channel manager
                self.channel_manager.rename_channel(channel_id, ai_name)
                
                # Update the ListItem label directly in channel list
                try:
                    list_control = self.getControl(2000)
                    for i in range(list_control.size()):
                        item = list_control.getListItem(i)
                        if item.getProperty('channel_id') == channel_id:
                            item.setLabel(ai_name)
                            break
                except Exception as e:
                    xbmc.log(f"[MyTV] Failed to update list item label: {e}", xbmc.LOGWARNING)
                
                # Update SeriesGrid title if open
                if update_grid_title:
                    try:
                        self.setProperty('WCS.MyTV.SeriesGrid.Title', ai_name)
                    except Exception:
                        pass
                
                # Show notification
                self._show_toast(f"Název: {ai_name}", "success")
                
            except Exception as e:
                xbmc.log(f"[MyTV] AI channel naming failed: {e}", xbmc.LOGERROR)
                self._show_toast("AI generování selhalo", "error")
        
        thread = threading.Thread(target=worker, daemon=True)
        thread.start()

    def _seek_to_resume(self, channel_id, tmdb_id):
        """Start background seek to resume position for first playlist item.
        
        Args:
            channel_id: Channel ID for history lookup
            tmdb_id: TMDb ID of the first episode
        """
        if not channel_id or not tmdb_id:
            return
        
        try:
            resume = ChannelHistory.get_resume_point(channel_id, tmdb_id)
            if not resume or resume['position'] <= 10:
                return
            
            resume_position = resume['position']
            
            def _seek_worker():
                import time as _time
                for _ in range(50):
                    _time.sleep(0.2)
                    try:
                        if xbmc.Player().isPlaying() and xbmc.Player().getTime() > 0:
                            xbmc.Player().seekTime(resume_position)
                            xbmc.log(f"[MyTV] Resumed at {resume_position:.0f}s", xbmc.LOGINFO)
                            return
                    except Exception:
                        pass
            
            import threading
            threading.Thread(target=_seek_worker, daemon=True).start()
        except Exception as e:
            xbmc.log(f"[MyTV] Resume seek error: {e}", xbmc.LOGERROR)

    def _play_broadcast(self):
        """Plays all items in the upcoming program list continuously."""
        list_control = self.getControl(9000)
        size = list_control.size()
        if size == 0:
            self._show_toast("Žádný program k vysílání", "warning")
            return
            
        self.close()
        
        # Get Playlist
        playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
        playlist.clear()
        
        # Add all items to playlist
        addon_id = self.addon.getAddonInfo('id')
        channel_list = self.getControl(2000)
        current_channel_item = channel_list.getSelectedItem()
        channel_id = current_channel_item.getProperty('channel_id') if current_channel_item else ''
        
        for i in range(size):
            item = list_control.getListItem(i)
            
            if not item.getProperty('series_name') or not item.getProperty('season') or not item.getProperty('episode'):
                continue

            item_data = PlaylistBuilder.extract_item_data(item)
            url = PlaylistBuilder.build_playlist_url(addon_id, item_data, channel_id)
            li = PlaylistBuilder.build_playlist_listitem(item_data)
            playlist.add(url, li)
            
        # Get first item's tmdb_id for resume
        first_item = list_control.getListItem(0)
        first_tmdb_id = first_item.getProperty('tmdb_id') if first_item else ''
        
        # Start playback
        xbmc.Player().play(playlist)
        
        # Resume seek for first episode
        self._seek_to_resume(channel_id, first_tmdb_id)
        
        # Aktivovat countdown monitor pro playlist
        from wcs.playback.PlaylistCountdownMonitor import get_playlist_monitor
        get_playlist_monitor().activate()

    # ==================================================================================
    # PLAY SELECTION DIALOG
    # ==================================================================================
    
    def _show_play_selection_dialog(self):
        """Show play selection dialog with episode info."""
        list_control = self.getControl(9000)
        item = list_control.getSelectedItem()
        if not item:
            return
        
        # Store selected item data for later use
        self._play_selection_item = item
        
        # Set properties for dialog display
        self.setProperty('WCS.PlaySelect.SeriesName', item.getProperty('series_name') or item.getLabel())
        # Reformat ep_code from S01E01 to "Sezona 1 Epizoda 1"
        raw_code = item.getProperty('ep_code') or ''
        if raw_code and raw_code.startswith('S') and 'E' in raw_code:
            try:
                parts = raw_code[1:].split('E')
                season_num = int(parts[0])
                episode_num = int(parts[1])
                ep_code_fmt = f"Sez\u00f3na {season_num} Epizoda {episode_num}"
            except (ValueError, IndexError):
                ep_code_fmt = raw_code
        else:
            ep_code_fmt = raw_code
        self.setProperty('WCS.PlaySelect.EpCode', ep_code_fmt)
        self.setProperty('WCS.PlaySelect.EpTitle', item.getProperty('episode_title') or '')
        self.setProperty('WCS.PlaySelect.Fanart', item.getProperty('fanart') or '')
        self.setProperty('WCS.PlaySelect.Poster', item.getProperty('poster') or '')
        self.setProperty('WCS.PlaySelect.SeriesLogo', item.getProperty('series_logo') or '')
        self.setProperty('WCS.PlaySelect.Plot', item.getProperty('plot') or '')
        
        # Build metadata parts (only non-empty)
        meta_parts = []
        year = item.getProperty('year')
        if year:
            meta_parts.append(year)
            self.setProperty('WCS.PlaySelect.Year', year)
        else:
            self.setProperty('WCS.PlaySelect.Year', '')
        
        genre = item.getProperty('genre')
        if genre:
            meta_parts.append(genre)
            self.setProperty('WCS.PlaySelect.Genre', genre)
        else:
            self.setProperty('WCS.PlaySelect.Genre', '')
        
        runtime = item.getProperty('runtime')
        if runtime:
            runtime_clean = runtime.replace(' min', '').strip()
            runtime_str = f"{runtime_clean} min" if runtime_clean else ''
            if runtime_str:
                meta_parts.append(runtime_str)
                self.setProperty('WCS.PlaySelect.Runtime', runtime_str)
        if not runtime or not runtime_str:
            self.setProperty('WCS.PlaySelect.Runtime', '')
        
        # Format rating as "★ 8.2/10"
        rating = item.getProperty('rating')
        if rating:
            try:
                rating_str = f"\u2605 {float(rating):.1f}/10"
            except (ValueError, TypeError):
                rating_str = f"\u2605 {rating}/10"
            meta_parts.append(rating_str)
            self.setProperty('WCS.PlaySelect.Rating', rating_str)
        else:
            self.setProperty('WCS.PlaySelect.Rating', '')
        
        # Combined metadata line for display
        self.setProperty('WCS.PlaySelect.MetaLine', '   |   '.join(meta_parts))
        
        # Still (episode screenshot) - fallback to thumb or fanart
        still = item.getArt('thumb') or item.getProperty('thumb') or ''
        self.setProperty('WCS.PlaySelect.Still', still)
        
        # Resume check -- dynamicky label pro button 9023
        channel_list = self.getControl(2000)
        current_channel = channel_list.getSelectedItem()
        ch_id = current_channel.getProperty('channel_id') if current_channel else ''
        tmdb = item.getProperty('tmdb_id') or ''
        if ch_id and tmdb:
            try:
                from wcs.ai import ChannelHistory
                resume = ChannelHistory.get_resume_point(ch_id, tmdb)
                if resume and resume['position'] > 10:
                    self.setProperty('WCS.PlaySelect.PlayEpLabel', 'Pokračovat')
                else:
                    self.setProperty('WCS.PlaySelect.PlayEpLabel', 'Přehrát epizodu')
            except Exception:
                self.setProperty('WCS.PlaySelect.PlayEpLabel', 'Přehrát epizodu')
        else:
            self.setProperty('WCS.PlaySelect.PlayEpLabel', 'Přehrát epizodu')
        
        # The rounded Modal Card is the only play selection style (the style setting was removed)
        style = 'modal'
        self.setProperty('WCS.PlaySelect.Style', style)
        
        # Save source focus for restoring on close
        self._play_selection_source_focus = self.getFocusId()
        
        # Lock scroll position only when opened from program list
        if self._play_selection_source_focus == 9000:
            self.setProperty('WCS.MyTV.PlaySelection.ScrollLock', 'true')
        
        # Show dialog and focus first button
        self.setProperty('WCS.MyTV.PlaySelection', 'true')
        self.setFocusId(9023)
    
    def _hide_play_selection_dialog(self):
        """Hide play selection dialog."""
        self._play_selection_item = None
        # Clear trigger first (hides dialog buttons)
        self.clearProperty('WCS.MyTV.PlaySelection')
        self.clearProperty('WCS.MyTV.PlaySelection.ScrollLock')
        # Wait for Kodi to process visibility change (buttons must become invisible first)
        # Without this delay, setFocusId fails because the dialog buttons still hold focus
        xbmc.sleep(50)
        # Restore focus to source control
        source_id = getattr(self, '_play_selection_source_focus', 9000)
        self.setFocusId(source_id)
    
    # ==================================================================================
    # EDIT PROGRESS DIALOG
    # ==================================================================================
    
    def _show_edit_progress_dialog(self):
        """Show edit progress dialog for the current series."""
        item = getattr(self, '_play_selection_item', None)
        if not item:
            return
        
        tmdb_id = item.getProperty('tmdb_id')
        series_name = item.getProperty('series_name') or item.getLabel()
        if not tmdb_id:
            self._show_toast("Chybí TMDb ID seriálu", "error")
            return
        
        # Get current channel ID
        channel_list = self.getControl(2000)
        current_channel_item = channel_list.getSelectedItem()
        channel_id = current_channel_item.getProperty('channel_id') if current_channel_item else ''
        
        # Store context
        self._edit_progress_tmdb_id = tmdb_id
        self._edit_progress_series_name = series_name
        self._edit_progress_channel_id = channel_id
        self._edit_progress_season_count = 0
        
        # Set properties
        self.setProperty('WCS.EditProgress.SeriesName', series_name)
        self.setProperty('WCS.EditProgress.Loading', 'true')
        self.setProperty('WCS.EditProgress.ScrollRow', '0')
        self.setProperty('WCS.EditProgress.SeasonCount', '0')
        
        # Show overlay
        self.setProperty('WCS.MyTV.EditProgress', 'true')
        
        # Load data async
        import threading
        thread = threading.Thread(
            target=self._load_edit_progress_data,
            args=(tmdb_id, channel_id),
            daemon=True
        )
        thread.start()
    
    def _hide_edit_progress_dialog(self):
        """Hide edit progress dialog and clean up."""
        # Clear all edit progress properties
        self.clearProperty('WCS.MyTV.EditProgress')
        self.clearProperty('WCS.EditProgress.Loading')
        self.clearProperty('WCS.EditProgress.ScrollRow')
        self.clearProperty('WCS.EditProgress.SeriesName')
        
        # Clear season labels, lists and Show properties
        season_count = getattr(self, '_edit_progress_season_count', 0)
        for i in range(1, season_count + 1):
            self.clearProperty(f'WCS.EditProgress.Season{i}.Label')
            self.clearProperty(f'WCS.EditProgress.Season{i}.Show')
            try:
                self.getControl(9400 + i).reset()
            except Exception:
                pass
        self.clearProperty('WCS.EditProgress.SeasonCount')
        
        # Clean up context
        self._edit_progress_tmdb_id = None
        self._edit_progress_series_name = None
        self._edit_progress_channel_id = None
        self._edit_progress_season_count = 0
        
        xbmc.sleep(50)
    
    def _update_visible_seasons(self, focused_season_idx):
        """Show only 4 seasons around focused_season_idx (0-based). Hide rest."""
        total = getattr(self, '_edit_progress_season_count', 0)
        if total == 0:
            return
        # First visible index (0-based), keep focused in view
        first = max(0, min(focused_season_idx - 1, total - 4))
        first = max(0, first)
        for i in range(total):
            if first <= i < first + 4:
                self.setProperty(f'WCS.EditProgress.Season{i + 1}.Show', 'true')
            else:
                self.clearProperty(f'WCS.EditProgress.Season{i + 1}.Show')
        # Scroll offset
        self.setProperty('WCS.EditProgress.ScrollRow', str(first))
    
    def _load_edit_progress_data(self, tmdb_id, channel_id):
        """Load seasons and episodes from TMDb (runs in background thread)."""
        import requests
        try:
            from wcs.metadata.TMDbClient import get_tmdb_api_key
            api_key = get_tmdb_api_key(self.addon)
            
            # Get series info with seasons
            url = f'https://api.themoviedb.org/3/tv/{tmdb_id}?api_key={api_key}&language=cs-CZ'
            response = requests.get(url)
            response.raise_for_status()
            series_data = response.json()
            
            seasons = series_data.get('seasons', [])
            # Filter out Specials (season 0) and limit to 10
            regular_seasons = [s for s in seasons if s.get('season_number', 0) > 0][:10]
            
            if not regular_seasons:
                self.clearProperty('WCS.EditProgress.Loading')
                self._show_toast("Žádné sezóny", "warning")
                return
            
            # Get current progress
            progress_season, progress_episode = ProgressManager.get_progress(
                channel_id, tmdb_id
            )
            
            # Load each season's episodes
            focus_season_idx = 0  # Which season list to focus (0-based index)
            for idx, season in enumerate(regular_seasons):
                season_num = season['season_number']
                season_label = season.get('name') or f"Sezóna {season_num}"
                
                try:
                    season_url = f'https://api.themoviedb.org/3/tv/{tmdb_id}/season/{season_num}?api_key={api_key}&language=cs-CZ'
                    s_response = requests.get(season_url)
                    s_response.raise_for_status()
                    season_data = s_response.json()
                    episodes = season_data.get('episodes', [])
                except Exception as e:
                    xbmc.log(f"[MyTV] EditProgress: Failed to load season {season_num}: {e}", xbmc.LOGERROR)
                    episodes = []
                
                # Set season label property
                self.setProperty(f'WCS.EditProgress.Season{idx + 1}.Label', season_label)
                
                # Populate the list control
                self._populate_season_list(
                    9401 + idx, season_num, episodes,
                    progress_season, progress_episode
                )
                
                # Track which season has the current progress
                if season_num == progress_season:
                    focus_season_idx = idx
            
            # Set season count (makes controls visible)
            self._edit_progress_season_count = len(regular_seasons)
            self.setProperty('WCS.EditProgress.SeasonCount', str(len(regular_seasons)))
            
            # Clear loading
            self.clearProperty('WCS.EditProgress.Loading')
            
            # Show first 4 seasons (or centered on progress)
            self._update_visible_seasons(focus_season_idx)
            
            # Focus the season with current progress
            focus_list_id = 9401 + focus_season_idx
            xbmc.sleep(100)
            try:
                self.setFocusId(focus_list_id)
                # Try to select the current episode in the list
                focus_list = self.getControl(focus_list_id)
                for i in range(focus_list.size()):
                    item = focus_list.getListItem(i)
                    if item.getProperty('is_current') == 'true':
                        focus_list.selectItem(i)
                        break
            except Exception as e:
                xbmc.log(f"[MyTV] EditProgress: Focus error: {e}", xbmc.LOGWARNING)
                self.setFocusId(9401)
                
        except Exception as e:
            xbmc.log(f"[MyTV] EditProgress: Load failed: {e}", xbmc.LOGERROR)
            self.clearProperty('WCS.EditProgress.Loading')
            self._show_toast("Nepodařilo se načíst data", "error")
    
    def _populate_season_list(self, list_id, season_num, episodes, progress_season, progress_episode):
        """Populate a season list control with episode items."""
        try:
            list_control = self.getControl(list_id)
            list_control.reset()
            
            for ep in episodes:
                ep_num = ep.get('episode_number', 0)
                ep_label = f"E{ep_num:02d}"
                
                item = xbmcgui.ListItem(label=ep_label)
                
                # Episode still image
                still = ''
                if ep.get('still_path'):
                    still = f"https://image.tmdb.org/t/p/w300{ep['still_path']}"
                item.setArt({'thumb': still})
                
                # Store season/episode for click handler
                item.setProperty('season_num', str(season_num))
                item.setProperty('episode_num', str(ep_num))
                item.setProperty('ep_label', ep_label)
                item.setProperty('episode_title', ep.get('name', ''))
                
                # Determine watched status
                is_watched = False
                is_current = False
                
                if season_num < progress_season:
                    is_watched = True
                elif season_num == progress_season:
                    if ep_num < progress_episode:
                        is_watched = True
                    elif ep_num == progress_episode:
                        is_watched = True
                        is_current = True
                    # Next episode after progress = the one to watch next
                    elif ep_num == progress_episode + 1:
                        is_current = True
                
                # Special case: progress is (1, 0) = brand new, first episode is current
                if progress_season == 1 and progress_episode == 0:
                    is_watched = False
                    is_current = (season_num == 1 and ep_num == 1)
                
                # Visual properties
                if is_watched and not is_current:
                    item.setProperty('dimcolor', '50FFFFFF')  # Dimmed
                    item.setProperty('textcolor', '60FFFFFF')
                else:
                    item.setProperty('dimcolor', 'FFFFFFFF')  # Full brightness
                    item.setProperty('textcolor', 'BBFFFFFF')
                
                if is_current:
                    item.setProperty('is_current', 'true')
                
                list_control.addItem(item)
                
        except Exception as e:
            xbmc.log(f"[MyTV] EditProgress: Populate list {list_id} error: {e}", xbmc.LOGERROR)
    
    def _handle_edit_progress_click(self, controlId):
        """Handle click on episode in edit progress dialog. Returns True if handled."""
        if self.getProperty('WCS.MyTV.EditProgress') != 'true':
            return False
        
        # Season list IDs: 9401-9410
        if 9401 <= controlId <= 9410:
            try:
                list_control = self.getControl(controlId)
                selected = list_control.getSelectedItem()
                if not selected:
                    return True
                
                season = int(selected.getProperty('season_num'))
                episode = int(selected.getProperty('episode_num'))
                tmdb_id = self._edit_progress_tmdb_id
                channel_id = self._edit_progress_channel_id
                
                if not tmdb_id or not channel_id:
                    return True
                
                # The clicked episode = "next to watch"
                # So the "last watched" is the previous one
                # If episode 1 is clicked, progress = (season, 0) meaning fresh start of that season
                # If episode > 1, progress = (season, episode - 1)
                if episode > 1:
                    new_progress_season = season
                    new_progress_episode = episode - 1
                else:
                    # Episode 1 clicked = start from beginning of this season
                    # Set progress to "before first" of this season
                    if season > 1:
                        new_progress_season = season - 1
                        new_progress_episode = 99  # Arbitrary high = finished previous season
                    else:
                        new_progress_season = 1
                        new_progress_episode = 0
                
                # Update progress
                ProgressManager.update_progress(
                    channel_id, tmdb_id, new_progress_season, new_progress_episode
                )
                
                xbmc.log(
                    f"[MyTV] EditProgress: Set progress to S{new_progress_season:02d}E{new_progress_episode:02d} "
                    f"(next: S{season:02d}E{episode:02d})",
                    xbmc.LOGINFO
                )
                # Save static properties from old play selection item before it's destroyed
                old_item = getattr(self, '_play_selection_item', None)
                static_props = {}
                if old_item:
                    for prop in ('series_name', 'poster', 'fanart', 'series_logo',
                                 'plot', 'year', 'genre', 'runtime', 'rating',
                                 'tmdb_id', 'channel_id'):
                        try:
                            static_props[prop] = old_item.getProperty(prop) or ''
                        except Exception:
                            static_props[prop] = ''
                
                # Save data from selected item BEFORE hiding dialog
                # (hide resets lists and destroys ListItem objects)
                ep_title = selected.getProperty('episode_title') or selected.getLabel() or ''
                ep_still = selected.getArt('thumb') or ''
                
                # Close edit dialog only
                self._hide_edit_progress_dialog()
                
                # Invalidate program list cache via ProgressManager
                ProgressManager.invalidate_program_cache(channel_id, self._cache)
                
                self.refresh_program_view()
                
                # Build new ListItem for play selection (independent of async refresh)
                new_item = xbmcgui.ListItem(ep_title)
                new_item.setProperty('series_name', static_props.get('series_name', ''))
                new_item.setProperty('season', str(season))
                new_item.setProperty('episode', str(episode))
                new_item.setProperty('episode_title', ep_title)
                new_item.setProperty('ep_code', f"S{season:02d}E{episode:02d}")
                new_item.setProperty('tmdb_id', static_props.get('tmdb_id', ''))
                new_item.setProperty('channel_id', static_props.get('channel_id', ''))
                new_item.setProperty('poster', static_props.get('poster', ''))
                new_item.setProperty('fanart', static_props.get('fanart', ''))
                new_item.setProperty('series_logo', static_props.get('series_logo', ''))
                new_item.setProperty('plot', static_props.get('plot', ''))
                new_item.setProperty('year', static_props.get('year', ''))
                new_item.setProperty('genre', static_props.get('genre', ''))
                new_item.setProperty('runtime', static_props.get('runtime', ''))
                new_item.setProperty('rating', static_props.get('rating', ''))
                if ep_still:
                    new_item.setArt({'thumb': ep_still})
                self._play_selection_item = new_item
                
                # Update play selection properties with new episode data
                ep_code_fmt = f"Sezona {season} Epizoda {episode}"
                self.setProperty('WCS.PlaySelect.EpCode', ep_code_fmt)
                self.setProperty('WCS.PlaySelect.EpTitle', ep_title)
                if ep_still:
                    self.setProperty('WCS.PlaySelect.Still', ep_still)
                
                # Return focus to play selection
                self.setFocusId(9023)
                
                self._show_toast(
                    f"Postup nastaven: S{season:02d}E{episode:02d}",
                    "success"
                )
                
            except Exception as e:
                xbmc.log(f"[MyTV] EditProgress: Click error: {e}", xbmc.LOGERROR)
            return True
        
        # Close button
        if controlId == 9420:
            self._hide_edit_progress_dialog()
            # Return focus to play selection
            self.setFocusId(9026)
            return True
        
        return False
    
    def _handle_edit_progress_action(self, action):
        """Handle back/escape in edit progress dialog. Returns True if handled."""
        if self.getProperty('WCS.MyTV.EditProgress') != 'true':
            return False
        
        action_id = action.getId()
        if action_id in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK):
            self._hide_edit_progress_dialog()
            # Return focus to play selection button
            self.setFocusId(9026)
            return True
        
        # Handle vertical scroll when navigating between season lists
        focused_id = self.getFocusId()
        if 9401 <= focused_id <= 9410:
            season_idx = focused_id - 9401
            self._update_visible_seasons(season_idx)
        
        return False

    
    def _play_hero_episode(self):
        """Play the hero zone episode directly (respects autoplay setting).
        
        Gets the current item from program list (9000) - the same item
        displayed in the hero zone - and plays it directly without showing
        the play selection dialog.
        """
        list_control = self.getControl(9000)
        item = list_control.getSelectedItem()
        if not item:
            return
        
        # Get channel ID for progress tracking
        channel_list = self.getControl(2000)
        current = channel_list.getSelectedItem()
        channel_id = current.getProperty('channel_id') if current else ''
        
        # Check autoplay setting
        autoplay = self.addon.getSettingBool('autoplay_first_item')
        
        self.close()
        
        tmdb_id = item.getProperty('tmdb_id') or ''
        
        if autoplay:
            # Autoplay: create playlist with single item
            playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
            playlist.clear()
            
            item_data = PlaylistBuilder.extract_item_data(item)
            url = PlaylistBuilder.build_playlist_url(self.addon.getAddonInfo('id'), item_data, channel_id)
            li = PlaylistBuilder.build_playlist_listitem(item_data)
            playlist.add(url, li)
            xbmc.Player().play(playlist)
            
            # Resume seek for first episode
            self._seek_to_resume(channel_id, tmdb_id)
            
            # Aktivovat resume monitor (uklada pozici kazdych 30s)
            from wcs.playback.PlaylistCountdownMonitor import get_playlist_monitor
            get_playlist_monitor().activate(meta={
                'channel_id': channel_id,
                'tmdb_id': tmdb_id,
                'season': int(item.getProperty('season') or 0),
                'episode': int(item.getProperty('episode') or 0),
            })
        else:
            # No autoplay: show search dialog for manual source selection
            from wcs.utils import search_and_play_episode
            search_and_play_episode(
                series_name=item.getProperty('series_name') or '',
                season=int(item.getProperty('season') or 0),
                episode=int(item.getProperty('episode') or 0),
                ep_name=item.getProperty('episode_title') or '',
                plot=item.getProperty('plot') or '',
                poster=item.getProperty('poster') or '',
                fanart=item.getProperty('fanart') or '',
                rating=item.getProperty('rating') or '',
                genre=item.getProperty('genre') or '',
                addon=self.addon,
                year=item.getProperty('year') or '',
                tmdb_id=item.getProperty('tmdb_id') or '',
                mytv_channel_id=channel_id,
                episode_thumb=item.getArt('thumb') or ''
            )

    def _play_single_episode(self):
        """Play only the selected single episode.
        
        Respects autoplay_first_item setting:
        - true: autoplay best source via playlist
        - false: show search dialog for source selection
        """
        selected_item = getattr(self, '_play_selection_item', None)
        if not selected_item:
            return
        
        # Get channel ID for progress tracking
        channel_list = self.getControl(2000)
        current = channel_list.getSelectedItem()
        channel_id = current.getProperty('channel_id') if current else ''
        
        # Check autoplay setting
        autoplay = self.addon.getSettingBool('autoplay_first_item')
        
        self.close()
        
        # Zjistit resume pozici
        resume_position = 0
        tmdb_id = selected_item.getProperty('tmdb_id') or ''
        if channel_id and tmdb_id:
            try:
                from wcs.ai import ChannelHistory
                resume = ChannelHistory.get_resume_point(channel_id, tmdb_id)
                if resume and resume['position'] > 10:
                    resume_position = resume['position']
            except Exception:
                pass
        
        if autoplay:
            # Autoplay: create playlist with single item (resolves best source automatically)
            playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
            playlist.clear()
            
            item_data = PlaylistBuilder.extract_item_data(selected_item)
            url = PlaylistBuilder.build_playlist_url(self.addon.getAddonInfo('id'), item_data, channel_id)
            li = PlaylistBuilder.build_playlist_listitem(item_data)
            playlist.add(url, li)
            xbmc.Player().play(playlist)
            
            # Seek na resume pozici
            if resume_position > 0:
                def _seek_after_start():
                    import time as _time
                    for _ in range(50):
                        _time.sleep(0.2)
                        try:
                            if xbmc.Player().isPlaying() and xbmc.Player().getTime() > 0:
                                xbmc.Player().seekTime(resume_position)
                                xbmc.log(f"[MyTV] Play Selection: Resumed at {resume_position:.0f}s", xbmc.LOGINFO)
                                return
                        except Exception:
                            pass
                import threading
                threading.Thread(target=_seek_after_start, daemon=True).start()
            
            # Aktivovat resume monitor (uklada pozici kazdych 30s)
            from wcs.playback.PlaylistCountdownMonitor import get_playlist_monitor
            get_playlist_monitor().activate(meta={
                'channel_id': channel_id,
                'tmdb_id': tmdb_id,
                'season': int(selected_item.getProperty('season') or 0),
                'episode': int(selected_item.getProperty('episode') or 0),
            })
        else:
            # No autoplay: show search dialog for manual source selection
            from wcs.utils import search_and_play_episode
            search_and_play_episode(
                series_name=selected_item.getProperty('series_name') or '',
                season=int(selected_item.getProperty('season') or 0),
                episode=int(selected_item.getProperty('episode') or 0),
                ep_name=selected_item.getProperty('episode_title') or '',
                plot=selected_item.getProperty('plot') or '',
                poster=selected_item.getProperty('poster') or '',
                fanart=selected_item.getProperty('fanart') or '',
                rating=selected_item.getProperty('rating') or '',
                genre=selected_item.getProperty('genre') or '',
                addon=self.addon,
                year=selected_item.getProperty('year') or '',
                tmdb_id=selected_item.getProperty('tmdb_id') or '',
                mytv_channel_id=channel_id,
                episode_thumb=selected_item.getArt('thumb') or ''
            )
    
    def _play_series_only(self):
        """Play only episodes from the selected series, starting from selected episode."""
        from wcs.metadata import TMDbClient
        
        selected_item = getattr(self, '_play_selection_item', None)
        if not selected_item:
            # Fallback: get current item from program list (hero zone call)
            list_control = self.getControl(9000)
            selected_item = list_control.getSelectedItem()
        if not selected_item:
            return
        
        series_name = selected_item.getProperty('series_name')
        selected_tmdb_id = selected_item.getProperty('tmdb_id')
        selected_season = selected_item.getProperty('season')
        selected_episode = selected_item.getProperty('episode')
        
        if not selected_tmdb_id or not selected_season or not selected_episode:
            return
        
        # Get settings
        try:
            upcoming_count_val = self.addon.getSetting('mytv_upcoming_count')
            upcoming_count = int(upcoming_count_val) if upcoming_count_val else 12
        except:
            upcoming_count = 12
        
        # Get channel ID
        channel_list = self.getControl(2000)
        current_channel_item = channel_list.getSelectedItem()
        channel_id = current_channel_item.getProperty('channel_id') if current_channel_item else ''
        
        self.close()
        
        # Create playlist
        playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
        playlist.clear()
        
        # First item is the selected episode (we have all metadata)
        addon_id = self.addon.getAddonInfo('id')
        first_data = PlaylistBuilder.extract_item_data(selected_item)
        first_url = PlaylistBuilder.build_playlist_url(addon_id, first_data, channel_id)
        first_li = PlaylistBuilder.build_playlist_listitem(first_data)
        playlist.add(first_url, first_li)
        
        # Generate remaining episodes from TMDb
        curr_season = int(selected_season)
        curr_episode = int(selected_episode)
        items_added = 1
        
        while items_added < upcoming_count:
            # Get next episode from TMDb
            cache_key = f"episode_{selected_tmdb_id}_{curr_season}_{curr_episode + 1}"
            next_meta = self._cache.get(cache_key)
            
            if not next_meta:
                next_meta = TMDbClient.get_next_episode_from_tmdb(selected_tmdb_id, curr_season, curr_episode, self.addon)
                if next_meta:
                    self._cache.set(cache_key, next_meta, ttl_seconds=7 * 24 * 3600)
            
            if not next_meta:
                break
            
            item_data = PlaylistBuilder.extract_from_tmdb_meta(next_meta, series_name, selected_tmdb_id)
            url = PlaylistBuilder.build_playlist_url(addon_id, item_data, channel_id)
            li = PlaylistBuilder.build_playlist_listitem(item_data)
            
            playlist.add(url, li)
            items_added += 1
            curr_season = next_meta.get('season', curr_season)
            curr_episode = next_meta.get('episode', curr_episode)
        
        xbmc.Player().play(playlist)
        
        # Resume seek for first episode
        self._seek_to_resume(channel_id, selected_tmdb_id)
        
        # Aktivovat countdown monitor pro playlist
        from wcs.playback.PlaylistCountdownMonitor import get_playlist_monitor
        get_playlist_monitor().activate()
    
    def _play_channel_from_selected(self):
        """Play channel mix starting from selected position, generating more if needed."""
        import random
        from wcs.metadata import TMDbClient
        
        selected_item = getattr(self, '_play_selection_item', None)
        if not selected_item:
            return
        
        list_control = self.getControl(9000)
        size = list_control.size()
        selected_pos = list_control.getSelectedPosition()
        
        # Get settings
        try:
            upcoming_count_val = self.addon.getSetting('mytv_upcoming_count')
            upcoming_count = int(upcoming_count_val) if upcoming_count_val else 12
        except:
            upcoming_count = 12
        
        block_size_setting = self.addon.getSetting('mytv_block_size') or "2"
        min_block, max_block = 1, 1
        if '-' in block_size_setting:
            try:
                parts = block_size_setting.split('-')
                min_block, max_block = int(parts[0]), int(parts[1])
            except:
                pass
        else:
            try:
                val = int(block_size_setting)
                min_block = max_block = val
            except:
                pass
        
        # Get channel info
        channel_list = self.getControl(2000)
        current_channel_item = channel_list.getSelectedItem()
        channel_id = current_channel_item.getProperty('channel_id') if current_channel_item else ''
        
        channel = self.channel_manager.get_channel_by_id(channel_id)
        series_list = channel.series_list if channel and hasattr(channel, 'series_list') else []
        
        self.close()
        
        # Create playlist
        playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
        playlist.clear()
        
        items_added = 0
        
        # Step 1: Add items from existing upcoming list (from selected position)
        # Track last episode for each series (for later generation)
        series_progress = {}  # {tmdb_id: (season, episode)}
        
        addon_id = self.addon.getAddonInfo('id')
        
        for i in range(selected_pos, size):
            if items_added >= upcoming_count:
                break
            
            item = list_control.getListItem(i)
            item_data = PlaylistBuilder.extract_item_data(item)
            url = PlaylistBuilder.build_playlist_url(addon_id, item_data, channel_id)
            li = PlaylistBuilder.build_playlist_listitem(item_data)
            playlist.add(url, li)
            items_added += 1
            
            # Track progress for this series
            tmdb_id = item.getProperty('tmdb_id')
            season = item.getProperty('season')
            episode = item.getProperty('episode')
            if tmdb_id and season and episode:
                series_progress[tmdb_id] = (int(season), int(episode))
        
        # Step 2: Generate more episodes if needed
        if items_added < upcoming_count and series_list:
            # Prepare series candidates with their progress
            series_candidates = []
            for series in series_list:
                series_id = str(series.get('id'))
                series_name = series.get('name')
                if series_id in series_progress:
                    curr_s, curr_e = series_progress[series_id]
                else:
                    # Get from channel history via ProgressManager
                    curr_s, curr_e = ProgressManager.get_progress(channel_id, series_id)
                series_candidates.append({
                    'id': series_id,
                    'name': series_name,
                    'season': curr_s,
                    'episode': curr_e
                })
            
            random.shuffle(series_candidates)
            candidate_idx = 0
            max_loops = (upcoming_count - items_added) * 2
            loops = 0
            
            while items_added < upcoming_count and series_candidates and loops < max_loops:
                loops += 1
                
                candidate = series_candidates[candidate_idx % len(series_candidates)]
                candidate_idx += 1
                
                block_size = random.randint(min_block, max_block)
                
                for _ in range(block_size):
                    if items_added >= upcoming_count:
                        break
                    
                    series_id = candidate['id']
                    series_name = candidate['name']
                    curr_s = candidate['season']
                    curr_e = candidate['episode']
                    
                    # Get next episode
                    cache_key = f"episode_{series_id}_{curr_s}_{curr_e + 1}"
                    next_meta = self._cache.get(cache_key)
                    
                    if not next_meta:
                        next_meta = TMDbClient.get_next_episode_from_tmdb(series_id, curr_s, curr_e, self.addon)
                        if next_meta:
                            self._cache.set(cache_key, next_meta, ttl_seconds=7 * 24 * 3600)
                    
                    if not next_meta:
                        break
                    
                    item_data = PlaylistBuilder.extract_from_tmdb_meta(next_meta, series_name, series_id)
                    url = PlaylistBuilder.build_playlist_url(addon_id, item_data, channel_id)
                    li = PlaylistBuilder.build_playlist_listitem(item_data)
                    
                    playlist.add(url, li)
                    items_added += 1
                    
                    # Update candidate progress
                    candidate['season'] = next_meta.get('season', curr_s)
                    candidate['episode'] = next_meta.get('episode', curr_e)
        
        if playlist.size() > 0:
            # Get first item's tmdb_id for resume
            first_item = list_control.getListItem(selected_pos) if selected_pos < size else None
            first_tmdb_id = first_item.getProperty('tmdb_id') if first_item else ''
            
            xbmc.Player().play(playlist)
            
            # Resume seek for first episode
            self._seek_to_resume(channel_id, first_tmdb_id)
            
            # Aktivovat countdown monitor pro playlist
            from wcs.playback.PlaylistCountdownMonitor import get_playlist_monitor
            get_playlist_monitor().activate()
    


    def _play_program_item(self):
        list_control = self.getControl(9000)
        item = list_control.getSelectedItem()
        if not item: return
        
        series_name = item.getProperty('series_name')
        season = item.getProperty('season')
        episode = item.getProperty('episode')
        tmdb_id = item.getProperty('tmdb_id')
        
        # Metadata pro utils.play_episode_from_addon
        episode_title = item.getProperty('episode_title')
        plot = item.getProperty('plot')
        rating = item.getProperty('rating')
        poster = item.getProperty('poster')
        fanart = item.getProperty('fanart')
        genre = item.getProperty('genre')
        year = item.getProperty('year')
        
        if not series_name or not season or not episode:
             return

        # Get current channel ID for history tracking
        channel_list = self.getControl(2000)
        current_channel_item = channel_list.getSelectedItem()
        channel_id = current_channel_item.getProperty('channel_id') if current_channel_item else ''

        # Get episode still for playback dialog
        episode_thumb = item.getArt('thumb') or ''
        
        self.close()
        
        # Trigger play action via plugin URL - using standard utils function
        url = (f"plugin://{self.addon.getAddonInfo('id')}?action=play_episode_from_addon"
               f"&series_name={quote_plus(series_name)}"
               f"&season_number={season}"
               f"&episode_number={episode}"
               f"&tmdb_id={tmdb_id}"
               f"&episode_title={quote_plus(episode_title)}"
               f"&plot={quote_plus(plot)}"
               f"&rating={quote_plus(rating)}"
               f"&poster={quote_plus(poster)}"
               f"&fanart={quote_plus(fanart)}"
               f"&genre={quote_plus(genre)}"
               f"&year={quote_plus(year)}"
               f"&episode_thumb={quote_plus(episode_thumb)}"
               f"&mytv_channel_id={quote_plus(channel_id)}")
        
        xbmc.executebuiltin(f"PlayMedia({url})")

    def _play_current_channel(self):
        """Spustí přehrávání aktuálního kanálu (náhodná epizoda Next Up)."""
        list_control = self.getControl(2000)
        item = list_control.getSelectedItem()
        if not item: return
        
        channel_id = item.getProperty('channel_id')
        
        # Najít další epizodu pro přehrání
        next_episode = self.channel_manager.get_random_episode_for_channel(channel_id)
        
        if not next_episode:
            self._show_toast("Kanál nemá žádné dostupné epizody", "warning")
            return
            
        self.close()
        
        # Přehrát epizodu pomocí standardní akce
        series_name = next_episode.get('series_name') or next_episode.get('series_title')
        season = next_episode.get('season_number')
        episode = next_episode.get('episode_number')
        
        url = (f"plugin://{self.addon.getAddonInfo('id')}?action=play_episode_from_addon"
               f"&series_name={quote_plus(series_name)}"
               f"&season_number={season}"
               f"&episode_number={episode}"
               f"&tmdb_id={next_episode.get('tmdb_id', '')}"
               f"&episode_title={quote_plus(next_episode.get('episode_title', ''))}"
               f"&plot={quote_plus(next_episode.get('episode_plot', ''))}"
               f"&rating={quote_plus(str(next_episode.get('episode_rating', '')))}"
               f"&poster={quote_plus(next_episode.get('series_poster', ''))}"
               f"&fanart={quote_plus(next_episode.get('series_fanart', ''))}"
               f"&year={quote_plus(str(next_episode.get('episode_year', '')))}"
               f"&mytv_channel_id={quote_plus(channel_id)}")
        
        xbmc.executebuiltin(f"PlayMedia({url})")

    def _populate_mytv_chat_buttons(self):
        """Populate chat sidebar buttons with MyTV-specific options."""
        buttons = [
            {"label": "Vytvořit Comedy Channel", "value": "create_channel:comedy"},
            {"label": "Vytvořit Drama Channel", "value": "create_channel:drama"},
            {"label": "Vytvořit Scifi Channel", "value": "create_channel:scifi"},
        ]
        self.update_buttons(buttons)

    def _handle_grid_click(self):
        # Block sidebar during this operation
        self._block_sidebar = True
        try:
            grid = self.getControl(9100)
            item = grid.getSelectedItem()
            if not item: return
            
            # Check if it's the "Add Series" placeholder
            if item.getProperty('is_add_button') == 'true':
                # Show custom Add Series dialog overlay
                self._show_add_series_dialog()
                return

            # Show Series Detail dialog instead of context menu
            series_id = item.getProperty('series_id')
            channel_id = item.getProperty('channel_id')
            self._show_series_detail_dialog(series_id, channel_id)
        finally:
            self._block_sidebar = False

    def _add_from_my_series(self):
        # Block sidebar from opening during this operation
        self._block_sidebar = True
        try:
            # Get My Series
            my_series = user_data.load_tmdb_series(self.addon)
            if not my_series:
                 self._show_toast("Nenalezeny žádné uložené seriály", "warning")
                 return
                 
            # Filter out already added ones?
            list_control = self.getControl(2000)
            channel_id = list_control.getSelectedItem().getProperty('channel_id')
            channel = self.channel_manager.get_channel_by_id(channel_id)
            current_ids = [str(s.get('id')) for s in channel.series_list]
            
            candidates = [s for s in my_series if str(s.get('id')) not in current_ids]
            
            if not candidates:
                 self._show_toast("Všechny vaše seriály už jsou v kanálu", "info")
                 return
                 
            # Multi select
            labels = [f"{s.get('name')} ({s.get('year', '')})" for s in candidates]
            selected = xbmcgui.Dialog().multiselect("Přidat do kanálu", labels)
            
            if selected:
                for idx in selected:
                    self.channel_manager.toggle_series_in_channel(channel_id, candidates[idx])
                # Regenerate channel composite icon
                self.channel_manager.generate_channel_composite(channel_id)
                # Refresh editor view
                self.update_editor_view()
                self.refresh_program_view(shuffle=True)
                self.refresh_channels_view(set_focus=False)  # Update channel icons
                self.setFocusId(9100)  # Changed from 3000 to correct ID
                self._show_toast("Seriály přidány", "success")
        finally:
            # Unblock sidebar
            self._block_sidebar = False

    def _add_new_search(self):
        # Block sidebar during this operation
        self._block_sidebar = True
        try:
            import requests
            from wcs.metadata.TMDbClient import get_tmdb_api_key
            
            search_query = xbmcgui.Dialog().input("Hledat seriál", type=xbmcgui.INPUT_ALPHANUM)
            if not search_query: return
            
            try:
                url = f'https://api.themoviedb.org/3/search/tv?api_key={get_tmdb_api_key()}&language=cs-CZ&query={quote_plus(search_query)}'
                response = requests.get(url, timeout=10)
                results = response.json().get('results', [])
            except Exception:
                return
                
            if not results:
                 self._show_toast("Nic nenalezeno", "warning")
                 return
                 
            choices = [f"{item['name']} ({item.get('first_air_date', '')[:4]})" for item in results]
            selected_idx = xbmcgui.Dialog().select("Vyberte seriál", choices)
            
            if selected_idx >= 0:
                selected_item = results[selected_idx]
                list_control = self.getControl(2000)
                channel_id = list_control.getSelectedItem().getProperty('channel_id')
                
                self.channel_manager.toggle_series_in_channel(channel_id, selected_item)
                # Full refresh
                self.update_editor_view()
                self.refresh_program_view(shuffle=True)
                self.setFocusId(9100)
                self._show_toast(f"'{selected_item['name']}' přidáno", "success")
        finally:
            self._block_sidebar = False

    # ==================================================================================
    # ADD SERIES DIALOG METHODS
    # ==================================================================================
    
    def _show_add_series_dialog(self):
        """Show the inline Add Media dialog for series."""
        # Get current channel ID
        list_control = self.getControl(2000)
        channel_id = list_control.getSelectedItem().getProperty('channel_id')
        
        # Create controller using unified AddMediaDialog with media_type='tv'
        self._add_series_controller = create_add_media_controller(
            self, self.addon, self.channel_manager, channel_id,
            on_close_callback=None,
            media_type='tv'
        )
        
        # Show the inline dialog
        self._add_series_controller.show()
    
    def _close_add_series_dialog(self):
        """Close the inline Add Series dialog and refresh if needed."""
        if hasattr(self, '_add_series_controller') and self._add_series_controller:
            series_added = self._add_series_controller.was_series_added()
            channel_id = self._add_series_controller.channel_id
            self._add_series_controller.hide()
            self._add_series_controller = None
            
            # Check if this was a newly created channel that needs auto-naming
            pending_channel_id = getattr(self, '_pending_new_channel_id', None)
            xbmc.log(f"[MyTV] Checking pending_channel_id: {pending_channel_id}, channel_id: {channel_id}", xbmc.LOGWARNING)
            
            if pending_channel_id and pending_channel_id == channel_id:
                # Clear the pending flag
                self._pending_new_channel_id = None
                
                channel = self.channel_manager.get_channel_by_id(channel_id)
                xbmc.log(f"[MyTV] Channel found: {channel is not None}, series_list: {len(channel.series_list) if channel and channel.series_list else 0}", xbmc.LOGWARNING)
                
                if not channel or not channel.series_list:
                    # No series added - delete the empty channel (no history to delete for new channel)
                    self.channel_manager.delete_channel(channel_id)
                    self._show_toast("Kanál nebyl vytvořen - nepřidali jste žádné seriály", "warning")
                else:
                    # Generate channel name based on genres
                    from wcs.ai.ChannelNameGenerator import ChannelNameGenerator
                    generator = ChannelNameGenerator(self.addon)
                    
                    # Check naming mode from settings (text value from select)
                    naming_mode = self.addon.getSetting('mytv_channel_naming') or ''
                    
                    # Check if AI mode is selected (contains "AI" in the value)
                    if 'AI' in naming_mode or naming_mode == '1':
                        # AI mode - use temporary name and generate asynchronously
                        temp_name = generator.generate_from_genres(channel.series_list)
                        self.channel_manager.rename_channel(channel_id, temp_name)
                        self.channel_manager.generate_channel_composite(channel_id)
                        
                        # Start async AI name generation (notification will come when done)
                        self._generate_ai_channel_name_async(channel_id, channel.series_list, generator)
                    else:
                        # Sync genre-based naming
                        new_name = generator.generate_from_genres(channel.series_list)
                        self.channel_manager.rename_channel(channel_id, new_name)
                        self.channel_manager.generate_channel_composite(channel_id)
                        self._show_toast(f"Kanál '{new_name}' vytvořen", "success")
                
                # Full refresh (new channel = shuffle)
                self.current_channel_index = 0
                self.refresh_channels_view(set_focus=False)
                self.update_editor_view()
                self.refresh_program_view(shuffle=True)
                self._delayed_focus(2000, delay=0.2)
                return
            
            # Normal flow - just refresh if series were added/removed
            if series_added:
                # Regenerate channel composite icon
                self.channel_manager.generate_channel_composite(channel_id)
                self.update_editor_view()
                self.refresh_program_view(shuffle=True)
                self.refresh_channels_view(set_focus=False)  # Update channel icons
            
            # Return focus to the grid
            self.setFocusId(9100)
    
    def _is_add_series_dialog_active(self):
        """Check if Add Series dialog is currently active."""
        return hasattr(self, '_add_series_controller') and self._add_series_controller and self._add_series_controller.is_active()
    
    def _handle_add_series_action(self, action):
        """Delegate action to Add Series controller. Returns True if handled."""
        if self._is_add_series_dialog_active():
            handled = self._add_series_controller.onAction(action)
            # Check if dialog was closed (e.g. via ESC/Back)
            # Guard against None - callback may have already cleaned up controller
            if handled and self._add_series_controller and not self._add_series_controller.is_active():
                self._close_add_series_dialog()
            return handled
        return False
    
    def _handle_add_series_click(self, controlId):
        """Delegate click to Add Series controller. Returns True if handled."""
        if self._is_add_series_dialog_active():
            handled = self._add_series_controller.onClick(controlId)
            # Check if dialog was closed - guard against None
            if handled and self._add_series_controller and not self._add_series_controller.is_active():
                self._close_add_series_dialog()
            return handled
        return False
    
    def _handle_add_series_focus(self, controlId):
        """Delegate focus to Add Series controller."""
        if self._is_add_series_dialog_active():
            self._add_series_controller.onFocus(controlId)
    

    # ==================================================================================
    # SERIES DETAIL DIALOG METHODS
    # ==================================================================================
    
    def _show_series_detail_dialog(self, series_id, channel_id, return_focus_id=9100):
        """Show the inline Series Detail dialog with series info."""
        # Store current series info for removal action
        self._series_detail_series_id = series_id
        self._series_detail_channel_id = channel_id
        self._series_detail_return_focus_id = return_focus_id
        
        # Get series data from channel
        channel = self.channel_manager.get_channel_by_id(channel_id)
        series_data = None
        if channel:
            for s in channel.series_list:
                if str(s.get('id')) == str(series_id):
                    series_data = s
                    break
        
        if not series_data:
            return
        
        # Fetch additional metadata from TMDb if needed
        metadata = self._get_series_metadata(series_id, series_data)
        
        # Set window properties for XML
        self.setProperty('WCS.MyTV.SeriesDetail.Name', metadata.get('name', ''))
        self.setProperty('WCS.MyTV.SeriesDetail.Year', str(metadata.get('year', '')))
        self.setProperty('WCS.MyTV.SeriesDetail.Genre', metadata.get('genre', ''))
        self.setProperty('WCS.MyTV.SeriesDetail.Rating', str(metadata.get('rating', '')))
        self.setProperty('WCS.MyTV.SeriesDetail.Plot', metadata.get('plot', ''))
        self.setProperty('WCS.MyTV.SeriesDetail.Fanart', metadata.get('fanart', ''))
        self.setProperty('WCS.MyTV.SeriesDetail.Logo', metadata.get('logo', ''))
        self.setProperty('WCS.MyTV.SeriesDetail.Poster', metadata.get('poster', ''))
        
        # Show the dialog
        self.setProperty('WCS.MyTV.SeriesDetail.Active', 'true')
        
        # Focus on remove button
        self._delayed_focus(9300, delay=0.1)
    
    def _get_series_metadata(self, series_id, series_data):
        """Get series metadata from cache or TMDb."""
        import requests
        from wcs.metadata.TMDbClient import get_tmdb_api_key
        
        # Try cache first
        cache_key = f"series_detail_{series_id}"
        cached = self._cache.get(cache_key)
        if cached:
            return cached
        
        # Build basic metadata from what we have
        metadata = {
            'name': series_data.get('name', ''),
            'year': series_data.get('year', '') or (series_data.get('first_air_date', '')[:4] if series_data.get('first_air_date') else ''),
            'genre': '',
            'rating': '',
            'plot': series_data.get('overview', '') or series_data.get('plot', ''),
            'fanart': '',
            'logo': '',
            'poster': ''
        }
        
        # Get poster
        poster = series_data.get('poster_path') or series_data.get('poster')
        if poster:
            if not poster.startswith('http'):
                poster = f"https://image.tmdb.org/t/p/w500{poster}"
            metadata['poster'] = poster
        
        # Fetch full details from TMDb
        try:
            api_key = get_tmdb_api_key(self.addon)
            
            # Fetch series details
            details_url = f'https://api.themoviedb.org/3/tv/{series_id}?api_key={api_key}&language=cs-CZ'
            response = requests.get(details_url, timeout=10)
            if response.status_code == 200:
                details = response.json()
                
                metadata['name'] = details.get('name', metadata['name'])
                metadata['plot'] = details.get('overview', metadata['plot'])
                metadata['rating'] = f"{details.get('vote_average', 0):.1f}"
                
                # Year
                if details.get('first_air_date'):
                    metadata['year'] = details['first_air_date'][:4]
                
                # Genres
                genres = details.get('genres', [])
                if genres:
                    metadata['genre'] = ', '.join([g.get('name', '') for g in genres[:3]])
                
                # Fanart/backdrop
                backdrop = details.get('backdrop_path')
                if backdrop:
                    metadata['fanart'] = f"https://image.tmdb.org/t/p/w1280{backdrop}"
            
            # Fetch images for clearlogo
            images_url = f'https://api.themoviedb.org/3/tv/{series_id}/images?api_key={api_key}'
            response = requests.get(images_url, timeout=10)
            if response.status_code == 200:
                images = response.json()
                logos = images.get('logos', [])
                for logo in logos:
                    if logo.get('iso_639_1') in ('en', 'cs', None):
                        metadata['logo'] = f"https://image.tmdb.org/t/p/w500{logo['file_path']}"
                        break
            
            # Cache for 7 days
            self._cache.set(cache_key, metadata, ttl_seconds=7 * 24 * 3600)
        except Exception as e:
            xbmc.log(f"[MyTV] Error fetching series details: {e}", xbmc.LOGWARNING)
        
        return metadata
    
    def _close_series_detail_dialog(self, removed=False):
        """Close the inline Series Detail dialog."""
        self.clearProperty('WCS.MyTV.DeleteSeriesConfirm')
        self.clearProperty('WCS.MyTV.SeriesDetail.Active')
        
        if removed:
            # Refresh views
            self.channel_manager.generate_channel_composite(self._series_detail_channel_id)
            self.update_editor_view()
            self.refresh_program_view(shuffle=True)
            self.refresh_channels_view(set_focus=False)
        
        # Return focus to source control
        return_id = getattr(self, '_series_detail_return_focus_id', 9100)
        self.setFocusId(return_id)
    
    def _is_series_detail_dialog_active(self):
        """Check if Series Detail dialog is currently active."""
        return bool(self.getProperty('WCS.MyTV.SeriesDetail.Active'))
    
    # ==================================================================================
    # SERIES GRID SUB-DIALOG (Apple TV)
    # ==================================================================================
    
    def _show_series_grid_dialog(self):
        """Show the series grid sub-dialog for current channel."""
        list_control = self.getControl(2000)
        item = list_control.getSelectedItem()
        if not item:
            return
        
        channel_id = item.getProperty('channel_id')
        channel_name = item.getLabel() or ''
        
        # Set properties for XML visibility and title
        self.setProperty('WCS.MyTV.SeriesGrid', 'true')
        self.setProperty('WCS.MyTV.SeriesGrid.Title', channel_name)
        
        # Populate 9100 grid with series from this channel
        self.update_editor_view()
        
        # Focus on Play Channel button
        self._delayed_focus(9110, delay=0.1)

    def _close_series_grid_dialog(self):
        """Close the series grid sub-dialog."""
        self.clearProperty('WCS.MyTV.SeriesGrid')
        self.clearProperty('WCS.MyTV.SeriesGrid.Title')
        
        # Return focus to channel list
        self.setFocusId(2000)

    def _enter_reorder_mode(self):
        """Enter channel reorder mode from Series Grid dialog.
        
        Uses navigate-then-place approach:
        - List handles LEFT/RIGHT navigation natively
        - User navigates to target position and presses OK to place channel
        - BACK cancels the operation
        """
        list_control = self.getControl(2000)
        item = list_control.getSelectedItem()
        if not item or item.getProperty('is_create_button') == 'true':
            return
        
        # Store original position and channel for move operation
        self._reorder_channel_id = item.getProperty('channel_id')
        self._reorder_original_index = list_control.getSelectedPosition()
        self._reorder_channel_name = item.getLabel()
        
        # Close Series Grid, show channel list in reorder mode
        self._close_series_grid_dialog()
        
        # Activate reorder mode
        self.setProperty('WCS.MyTV.ReorderMode', 'true')
        self.setProperty('WCS.MyTV.ReorderName', self._reorder_channel_name or '')
        
        # Focus on channel list
        self.setFocusId(2000)
    
    def _confirm_reorder(self):
        """Confirm channel reorder -- move channel to current cursor position."""
        channel_id = getattr(self, '_reorder_channel_id', None)
        if not channel_id:
            self._exit_reorder_mode(confirm=False)
            return
        
        list_control = self.getControl(2000)
        target_pos = list_control.getSelectedPosition()
        original_index = getattr(self, '_reorder_original_index', 0)
        
        # Don't allow placing on the "Create" button
        target_item = list_control.getSelectedItem()
        if target_item and target_item.getProperty('is_create_button') == 'true':
            self._show_toast("Nelze umístit na tuto pozici", "warning")
            return
        
        if target_pos == original_index:
            # Same position -- no move needed
            self._exit_reorder_mode(confirm=False)
            return
        
        # Move the channel in data model
        self.channel_manager.move_channel(channel_id, target_pos)
        self.current_channel_index = target_pos
        
        # Refresh and exit
        self.refresh_channels_view(set_focus=False)
        list_control.selectItem(target_pos)
        self.update_editor_view()
        self.refresh_program_view()
        
        self._exit_reorder_mode(confirm=True)
    
    def _exit_reorder_mode(self, confirm=True):
        """Exit channel reorder mode.
        
        Args:
            confirm: If True, show success toast. If False, show cancel toast.
        """
        if confirm:
            channel_name = getattr(self, '_reorder_channel_name', '')
            self._show_toast(f"Kanál '{channel_name}' přesunut", "success")
        else:
            self._show_toast("Přesun zrušen", "info")
        
        # Clear reorder state
        self.clearProperty('WCS.MyTV.ReorderMode')
        self.clearProperty('WCS.MyTV.ReorderName')
        self._reorder_channel_id = None
        self._reorder_original_index = None
        self._reorder_channel_name = None
        
        # Ensure focus on channel list
        self.setFocusId(2000)

    def _enter_program_reorder_mode(self):
        """Enter program reorder mode from Play Selection dialog.
        
        Uses navigate-then-place approach on program list (horizontal):
        - List handles LEFT/RIGHT navigation natively
        - User navigates to target position and presses OK to place episode
        - BACK cancels the operation
        """
        # Close Play Selection dialog if open
        if self.getProperty('WCS.MyTV.PlaySelection') == 'true':
            self._hide_play_selection_dialog()
        
        self._activate_program_reorder()
    
    def _enter_program_reorder_mode_direct(self):
        """Enter program reorder mode directly from long press on program list.
        
        No dialog to close -- activates reorder mode immediately.
        """
        self._activate_program_reorder()
    
    def _activate_program_reorder(self):
        """Shared logic for activating program reorder mode."""
        program_list = self.getControl(9000)
        selected_pos = program_list.getSelectedPosition()
        if selected_pos < 0:
            return
        
        item = program_list.getSelectedItem()
        if not item:
            return
        
        # Store original position and episode info for move operation
        self._program_reorder_original_index = selected_pos
        self._program_reorder_ep_name = item.getProperty('series_name') or item.getLabel()
        self._program_reorder_ep_code = item.getProperty('ep_code') or ''
        
        # Activate program reorder mode
        ep_label = self._program_reorder_ep_name
        if self._program_reorder_ep_code:
            ep_label = f"{ep_label} {self._program_reorder_ep_code}"
        self.setProperty('WCS.MyTV.ProgramReorderMode', 'true')
        self.setProperty('WCS.MyTV.ProgramReorderName', ep_label)
        
        # Focus on program list
        self.setFocusId(9000)
    
    def _confirm_program_reorder(self):
        """Confirm program reorder -- move episode to current cursor position."""
        original_index = getattr(self, '_program_reorder_original_index', None)
        if original_index is None:
            self._exit_program_reorder_mode(confirm=False)
            return
        
        program_list = self.getControl(9000)
        target_pos = program_list.getSelectedPosition()
        
        if target_pos < 0 or target_pos == original_index:
            # Same position -- no move needed
            self._exit_program_reorder_mode(confirm=False)
            return
        
        # Get current channel ID for cache operations
        list_control = self.getControl(2000)
        channel_item = list_control.getSelectedItem()
        channel_id = channel_item.getProperty('channel_id') if channel_item else None
        
        if not channel_id:
            self._exit_program_reorder_mode(confirm=False)
            return
        
        # Move episode in cache data
        cache_key = f"program_list_{channel_id}"
        cached_items = self._cache.get(cache_key)
        
        if cached_items and 0 <= original_index < len(cached_items) and 0 <= target_pos < len(cached_items):
            # Remove from original position and insert at target
            item_data = cached_items.pop(original_index)
            cached_items.insert(target_pos, item_data)
            
            # Save updated cache
            self._cache.set(cache_key, cached_items, ttl_seconds=24 * 3600)
            
            # Save reorder persistently (survives cache invalidation)
            self._save_program_reorder(channel_id, cached_items)
            
            # Reload program list from updated cache
            self._instant_load_from_cache(channel_id)
            
            # Re-select the moved item at its new position
            program_list.selectItem(target_pos)
            
            xbmc.log(f"[MyTV] Moved program item from {original_index} to {target_pos} in channel {channel_id}", xbmc.LOGINFO)
            
            # Flush cache to disk immediately (survive dialog close/Kodi restart)
            self._cache.flush()
        
        self._exit_program_reorder_mode(confirm=True)
    
    def _exit_program_reorder_mode(self, confirm=True):
        """Exit program reorder mode.
        
        Args:
            confirm: If True, show success toast. If False, show cancel toast.
        """
        if confirm:
            ep_name = getattr(self, '_program_reorder_ep_name', '')
            self._show_toast(f"'{ep_name}' přesunuto", "success")
        else:
            self._show_toast("Přesun zrušen", "info")
        
        # Clear program reorder state
        self.clearProperty('WCS.MyTV.ProgramReorderMode')
        self.clearProperty('WCS.MyTV.ProgramReorderName')
        self._program_reorder_original_index = None
        self._program_reorder_ep_name = None
        self._program_reorder_ep_code = None
        
        # Ensure focus on program list
        self.setFocusId(9000)

    def _save_program_reorder(self, channel_id, items_data):
        """Save program reorder persistently (survives program_list cache invalidation).
        
        Stores episode identity keys (tmdb_id + ep_code) in order.
        Uses prefix 'program_reorder_' which is NOT cleared by delete_prefix('program_list_').
        
        Args:
            channel_id: Channel ID
            items_data: List of item dicts (from cache format)
        """
        try:
            order_keys = []
            for item in items_data:
                props = item.get('properties', {})
                tmdb_id = props.get('tmdb_id', '')
                ep_code = props.get('ep_code', '')
                if tmdb_id and ep_code:
                    order_keys.append(f"{tmdb_id}_{ep_code}")
            
            if order_keys:
                reorder_key = f"program_reorder_{channel_id}"
                self._cache.set(reorder_key, order_keys, ttl_seconds=30 * 24 * 3600)
                xbmc.log(f"[MyTV] Saved program reorder for channel {channel_id}: {len(order_keys)} items", xbmc.LOGINFO)
        except Exception as e:
            xbmc.log(f"[MyTV] Error saving program reorder: {e}", xbmc.LOGWARNING)
    
    def _apply_program_reorder(self, channel_id):
        """Apply saved program reorder to current program list.
        
        Reads saved order keys and re-sorts the program list cache accordingly.
        Called after async program load to restore user's custom order.
        
        Args:
            channel_id: Channel ID
        """
        try:
            reorder_key = f"program_reorder_{channel_id}"
            saved_order = self._cache.get(reorder_key)
            if not saved_order:
                return  # No saved reorder
            
            cache_key = f"program_list_{channel_id}"
            cached_items = self._cache.get(cache_key)
            if not cached_items:
                return
            
            # Build lookup: key -> item
            item_map = {}
            unmatched = []
            for item in cached_items:
                props = item.get('properties', {})
                tmdb_id = props.get('tmdb_id', '')
                ep_code = props.get('ep_code', '')
                key = f"{tmdb_id}_{ep_code}" if tmdb_id and ep_code else None
                if key and key in saved_order:
                    item_map[key] = item
                else:
                    unmatched.append(item)
            
            # Rebuild list in saved order
            reordered = []
            for key in saved_order:
                if key in item_map:
                    reordered.append(item_map[key])
            
            # Add any new items not in saved order at the end
            reordered.extend(unmatched)
            
            if not reordered:
                return  # No matching items found in saved order
            
            # Save reordered list and reload UI
            self._cache.set(cache_key, reordered, ttl_seconds=24 * 3600)
            self._instant_load_from_cache(channel_id)
            
            xbmc.log(f"[MyTV] Applied saved program reorder for channel {channel_id}", xbmc.LOGINFO)
        except Exception as e:
            xbmc.log(f"[MyTV] Error applying program reorder: {e}", xbmc.LOGWARNING)

    def _regenerate_channel_name_from_grid(self):
        """Regenerate channel name using AI from Series Grid dialog."""
        list_control = self.getControl(2000)
        item = list_control.getSelectedItem()
        if not item:
            return
        
        channel_id = item.getProperty('channel_id')
        channel = self.channel_manager.get_channel_by_id(channel_id)
        if not channel or not channel.series_list:
            self._show_toast("Kanál nemá žádné seriály", "warning")
            return
        
        self._show_toast("Generuji název pomocí AI...", "info")
        
        from wcs.ai.ChannelNameGenerator import ChannelNameGenerator
        generator = ChannelNameGenerator(self.addon)
        current_name = item.getLabel() or None
        
        self._generate_ai_channel_name_async(
            channel_id, channel.series_list, generator,
            current_name=current_name, update_grid_title=True
        )
    

    def _is_series_grid_dialog_active(self):
        """Check if Series Grid dialog is currently active."""
        return self.getProperty('WCS.MyTV.SeriesGrid') == 'true'
    
    def _handle_series_detail_action(self, action):
        """Handle actions for Series Detail dialog. Returns True if handled."""
        if not self._is_series_detail_dialog_active():
            return False
        
        action_id = action.getId()
        
        # ESC / Back - check confirmation dialog first
        if action_id in [10, 92]:  # ACTION_PREVIOUS_MENU, ACTION_NAV_BACK
            # If confirmation dialog is active, just close it (don't close the whole dialog)
            if self.getProperty('WCS.MyTV.DeleteSeriesConfirm') == 'true':
                self._cancel_delete_series_confirmation()
                return True
            # Otherwise close the series detail dialog
            self._close_series_detail_dialog(removed=False)
            return True
        
        return False
    
    def _handle_series_detail_click(self, controlId):
        """Handle click events for Series Detail dialog. Returns True if handled."""
        if not self._is_series_detail_dialog_active():
            return False
        
        # Remove from channel button - show confirmation
        if controlId == 9300:
            self._show_delete_series_confirmation()
            return True
        
        # Confirm delete series button
        elif controlId == 9305:
            self._confirm_delete_series()
            return True
        
        # Cancel delete series button
        elif controlId == 9306:
            self._cancel_delete_series_confirmation()
            return True
        
        # Close button
        elif controlId == 9301:
            self._close_series_detail_dialog(removed=False)
            return True
        
        # Play game button
        elif controlId == 9302:
            series_id = self._series_detail_series_id
            series_name = self.getProperty('WCS.MyTV.SeriesDetail.Name') or ''
            poster = self.getProperty('WCS.MyTV.SeriesDetail.Poster') or ''
            fanart = self.getProperty('WCS.MyTV.SeriesDetail.Fanart') or ''
            logo = self.getProperty('WCS.MyTV.SeriesDetail.Logo') or ''
            plot = self.getProperty('WCS.MyTV.SeriesDetail.Plot') or ''
            
            # Open game dialog as sub-dialog (Series Detail stays open)
            self._show_game_dialog(series_id, series_name, plot, poster, fanart, logo)
            return True
        
        # Play series button (NEW - 9304)
        elif controlId == 9304:
            from urllib.parse import quote_plus
            from wcs.metadata import TMDbClient
            
            series_id = self._series_detail_series_id
            series_name = self.getProperty('WCS.MyTV.SeriesDetail.Name') or ''
            
            # Close dialog and main window
            self._close_series_detail_dialog(removed=False)
            self.close()
            
            # Get episode count from settings
            try:
                upcoming_count_val = self.addon.getSetting('mytv_upcoming_count')
                upcoming_count = int(upcoming_count_val) if upcoming_count_val else 12
            except:
                upcoming_count = 12
            
            # Get progress from channel history (NOT global recently_played)
            channel_id = getattr(self, '_series_detail_channel_id', '') or ''
            last_season, last_episode = ProgressManager.get_progress(channel_id, series_id)
            
            # Build playlist
            playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
            playlist.clear()
            
            addon_id = self.addon.getAddonInfo('id')
            curr_s, curr_e = last_season, last_episode
            episodes_added = 0
            
            while episodes_added < upcoming_count:
                next_meta = TMDbClient.get_next_episode_from_tmdb(series_id, curr_s, curr_e, self.addon)
                if not next_meta:
                    break
                
                item_data = PlaylistBuilder.extract_from_tmdb_meta(next_meta, series_name, series_id)
                url = PlaylistBuilder.build_playlist_url(addon_id, item_data, channel_id)
                li = PlaylistBuilder.build_playlist_listitem(item_data)
                
                playlist.add(url, li)
                episodes_added += 1
                curr_s, curr_e = next_meta['season'], next_meta['episode']
            
            if episodes_added > 0:
                xbmc.Player().play(playlist)
            else:
                # No episodes found - reopen and show toast
                # Can't easily reopen, just log
                xbmc.log(f"[MyTV] No episodes found for series {series_name}", xbmc.LOGWARNING)
            
            return True
        
        # Similar button
        elif controlId == 9303:
            import requests
            from wcs.metadata.TMDbClient import get_tmdb_api_key
            
            series_id = self._series_detail_series_id
            series_name = self.getProperty('WCS.MyTV.SeriesDetail.Name') or 'Seriál'
            channel_id = self._series_detail_channel_id
            
            # Close dialog first
            self._close_series_detail_dialog(removed=False)
            
            try:
                # Fetch recommendations from TMDb
                url = f'https://api.themoviedb.org/3/tv/{series_id}/recommendations?api_key={get_tmdb_api_key()}&language=cs-CZ'
                response = requests.get(url, timeout=10)
                results = response.json().get('results', [])
                
                if not results:
                    self._show_toast("Žádné podobné seriály", "info")
                    return True
                
                # Show Add Series dialog with similar series
                self._show_add_series_dialog()
                
                # Populate with similar series
                if self._add_series_controller:
                    # Filter out already in channel
                    channel = self.channel_manager.get_channel_by_id(channel_id)
                    current_ids = {str(s.get('id')) for s in channel.series_list} if channel else set()
                    filtered = [r for r in results if str(r.get('id')) not in current_ids][:20]
                    
                    if filtered:
                        self._add_series_controller._items = filtered
                        self._add_series_controller.parent.setProperty('WCS.MyTV.AddDialog.ListingMode', 'similar')
                        self._add_series_controller.parent.setProperty('WCS.MyTV.AddDialog.ListingTitle', f"Podobné: {series_name}")
                        self._add_series_controller._populate_list(filtered)
                        self._add_series_controller.parent.setFocusId(9211)
                    else:
                        self._show_toast("Všechny podobné už jsou v kanálu", "info")
                        
            except Exception as e:
                xbmc.log(f"[MyTV] Error finding similar: {e}", xbmc.LOGERROR)
                self._show_toast("Chyba při hledání", "error")
            return True
        
        return False

    # ==================================================================================
    # MILIONAR GAME DIALOG METHODS
    # ==================================================================================

    def _show_game_dialog(self, series_id, title, plot, poster, fanart, logo,
                         episode_title='', season_number=None, episode_number=None,
                         media_type='series'):
        """Open the Millionaire game (the modal card) over MyTV; MyTV stays open underneath."""
        from wcs.games import millionaire
        millionaire.open_game(millionaire.Subject(
            title=title, media_type=media_type, plot=plot, poster=poster or fanart, episode_title=episode_title,
            season_number=season_number, episode_number=episode_number, backdrop=fanart, tmdb_id=str(series_id or '')))

    # ==================================================================================
    # BACKGROUND GENERATION METHODS
    # ==================================================================================
    
    def _is_background_processing_needed(self):
        """Check if background needs PIL processing (effects or collages)."""
        # Collage modes always need processing
        if self._bg_mode in (self.BG_COLLAGE_POSTERS, self.BG_COLLAGE_STILLS, self.BG_COLLAGE_MIX):
            return True
        
        # Check if effects are enabled
        blur_enabled = self.addon.getSetting('mytv_background_blur') == 'true'
        glow_enabled = self.addon.getSetting('mytv_background_glow') == 'true'
        return blur_enabled or glow_enabled
    
    def _setup_background_properties(self):
        """Read background settings and set window properties for XML. Returns mode_idx."""
        # Background mode - Kodi returns text value, need to map to index
        mode_value = self.addon.getSetting('mytv_background_mode') or 'Fanart seriálu'
        
        # Map text values to indices
        mode_map = {
            'Statické': 0,
            'Fanart seriálu': 1,
            'Still epizody': 2,
            'Koláž posterů': 3,
            'Koláž stillů': 4,
            'Mix koláž': 5
        }
        mode_idx = mode_map.get(mode_value, 1)  # Default to fanart
        self._bg_mode = mode_idx  # Store for later use
        
        xbmc.log(f"[MyTV] Background mode: '{mode_value}' -> {mode_idx}", xbmc.LOGINFO)
        self.setProperty('WCS.MyTV.BackgroundMode', str(mode_idx))
        
        # Dimming
        dim_pct = self.addon.getSetting('mytv_background_dim') or '50'
        try:
            dim_val = int(dim_pct)
        except ValueError:
            dim_val = 50
        # Convert percentage to hex alpha (0-90% -> hex value)
        dim_alpha = int(255 * (dim_val / 100))
        dim_hex = f"{dim_alpha:02X}000000"
        self.setProperty('WCS.MyTV.BackgroundDim', dim_hex)
        
        # Animation - Ken Burns effect with selectable style
        anim_enabled = self.addon.getSetting('mytv_background_animation') == 'true'
        self.setProperty('WCS.MyTV.Animation', 'true' if anim_enabled else '')
        
        if anim_enabled:
            # Read animation style and set property for XML conditions
            style = self.addon.getSetting('mytv_anim_style') or 'Jemny dech'
            style_map = {
                'Jemny dech': '0',        # Subtle zoom + slight drift, 45s
                'Pulzujici srdce': '1',   # Only zoom, no slide, 30s
                'Plynouci tok': '2',      # Only horizontal slide, 50s
                'Dynamicka vlna': '3',    # Big zoom + diagonal, fast 25s
                'Zrcadlovy odraz': '4'    # Medium zoom, opposite diagonal, 40s
            }
            style_idx = style_map.get(style, '0')
            self.setProperty('WCS.MyTV.AnimStyle', style_idx)
            xbmc.log(f"[MyTV] Animation: style={style} (idx={style_idx})", xbmc.LOGINFO)
    
    def _update_background(self, channel_id=None):
        """Update background image based on current mode and selected channel."""
        mode_str = self.getProperty('WCS.MyTV.BackgroundMode') or '1'
        try:
            mode = int(mode_str)
        except ValueError:
            mode = self.BG_FANART
        
        # Check if effects are enabled
        blur_enabled = self.addon.getSetting('mytv_background_blur') == 'true'
        glow_enabled = self.addon.getSetting('mytv_background_glow') == 'true'
        effects_enabled = blur_enabled or glow_enabled
        
        # If no effects and not collage mode, XML handles backgrounds directly - no action needed
        # Don't clear property - keep any cached background visible
        if not effects_enabled and mode in (self.BG_STATIC, self.BG_FANART, self.BG_STILL):
            return
        
        # All other cases need PIL processing
        if not PIL_AVAILABLE:
            xbmc.log("[MyTV] PIL not available, effects disabled", xbmc.LOGWARNING)
            return
        
        # Static mode doesn't need channel info - uses fixed image
        # Other modes need channel data for fanart/stills/collages
        channel = None
        if mode != self.BG_STATIC:
            # Get channel info
            if not channel_id:
                try:
                    list_control = self.getControl(2000)
                    idx = list_control.getSelectedPosition()
                    if idx >= 0:
                        item = list_control.getListItem(idx)
                        channel_id = item.getProperty('channel_id')
                except:
                    return
            
            if not channel_id:
                return
            
            channel = self.channel_manager.get_channel_by_id(channel_id)
            if not channel:
                return
        
        # Generate background in background thread
        import threading
        threading.Thread(
            target=self._generate_background_async,
            args=(mode, channel_id, channel),
            daemon=True
        ).start()
    
    def _generate_background_async(self, mode, channel_id, channel):
        """Generate background with effects in background thread."""
        try:
            # Get effect settings for cache key
            blur_on = self.addon.getSetting('mytv_background_blur') == 'true'
            blur_r = self.addon.getSetting('mytv_background_blur_radius') or '5'
            glow_on = self.addon.getSetting('mytv_background_glow') == 'true'
            glow_i = self.addon.getSetting('mytv_background_glow_intensity') or '3'
            effects_key = f"b{blur_on}{blur_r}g{glow_on}{glow_i}"
            
            result_image = None
            source_url = None
            
            # Mode 0: Static background
            if mode == self.BG_STATIC:
                source_url = 'special://home/addons/plugin.video.milionar/resources/media/hub_background_v2.jpg'
                cache_key = f"bg_static_{effects_key}"
            
            # Mode 1: Fanart from first program item
            elif mode == self.BG_FANART:
                try:
                    program_list = self.getControl(9000)
                    if program_list.size() > 0:
                        source_url = program_list.getListItem(0).getArt('fanart')
                except:
                    pass
                if not source_url:
                    return
                cache_key = f"bg_fanart_{hash(source_url)}_{effects_key}"
            
            # Mode 2: Still from first program item
            elif mode == self.BG_STILL:
                try:
                    program_list = self.getControl(9000)
                    if program_list.size() > 0:
                        source_url = program_list.getListItem(0).getArt('thumb')
                except:
                    pass
                if not source_url:
                    return
                cache_key = f"bg_still_{hash(source_url)}_{effects_key}"
            
            # Mode 3-5: Collages
            else:
                images = []
                
                if mode == self.BG_COLLAGE_POSTERS:
                    if channel and channel.series_list:
                        for series in channel.series_list[:12]:
                            poster = series.get('poster_path') or series.get('poster')
                            if poster:
                                if not poster.startswith('http'):
                                    poster = f"https://image.tmdb.org/t/p/w342{poster}"
                                images.append(('poster', poster))
                
                elif mode == self.BG_COLLAGE_STILLS:
                    try:
                        program_list = self.getControl(9000)
                        for i in range(min(12, program_list.size())):
                            item = program_list.getListItem(i)
                            thumb = item.getArt('thumb')
                            if thumb:
                                images.append(('still', thumb))
                    except:
                        pass
                
                elif mode == self.BG_COLLAGE_MIX:
                    if channel and channel.series_list:
                        for series in channel.series_list[:6]:
                            poster = series.get('poster_path') or series.get('poster')
                            if poster:
                                if not poster.startswith('http'):
                                    poster = f"https://image.tmdb.org/t/p/w342{poster}"
                                images.append(('poster', poster))
                    try:
                        program_list = self.getControl(9000)
                        for i in range(min(6, program_list.size())):
                            item = program_list.getListItem(i)
                            thumb = item.getArt('thumb')
                            if thumb:
                                images.append(('still', thumb))
                    except:
                        pass
                
                if not images:
                    return
                
                cache_key = f"bg_collage_{mode}_{channel_id}_{len(images)}_{effects_key}"
            
            cache_path = self._cache.get_asset_path(cache_key)
            if cache_path:
                self.setProperty('WCS.MyTV.Background.Custom', cache_path)
                # Set Ready for ALL modes (background is ready from cache)
                self.setProperty('WCS.MyTV.Background.Ready', 'true')
                return
            
            # Generate/download base image
            if mode in (self.BG_STATIC, self.BG_FANART, self.BG_STILL):
                # Single image modes
                result_image = self._download_image(source_url)
                if result_image:
                    result_image = result_image.convert('RGB')
                    # Resize to 1920x1080
                    result_image = self._resize_to_cover(result_image, 1920, 1080)
            else:
                # Collage modes
                if mode == self.BG_COLLAGE_MIX:
                    result_image = self._generate_mix_collage(images)
                else:
                    result_image = self._generate_collage(images)
            
            if result_image:
                # Apply effects
                result_image = self._apply_effects(result_image)
                
                cache_path = self._cache.save_pil_image(cache_key, result_image)
                if cache_path:
                    self.setProperty('WCS.MyTV.Background.Custom', cache_path)
                    # Set Ready for ALL modes with effects (background just generated)
                    self.setProperty('WCS.MyTV.Background.Ready', 'true')
        
        except Exception as e:
            xbmc.log(f"[MyTV] Error generating background: {e}", xbmc.LOGERROR)

    def _resize_to_cover(self, image, target_w, target_h):
        """Resize image to cover target dimensions (crop excess)."""
        img_ratio = image.width / image.height
        target_ratio = target_w / target_h
        
        if img_ratio > target_ratio:
            new_h = target_h
            new_w = int(new_h * img_ratio)
        else:
            new_w = target_w
            new_h = int(new_w / img_ratio)
        
        image = image.resize((new_w, new_h), Image.LANCZOS)
        
        left = (new_w - target_w) // 2
        top = (new_h - target_h) // 2
        return image.crop((left, top, left + target_w, top + target_h))
    
    def _generate_collage(self, images):
        """Generate a grid collage from images."""
        import requests
        from io import BytesIO
        
        # Determine grid size
        count = len(images)
        if count <= 4:
            cols, rows = 2, 2
        elif count <= 6:
            cols, rows = 3, 2
        elif count <= 9:
            cols, rows = 3, 3
        else:
            cols, rows = 4, 3
        
        # Canvas size (1920x1080)
        canvas_w, canvas_h = 1920, 1080
        cell_w = canvas_w // cols
        cell_h = canvas_h // rows
        
        canvas = Image.new('RGB', (canvas_w, canvas_h), (20, 20, 20))
        
        for i, (img_type, url) in enumerate(images[:cols*rows]):
            if i >= cols * rows:
                break
            
            try:
                img = self._download_image(url)
                if img:
                    # Resize to fill cell
                    img = img.convert('RGB')
                    img_ratio = img.width / img.height
                    cell_ratio = cell_w / cell_h
                    
                    if img_ratio > cell_ratio:
                        new_h = cell_h
                        new_w = int(new_h * img_ratio)
                    else:
                        new_w = cell_w
                        new_h = int(new_w / img_ratio)
                    
                    img = img.resize((new_w, new_h), Image.LANCZOS)
                    
                    # Crop to center
                    left = (new_w - cell_w) // 2
                    top = (new_h - cell_h) // 2
                    img = img.crop((left, top, left + cell_w, top + cell_h))
                    
                    # Paste to canvas
                    col = i % cols
                    row = i // cols
                    canvas.paste(img, (col * cell_w, row * cell_h))
            except Exception as e:
                xbmc.log(f"[MyTV] Collage image error: {e}", xbmc.LOGWARNING)
        
        return canvas
    
    def _generate_mix_collage(self, images):
        """Generate creative mixed collage with varying sizes."""
        import random
        from io import BytesIO
        
        canvas_w, canvas_h = 1920, 1080
        canvas = Image.new('RGB', (canvas_w, canvas_h), (15, 15, 20))
        
        random.shuffle(images)
        
        # Define varying cell sizes for creative layout
        positions = [
            (0, 0, 640, 540),      # Large left top
            (0, 540, 640, 540),    # Large left bottom
            (640, 0, 480, 360),    # Medium center top
            (640, 360, 480, 360),  # Medium center middle
            (640, 720, 480, 360),  # Medium center bottom
            (1120, 0, 400, 270),   # Small right top 1
            (1520, 0, 400, 270),   # Small right top 2
            (1120, 270, 400, 270), # Small right mid 1
            (1520, 270, 400, 270), # Small right mid 2
            (1120, 540, 800, 540), # Large right bottom
        ]
        
        for i, (img_type, url) in enumerate(images[:len(positions)]):
            if i >= len(positions):
                break
            
            x, y, w, h = positions[i]
            
            try:
                img = self._download_image(url)
                if img:
                    img = img.convert('RGB')
                    
                    # Resize maintaining aspect ratio then crop
                    img_ratio = img.width / img.height
                    cell_ratio = w / h
                    
                    if img_ratio > cell_ratio:
                        new_h = h
                        new_w = int(new_h * img_ratio)
                    else:
                        new_w = w
                        new_h = int(new_w / img_ratio)
                    
                    img = img.resize((new_w, new_h), Image.LANCZOS)
                    
                    left = (new_w - w) // 2
                    top = (new_h - h) // 2
                    img = img.crop((left, top, left + w, top + h))
                    
                    canvas.paste(img, (x, y))
            except Exception as e:
                xbmc.log(f"[MyTV] Mix collage image error: {e}", xbmc.LOGWARNING)
        
        return canvas
    
    def _apply_effects(self, image):
        """Apply blur/glow effects based on settings."""
        # Blur
        if self.addon.getSetting('mytv_background_blur') == 'true':
            try:
                radius = int(self.addon.getSetting('mytv_background_blur_radius') or '5')
                image = image.filter(ImageFilter.GaussianBlur(radius=radius))
            except:
                pass
        
        # Glow (brightness + blur for glow effect)
        if self.addon.getSetting('mytv_background_glow') == 'true':
            try:
                from PIL import ImageEnhance
                intensity = int(self.addon.getSetting('mytv_background_glow_intensity') or '3')
                # Increase brightness
                enhancer = ImageEnhance.Brightness(image)
                image = enhancer.enhance(1 + (intensity * 0.05))
                # Subtle blur for glow
                image = image.filter(ImageFilter.GaussianBlur(radius=intensity))
            except:
                pass
        
        return image
    
    def _download_image(self, url):
        """Download image from URL and return PIL Image."""
        import requests
        from io import BytesIO
        
        try:
            # Handle special:// paths
            if url.startswith('special://'):
                local_path = xbmcvfs.translatePath(url)
                if xbmcvfs.exists(local_path):
                    return Image.open(local_path)
                return None
            
            # Download from URL
            response = requests.get(url, timeout=10)
            if response.status_code == 200:
                return Image.open(BytesIO(response.content))
        except Exception as e:
            xbmc.log(f"[MyTV] Download image error: {url} - {e}", xbmc.LOGWARNING)
        
        return None


def show_my_tv_dialog(addon, show_nav_sidebar=False, nav_position=0, channel_id=None, context_uid='', context_row='', edit=False):
    # Vybrat XML podle nastaveni designu (0=Klasicky, 1=Moderni, 2=Apple TV)
    design = addon.getSetting('mytv_design') or '1'
    design_map = {
        '0': 'ai/DialogMyTV_classic.xml',
        '1': 'ai/DialogMyTV.xml',
        '2': 'ai/DialogMyTV_appletv.xml',
        '3': 'ai/DialogMyTV_kodi.xml',
    }
    xml_file = design_map.get(design, 'ai/DialogMyTV.xml')
    
    dialog = MyTVDialog(
        xml_file,
        addon.getAddonInfo('path'),
        "Default",
        "1080i",
        show_nav_sidebar=show_nav_sidebar,
        nav_position=nav_position,
        initial_channel_id=channel_id,
        context_uid=context_uid,
        context_row=context_row,
        initial_edit=edit
    )
    dialog.doModal()
    del dialog
