import os
import sys
import time
import requests
import subprocess
import gzip
import math
import threading
import socket
from datetime import datetime, timedelta,timezone
from PyQt5.QtWidgets import (
    QMainWindow, QVBoxLayout, QListWidget, QWidget, QHBoxLayout,
    QListWidgetItem, QLabel, QSplitter, QFrame, QPushButton, QSlider, QSizePolicy,
    QScrollArea,QGridLayout,QFileDialog,QGraphicsView, QGraphicsScene, QGraphicsRectItem, QGraphicsSimpleTextItem, 
    QApplication, QGraphicsLineItem,QGraphicsTextItem,QGraphicsItem,QLineEdit
)
from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal,QPointF,QRectF,QEvent
from PyQt5.QtGui import QFont, QColor,QBrush, QColor, QPen,QPainter,QGuiApplication
import pytz
import vlc
import xml.etree.ElementTree as ET

RED_LINE_SHIFT_SECONDS = 700  # 10-minute forward shift

php_proc = None  # Keep reference to PHP server to prevent it from being garbage collected

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()))

        self.playback_timeout_timer = QTimer()
        self.playback_timeout_timer.setSingleShot(True)
        self.playback_timeout_timer.timeout.connect(self.check_playback_timeout)

    def play_stream(self, url):
        try:
            self.media_player.stop()  # Make sure it's fully stopped before new media
        except Exception:
            pass

        media = self.instance.media_new(url)

        # ✅ Optimized for faster startup
        media.add_option(":network-caching=1000")
        media.add_option(":file-caching=500")
        media.add_option(":tcp-caching=500")
        media.add_option(":avcodec-hw=any")  # ✅ Try hardware decoding when possible
        media.add_option("--no-drop-late-frames")
        media.add_option("--no-skip-frames")
        media.add_option(":http-reconnect")
        #media.add_option(":http-continuous")
        media.add_option(":ffmpeg-skiploopfilter=all")
        media.add_option(":ffmpeg-skipframe=default")

        self.media_player.set_media(media)
        self.media_player.audio_set_volume(70)
        self.media_player.play()

        # Start timeout safety
        self.playback_timeout_timer.start(10000)

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

    def release(self):
        try:
            self.media_player.release()
        except Exception: pass
        try:
            self.instance.release()
        except Exception: pass

    def check_playback_timeout(self):
        if not self.media_player.is_playing():
            print("[VLC] Stream timeout – stopping playback")
            self.media_player.stop()

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):
        self.channel_video_info = {}
        self.epg_grid_window = None

        super().__init__()
        self.setWindowTitle("Playlist and 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.init_ui()
        

        self.epg_data = ET.Element("tv")  # placeholder in case it's accessed early
       
        
        self.download_playlist()  # This will later call load_playlist() which fills channel_order

       
        
        screen = QApplication.primaryScreen().availableGeometry()
        half_width = screen.width() // 2
        full_height = screen.height()

        self.resize(half_width, full_height - 50)  # Shrink to avoid going off screen
        self.move(half_width, 0)  # Position on right half

        # Position IPTV window in top-right quarter of screen
        screen = QApplication.primaryScreen().availableGeometry()
        half_width = screen.width() // 2
        half_height = screen.height() // 2
        self.setGeometry(half_width, 30, half_width, half_height - 30)
        
        self.stream_error_label = None  # for "Stream not available" overlay

    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)
        self.channel_list.hide()  # ✅ Hide the old playlist

        # 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

        # Timer to auto-refresh displayed EPG info every 60 seconds
        self.epg_live_timer = QTimer()
        self.epg_live_timer.timeout.connect(self.update_epg_info)
        self.epg_live_timer.start(60000)  # 60 seconds

        # 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)

        self.last_epg_title = ""
        self.last_epg_desc = ""

    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=None, resolution=None):
        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 self.epg_data is None 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 self.current_channel_epg:
                stop_time_str = self.current_channel_epg.get('stop')
                try:
                    stop_time = datetime.strptime(stop_time_str[:14], "%Y%m%d%H%M%S").replace(tzinfo=pytz.utc).astimezone(pytz.timezone("Asia/Karachi"))
                    now = datetime.now(pytz.timezone("Asia/Karachi"))

                    if now >= stop_time:
                        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"[EPG Timing Check] Failed to parse stop time: {e}")
                    
                    
        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.insert_dummy_epg_gaps()
            self.epg_last_update = datetime.now()

            if self.epg_grid_window:
                # Defer overlay creation safely inside update_epg()
                self.epg_grid_window.update_epg(self.epg_data, self.channel_order, self.channel_video_info)
            else:
                # Create fresh EPGGridWindow AFTER full EPG is available
                self.epg_grid_window = EPGGridWindow(self.epg_data, self.channel_order, self.channel_video_info, self)
                self.epg_grid_window.show()
                        
            
            
            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 insert_dummy_epg_gaps(self):
        if self.epg_data is None:
            return

        local_tz = pytz.timezone("Asia/Karachi")
        timeline_start = self.epg_grid_window.timeline_start if self.epg_grid_window else datetime.now(local_tz)
        timeline_end = self.epg_grid_window.timeline_end if self.epg_grid_window else timeline_start + timedelta(hours=6)

        for _, ch_id in self.channel_order:
            progs = [p for p in self.epg_data.findall(".//programme") if p.get("channel") == ch_id]

            # Convert to (start, stop, element)
            parsed = []
            for p in progs:
                try:
                    start = datetime.strptime(p.get("start")[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc).astimezone(local_tz)
                    stop = datetime.strptime(p.get("stop")[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc).astimezone(local_tz)
                    parsed.append((start, stop, p))
                except:
                    continue

            parsed.sort(key=lambda x: x[0])
            new_programmes = []

            current_time = timeline_start

            for start, stop, orig_elem in parsed:
                if start > current_time:
                    # Insert dummy gap
                    gap_start = current_time
                    gap_end = start
                    while gap_start < gap_end:
                        next_gap = min(gap_end, gap_start + timedelta(hours=1))
                        dummy = ET.Element("programme")
                        dummy.set("channel", ch_id)
                        dummy.set("start", gap_start.astimezone(timezone.utc).strftime("%Y%m%d%H%M%S") + " +0000")
                        dummy.set("stop", next_gap.astimezone(timezone.utc).strftime("%Y%m%d%H%M%S") + " +0000")

                        title = ET.SubElement(dummy, "title")
                        title.text = "No Program Information"
                        new_programmes.append(dummy)

                        gap_start = next_gap

                new_programmes.append(orig_elem)
                current_time = max(current_time, stop)

            # Fill any remaining gap to timeline_end
            if current_time < timeline_end:
                gap_start = current_time
                while gap_start < timeline_end:
                    next_gap = min(timeline_end, gap_start + timedelta(hours=1))
                    dummy = ET.Element("programme")
                    dummy.set("channel", ch_id)
                    dummy.set("start", gap_start.astimezone(timezone.utc).strftime("%Y%m%d%H%M%S") + " +0000")
                    dummy.set("stop", next_gap.astimezone(timezone.utc).strftime("%Y%m%d%H%M%S") + " +0000")

                    title = ET.SubElement(dummy, "title")
                    title.text = "No Program Information"
                    new_programmes.append(dummy)

                    gap_start = next_gap

            # Remove old and insert new
            for p in progs:
                self.epg_data.remove(p)
            for p in new_programmes:
                self.epg_data.append(p)


    def toggle_epg_grid(self):
        if self.epg_grid_window is None:
            # Use dummy XML if EPG not ready yet
            dummy_epg = self.epg_data if self.epg_data is not None else ET.Element("tv")
            self.epg_grid_window = EPGGridWindow(dummy_epg, self.channel_order, self.channel_video_info, self)
        
        if self.epg_grid_window.isVisible():
            self.epg_grid_window.hide()
        else:
            self.epg_grid_window.show()

    def detect_resolution(self, item, base):
        if not self.player.media_player.is_playing():
            print("[VLC] Stream failed — displaying fallback message")
            self.player.media_player.stop()

            # Remove any previous error label
            if self.stream_error_label:
                self.stream_error_label.deleteLater()
                self.stream_error_label = None

            # Show persistent "Stream not available" message
            self.stream_error_label = QLabel("Stream is not available", self.video_frame)
            self.stream_error_label.setAlignment(Qt.AlignCenter)
            self.stream_error_label.setStyleSheet("""
                color: red; 
                font-size: 18pt; 
                background-color: rgba(0, 0, 0, 180);
            """)
            self.stream_error_label.setGeometry(0, 0, self.video_frame.width(), self.video_frame.height())
            self.stream_error_label.show()

            # ✅ Restart PHP only if needed and it's a PHP-based stream
            url = item.data(Qt.UserRole)
            if url and url.startswith("http://localhost:8888"):
                if php_proc is None or php_proc.poll() is not None:
                    print("[PHP] Detected PHP server is not running. Restarting...")
                    restart_php_server(self.php_dir)
                else:
                    print("[PHP] Forcing restart due to stuck stream...")
                    restart_php_server(self.php_dir)

            self.current_channel_resolution = "Stream not available"
            self.update_epg_info(base, "Stream not available")
            return

        # ✅ Stream is good — remove error label
        if self.stream_error_label:
            self.stream_error_label.deleteLater()
            self.stream_error_label = None

        # Continue with resolution and FPS
        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"
            self.channel_video_info[base] = (label, f"{fps:.2f}")
            item.setText(f"{base} ({info})")
            self.current_channel_name = base
            self.current_channel_resolution = info
            self.update_epg_info(base, info)

            if self.epg_grid_window:
                self.epg_grid_window.refresh_channel_fps_info()

    def on_channel_clicked(self, item: QListWidgetItem):
        url = item.data(Qt.UserRole)
        if not url:
            return

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

        self.log_debug(f"Playing stream: {url}")
        self.player.stop()
        self.player.play_stream(url)

        def retry_if_failed():
            if self.player.media_player.is_playing():
                return  # ✅ First try succeeded

            self.log_debug("[RETRY] First attempt failed, retrying stream...")

            self.player.stop()
            self.player.release()
            self.player = VLCPlayer(self.video_frame)

            if url.startswith("http://localhost:8888"):
                self.log_debug("[RETRY] Waiting for PHP server to be ready...")

                # ✅ Try to wait up to 3 seconds max
                if not wait_for_php_server(timeout=3):
                    self.log_debug("[RETRY] PHP not ready. Restarting and waiting again...")
                    restart_php_server(self.php_dir)
                    time.sleep(1.0)
                    if not wait_for_php_server(timeout=3):
                        self.log_debug("[RETRY] PHP still not ready. Giving up.")
                        return

            self.player.play_stream(url)

        QTimer.singleShot(10000, retry_if_failed)

        channel_id = item.data(Qt.UserRole + 1)
        if channel_id:
            self.current_channel_id = channel_id
            self.update_current_epg()

        QTimer.singleShot(5000, 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}")
                QTimer.singleShot(500, self.update_epg_data)  # Delay download to avoid blocking UI
            
            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))
        
            if self.epg_grid_window is None:
                self.epg_grid_window = EPGGridWindow(self.epg_data, self.channel_order, self.channel_video_info, self)
                self.epg_grid_window.show()
        
        
        except Exception as e:
            self.log_debug(f"Failed to load playlist: {e}")

    def closeEvent(self, event):
        self.log_debug("Application closing - starting async cleanup")

        # Hide the main window immediately for fast visual exit
        self.hide()

        # Defer cleanup after 100ms so UI can close first
        QTimer.singleShot(100, self.cleanup_resources)

        event.accept()  # ✅ allow close to continue
        super().closeEvent(event)

    def cleanup_resources(self):
        self.log_debug("Cleaning up timers and resources")

        if self.epg_grid_window:
            self.epg_grid_window.close()
            self.epg_grid_window.deleteLater()
            self.epg_grid_window = None

        self.clock_timer.stop()
        self.epg_update_timer.stop()

        self.player.stop()
        self.player.release()

        global php_proc
        if php_proc:
            try:
                php_proc.terminate()
                php_proc.wait(2)
            except Exception as e:
                self.log_debug(f"[PHP Cleanup] Error: {e}")
            php_proc = None

        self.log_debug("Cleanup finished — exiting app")
        QTimer.singleShot(100, QApplication.instance().quit)  # exit only after UI fully cleaned

