import xbmc
import xbmcgui
import xbmcaddon
import xbmcvfs
import time
import os
import json
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Any
from tools import debug_log, get_custom_groups, get_epg_categories_enabled
from analytics import track, track_error, track_feature
from epggrid_context_menu import show_context_menu
from epggrid_filters import filter_channels_by_category
from epggrid_mpv import launch_mpv_for_current_channel
from epggrid_program_info import update_program_info_properties
from themes import apply_theme_to_window

# Category colors for EPG grid - maps program categories to display colors
# Format: 'category_name': 'color_name' or 'ARGB_HEX'
CATEGORY_COLORS = {
    # Sports - Blue
    'sports': 'blue',
    'sport': 'blue',
    'football': 'blue',
    'basketball': 'blue',
    'baseball': 'blue',
    'hockey': 'blue',
    'soccer': 'blue',
    'tennis': 'blue',
    'golf': 'blue',
    'racing': 'blue',
    'motorsport': 'blue',
    'ufc': 'blue',
    'boxing': 'blue',
    'mma': 'blue',
    'wwe': 'blue',
    'nfl': 'blue',
    'nba': 'blue',
    'mlb': 'blue',
    'nhl': 'blue',
    
    # Comedy - Green
    'comedy': 'green',
    'sitcom': 'green',
    'stand-up': 'green',
    'standup': 'green',
    
    # News - Pink/Magenta
    'news': 'pink',
    'breaking news': 'pink',
    'weather': 'pink',
    
    # Movies - Red
    'movie': 'red',
    'movies': 'red',
    'film': 'red',
    'cinema': 'red',
    
    # Entertainment - Orange
    'entertainment': 'orange',
    'reality': 'orange',
    'talk show': 'orange',
    'talk show': 'orange',
    'game show': 'orange',
    'variety': 'orange',
    
    # Documentary - Cyan
    'documentary': 'cyan',
    'docu': 'cyan',
    'nature': 'cyan',
    'science': 'cyan',
    
    # Kids - Yellow
    'kids': 'yellow',
    'children': 'yellow',
    'animation': 'yellow',
    'animated': 'yellow',
    'cartoon': 'yellow',
    
    # Music - Purple
    'music': 'purple',
    'concert': 'purple',
    'musical': 'purple',
    'concert': 'purple',
}

def get_category_color(category) -> str:
    """
    Get the color for a program category.
    
    Args:
        category: The category string from the program (can be string or list)
        
    Returns:
        Color string (Kodi color name or ARGB hex), or empty string for default
    """
    # Handle list categories (some programs have multiple categories like ['Series', 'News'])
    if isinstance(category, list):
        if not category:
            return ''
        # Check each category in the list for a match
        for cat in category:
            result = get_category_color(cat)
            if result:
                return result
        return ''
    
    if not category:
        return ''
    
    # Normalize category to lowercase for matching
    try:
        category_lower = str(category).lower().strip()
    except:
        return ''
    
    # Check for exact or partial match in our color mapping
    for key, color in CATEGORY_COLORS.items():
        try:
            if key in category_lower or category_lower in key:
                return color
        except:
            continue
    
    # If no match found, try splitting by comma or space and check each word
    # This handles cases like "Series, News" or "News Magazine"
    words = category_lower.replace(',', ' ').split()
    if len(words) > 1:
        # Check each word individually
        for word in words:
            word = word.strip()
            if not word:
                continue
            for key, color in CATEGORY_COLORS.items():
                if key == word or word == key:
                    return color
    
    # No match found, return empty string (default color)
    return ''

def format_program_label(title, category) -> str:
    """
    Format program title with color based on category.
    
    Args:
        title: The program title
        category: The program category (can be string or list)
        
    Returns:
        Formatted label with color tag if category matches
    """
    if not title:
        return 'No Program'
    
    try:
        color = get_category_color(category)
        if color:
            return f"[B][COLOR={color}]{title}[/COLOR][/B]"
    except:
        pass
    
    return title

addon = xbmcaddon.Addon()
ADDON = xbmcaddon.Addon()

