import os
import sys
import time
import requests
import subprocess
import gzip
import io
from datetime import datetime, timedelta,timezone
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QVBoxLayout, QListWidget, QWidget, QHBoxLayout,
    QListWidgetItem, QLabel, QSplitter, QFrame, QPushButton, QSlider, QSizePolicy,
    QScrollArea,QGridLayout
)
from PyQt5.QtCore import Qt, QTimer, QEvent, QTime, QUrl, QThread, pyqtSignal
from PyQt5.QtGui import QFont
import pytz
import vlc
import xml.etree.ElementTree as ET

class VideoFrame(QFrame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setStyleSheet("background-color: black;")

class VLCPlayer:
    def __init__(self, video_frame):
        self.instance = vlc.Instance()
        self.media_player = self.instance.media_player_new()
        if sys.platform == "win32":
            self.media_player.set_hwnd(int(video_frame.winId()))
        else:
            self.media_player.set_xwindow(int(video_frame.winId()))

    def play_stream(self, url):
        media = self.instance.media_new(url)
        media.add_option(":network-caching=1000")
        self.media_player.set_media(media)
        self.media_player.play()
        self.media_player.audio_set_volume(70)

    def stop(self):
        self.media_player.stop()

    def release(self):
        self.media_player.release()
        self.instance.release()

class EPGDownloader(QThread):
    epg_downloaded = pyqtSignal(bytes)
    error_occurred = pyqtSignal(str)

    def __init__(self, epg_url):
        super().__init__()
        self.epg_url = epg_url

    def run(self):
        try:
            response = requests.get(self.epg_url, timeout=10)
            response.raise_for_status()
            self.epg_downloaded.emit(response.content)
        except Exception as e:
            self.error_occurred.emit(f"EPG download failed: {str(e)}")

class IPTVWindow(QMainWindow):
    def __init__(self, php_dir):
        super().__init__()
        self.setWindowTitle("IPTV Player with EPG")
        self.setGeometry(100, 100, 1280, 720)
        self.php_dir = php_dir
        self._is_fullscreen = False
        self._normal_geometry = None
        self._normal_flags = None
        self.current_channel_epg = None
        self.epg_url = None
        self.epg_data = None
        self.epg_last_update = None
        self.debug_log = []
        
        self.channel_order = []  # ✅ Prevent AttributeError

        self.epg_grid_window = None
        
        self.init_ui()
        self.installEventFilter(self)
        self.download_playlist()

    def log_debug(self, message):
        """Add debug message to log and print to console"""
        timestamp = datetime.now().strftime("%H:%M:%S")
        full_message = f"[{timestamp}] {message}"
        self.debug_log.append(full_message)
        print(full_message)
        
        if hasattr(self, 'debug_display'):
            self.debug_display.addItem(full_message)
            self.debug_display.scrollToBottom()

    def init_ui(self):
        self.splitter = QSplitter(Qt.Horizontal)
        
        # Left panel with channels
        self.channel_list = QListWidget()
        self.channel_list.itemClicked.connect(self.on_channel_clicked)
        self.splitter.addWidget(self.channel_list)

        # Right panel with video and EPG
        right_panel = QWidget()
        layout = QVBoxLayout(right_panel)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self.video_container = QWidget()
        self.video_container.setStyleSheet("background-color: black;")
        self.video_container.setMinimumHeight(400)
        video_container_layout = QVBoxLayout(self.video_container)
        video_container_layout.setContentsMargins(0, 0, 0, 0)

        self.video_frame = VideoFrame(self.video_container)
        self.video_frame.mouseDoubleClickEvent = self.toggle_fullscreen
        video_container_layout.addWidget(self.video_frame)

        layout.addWidget(self.video_container, stretch=3)

        self.player = VLCPlayer(self.video_frame)

        # EPG Display with new formatting
        self.epg_display = QLabel()
        self.epg_display.setStyleSheet("""
            background-color: #222;
            color: white;
            padding: 10px;
        """)
        self.epg_display.setWordWrap(True)
        self.epg_display.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        layout.addWidget(self.epg_display, stretch=1)

        # Timers
        self.clock_timer = QTimer()
        self.clock_timer.timeout.connect(self.refresh_epg_time)
        self.clock_timer.start(1000)
       
        self.epg_update_timer = QTimer()
        self.epg_update_timer.timeout.connect(self.update_epg_data)
        self.epg_update_timer.start(3600000)  # Update EPG every hour

        # Controls
        self.controls = QWidget()
        ctl_layout = QHBoxLayout(self.controls)
        for text, slot in [("⏮️", self.play_previous_channel),
                           ("⏯️", self.toggle_play_pause),
                           ("⏭️", self.play_next_channel),
                           ("🖵", self.toggle_fullscreen),
                           ("📅", self.toggle_epg_grid),
                           ("🔄", self.update_epg_data)]:
            btn = QPushButton(text)
            btn.clicked.connect(slot)
            ctl_layout.addWidget(btn)

        self.volume_slider = QSlider(Qt.Horizontal)
        self.volume_slider.setRange(0, 200)
        self.volume_slider.setValue(70)
        self.volume_slider.setToolTip("Volume")
        self.volume_slider.valueChanged.connect(self.set_volume)
        ctl_layout.addWidget(self.volume_slider)

        self.mute_button = QPushButton("🔊")
        self.mute_button.setCheckable(True)
        self.mute_button.clicked.connect(self.toggle_mute)
        ctl_layout.addWidget(self.mute_button)

        layout.addWidget(self.controls)
        self.splitter.addWidget(right_panel)
        self.splitter.setSizes([300, 980])
        self.setCentralWidget(self.splitter)

    def set_volume(self, value):
        self.player.media_player.audio_set_volume(value)
        if value == 0:
            self.player.media_player.audio_set_mute(True)
            self.mute_button.setChecked(True)
            self.mute_button.setText("🔇")
        else:
            self.player.media_player.audio_set_mute(False)
            self.mute_button.setChecked(False)
            self.mute_button.setText("🔊")

    def toggle_mute(self):
        is_muted = self.player.media_player.audio_get_mute()
        self.player.media_player.audio_set_mute(not is_muted)
        self.mute_button.setText("🔇" if not is_muted else "🔊")
        self.mute_button.setChecked(not is_muted)

    def update_epg_info(self, name, resolution):
        if not hasattr(self, 'current_channel_name'):
            return
            
        # Create formatted EPG text
        epg_text = ""
        
        # Channel name (top left) and resolution (top right)
        channel_font = QFont()
        channel_font.setPointSize(14)
        channel_font.setBold(True)
        
        epg_text += "<table width='100%' style='font-size: 11pt; font-weight: bold;'>"
        epg_text += f"<tr><td align='left'>{name}</td><td align='right'>{resolution}</td></tr>"
        epg_text += "</table>"
        
        # Current program
        if self.current_channel_epg:
            # Title (big and bold)
            epg_text += f"<div style='font-size: 12pt; font-weight: bold; margin-top: 10px;'>"
            epg_text += f"{self.current_channel_epg.get('title', 'No Programme Information')}"
            epg_text += "</div>"
            
            # Time and duration
            if 'start' in self.current_channel_epg and 'stop' in self.current_channel_epg:
                start = self.current_channel_epg['start']
                stop = self.current_channel_epg['stop']
                try:
                    # Convert GMT times to aware datetime objects
                    gmt_start = datetime.strptime(start[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
                    gmt_stop = datetime.strptime(stop[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)

                    # Convert to local time (Pakistan time here, change if needed)
                    local_tz = pytz.timezone("Asia/Karachi")
                    start_local = gmt_start.astimezone(local_tz)
                    stop_local = gmt_stop.astimezone(local_tz)
                    now_local = datetime.now(local_tz)

                    # Format time display
                    start_time_str = start_local.strftime("%I:%M%p").lstrip('0')
                    stop_time_str = stop_local.strftime("%I:%M%p").lstrip('0')
                    duration = int((stop_local - start_local).total_seconds() // 60)

                    # Progress percentage
                    total_seconds = (stop_local - start_local).total_seconds()
                    elapsed_seconds = (now_local - start_local).total_seconds()
                    progress_percent = max(0, min(100, int((elapsed_seconds / total_seconds) * 100))) if total_seconds > 0 else 0

                    # Build HTML progress bar
                    progress_bar_html = f"""
                    <div style='background: #444; width: 100%; height: 8px; border-radius: 4px; margin: 5px 0;'>
                    <div style='background: #0f0; width: {progress_percent}%; height: 100%; border-radius: 4px;'></div>
                    </div>
                    """

                    # Add everything to epg_text
                    epg_text += f"<div style='font-size: 10pt; margin-top: 5px;'>"
                    epg_text += f"{start_time_str} - {stop_time_str} ({duration} mins)"
                    epg_text += progress_bar_html
                    epg_text += "</div>"
                except:
                    pass
            
            # Description
            if 'desc' in self.current_channel_epg and self.current_channel_epg['desc']:
                epg_text += f"<div style='font-size: 10pt; margin-top: 5px;'>"
                epg_text += f"{self.current_channel_epg['desc']}"
                epg_text += "</div>"
            
            # Next program
            if self.next_program_epg:
                epg_text += f"<div style='font-size: 11pt; font-weight: bold; margin-top: 15px;'>"
                epg_text += f"Next: {self.next_program_epg.get('title', 'No information')}"
                epg_text += "</div>"
                
                if 'start' in self.next_program_epg and 'stop' in self.next_program_epg:
                    start = self.next_program_epg['start']
                    stop = self.next_program_epg['stop']
                    try:
                        # Convert GMT times to aware datetime objects
                        gmt_start = datetime.strptime(start[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
                        gmt_stop = datetime.strptime(stop[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)

                        # Convert to local time (Pakistan time here, change if needed)
                        local_tz = pytz.timezone("Asia/Karachi")
                        start_local = gmt_start.astimezone(local_tz)
                        stop_local = gmt_stop.astimezone(local_tz)

                        # Format time display
                        start_time_str = start_local.strftime("%I:%M%p").lstrip('0')
                        stop_time_str = stop_local.strftime("%I:%M%p").lstrip('0')

                        epg_text += f"<div style='font-size: 10pt;'>"
                        epg_text += f"{start_time_str} - {stop_time_str}"
                        epg_text += "</div>"
                    except:
                        pass
                


                
        else:
            epg_text += "<div style='font-size: 12pt; font-weight: bold; margin-top: 10px;'>"
            epg_text += "No Programme Information"
            epg_text += "</div>"
        
        self.epg_display.setText(epg_text)

    def update_current_epg(self):
        if not self.epg_data or not hasattr(self, 'current_channel_id'):
            return
            
        try:
            # Get current LOCAL time
            now_local = datetime.now()

            # Convert to equivalent GMT time string for matching with EPG (which is in GMT)
            now_gmt = now_local.astimezone(pytz.utc)
            current_time = now_gmt.strftime("%Y%m%d%H%M%S")
            found_current = False
            found_next = False
            
            for programme in self.epg_data.findall('.//programme'):
                channel_id = programme.get('channel')
                if channel_id != self.current_channel_id:
                    continue
                    
                start = programme.get('start')
                stop = programme.get('stop')
                
                if not found_current and start <= current_time <= stop:
                    # Current program
                    self.current_channel_epg = {
                        'title': programme.find('title').text if programme.find('title') is not None else 'No Programme Information',
                        'start': start,
                        'stop': stop,
                        'desc': programme.find('desc').text if programme.find('desc') is not None else ''
                    }
                    found_current = True
                elif found_current and not found_next and start > current_time:
                    # Next program
                    self.next_program_epg = {
                        'title': programme.find('title').text if programme.find('title') is not None else 'No information',
                        'start': start,
                        'stop': stop
                    }
                    found_next = True
                    break
            
            if not found_current:
                self.current_channel_epg = None
                self.next_program_epg = None
            elif not found_next:
                self.next_program_epg = None
                
            # Update the display
            if hasattr(self, 'current_channel_name') and hasattr(self, 'current_channel_resolution'):
                self.update_epg_info(self.current_channel_name, self.current_channel_resolution)
                                   
        except Exception as e:
            print(f"Error updating current EPG: {str(e)}")

    def refresh_epg_time(self):
        if not hasattr(self, 'current_channel_name') or not hasattr(self, 'current_channel_resolution'):
            return
            
        text = self.epg_display.text()
        parts = text.split("\n", 1)
        if len(parts) == 2:
            first_line, epg_line = parts
            if "|" in first_line:
                name_resolution = first_line.split("|")
                if len(name_resolution) >= 2:
                    name = name_resolution[0].replace("<b>", "").replace("</b>", "").strip()
                    resolution = name_resolution[1].strip()
                    self.update_epg_info(name, resolution, self.current_channel_epg)

    def update_epg_data(self):
        if not self.epg_url:
            self.log_debug("No EPG URL available")
            return
            
        self.log_debug(f"Downloading EPG data from: {self.epg_url}")
        
        self.epg_downloader = EPGDownloader(self.epg_url)
        self.epg_downloader.epg_downloaded.connect(self.process_epg_data)
        self.epg_downloader.error_occurred.connect(self.log_debug)
        self.epg_downloader.start()

    def process_epg_data(self, epg_bytes):
        try:
            # Try to decompress the data
            try:
                epg_xml = gzip.decompress(epg_bytes).decode('utf-8')
                self.log_debug("EPG data decompressed successfully")
            except:
                epg_xml = epg_bytes.decode('utf-8')
                self.log_debug("EPG data was not compressed")
            
            # Save the raw XML for debugging
            debug_epg_path = os.path.join(self.php_dir, "last_epg.xml")
            with open(debug_epg_path, "w", encoding="utf-8") as f:
                f.write(epg_xml)
            self.log_debug(f"Saved EPG data to {debug_epg_path}")
            
            # Parse the XML
            self.epg_data = ET.fromstring(epg_xml)
            self.epg_last_update = datetime.now()
            self.log_debug(f"EPG data parsed successfully, last update: {self.epg_last_update}")
            
            # Update current channel's EPG if available
            if hasattr(self, 'current_channel_id'):
                self.update_current_epg()
                
        except Exception as e:
            self.log_debug(f"Error processing EPG data: {str(e)}")
 
    def toggle_epg_grid(self):
        if not self.epg_data:
            self.log_debug("EPG data not loaded yet.")
            return

        if self.epg_grid_window is None:
            self.epg_grid_window = EPGGridWindow(self.epg_data, self.channel_order)

        if self.epg_grid_window.isVisible():
            self.epg_grid_window.hide()
        else:
            self.epg_grid_window.show()

    def detect_resolution(self, item, base):
        w = self.player.media_player.video_get_width()
        h = self.player.media_player.video_get_height()
        fps = self.player.media_player.get_fps()
        if w > 0 and h > 0:
            label = "SD"
            if w >= 3840:
                label = "4K"
            elif w >= 1920:
                label = "FHD"
            elif w >= 1280:
                label = "HD"
            info = f"{label}-{fps:.2f} FPS"
            item.setText(f"{base} ({info})")
            self.current_channel_name = base
            self.current_channel_resolution = info
            self.update_epg_info(base, info)

    def on_channel_clicked(self, item: QListWidgetItem):
        url = item.data(Qt.UserRole)
        self.player.play_stream(url)

        # Get channel ID from item data
        channel_id = item.data(Qt.UserRole + 1)
        if channel_id:
            self.current_channel_id = channel_id
            self.update_current_epg()

        base = item.text().split('(')[0].strip()
        item.setText(f"{base} (Loading...)")
        self.current_channel_name = base
        self.update_epg_info(base, "Loading...")

        QTimer.singleShot(4000, lambda: self.detect_resolution(item, base))

    def toggle_play_pause(self):
        mp = self.player.media_player
        if mp.is_playing():
            mp.pause()
        else:
            mp.play()

    def play_next_channel(self):
        idx = self.channel_list.currentRow()
        if idx < self.channel_list.count() - 1:
            self.channel_list.setCurrentRow(idx+1)
            self.on_channel_clicked(self.channel_list.currentItem())

    def play_previous_channel(self):
        idx = self.channel_list.currentRow()
        if idx > 0:
            self.channel_list.setCurrentRow(idx-1)
            self.on_channel_clicked(self.channel_list.currentItem())

    def toggle_fullscreen(self, event=None):
        if not self._is_fullscreen:
            self._normal_geometry = self.geometry()
            self._normal_flags = self.windowFlags()
            self.channel_list.hide()
            self.epg_display.hide()
            self.controls.hide()
            self.setWindowFlags(self._normal_flags | Qt.FramelessWindowHint)
            self.showFullScreen()
        else:
            self.setWindowFlags(self._normal_flags)
            self.showNormal()
            if self._normal_geometry:
                self.setGeometry(self._normal_geometry)
            self.channel_list.show()
            self.epg_display.show()
            self.controls.show()
        self._is_fullscreen = not self._is_fullscreen

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Escape and self._is_fullscreen:
            self.toggle_fullscreen()
        else:
            super().keyPressEvent(event)

    def download_playlist(self):
        try:
            os.makedirs(self.php_dir, exist_ok=True)
            url = "https://iptv.nywebforum.com/playlist.m3u"
            self.log_debug(f"Downloading playlist from: {url}")
            
            response = requests.get(url, timeout=10)
            content = response.text.replace(
                "https://iptv.nywebforum.com", "http://localhost:8888"
            )
            
            # Extract EPG URL from the first line if present
            first_line = response.text.split('\n')[0]
            if first_line.startswith("#EXTM3U") and "url-tvg=" in first_line:
                self.epg_url = first_line.split('url-tvg="')[1].split('"')[0]
                self.log_debug(f"Found EPG URL: {self.epg_url}")
                self.update_epg_data()  # Download EPG immediately
            
            path = os.path.join(self.php_dir, "playlist.m3u")
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
            self.log_debug(f"Playlist saved to: {path}")
            self.load_playlist(path)
        except Exception as e:
            self.log_debug(f"Failed to download playlist: {e}")

    def load_playlist(self, path):
        try:
            self.log_debug(f"Loading playlist from: {path}")
            lines = open(path, encoding="utf-8").read().splitlines()
            self.channel_list.clear()
            first = None
            name = None
            channel_id = None

            self.channel_order = []  # ✅ Reset and initialize channel order

            for i, l in enumerate(lines):
                if l.startswith("#EXTINF:"):
                    name = l.split(',')[-1].strip()
                    if 'tvg-id="' in l:
                        channel_id = l.split('tvg-id="')[1].split('"')[0]
                        self.log_debug(f"Found channel ID: {channel_id} for {name}")
                elif l and not l.startswith("#") and name:
                    it = QListWidgetItem(f"{name} (Loading...)")
                    it.setData(Qt.UserRole, l)
                    if channel_id:
                        it.setData(Qt.UserRole + 1, channel_id)

                    self.channel_list.addItem(it)
                    self.channel_order.append((name, channel_id))  # ✅ Maintain order for EPG

                    if first is None:
                        first = it
                    name = None
                    channel_id = None

            if first:
                self.channel_list.setCurrentItem(first)
                self.log_debug(f"Found {self.channel_list.count()} channels, selecting first one")
                QTimer.singleShot(1000, lambda: self.on_channel_clicked(first))
        except Exception as e:
            self.log_debug(f"Failed to load playlist: {e}")

    def closeEvent(self, event):
        self.log_debug("Application closing - cleaning up resources")
        
        if self.epg_grid_window and self.epg_grid_window.isVisible():
            self.epg_grid_window.close()

        # Stop timers
        self.clock_timer.stop()
        self.epg_update_timer.stop()
        
        # Stop player
        self.player.stop()
        self.player.release()
        
        # Stop any ongoing EPG download
        if hasattr(self, 'epg_downloader') and self.epg_downloader.isRunning():
            self.epg_downloader.quit()
            self.epg_downloader.wait()
        
        super().closeEvent(event)

class EPGGridWindow(QMainWindow):
    def __init__(self, epg_data, channel_order):
        super().__init__()
        self.setWindowTitle("Full EPG")
        self.resize(1400, 800)
        self.setStyleSheet("""
            background-color: #222; 
            color: white; 
            font-size: 10pt;
            QLabel {
                padding: 5px;
            }
        """)

        self.epg_data = epg_data
        self.channel_order = channel_order  # List of (name, tvg-id)
        self.row_height = 60  # Fixed height for each row
        self.timeline_slot_width = 150  # Width for each 30-minute slot

        # === Timeline Calculation ===
        now = datetime.utcnow().replace(tzinfo=pytz.utc).astimezone(pytz.timezone("Asia/Karachi"))
        self.timeline_start = now - timedelta(days=1)  # start from yesterday
        self.timeline_end = now + timedelta(days=2)    # up to day after tomorrow
        self.timeline = []
        t = self.timeline_start
        while t <= self.timeline_end:
            self.timeline.append(t)
            t += timedelta(minutes=30)

        # === Main Layout ===
        main_widget = QWidget()
        main_layout = QVBoxLayout(main_widget)
        main_layout.setContentsMargins(0, 0, 0, 0)

        # === Top Bar with Clock and Close Button ===
        # === Top Bar with Close Button (clock removed) ===
        top_bar = QHBoxLayout()
        close_btn = QPushButton("❌ Close EPG")
        close_btn.setStyleSheet("font-size: 14px; background-color: red; color: white; padding: 5px;")
        close_btn.clicked.connect(self.close)
        top_bar.addStretch()
        top_bar.addWidget(close_btn)
        main_layout.addLayout(top_bar)




        # === Scrollable Timeline + Grid ===
        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True)
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)

        container = QWidget()
        container_layout = QVBoxLayout(container)
        container_layout.setContentsMargins(0, 0, 0, 0)

        # === Timeline Header ===
        # === Timeline Header with Clock in First Column ===
        header_layout = QHBoxLayout()
        header_layout.setSpacing(1)
        header_layout.setContentsMargins(0, 0, 0, 0)  # No margin hack needed

        # Clock in first column (same height as timeline cells)
        self.clock_label = QLabel()
        self.clock_label.setStyleSheet("background-color: #333; font-size: 10pt; font-weight: bold; padding: 5px;")
        self.clock_label.setAlignment(Qt.AlignLeft)
        self.clock_label.setFixedSize(300, 40)  # Matches channel column width
        header_layout.addWidget(self.clock_label)



        for t in self.timeline:
            label = QLabel(f"{t.strftime('%a %b %d')}<br>{t.strftime('%I:%M %p').lstrip('0')}")
            label.setTextFormat(Qt.RichText)
            label.setAlignment(Qt.AlignCenter)
            label.setStyleSheet("background-color: #333; font-weight: bold;")
            label.setFixedSize(self.timeline_slot_width, 40)  # Fixed size for timeline header
            header_layout.addWidget(label)

        # Timer for clock update
        self.update_clock()
        timer = QTimer(self)
        timer.timeout.connect(self.update_clock)
        timer.start(1000)

        container_layout.addLayout(header_layout)

        # === EPG Grid Section ===
        grid_layout = QHBoxLayout()
        grid_layout.setContentsMargins(0, 0, 0, 0)

        # Left: Channel Names
        channel_list_widget = QWidget()
        channel_list_layout = QVBoxLayout(channel_list_widget)
        channel_list_layout.setSpacing(1)
        channel_list_layout.setContentsMargins(0, 0, 0, 0)

        # Right: EPG Grid
        epg_grid_widget = QWidget()
        self.epg_layout = QGridLayout(epg_grid_widget)
        self.epg_layout.setSpacing(1)
        self.epg_layout.setContentsMargins(0, 0, 0, 0)

        # Build both channel list and EPG grid together to ensure perfect alignment
        for row, (ch_name, ch_id) in enumerate(self.channel_order):
            # Add channel name to left panel
            label = QLabel(ch_name)
            label.setWordWrap(True)
            label.setStyleSheet(f"background-color: {'#333' if row % 2 else '#444'};")
            label.setFixedHeight(self.row_height)
            channel_list_layout.addWidget(label)

            # Add EPG data to grid (or placeholder if no data)
            self.add_channel_epg(row, ch_id)

        channel_scroll = QScrollArea()
        channel_scroll.setWidget(channel_list_widget)
        channel_scroll.setFixedWidth(300)
        channel_scroll.setWidgetResizable(True)
        channel_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        grid_layout.addWidget(channel_scroll)

        epg_scroll = QScrollArea()
        epg_scroll.setWidget(epg_grid_widget)
        epg_scroll.setWidgetResizable(True)
        grid_layout.addWidget(epg_scroll)

        # Link scrollbars vertically
        channel_scroll.verticalScrollBar().valueChanged.connect(epg_scroll.verticalScrollBar().setValue)
        epg_scroll.verticalScrollBar().valueChanged.connect(channel_scroll.verticalScrollBar().setValue)

        container_layout.addLayout(grid_layout)
        scroll_area.setWidget(container)
        main_layout.addWidget(scroll_area)

        self.setCentralWidget(main_widget)

    def add_channel_epg(self, row, channel_id):
        bg = "#333" if row % 2 else "#444"
        programs = [p for p in self.epg_data.findall(".//programme") if p.get("channel") == channel_id]

        # Convert all valid program blocks
        cells = []
        for prog in programs:
            try:
                start = datetime.strptime(prog.get("start")[:14], "%Y%m%d%H%M%S").replace(tzinfo=pytz.utc).astimezone(pytz.timezone("Asia/Karachi"))
                stop = datetime.strptime(prog.get("stop")[:14], "%Y%m%d%H%M%S").replace(tzinfo=pytz.utc).astimezone(pytz.timezone("Asia/Karachi"))

                if stop <= self.timeline_start or start >= self.timeline_end:
                    continue

                title = prog.findtext("title", "No Title")

                # Clamp start/stop within timeline range
                start_offset = max(0, int((start - self.timeline_start).total_seconds() // 1800))
                end_offset = min(len(self.timeline), int((stop - self.timeline_start).total_seconds() // 1800))
                duration = max(1, end_offset - start_offset)

                cells.append((start_offset, duration, title))
            except:
                continue

        # Sort by start offset just in case
        cells.sort()

        current_col = 0
        for start_offset, duration, title in cells:
            # Fill gap before program
            if start_offset > current_col:
                gap_duration = start_offset - current_col
                filler = QLabel("")  # Empty box
                filler.setStyleSheet(f"""
                    background-color: {bg};
                    border: 1px solid #555;
                """)
                filler.setFixedHeight(self.row_height)
                self.epg_layout.addWidget(filler, row, current_col, 1, gap_duration)
                current_col = start_offset

            # Add actual program cell
            cell = QLabel(title)
            cell.setStyleSheet(f"""
                background-color: {bg};
                border: 1px solid #555;
            """)
            cell.setWordWrap(True)
            cell.setFixedHeight(self.row_height)
            self.epg_layout.addWidget(cell, row, start_offset, 1, duration)
            current_col = start_offset + duration

        # Fill gap after last program
        if current_col < len(self.timeline):
            filler = QLabel("")
            filler.setStyleSheet(f"""
                background-color: {bg};
                border: 1px solid #555;
            """)
            filler.setFixedHeight(self.row_height)
            self.epg_layout.addWidget(filler, row, current_col, 1, len(self.timeline) - current_col)


    def update_clock(self):
        now = datetime.now(pytz.timezone("Asia/Karachi"))
        self.clock_label.setText(now.strftime("%a %b %d, %I:%M:%S %p"))
        
def start_php_server(php_dir, port=8888):
    try:
        os.chdir(php_dir)
        return subprocess.Popen([
            "php", "-S", f"localhost:{port}"],
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
            creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
        )
    except Exception as e:
        print(f"Failed to start PHP server: {e}")
        return None


if __name__ == "__main__":
    php_dir = os.path.join(os.path.dirname(__file__), "php_files")
    os.makedirs(php_dir, exist_ok=True)
    php_proc = start_php_server(php_dir)
    app = QApplication(sys.argv)
    window = IPTVWindow(php_dir)
    window.show()
    exit_code = app.exec_()
    
    # Ensure clean exit
    if php_proc:
        php_proc.terminate()
        php_proc.wait(2)
    sys.exit(exit_code)