class TimelineLabel(QLabel):
    def __init__(self, text, start_offset, duration, parent_grid, timeline_slot_width):
        super().__init__()
        self.parent_grid = parent_grid
        self.full_text = text
        self.start_offset = start_offset
        self.duration = duration
        self.parent_grid = parent_grid
        self.timeline_slot_width = timeline_slot_width
        self.setStyleSheet("""
            border: 1px solid #555;
            color: white;
        """)
        self.setWordWrap(True)                      # Allow multi-line
        self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.setText(self.full_text)               # Let QLabel handle the text

        self.setStyleSheet("""
            border: 1px solid #555;
            color: white;
            font-size: 10pt;
            padding: 2px;  /* reduced padding */
            background-color: #333;
        """)

        self.setStyleSheet("border: 1px solid #444; padding: 2px; background-color: #333;")
        
    def mouseDoubleClickEvent(self, event):
        print("[DEBUG] Program grid double-click detected")

        if self.parent_grid and self.parent_grid.main_window:
            layout = self.parent_grid.epg_layout
            index = layout.indexOf(self)
            if index != -1:
                row, _, _, _ = layout.getItemPosition(index)
                print(f"[DEBUG] EPG row double-clicked: {row}")
                if 0 <= row < self.parent_grid.main_window.channel_list.count():
                    item = self.parent_grid.main_window.channel_list.item(row)
                    print(f"[DEBUG] Attempting to play channel from program grid: {item.text()}")
                    self.parent_grid.main_window.channel_list.setCurrentItem(item)
                    self.parent_grid.main_window.on_channel_clicked(item)
        event.accept()