class EPGGrid(xbmcgui.WindowXML):
    def __init__(self, xml_file: str, addon_path: str, channels: List[Dict[str, Any]] = None, programs: List[Dict[str, Any]] = None, is_favorites_view: bool = False, is_sports_view: bool = False):
        """Initialize EPG Grid window"""
        self.is_closing = False  # Flag to stop background processes
        try:
            super().__init__(xml_file, addon_path, "default")
        except Exception as e:
            xbmcgui.Dialog().ok("Error", f"Failed to initialize EPG: {str(e)}")
            raise
            
        self.is_favorites_view = is_favorites_view
        self.is_sports_view = is_sports_view
        self.all_channels = channels if channels else []
        self.channels = self.all_channels[:]
        self.current_category = ''
        self.last_category = ''
        self.programs = programs or []
        self.channel_panel = None
        self.program_cache = {}
        self.visible_rows = 5000
        # Remove any channel slicing or limits
        self.all_channels = channels if channels else []
        self.channels = self.all_channels[:]
        self.current_page = 0
        self.current_channel_idx = 0  # Track absolute channel index
        self.time_scroll_pos = 0
        self.last_play_time = 0  # Initialize for playback
        self.selected_program = None
        self.categories = []
        self.button_controls = {}
        self.local_tz = datetime.now().astimezone().tzinfo
        self.lazy_loading_enabled = False
        self.auto_refresh_enabled = True
        self.jump_to_now_enabled = True
        self.auto_refresh_idle_threshold_sec = 300
        self.last_interaction_time = time.time()
        self.last_time_sync = time.time()
        self.is_android = 'ANDROID_DATA' in os.environ or 'ANDROID_ROOT' in os.environ
        self.android_optimize_mode = False
        self.android_input_debounce_ms = 120
        self.program_index = {}
        self.loaded_channel_ids = set()
        self.set_time_slots()
        # Defensive: avoid NoneType in len()
        prog_len = len(programs) if programs is not None else 0
        chan_len = len(channels) if channels is not None else 0
        debug_log(f"DEBUG: EPGGrid init, programs_len={prog_len}, channels_len={chan_len}", xbmc.LOGINFO)

        try:
            self.lazy_loading_enabled = ADDON.getSettingBool('epg_lazy_loading')
        except Exception:
            self.lazy_loading_enabled = ADDON.getSetting('epg_lazy_loading') == 'true'

        try:
            self.auto_refresh_enabled = ADDON.getSettingBool('epg_auto_refresh')
        except Exception:
            self.auto_refresh_enabled = ADDON.getSetting('epg_auto_refresh') == 'true'

        try:
            self.jump_to_now_enabled = ADDON.getSettingBool('epg_jump_to_now')
        except Exception:
            self.jump_to_now_enabled = ADDON.getSetting('epg_jump_to_now') == 'true'

        if self.is_android:
            try:
                self.android_optimize_mode = ADDON.getSettingBool('android_optimize_mode')
            except Exception:
                self.android_optimize_mode = ADDON.getSetting('android_optimize_mode') == 'true'
            try:
                self.android_input_debounce_ms = max(50, int(ADDON.getSetting('android_input_debounce_ms') or '120'))
            except Exception:
                self.android_input_debounce_ms = 120
            try:
                if self.android_optimize_mode and ADDON.getSettingBool('android_epg_compact_mode'):
                    self.visible_rows = 80
            except Exception:
                pass

        if self.lazy_loading_enabled:
            self.build_program_index()
            self.preload_current_page_cache()
        else:
            self.build_program_cache()

    def onInit(self):
        """Initialize grid after window creation"""
        # Apply theme colors to window first
        try:
            apply_theme_to_window(self)
        except Exception as e:
            debug_log(f"EPGGrid: Error applying theme: {str(e)}", xbmc.LOGERROR)
        
        try:
            self.last_category = ''
            self.connect_category_filter()
            self.categories = self.get_unique_categories()
            self.channel_panel = self.getControl(110)  # Single panel for channels and programs
            if not self.channel_panel:
                raise Exception("Failed to get channel panel")
                
            self.create_category_buttons()
            self.create_grid()
            try:
                self.setFocusId(110)
            except Exception as e:
                debug_log(f"DEBUG: Could not focus control 110: {str(e)}", xbmc.LOGWARNING)
                # Try to focus any available control
                try:
                    self.setFocusId(200)
                except:
                    pass
            self.channel_panel.selectItem(0)
            self.update_selected_program_info()
            
        except Exception as e:
            xbmcgui.Dialog().ok("Error", f"Failed to initialize EPG: {str(e)}")
            self.close()

    def get_unique_categories(self) -> List[str]:
        """Extract unique categories from channels or programs."""
        if self.is_favorites_view:
            return []
            
        categories = set()
        for channel in self.all_channels:
            group = channel.get('group', '')
            if group:
                categories.add(group)
                
        unique_categories = sorted(list(categories))
        return unique_categories

    def _parse_epg_datetime(self, value):
        """Parse XMLTV datetime values to local timezone-aware datetime."""
        local_tz = self.local_tz
        if isinstance(value, str):
            try:
                if ' -0' in value or ' +0' in value:
                    value = value.replace(' ', 'T')
                    dt = datetime.strptime(value, '%Y%m%d%H%M%ST%z')
                    return dt.astimezone(local_tz)
                dt = datetime.strptime(value[:14], '%Y%m%d%H%M%S')
                utc_tz = timezone(timedelta(hours=0))
                dt = dt.replace(tzinfo=utc_tz)
                return dt.astimezone(local_tz)
            except ValueError:
                return None
        if isinstance(value, datetime):
            if value.tzinfo is None:
                return value.replace(tzinfo=local_tz)
            return value.astimezone(local_tz)
        return None

    def build_program_index(self):
        """Index raw programs by channel for faster lazy parsing."""
        self.program_index = {}
        if not self.programs:
            return

        channel_ids = {channel.get('tvg_id') for channel in self.all_channels if channel.get('tvg_id')}
        for program in self.programs:
            channel = program.get('channel')
            if not channel or channel not in channel_ids:
                continue
            self.program_index.setdefault(channel, []).append(program)

    def build_program_cache_for_channels(self, channel_ids):
        """Build parsed cache entries for only the given channel IDs."""
        if self.is_closing:
            return

        if not channel_ids:
            return

        now = datetime.now().astimezone(self.local_tz)
        start_window = now - timedelta(hours=7)
        end_time = now + timedelta(hours=24)

        for channel_id in channel_ids:
            if self.is_closing or channel_id in self.loaded_channel_ids:
                continue

            channel_programs = self.program_index.get(channel_id, [])
            parsed_list = []
            for program in channel_programs:
                try:
                    start_dt = self._parse_epg_datetime(program.get('start'))
                    stop_dt = self._parse_epg_datetime(program.get('stop'))
                    if not start_dt or not stop_dt:
                        continue
                    if stop_dt < start_window or start_dt > end_time:
                        continue

                    parsed_list.append({
                        'start': start_dt,
                        'stop': stop_dt,
                        'title': program.get('title', 'No Title'),
                        'channel': channel_id,
                        'desc': program.get('desc', 'No description available'),
                        'category': program.get('category', ''),
                        'episode': program.get('episode-num', ''),
                        'rating': program.get('rating', ''),
                        'sub_title': program.get('sub-title', ''),
                        'actors': program.get('actors', []),
                        'icon': program.get('icon', None)
                    })
                except Exception:
                    continue

            parsed_list.sort(key=lambda x: x['start'])
            self.program_cache[channel_id] = parsed_list
            self.loaded_channel_ids.add(channel_id)

    def preload_current_page_cache(self):
        """Preload parsed EPG cache for currently visible page channels."""
        if not self.channels:
            return
        start_idx = self.current_page * self.visible_rows
        end_idx = min(start_idx + self.visible_rows, len(self.channels))
        channel_ids = [
            channel.get('tvg_id')
            for channel in self.channels[start_idx:end_idx]
            if channel.get('tvg_id')
        ]
        self.build_program_cache_for_channels(channel_ids)

    def build_program_cache(self):
        """Build cache of programs for all currently visible channels (legacy full mode)."""
        if self.is_closing:
            return
        self.program_cache = {}
        self.loaded_channel_ids = set()

        if not self.programs:
            debug_log(f"DEBUG: build_program_cache: no programs provided", xbmc.LOGERROR)
            return

        if not self.program_index:
            self.build_program_index()

        all_channel_ids = [channel.get('tvg_id') for channel in self.channels if channel.get('tvg_id')]
        self.build_program_cache_for_channels(all_channel_ids)

    def format_time(self, dt: datetime) -> str:
        """Format time according to user preference."""
        time_format = ADDON.getSetting('time_format')
        if time_format == '12h':
            return dt.strftime('%I:%M %p').lstrip('0')
        return dt.strftime('%H:%M')

    def set_time_slots(self):
        """Set up time slots for the grid."""
        now = datetime.now().astimezone(self.local_tz)
        current_minute = now.minute
        start_time = now.replace(minute=0, second=0, microsecond=0)
        if current_minute >= 30:
            start_time += timedelta(minutes=30)
        self.time_slots = []
        start_time = start_time - timedelta(hours=0)
        for i in range(96):
            slot_start = start_time + timedelta(minutes=30 * i)
            slot_end = slot_start + timedelta(minutes=30)
            self.time_slots.append((slot_start, slot_end))

    def update_time_labels(self):
        """Update time slot labels based on current scroll position."""
        for display_idx in range(4):
            slot_idx = self.time_scroll_pos + display_idx
            if slot_idx < len(self.time_slots):
                slot_start = self.time_slots[slot_idx][0]
                self.setProperty(f'time{display_idx}', self.format_time(slot_start))
            else:
                self.setProperty(f'time{display_idx}', '')

    def maybe_refresh_time_slots(self, force=False):
        """Refresh time slots after inactivity so EPG returns to current time without reopening."""
        if self.is_closing:
            return False

        if not force and not self.auto_refresh_enabled:
            return False

        now_ts = time.time()
        if not force:
            idle_time = now_ts - self.last_interaction_time
            if idle_time < self.auto_refresh_idle_threshold_sec:
                return False

        self.set_time_slots()

        if self.jump_to_now_enabled:
            self.time_scroll_pos = 0

        max_scroll = max(0, len(self.time_slots) - 4)
        if self.time_scroll_pos > max_scroll:
            self.time_scroll_pos = max_scroll
        if self.time_scroll_pos < 0:
            self.time_scroll_pos = 0

        self.last_time_sync = now_ts
        debug_log(f"DEBUG: Refreshed EPG time slots (force={force}, jump_to_now={self.jump_to_now_enabled})", xbmc.LOGINFO)
        return True

    def get_program_for_channel(self, channel_id: str, slot_start: datetime, slot_end: datetime) -> Dict[str, Any]:
        """Get program for a channel that overlaps with the given time slot."""
        if channel_id not in self.program_cache:
            return None
        for program in self.program_cache[channel_id]:
            prog_start = program['start']
            prog_stop = program['stop']
            if prog_start < slot_end and prog_stop > slot_start:
                return program
        return None

    def connect_category_filter(self):
        """Initialize category filter."""
        try:
            self.setProperty('CategoryFilter', '')
        except Exception:
            pass

    def create_category_buttons(self):
        """Dynamically populate category list control."""
        try:
            category_list = self.getControl(200)
            category_list.reset()
            item = xbmcgui.ListItem("All")
            item.setProperty("Category", "")
            category_list.addItem(item)
            
            # Get enabled EPG categories
            enabled_categories = get_epg_categories_enabled()
            
            # Add categories that are enabled
            for category in self.categories:
                # Check if category is explicitly disabled
                if category in enabled_categories and not enabled_categories[category]:
                    continue  # Skip disabled categories
                item = xbmcgui.ListItem(category)
                item.setProperty("Category", category)
                category_list.addItem(item)
            
            # Only add custom groups for the main EPG guide (not sports or favorites view)
            # Custom groups are created in the main guide and won't have matching channels in other views
            if not self.is_sports_view and not self.is_favorites_view:
                custom_groups = get_custom_groups()
                if custom_groups:
                    # Add separator item
                    item = xbmcgui.ListItem("--- Custom Groups ---")
                    item.setProperty("Category", "__SEPARATOR__")
                    category_list.addItem(item)
                    for group in custom_groups:
                        group_name = group.get('name', 'Unnamed')
                        item = xbmcgui.ListItem(f"[C] {group_name}")
                        item.setProperty("Category", "CUSTOM:" + group_name)
                        category_list.addItem(item)
        except Exception as e:
            debug_log(f"DEBUG: create_category_buttons error: {str(e)}", xbmc.LOGERROR)

    def check_category_filter(self):
        """Check and apply category filter if changed."""
        try:
            if not self.channel_panel:
                return
            category_list = self.getControl(200)
            selected_pos = category_list.getSelectedPosition()
            selected_item = category_list.getListItem(selected_pos)
            category = selected_item.getProperty("Category")
            if category == self.last_category:
                return
            self.last_category = category
            if category:
                self.filter_channels(category)
            else:
                self.channels = self.all_channels[:]
            self.current_page = 0
            self.current_channel_idx = -1
            if self.lazy_loading_enabled:
                self.program_cache = {}
                self.loaded_channel_ids = set()
                self.preload_current_page_cache()
            self.create_grid()
            self.setFocusId(200)
            # self.channel_panel.selectItem(0)
        except Exception:
            pass

    def create_custom_group_from_epg(self):
        """Create a custom group from currently loaded EPG channels"""
        dialog = xbmcgui.Dialog()
        
        if not self.all_channels:
            dialog.ok("No Channels", "No channels loaded in EPG.")
            return
        
        # Ask for group name
        group_name = dialog.input("Enter group name for this selection", defaultt="My Custom Group")
        if not group_name:
            return
        
        # Show channel selection from current EPG channels
        channel_names = [c.get('name', 'Unknown') for c in self.all_channels]
        selected = dialog.multiselect(f"Select channels for '{group_name}'", channel_names)
        
        if selected is None or len(selected) == 0:
            return
        
        # Build selected channel list with tvg_id (already matched with M3U8/EPG)
        selected_channels = []
        for i in selected:
            channel = self.all_channels[i]
            selected_channels.append({
                'title': channel.get('name', 'Unknown'),
                'link': channel.get('url', '') or channel.get('stream_url', ''),
                'tvg_id': channel.get('tvg_id', '')
            })
        
        # Get existing groups
        from tools import get_custom_groups, save_custom_groups
        groups = get_custom_groups()
        
        # Check for duplicate name
        if any(g.get('name') == group_name for g in groups):
            dialog.ok("Error", "A group with this name already exists.")
            return
        
        # Add new group
        new_group = {
            "name": group_name,
            "channels": selected_channels
        }
        groups.append(new_group)
        
        if save_custom_groups(groups):
            dialog.notification("Success", f"Group '{group_name}' created with {len(selected_channels)} channels", xbmcgui.NOTIFICATION_INFO, 3000)
            # Refresh the category buttons to show the new group
            self.create_category_buttons()
        else:
            dialog.ok("Error", "Failed to save group.")

    def filter_channels(self, category: str):
        """Filter channels by category."""
        try:
            self.current_category = category
            self.channels = filter_channels_by_category(self.all_channels, category, get_custom_groups, debug_log)
            self.current_page = 0
            self.current_channel_idx = -1
            if self.lazy_loading_enabled:
                self.program_cache = {}
                self.loaded_channel_ids = set()
                self.preload_current_page_cache()
            self.create_grid()
            self.setFocusId(200)
            # self.channel_panel.selectItem(0)
        except Exception as e:
            debug_log(f"DEBUG: filter_channels error: {str(e)}", xbmc.LOGERROR)
            self.channels = self.all_channels[:]
            self.create_grid()

    def create_grid(self):
        """Create the EPG grid display with show spanning."""
        try:
            if not self.channel_panel:
                return
            start_time = time.time()
            # Ensure current_channel_idx is within bounds
            if self.current_channel_idx >= len(self.channels):
                self.current_channel_idx = max(0, len(self.channels) - 1)
            if self.current_channel_idx < 0:
                self.current_page = 0
                start_idx = 0
            else:
                self.current_page = self.current_channel_idx // self.visible_rows
                start_idx = self.current_page * self.visible_rows
                # Restore original bounds logic for icons and navigation
                if self.current_channel_idx < start_idx:
                    self.current_channel_idx = start_idx
                elif self.current_channel_idx >= start_idx + self.visible_rows:
                    self.current_channel_idx = min(start_idx + self.visible_rows - 1, len(self.channels) - 1)
            # Fix: On last page, show remaining channels
            if self.current_page == ((len(self.channels) + self.visible_rows - 1) // self.visible_rows) - 1:
                end_idx = len(self.channels)
            else:
                end_idx = min(start_idx + self.visible_rows, len(self.channels))
                # Remove last_page override, revert to original paging logic
            self.channel_panel.reset()
            self.update_time_labels()
            
            # Ensure lazy cache exists for channels in the current page before rendering
            if self.lazy_loading_enabled:
                visible_channel_ids = [
                    channel.get('tvg_id')
                    for channel in self.channels[start_idx:end_idx]
                    if channel.get('tvg_id')
                ]
                self.build_program_cache_for_channels(visible_channel_ids)

            # Get the start time of the first visible slot
            first_slot_start = self.time_slots[self.time_scroll_pos][0] if self.time_scroll_pos < len(self.time_slots) else None
            
            for idx in range(start_idx, end_idx):
                try:
                    channel = self.channels[idx]
                    item = xbmcgui.ListItem(str(idx + 1))
                    item.setLabel2(channel['name'])
                    # Robust icon assignment: use 'logo' field directly (confirmed structure)
                    # Robust icon assignment: try logo, icon, logo_url, icon_url, fallback
                    icon_url = channel.get('logo') or channel.get('icon') or channel.get('logo_url') or channel.get('icon_url') or 'special://home/addons/plugin.video.jet_guide/resources/skins/default/720p/media/HDHR.png'
                    item.setArt({'icon': icon_url})
                    
                    # Track programs that have been shown for this channel to implement spanning
                    shown_programs = set()
                    
                    for display_idx in range(4):
                        slot_idx = self.time_scroll_pos + display_idx
                        if slot_idx < len(self.time_slots):
                            slot_start, slot_end = self.time_slots[slot_idx]
                            program = self.get_program_for_channel(channel['tvg_id'], slot_start, slot_end)
                            
                            if program:
                                # Use unique program identifier to track spanning
                                prog_key = (program.get('title', ''), program.get('start'))
                                prog_start = program.get('start')
                                
                                # Check if this is a continuation from a previous time slot
                                is_continuation = False
                                if prog_key in shown_programs:
                                    # Already shown in a previous display slot
                                    is_continuation = True
                                elif display_idx == 0 and first_slot_start and prog_start:
                                    # For first visible slot, check if show started before visible range
                                    if prog_start < first_slot_start:
                                        # Show started before visible range - it's a continuation
                                        is_continuation = True
                                
                                if is_continuation:
                                    # This program spans from a previous slot, mark as continuation
                                    item.setProperty(f'label{display_idx}', '')
                                    item.setProperty(f'continuation{display_idx}', 'true')
                                    # No grid line after a continuation
                                    item.setProperty(f'showGrid{display_idx}', 'false')
                                    continue
                                
                                # First time seeing this program - it's a new show starting here
                                shown_programs.add(prog_key)
                                
                                # Show grid line after this slot (for new shows)
                                item.setProperty(f'showGrid{display_idx}', 'true')
                                
                                program_title = program.get('title', 'No Program')
                                if program_title:
                                    # Calculate program duration and spanning
                                    prog_stop = program.get('stop')
                                    
                                    if prog_start and prog_stop:
                                        # Calculate how many 30-minute slots this program spans
                                        duration = prog_stop - prog_start
                                        # Each slot is 30 minutes
                                        span_slots = max(1, int(duration.total_seconds() / 1800) + 1)
                                        
                                        # Only span within visible range
                                        span_slots = min(span_slots, 4 - display_idx)
                                        
                                        # Set the span property for the first slot
                                        if span_slots > 1:
                                            item.setProperty(f'span{display_idx}', str(span_slots))
                                    
                                    # Try to apply category color - safe fallback
                                    try:
                                        program_category = program.get('category', '')
                                        color = get_category_color(program_category)
                                        if color:
                                            program_title = f"[B][COLOR={color}]{program_title}[/COLOR][/B]"
                                    except:
                                        pass  # Keep original title on any error
                                
                                item.setProperty(f'label{display_idx}', program_title)
                            else:
                                # No program - show grid line here
                                if self.lazy_loading_enabled and channel.get('tvg_id') not in self.loaded_channel_ids:
                                    item.setProperty(f'label{display_idx}', 'Loading...')
                                else:
                                    item.setProperty(f'label{display_idx}', 'No Program')
                                item.setProperty(f'continuation{display_idx}', 'false')
                                item.setProperty(f'showGrid{display_idx}', 'true')
                        else:
                            item.setProperty(f'label{display_idx}', 'No Program')
                            item.setProperty(f'continuation{display_idx}', 'false')
                            item.setProperty(f'showGrid{display_idx}', 'false')
                    if idx == self.current_channel_idx:
                        item.select(True)
                    self.channel_panel.addItem(item)
                except Exception:
                    pass
            row = self.channel_panel.getSelectedPosition()
            # debug_log(f"DEBUG: create_grid completed in {time.time() - start_time:.2f} seconds, current_page={self.current_page}, current_channel_idx={self.current_channel_idx}, row={row}", xbmc.LOGINFO)
        except Exception as e:
            debug_log(f"DEBUG: create_grid error: {str(e)}", xbmc.LOGERROR)

    def update_selected_program_info(self):
        """Update the program info based on current selection."""
        try:
            channel_idx = self.current_channel_idx
            row = self.channel_panel.getSelectedPosition()
            expected_row = channel_idx % self.visible_rows
            # debug_log(f"DEBUG: update_selected_program_info: channel_idx={channel_idx}, row={row}, expected_row={expected_row}, channels_len={len(self.channels)}", xbmc.LOGINFO)
            if channel_idx < len(self.channels):
                channel = self.channels[channel_idx]
                # debug_log(f"DEBUG: Selected channel: {channel.get('name', 'Unknown')} (tvg_id={channel.get('tvg_id', '')}), logo={channel.get('logo', 'None')}", xbmc.LOGINFO)
                slot_idx = self.time_scroll_pos
                # debug_log(f"DEBUG: slot_idx={slot_idx}, time_slots_len={len(self.time_slots)}", xbmc.LOGINFO)
                if slot_idx < len(self.time_slots):
                    slot_start, slot_end = self.time_slots[slot_idx]
                    # debug_log(f"DEBUG: Checking programs for slot: {slot_start.isoformat()} to {slot_end.isoformat()}", xbmc.LOGINFO)
                    program = self.get_program_for_channel(channel['tvg_id'], slot_start, slot_end)
                    # if program:
                    #     debug_log(f"DEBUG: Found program: title={program.get('title', 'Unknown')}, start={program.get('start').isoformat()}, stop={program.get('stop').isoformat()}, icon={program.get('icon', 'None')}", xbmc.LOGINFO)
                    # else:
                    #     debug_log(f"DEBUG: No program found for channel {channel.get('tvg_id')} in slot {slot_start.isoformat()} to {slot_end.isoformat()}", xbmc.LOGWARNING)
                    self.update_program_info(program, channel)
                else:
                    debug_log(f"DEBUG: Invalid slot_idx={slot_idx}, setting default info", xbmc.LOGWARNING)
                    self.update_program_info(None, channel)
            else:
                debug_log(f"DEBUG: Invalid channel_idx={channel_idx}, no channel selected", xbmc.LOGWARNING)
                self.update_program_info(None, None)
        except Exception as e:
            debug_log(f"DEBUG: update_selected_program_info error: {str(e)}", xbmc.LOGERROR)

    def update_program_info(self, program: Dict[str, Any], channel: Dict[str, Any] = None):
        """Update the program info window with given program details."""
        update_program_info_properties(self, addon, program, channel)

    def onAction(self, action):
        """Handle user actions."""
        try:
            action_id = action.getId()
            focus_id = self.getFocusId()
            debug_log(f"DEBUG: onAction received: action_id={action_id}, focus_id={focus_id}", xbmc.LOGINFO)

            if self.is_android and self.android_optimize_mode:
                now_ts = int(time.time() * 1000)
                last_ms = int(self.last_interaction_time * 1000)
                if now_ts - last_ms < self.android_input_debounce_ms:
                    return

            if self.maybe_refresh_time_slots():
                self.create_grid()
                if self.channel_panel and len(self.channels) > 0:
                    try:
                        self.channel_panel.selectItem(self.current_channel_idx % self.visible_rows)
                    except Exception:
                        pass
                self.update_selected_program_info()

            self.last_interaction_time = time.time()
            if action_id in [xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK]:
                debug_log(f"DEBUG: Back action detected, closing EPGGrid", xbmc.LOGINFO)
                self.close()
                return
            if action_id == xbmcgui.ACTION_SELECT_ITEM and focus_id == 200:
                debug_log(f"DEBUG: Category selection triggered, focus_id=200", xbmc.LOGINFO)
                self.check_category_filter()
                return
            if action_id in [xbmcgui.ACTION_MOVE_UP, xbmcgui.ACTION_MOVE_DOWN]:
                if focus_id == 200 and action_id == xbmcgui.ACTION_MOVE_DOWN and self.channel_panel:
                    try:
                        self.setFocusId(110)
                    except Exception as e:
                        debug_log(f"DEBUG: Could not focus control 110: {str(e)}", xbmc.LOGWARNING)
                    self.current_channel_idx = 0
                    self.channel_panel.selectItem(0)
                    self.update_selected_program_info()
                    return
                if focus_id == 110 and self.channel_panel:
                    pos = self.channel_panel.getSelectedPosition()
                    max_pages = (len(self.channels) + self.visible_rows - 1) // self.visible_rows
                    last_page = max_pages - 1
                    if action_id == xbmcgui.ACTION_MOVE_DOWN and self.current_channel_idx < len(self.channels) - 1:
                        self.current_channel_idx += 1
                        new_page = self.current_channel_idx // self.visible_rows
                        new_pos = self.current_channel_idx % self.visible_rows
                        if new_page != self.current_page:
                            self.current_page = new_page
                            self.create_grid()
                        self.channel_panel.selectItem(new_pos)
                        self.update_selected_program_info()
                        return
                    elif action_id == xbmcgui.ACTION_MOVE_UP and self.current_channel_idx > 0:
                        self.current_channel_idx -= 1
                        new_page = self.current_channel_idx // self.visible_rows
                        new_pos = self.current_channel_idx % self.visible_rows
                        if new_page != self.current_page:
                            self.current_page = new_page
                            self.create_grid()
                        self.channel_panel.selectItem(new_pos)
                        self.update_selected_program_info()
                        return
                    else:
                        self.update_selected_program_info()
                        return
            if action_id in [xbmcgui.ACTION_MOUSE_WHEEL_UP, xbmcgui.ACTION_MOUSE_WHEEL_DOWN,
                            xbmcgui.ACTION_GESTURE_SWIPE_UP, xbmcgui.ACTION_GESTURE_SWIPE_DOWN]:
                if focus_id == 110 and self.channel_panel:
                    new_channel_idx = self.current_channel_idx
                    if action_id in [xbmcgui.ACTION_MOUSE_WHEEL_UP, xbmcgui.ACTION_GESTURE_SWIPE_DOWN]:
                        if new_channel_idx > 0:
                            new_channel_idx -= 1
                            new_page = new_channel_idx // self.visible_rows
                            if new_page != self.current_page:
                                self.current_page = new_page
                                self.create_grid()
                            self.channel_panel.selectItem(new_channel_idx % self.visible_rows)
                            # Update current_channel_idx to match highlighted channel
                            row = self.channel_panel.getSelectedPosition()
                            self.current_channel_idx = self.current_page * self.visible_rows + row
                            debug_log(f"DEBUG: Wheel up/swipe down, channel_idx={self.current_channel_idx}, current_page={self.current_page}", xbmc.LOGINFO)
                            self.update_selected_program_info()
                    elif action_id in [xbmcgui.ACTION_MOUSE_WHEEL_DOWN, xbmcgui.ACTION_GESTURE_SWIPE_UP]:
                        if new_channel_idx < len(self.channels) - 1:
                            new_channel_idx += 1
                            new_page = new_channel_idx // self.visible_rows
                            if new_page != self.current_page:
                                self.current_page = new_page
                                self.create_grid()
                            self.channel_panel.selectItem(new_channel_idx % self.visible_rows)
                            # Update current_channel_idx to match highlighted channel
                            row = self.channel_panel.getSelectedPosition()
                            self.current_channel_idx = self.current_page * self.visible_rows + row
                            debug_log(f"DEBUG: Wheel down/swipe up, channel_idx={self.current_channel_idx}, current_page={self.current_page}", xbmc.LOGINFO)
                            self.update_selected_program_info()
                    return
            if action_id in [xbmcgui.ACTION_MOVE_LEFT, xbmcgui.ACTION_MOVE_RIGHT]:
                if focus_id == 110:
                    if action_id == xbmcgui.ACTION_MOVE_LEFT and self.time_scroll_pos > 0:
                        self.time_scroll_pos -= 1
                        debug_log(f"DEBUG: Scrolled left, time_scroll_pos={self.time_scroll_pos}", xbmc.LOGINFO)
                        self.create_grid()
                        self.channel_panel.selectItem(self.current_channel_idx % self.visible_rows)
                        self.update_selected_program_info()
                    elif action_id == xbmcgui.ACTION_MOVE_RIGHT and self.time_scroll_pos < len(self.time_slots) - 4:
                        self.time_scroll_pos += 1
                        debug_log(f"DEBUG: Scrolled right, time_scroll_pos={self.time_scroll_pos}", xbmc.LOGINFO)
                        self.create_grid()
                        self.channel_panel.selectItem(self.current_channel_idx % self.visible_rows)
                        self.update_selected_program_info()
                    return
            if action_id == xbmcgui.ACTION_SELECT_ITEM and focus_id == 110:
                debug_log(f"DEBUG: ACTION_SELECT_ITEM triggered for channel_idx={self.current_channel_idx}, focus_id=110", xbmc.LOGINFO)
                self.play_selected_channel()
                return
            if action_id == 117:  # Context menu
                debug_log(f"DEBUG: Context menu triggered for channel_idx={self.current_channel_idx}", xbmc.LOGINFO)
                row = self.channel_panel.getSelectedPosition()
                channel_idx = self.current_page * self.visible_rows + row
                if channel_idx < len(self.channels):
                    show_context_menu(self, self.channels[channel_idx])
        except Exception as e:
            debug_log(f"DEBUG: onAction error: {str(e)}", xbmc.LOGERROR)

    def return_to_video(self):
        """Closes the guide and returns to video playback."""
        self.close()
        xbmc.executebuiltin('Action(FullScreen)')

    def onClick(self, control_id: int):
        """Handle click events."""
        try:
            debug_log(f"DEBUG: onClick received: control_id={control_id}", xbmc.LOGINFO)

            if self.maybe_refresh_time_slots():
                self.create_grid()
                if self.channel_panel and len(self.channels) > 0:
                    try:
                        self.channel_panel.selectItem(self.current_channel_idx % self.visible_rows)
                    except Exception:
                        pass
                self.update_selected_program_info()

            self.last_interaction_time = time.time()
            if control_id == 200:  # Category selection
                try:
                    category_list = self.getControl(200)
                    if not category_list:
                        return
                    selected_pos = category_list.getSelectedPosition()
                    selected_item = category_list.getListItem(selected_pos)
                    if not selected_item:
                        return
                    category = selected_item.getProperty("Category")
                    display_name = selected_item.getLabel()
                    debug_log(f"DEBUG: Category filter clicked, category={category}, display_name={display_name}", xbmc.LOGINFO)
                    xbmcgui.Dialog().notification("Category Filter", f"Showing {display_name}", xbmcgui.NOTIFICATION_INFO, 2000)
                    self.setProperty('CategoryFilter', category)
                    self.check_category_filter()
                except Exception as e:
                    debug_log(f"DEBUG: Category filter click error: {str(e)}", xbmc.LOGERROR)
            elif control_id == 110 and self.channel_panel:
                # Sync current_channel_idx with highlighted channel on click/touch
                row = self.channel_panel.getSelectedPosition()
                self.current_channel_idx = self.current_page * self.visible_rows + row
                debug_log(f"DEBUG: Channel panel clicked, updated current_channel_idx={self.current_channel_idx}", xbmc.LOGINFO)
                self.update_selected_program_info()
                self.play_selected_channel()
            elif control_id == 110:  # Grid click
                debug_log(f"DEBUG: Grid clicked, control_id=110, channel_idx={self.current_channel_idx}", xbmc.LOGINFO)
                self.play_selected_channel()
            elif control_id == 400:  # Left scroll button
                if self.time_scroll_pos > 0:
                    self.time_scroll_pos -= 1
                    debug_log(f"DEBUG: Left button clicked, time_scroll_pos={self.time_scroll_pos}", xbmc.LOGINFO)
                    self.create_grid()
                    self.channel_panel.selectItem(self.current_channel_idx % self.visible_rows)
                    self.update_selected_program_info()
                try:
                    self.setFocusId(110)
                except Exception as e:
                    debug_log(f"DEBUG: Could not focus control 110: {str(e)}", xbmc.LOGWARNING)
            elif control_id == 401:  # Right scroll button
                if self.time_scroll_pos < len(self.time_slots) - 4:
                    self.time_scroll_pos += 1
                    debug_log(f"DEBUG: Right button clicked, time_scroll_pos={self.time_scroll_pos}", xbmc.LOGINFO)
                    self.create_grid()
                    self.channel_panel.selectItem(self.current_channel_idx % self.visible_rows)
                    self.update_selected_program_info()
                try:
                    self.setFocusId(110)
                except Exception as e:
                    debug_log(f"DEBUG: Could not focus control 110: {str(e)}", xbmc.LOGWARNING)
        except Exception as e:
            debug_log(f"DEBUG: onClick error: {str(e)}", xbmc.LOGERROR)
        if control_id == 199:
            launch_mpv_for_current_channel(self)
            return

    def play_selected_channel(self):
        """Play the selected channel via addon.py's play_channel, only if a valid channel is explicitly selected."""
        try:
            current_time = time.time()
            if current_time - self.last_play_time < 1.0:  # Debounce: ignore calls within 1 second
                debug_log(f"DEBUG: play_selected_channel ignored due to debounce, last_play_time={self.last_play_time}, current_time={current_time}", xbmc.LOGINFO)
                return
            # Only allow playback if a channel is explicitly selected (not -1 or None)
            if self.current_channel_idx is None or self.current_channel_idx < 0 or self.current_channel_idx >= len(self.channels):
                debug_log(f"DEBUG: play_selected_channel aborted: invalid current_channel_idx={self.current_channel_idx}", xbmc.LOGINFO)
                return
            self.last_play_time = current_time
            debug_log(f"DEBUG: play_selected_channel started, channel_idx={self.current_channel_idx}", xbmc.LOGINFO)
            
            channel_idx = self.current_channel_idx
            if channel_idx >= len(self.channels):
                debug_log(f"DEBUG: Invalid channel_idx={channel_idx}, channels_len={len(self.channels)}", xbmc.LOGERROR)
                xbmcgui.Dialog().notification('Error', 'Invalid channel selected', xbmcgui.NOTIFICATION_ERROR, 3000)
                return
            channel = self.channels[channel_idx]
            channel_id = channel.get('tvg_id', '')
            name = channel.get('name', 'Unknown')
            debug_log(f"DEBUG: Attempting to play channel: {name} (tvg_id={channel_id})", xbmc.LOGINFO)
            
            # Close all dialogs to prevent concurrent dialog crash
            debug_log(f"DEBUG: Closing all open dialogs before playback", xbmc.LOGINFO)
            xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
            xbmc.executebuiltin('Dialog.Close(busydialog)')
            xbmc.executebuiltin('Dialog.Close(all,true)')  # Close all dialog types
            xbmc.sleep(250)  # Increased delay to ensure dialogs are closed
            
            if not channel_id:
                debug_log(f"DEBUG: Channel ID missing for {name}", xbmc.LOGERROR)
                xbmcgui.Dialog().notification('Error', 'Channel ID missing', xbmcgui.NOTIFICATION_ERROR, 3000)
                return
            
            if self.is_sports_view and 'all_links' in channel and channel['all_links']:
                debug_log(f"DEBUG: Sports view active, showing stream selection for {name}", xbmc.LOGINFO)
                stream_names = channel.get('stream_names', [])
                dialog = xbmcgui.Dialog()
                choice = dialog.select('Choose Stream', stream_names)
                if choice >= 0:
                    url = channel['all_links'][choice]
                    debug_log(f"DEBUG: Selected stream URL: {url}", xbmc.LOGINFO)
                    if '(' in url:
                        url = url[:url.rindex('(')].strip()
                    list_item = xbmcgui.ListItem(name)
                    list_item.setPath(url)
                    debug_log(f"DEBUG: Playing stream directly: {url}", xbmc.LOGINFO)
                    track(event="play", channel_name=name, channel_id=channel_id, url=url, source="epggrid")
                    xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
                    xbmc.executebuiltin('Dialog.Close(busydialog)')
                    xbmc.executebuiltin('Dialog.Close(all,true)')
                    xbmc.sleep(250)  # Increased delay for direct playback
                    if isinstance(url, str) and (url.startswith("http") or url.endswith(".m3u8") or url.startswith("plugin://")):
                        try:
                            from youtube_guide import cancel_youtube_playback
                            cancel_youtube_playback()
                        except ImportError:
                            pass
                        xbmc.Player().play(url, list_item)
                        debug_log(f"DEBUG: Playback initiated for direct stream", xbmc.LOGINFO)
                    else:
                        debug_log(f"JetGuide DEBUG: Not playable (EPGGrid). Link: {url}", xbmc.LOGERROR)
                        xbmcgui.Dialog().ok("Error", f"This link is not a direct playable stream.\n\nLink: {url}")
                else:
                    debug_log(f"DEBUG: Stream selection cancelled for {name}", xbmc.LOGINFO)
                return
            
            url = channel.get('url', '')
            if not url:
                debug_log(f"DEBUG: No URL for {name}", xbmc.LOGERROR)
                xbmcgui.Dialog().notification('Error', 'No valid URL found for channel', xbmcgui.NOTIFICATION_ERROR, 3000)
                return

            if '|' in url:
                streams = [s.strip() for s in url.split('|') if s.strip()]
                stream_names = []
                for stream in streams:
                    if '(' in stream and ')' in stream:
                        stream_name = stream[stream.rindex('(')+1:stream.rindex(')')]
                        stream_names.append(stream_name)
                    else:
                        stream_names.append(f"Stream {len(stream_names)+1}")
                dialog = xbmcgui.Dialog()
                choice = dialog.select('Choose Stream', stream_names)
                if choice >= 0:
                    url = streams[choice].strip()
                    if '(' in url:
                        url = url[:url.rindex('(')].strip()
                else:
                    debug_log(f"DEBUG: Stream selection cancelled for {name}", xbmc.LOGINFO)
                    return

            list_item = xbmcgui.ListItem(name)
            list_item.setPath(url)
            debug_log(f"DEBUG: Playing directly from EPGGrid: {url}", xbmc.LOGINFO)
            track(event="play", channel_name=name, channel_id=channel_id, url=url, source="epggrid")
            xbmc.executebuiltin('Dialog.Close(busydialognocancel)')
            xbmc.executebuiltin('Dialog.Close(busydialog)')
            xbmc.executebuiltin('Dialog.Close(all,true)')
            xbmc.sleep(250)
            try:
                from youtube_guide import cancel_youtube_playback
                cancel_youtube_playback()
            except ImportError:
                pass
            xbmc.Player().play(url, list_item)
            debug_log(f"DEBUG: Playback initiated from EPGGrid for {name}", xbmc.LOGINFO)
            
        except Exception as e:
            debug_log(f"DEBUG: play_selected_channel error: {str(e)}", xbmc.LOGERROR)
            xbmcgui.Dialog().notification('Error', f'Failed to play channel: {str(e)}', xbmcgui.NOTIFICATION_ERROR, 5000)
            
    def close(self):
        """Clean up resources and close the window."""
        debug_log(f"DEBUG: EPGGrid closing, cleaning up resources", xbmc.LOGINFO)
        try:
            self.is_closing = True
            if hasattr(self, 'program_cache'):
                self.program_cache.clear()
                debug_log(f"DEBUG: Cleared program_cache", xbmc.LOGINFO)
            if hasattr(self, 'program_index'):
                self.program_index.clear()
                debug_log(f"DEBUG: Cleared program_index", xbmc.LOGINFO)
            if hasattr(self, 'loaded_channel_ids'):
                self.loaded_channel_ids.clear()
                debug_log(f"DEBUG: Cleared loaded_channel_ids", xbmc.LOGINFO)
            if hasattr(self, 'channels'):
                self.channels.clear()
                debug_log(f"DEBUG: Cleared channels", xbmc.LOGINFO)
            if hasattr(self, 'all_channels'):
                self.all_channels.clear()
                debug_log(f"DEBUG: Cleared all_channels", xbmc.LOGINFO)
            if hasattr(self, 'time_slots'):
                self.time_slots.clear()
                debug_log(f"DEBUG: Cleared time_slots", xbmc.LOGINFO)
            if hasattr(self, 'channel_panel') and self.channel_panel:
                self.channel_panel.reset()
                debug_log(f"DEBUG: Reset channel_panel", xbmc.LOGINFO)
            super().close()
            debug_log(f"DEBUG: EPGGrid closed", xbmc.LOGINFO)
        except Exception as e:
            debug_log(f"DEBUG: close error: {str(e)}", xbmc.LOGERROR)
def get_youtube_sleep_time():
    """Get the sleep time setting from addon settings"""
    try:
        sleep_time = int(ADDON.getSetting('youtube_sleep_time'))
        return sleep_time
    except:
        return 5  # Default fallback

def youtube_json_to_epggrid(json_path):
    from datetime import datetime, timedelta

    with open(json_path, 'r', encoding='utf-8') as f:
        data = json.load(f)

    channel_name = "Alternative Metal"
    tvg_id = "altmetal"
    logo = data['items'][0].get('thumbnail', '')
    channel = {
        'name': channel_name,
        'tvg_id': tvg_id,
        'logo': logo,
        'group': 'YouTube'
    }

    programs = []
    start_time = datetime.now()
    # Get user-configurable sleep time
    sleep_time = get_youtube_sleep_time()
    
    for item in data['items']:
        duration_parts = item['duration'].split(':')
        duration_seconds = int(duration_parts[0]) * 60 + int(duration_parts[1])
        end_time = start_time + timedelta(seconds=duration_seconds)
        programs.append({
            'title': item['title'],
            'channel': tvg_id,
            'desc': item['summary'],
            'start': start_time,
            'end': end_time,
            'link': item['link'],
            'logo': item.get('thumbnail', ''),
        })
        # Add configurable delay between programs
        start_time = end_time + timedelta(seconds=sleep_time)
    return [channel], programs