class NowLineOverlay(QWidget):
    def __init__(self, parent, grid_window):
        super().__init__(parent)
        self.grid_window = grid_window
        self.slot_width = grid_window.timeline_slot_width  # ✅ store slot width
        self.setAttribute(Qt.WA_TransparentForMouseEvents)
        self.setStyleSheet("background: transparent;")
        self.setVisible(True)  # ✅ Ensure it's visible
        

    def paintEvent(self, event):
        from PyQt5.QtGui import QPainter, QPen
        painter = QPainter(self)
        pen = QPen(Qt.red, 2)
        painter.setPen(pen)

        now = datetime.now(pytz.timezone("Asia/Karachi"))
        delta = now - self.grid_window.timeline_start
        adjusted_seconds = delta.total_seconds() + RED_LINE_SHIFT_SECONDS
       

        block_width = self.grid_window.timeline_slot_width  # should be 200 if set correctly

        # Total X position of the red line
        x = (adjusted_seconds / 1800) * block_width

        painter.drawLine(int(x), 0, int(x), self.height())

class EPGGridWindow(QMainWindow):
    def __init__(self, epg_data, channel_order, channel_video_info, main_window=None):
        super().__init__()
        self.setWindowTitle("EPG (Excel-style View)")

        screen = QGuiApplication.primaryScreen()
        screen_geometry = screen.geometry()
        width = screen_geometry.width() // 2
        height = int(screen_geometry.height() * 0.9)
        self.setGeometry(0, 0, width, height)

        self.epg_data = epg_data
        self.channel_order = channel_order
        self.channel_video_info = channel_video_info
        self.main_window = main_window

        self.channel_column_width = 310
        self.timeline_slot_width = 150
        self.row_height = 70
        self.timeline_row_height = 100

        now = datetime.now(pytz.timezone("Asia/Karachi"))

        # Align to :00 or :30
        if now.minute < 30:
            aligned_minutes = 0
        else:
            aligned_minutes = 30

        aligned_now = now.replace(minute=aligned_minutes, second=0, microsecond=0)

        # Shift timeline 15 minutes earlier so labels align with block dividers
        self.timeline_start = aligned_now - timedelta(days=1, minutes=15)
        self.timeline_end = aligned_now + timedelta(days=2)

        self.timeline = []
        t = self.timeline_start
        while t <= self.timeline_end:
            self.timeline.append(t)
            t += timedelta(minutes=30)
                
                
        
        self.timeline_end = aligned_now + timedelta(days=2)

        self.timeline = []
        t = self.timeline_start
        while t <= self.timeline_end:
            self.timeline.append(t)
            t += timedelta(minutes=30)

        self.timeline_scene = QGraphicsScene()

        self.channel_scene = QGraphicsScene()

        self.grid_scene = QGraphicsScene()
        self.timeline_scene.installEventFilter(self)
        self.channel_scene.installEventFilter(self)
        self.grid_scene.installEventFilter(self)


        self.timeline_view = QGraphicsView(self.timeline_scene)
        
        self.channel_view = QGraphicsView(self.channel_scene)
        self.grid_view = QGraphicsView(self.grid_scene)

        self.grid_view.setStyleSheet("border: none;")
        self.timeline_view.setStyleSheet("border: none;")
        self.channel_view.setStyleSheet("border-right: 1px solid #666;")

        self.grid_view.horizontalScrollBar().valueChanged.connect(self.timeline_view.horizontalScrollBar().setValue)
        self.grid_view.verticalScrollBar().valueChanged.connect(self.sync_vertical_scroll)
        self.channel_view.verticalScrollBar().valueChanged.connect(self.sync_vertical_scroll)

        self.timeline_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.timeline_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.channel_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        # self.channel_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        self.timeline_view.setStyleSheet("border-bottom: 2px solid #666;")

        container = QWidget()
        layout = QGridLayout(container)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)

        self.clock_label = QLabel()
        self.clock_label.setAlignment(Qt.AlignCenter)
        self.clock_label.setFixedSize(self.channel_column_width, self.timeline_row_height)
        self.clock_label.setStyleSheet("""
            background-color: #222;
            color: white;
            font-size: 12pt;
            font-weight: bold;
            border: none;
        """)
        layout.addWidget(self.clock_label, 0, 0)

        layout.addWidget(self.timeline_view, 0, 1)
        layout.addWidget(self.channel_view, 1, 0)
        layout.addWidget(self.grid_view, 1, 1)

        layout.setRowStretch(0, 0)  # timeline
        layout.setRowStretch(1, 1)  # grid

        control_layout = QHBoxLayout()

        self.search_box = QLineEdit()
        self.search_box.setPlaceholderText("Search channel...")
        self.search_box.setFixedWidth(250)
        self.search_box.textChanged.connect(self.filter_channels)
        # Add the search box on the left
        control_layout.addWidget(self.search_box)

        # Add stretch to separate it from buttons
        control_layout.addStretch(1)


        self.jump_now_btn = QPushButton("\u23F0 Jump to Now")
        self.jump_now_btn.setStyleSheet("font-size: 14px; padding: 5px;")
        self.jump_now_btn.clicked.connect(self.scroll_to_current_time)
        control_layout.addWidget(self.jump_now_btn)

        self.close_btn = QPushButton("\u274C Close EPG")
        self.close_btn.setStyleSheet("font-size: 14px; background-color: red; color: white; padding: 5px;")
        self.close_btn.clicked.connect(self.close)
        control_layout.addWidget(self.close_btn)




        layout.addLayout(control_layout, 2, 1, 1, 2, alignment=Qt.AlignRight)

        self.setCentralWidget(container)

        self.build_timeline()
        self.build_channels()
        self.build_program_grid()
        self.draw_now_line()

        self.clock_timer = QTimer(self)
        self.clock_timer.timeout.connect(self.update_clock)
        self.clock_timer.start(1000)

        self.now_line_timer = QTimer(self)
        self.now_line_timer.timeout.connect(self.update_now_line)
        self.now_line_timer.start(30000)

        self.setup_now_autorefresh()

        QTimer.singleShot(100, self.scroll_to_current_time)

    def filter_channels(self, text):
        text = text.strip().lower()

        for row_index, ch_tuple in enumerate(self.channel_order):
            ch_name = ch_tuple[0]  # Unpack the actual channel name
            match = text in ch_name.lower()

            # Filter channel list
            for item in self.channel_scene.items():
                if hasattr(item, "channel_index") and item.channel_index == row_index:
                    item.setVisible(match)

            # Filter program grid
            for item in self.grid_scene.items():
                if hasattr(item, "channel_index") and item.channel_index == row_index:
                    item.setVisible(match)

    def setup_now_autorefresh(self):
        now = datetime.now()
        next_half_hour = now.replace(minute=30 if now.minute < 30 else 0, second=0, microsecond=0)
        if now.minute >= 30:
            next_half_hour += timedelta(hours=1)

        delay_ms = int((next_half_hour - now).total_seconds() * 1000)

        QTimer.singleShot(delay_ms, self.start_now_timer)

    def start_now_timer(self):
        self.scroll_to_current_time()
        self._now_timer = QTimer()
        self._now_timer.timeout.connect(self.scroll_to_current_time)
        self._now_timer.start(30 * 60 * 1000)  # Every 30 minutes


    def update_clock(self):
        now = datetime.now(pytz.timezone("Asia/Karachi"))
        self.clock_label.setText(now.strftime("%a %b %d<br>%I:%M:%S %p"))

    def build_timeline(self):
        shift_px = self.timeline_slot_width // 2  # Shift timeline 15 mins earlier visually

        for index, t in enumerate(self.timeline):
            x = index * self.timeline_slot_width - shift_px

            # Draw background block (as before)
            rect = QGraphicsRectItem(x, 0, self.timeline_slot_width, self.timeline_row_height)
            rect.setBrush(QBrush(QColor("#333")))
            rect.setPen(QPen(QColor("#333")))
            self.timeline_scene.addItem(rect)

            # 🔁 Instead of showing t (start), label with t+30min (end of slot)
            label_time = t + timedelta(minutes=15)
            date_str = label_time.strftime("%a %b %d")   # e.g., "Fri Jun 07"
            time_str = label_time.strftime("%I:%M %p").lstrip("0")  # e.g., "3:30 PM"

            # Combine with line break
            full_label = f"{date_str}\n{time_str}"

            label = QGraphicsTextItem()
            label.setDefaultTextColor(QColor("white"))
            label.setHtml(f"""
                <div align="center" style="font-size:9pt; font-weight:bold; color:white;">
                    {date_str}<br>{time_str}
                </div>
            """)
            text_rect = label.boundingRect()
            label.setPos(
                x + (self.timeline_slot_width - text_rect.width()) / 2,
                (self.timeline_row_height - text_rect.height()) / 2
            )
           


            self.timeline_scene.addItem(label)

    def build_channels(self):
        for row, (ch_name, _) in enumerate(self.channel_order):
            y = row * self.row_height
            rect = QGraphicsRectItem(0, y, self.channel_column_width, self.row_height)
            rect.setBrush(QBrush(QColor("#444") if row % 2 == 0 else QColor("#333")))
            rect.setPen(QPen(QColor("#222")))
            rect.setAcceptHoverEvents(True)
            rect.setData(0, row)

            rect.setAcceptHoverEvents(True)
            rect.setFlag(QGraphicsRectItem.ItemIsSelectable, True)

            self.channel_scene.addItem(rect)

    def build_program_grid(self):
        local_tz = pytz.timezone("Asia/Karachi")

        for row, (ch_name, ch_id) in enumerate(self.channel_order):
            programs = [
                p for p in self.epg_data.findall(".//programme")
                if p.get("channel") == ch_id
            ]

            # Convert to (start, stop, title)
            blocks = []
            for prog in programs:
                try:
                    start = datetime.strptime(prog.get("start")[:14], "%Y%m%d%H%M%S").replace(tzinfo=pytz.utc).astimezone(local_tz)
                    stop = datetime.strptime(prog.get("stop")[:14], "%Y%m%d%H%M%S").replace(tzinfo=pytz.utc).astimezone(local_tz)
                    if stop <= self.timeline_start or start >= self.timeline_end:
                        continue
                    title = prog.findtext("title", "No Title")
                    blocks.append((start, stop, title))
                except Exception as e:
                    print(f"[ERROR] Parsing program failed: {e}")

            blocks.sort(key=lambda x: x[0])

            current_time = self.timeline_start

            for start, stop, title in blocks:
                # Fill any gap before this program
                if start > current_time:
                    gap_start = current_time
                    gap_end = start
                    while gap_start < gap_end:
                        next_gap = min(gap_end, gap_start + timedelta(minutes=60))
                        start_offset = int((gap_start - self.timeline_start).total_seconds() // 1800)
                        duration = int((next_gap - gap_start).total_seconds() // 1800)
                        self.draw_program_block(row, start_offset, duration, "No Program Information")
                        gap_start = next_gap

                # Draw real program
                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)
                self.draw_program_block(row, start_offset, duration, title)
                current_time = stop

            # Fill gap after last program to timeline_end
            if current_time < self.timeline_end:
                gap_start = current_time
                gap_end = self.timeline_end
                while gap_start < gap_end:
                    next_gap = min(gap_end, gap_start + timedelta(minutes=60))
                    start_offset = int((gap_start - self.timeline_start).total_seconds() // 1800)
                    duration = int((next_gap - gap_start).total_seconds() // 1800)
                    self.draw_program_block(row, start_offset, duration, "No Program Information")
                    gap_start = next_gap



    def draw_now_line(self):
        self.now_line = QGraphicsLineItem()
        self.now_line.setPen(QPen(QColor("red"), 2))
        self.grid_scene.addItem(self.now_line)
        self.update_now_line()

    def update_now_line(self):
        now = datetime.now(pytz.timezone("Asia/Karachi"))
        now -= timedelta(minutes=15)  # ✅ align red line with program grid

        if self.timeline_start <= now <= self.timeline_end:
            minutes_since_start = (now - self.timeline_start).total_seconds() / 60
            x = (minutes_since_start / 30) * self.timeline_slot_width
            self.now_line.setLine(x, 0, x, len(self.channel_order) * self.row_height)
            self.now_line.show()
        else:
            self.now_line.hide()

    def scroll_to_current_time(self):
        now = datetime.now(pytz.timezone("Asia/Karachi"))
        now += timedelta(minutes=30)  # match shifted timeline

        # Snap to start of current 30-minute block
        minute = now.minute
        aligned_minute = 0 if minute < 30 else 30
        aligned_now = now.replace(minute=aligned_minute, second=0, microsecond=0)

        # ✅ Calculate X coordinate
        minutes_since_start = (aligned_now - self.timeline_start).total_seconds() / 60
        x = (minutes_since_start / 30)* self.timeline_slot_width

        scroll_x = int(x - self.channel_column_width) +40
        self.grid_view.horizontalScrollBar().setValue(max(scroll_x, 0))

    def sync_vertical_scroll(self, value):
        self.channel_view.verticalScrollBar().setValue(value)
        self.grid_view.verticalScrollBar().setValue(value)

    def channel_double_click_handler(self, graphics_item):
        row = graphics_item.data(0)
        if row is not None and self.main_window:
            if 0 <= row < self.main_window.channel_list.count():
                channel_item = self.main_window.channel_list.item(row)
                self.main_window.channel_list.setCurrentItem(channel_item)
                self.main_window.on_channel_clicked(channel_item)

    def eventFilter(self, watched, event):
        if event.type() in [QEvent.GraphicsSceneHoverEnter, QEvent.GraphicsSceneHoverLeave, QEvent.GraphicsSceneMouseDoubleClick]:
            if hasattr(event, "scenePos"):
                pos = event.scenePos()
                items = watched.items(pos)
                for item in items:
                    if isinstance(item, QGraphicsRectItem):
                        row = item.data(0)
                        if event.type() == QEvent.GraphicsSceneHoverEnter:
                            item.setBrush(QBrush(QColor("#555")))
                            return True
                        elif event.type() == QEvent.GraphicsSceneHoverLeave:
                            color = "#444" if row % 2 == 0 else "#333"
                            item.setBrush(QBrush(QColor(color)))
                            return True
                        elif event.type() == QEvent.GraphicsSceneMouseDoubleClick:
                            if row is not None and self.main_window:
                                if 0 <= row < self.main_window.channel_list.count():
                                    channel_item = self.main_window.channel_list.item(row)
                                    self.main_window.channel_list.setCurrentItem(channel_item)
                                    self.main_window.on_channel_clicked(channel_item)
                                    return True
        return False

    def refresh_channel_fps_info(self):
        self.channel_scene.clear()
        self.build_channels()

    def update_epg(self, new_epg_data, channel_order, channel_video_info):
        self.epg_data = new_epg_data
        self.channel_order = channel_order
        self.channel_video_info = channel_video_info

        # ✅ Stop timers first
        if hasattr(self, "_scroll_timers"):
            for t in self._scroll_timers:
                t.stop()
            self._scroll_timers.clear()


        self.timeline_scene.clear()
        self.channel_scene.clear()
        self.grid_scene.clear()

        self.build_timeline()
        self.build_channels()
        self.build_program_grid()
        self.draw_now_line()

        # ✅ Force both scenes and views to match height exactly
        total_rows = len(self.channel_order)
        scene_height = total_rows * self.row_height
        scene_width = len(self.timeline) * self.timeline_slot_width

        self.grid_scene.setSceneRect(0, 0, scene_width, scene_height)
        self.channel_scene.setSceneRect(0, 0, self.channel_column_width, scene_height)

        self.grid_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.channel_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

        
    def build_channels(self):
        for row, (ch_name, _) in enumerate(self.channel_order):
            y = row * self.row_height
            rect = QGraphicsRectItem(0, y, self.channel_column_width, self.row_height)
            rect.setBrush(QBrush(QColor("#444") if row % 2 == 0 else QColor("#333")))
            rect.setPen(QPen(QColor("#222")))
            rect.setAcceptHoverEvents(True)
            rect.setData(0, row)
            self.channel_scene.addItem(rect)

            
            rect.setFlag(QGraphicsRectItem.ItemIsSelectable, True)

            name_item = QGraphicsSimpleTextItem(f"{row + 1}. {ch_name}")
            name_item.setFont(QFont("Arial", 10, QFont.Bold))
            name_item.setBrush(QColor("white"))
            name_item.setPos(10, y + 4)
            self.channel_scene.addItem(name_item)

            name_item.channel_index = row

            res, fps = self.channel_video_info.get(ch_name, ("NA", "NA"))
            info_item = QGraphicsSimpleTextItem(f"({res}, {fps} FPS)")
            info_item.setFont(QFont("Arial", 9))
            info_item.setBrush(QColor("#CCCCCC"))
            info_width = info_item.boundingRect().width()
            info_item.setPos(self.channel_column_width - info_width - 10, y + self.row_height / 2)
            self.channel_scene.addItem(info_item)

    def draw_program_block(self, row, col_start, duration, title):
        x = col_start * self.timeline_slot_width
        y = row * self.row_height
        w = duration * self.timeline_slot_width
        h = self.row_height - 1

        bg_color = "#444" if row % 2 == 0 else "#333"
        rect = QGraphicsRectItem(x, y, w, h)
        rect.setBrush(QBrush(QColor(bg_color)))
        rect.setPen(QPen(QColor("#222")))
        rect.setAcceptHoverEvents(True)
        rect.setData(0, row)
        self.grid_scene.addItem(rect)

        rect.channel_index = row

        repeat_width = self.timeline_slot_width * 2  # 1 hour = 2 x 30min slots

        if not hasattr(self, "_scroll_timers"):
            self._scroll_timers = []

        # Check if program ends within 30 mins and is longer than 1 hour
        now = datetime.now(pytz.timezone("Asia/Karachi"))
        start_time = self.timeline_start + timedelta(minutes=30 * col_start)
        stop_time = self.timeline_start + timedelta(minutes=30 * (col_start + duration))

        # Only apply right-alignment if:
        # 1. Duration >= 2 (i.e. > 30 mins)
        # 2. Program ENDS within next 30 mins
        # 3. Program STARTED more than 30 mins ago
        starts_within_30_mins = (now - start_time).total_seconds() <= 1800
        ends_within_30_mins = (stop_time - now).total_seconds() <= 1800

        ends_soon = (
            duration == 2 and
            ends_within_30_mins and
            not starts_within_30_mins  # ✅ program must NOT have started recently
        )

        def create_text_block(tx, tw, align="center"):
            container = QGraphicsRectItem(tx, y, tw, h)
            container.setPen(QPen(Qt.NoPen))
            container.setBrush(QBrush(Qt.NoBrush))
            container.setFlag(QGraphicsItem.ItemClipsChildrenToShape, True)
            self.grid_scene.addItem(container)

            text_item = QGraphicsTextItem(container)
            text_item.setDefaultTextColor(QColor("white"))
            font = QFont("Arial", 9)
            text_item.setFont(font)
            text_item.setTextWidth(tw)

            html = f"""
            <div align="{align}" style="line-height:1.2;">
                {title.replace('\n', '<br>')}
            </div>
            """
            text_item.setHtml(html)
            text_rect = text_item.boundingRect()

            # Horizontal offset
            if align == "left":
                offset_x = 5
            elif align == "right":
                offset_x = max(0, tw - text_rect.width() - 5)
            else:  # center
                offset_x = max(5, (tw - text_rect.width()) / 2)

            # Vertical offset
            if text_rect.height() <= h:
                offset_y = y + (h - text_rect.height()) / 2
            else:
                offset_y = y

            text_item.setPos(tx + offset_x, offset_y)

            if text_rect.height() > h:
                scroll_offset = text_rect.height() - h
                scroll_speed = 10
                interval_ms = 50
                pixels_per_tick = scroll_speed * (interval_ms / 1000)

                scroll_state = {
                    "base_x": tx + offset_x,
                    "y_pos": offset_y,
                    "max_offset": offset_y - scroll_offset,
                    "waiting": False,
                    "direction": -1
                }

                def scroll_text(text_item=text_item, state=scroll_state):
                    if state["waiting"]:
                        return

                    state["y_pos"] += state["direction"] * pixels_per_tick

                    if state["direction"] == -1 and state["y_pos"] < state["max_offset"]:
                        state["y_pos"] = state["max_offset"]
                        state["waiting"] = True
                        QTimer.singleShot(2000, lambda: change_direction(1, state))
                    elif state["direction"] == 1 and state["y_pos"] > offset_y:
                        state["y_pos"] = offset_y
                        state["waiting"] = True
                        QTimer.singleShot(2000, lambda: change_direction(-1, state))

                    text_item.setPos(state["base_x"], state["y_pos"])

                def change_direction(new_direction, state):
                    state["direction"] = new_direction
                    state["waiting"] = False

                timer = QTimer()
                timer.timeout.connect(lambda: scroll_text())
                timer.start(interval_ms)

                def on_text_item_destroyed():
                    timer.stop()

                text_item.destroyed.connect(on_text_item_destroyed)
                self._scroll_timers.append(timer)

        if ends_soon:
            # Just one right-aligned title at end
            create_text_block(x + w - repeat_width, repeat_width, align="right")
        else:
            # Repeat title every hour
            num_repeats = max(1, int(w // repeat_width))
            for i in range(num_repeats):
                tx = x + i * repeat_width
                tw = min(repeat_width, x + w - tx)
                create_text_block(tx, tw, align="center")





def start_php_server(php_dir, port=8888):

    global php_proc
    try:
        os.chdir(php_dir)
        php_proc = subprocess.Popen(
            ["php", "-S", f"localhost:{port}"],
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
            creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
        )
        return php_proc
    except Exception as e:
        print(f"Failed to start PHP server: {e}")
        return None

def restart_php_server(php_dir):
    global php_proc
    try:
        if php_proc:
            print("[PHP] Restarting embedded PHP server...")
            php_proc.terminate()
            php_proc.wait(timeout=2)
    except Exception as e:
        print(f"[PHP] Error terminating PHP server: {e}")
    
    # Start again
    php_proc = start_php_server(php_dir)

def monitor_php_server(php_dir):
    global php_proc
    if php_proc and php_proc.poll() is not None:
        print("[WARNING] PHP server exited. Restarting...")
        start_php_server(php_dir)

def wait_for_php_server(host='localhost', port=8888, timeout=3):
    """Wait until PHP server is accepting connections."""
    start_time = time.time()
    while time.time() - start_time < timeout:
        try:
            with socket.create_connection((host, port), timeout=0.5):
                return True
        except (ConnectionRefusedError, socket.timeout, OSError):
            time.sleep(0.1)
    return False



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)

    # ✅ Add this backup cleanup line
    app.aboutToQuit.connect(window.close)

    window.show()

    # ✅ Move PHP monitor timer here BEFORE app.exec_()
    php_timer = QTimer()
    php_timer.timeout.connect(lambda: monitor_php_server(php_dir))
    php_timer.start(15000)

    # ✅ Debug active threads after 1 second
    QTimer.singleShot(1000, lambda: print(f"[DEBUG] Threads at 1s: {[t.name for t in threading.enumerate()]}"))


    exit_code = app.exec_()



    # Stop PHP if still running
    if php_proc:
        php_proc.terminate()
        php_proc.wait(2)

    sys.exit(exit_code)