#!/usr/bin/env python3
"""
MBetter Client Discovery Application

A Qt6-based LAN discovery tool that automatically detects MBetterClient servers
on the local network and opens the dashboard in a web browser.

Compatible with Linux and Windows.
"""

import sys
import json
import socket
import threading
import webbrowser
from datetime import datetime
from typing import Optional, Dict, Any

try:
    from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, 
                                QWidget, QPushButton, QLabel, QTextEdit, QSystemTrayIcon, 
                                QMenu, QMessageBox, QGroupBox, QCheckBox, QSpinBox,
                                QLineEdit, QListWidget, QListWidgetItem)
    from PyQt6.QtCore import QTimer, pyqtSignal, QObject, Qt, QThread
    from PyQt6.QtGui import QIcon, QPixmap, QFont, QAction
except ImportError as e:
    print("Error: PyQt6 is required but not installed.")
    print("Please install it with: pip install PyQt6")
    sys.exit(1)


class UDPDiscoveryWorker(QObject):
    """Worker thread for UDP discovery"""
    
    server_found = pyqtSignal(dict)  # Signal when server is found
    error_occurred = pyqtSignal(str)  # Signal when error occurs
    
    def __init__(self, listen_port: int = 45123):
        super().__init__()
        self.listen_port = listen_port
        self.running = False
        self.socket: Optional[socket.socket] = None
    
    def start_listening(self):
        """Start listening for UDP broadcasts"""
        self.running = True
        
        try:
            # Create UDP socket
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.socket.bind(('', self.listen_port))
            self.socket.settimeout(2.0)  # 2 second timeout
            
            print(f"Listening for broadcasts on port {self.listen_port}...")
            
            while self.running:
                try:
                    data, addr = self.socket.recvfrom(1024)
                    self._process_broadcast(data, addr)
                    
                except socket.timeout:
                    continue  # Normal timeout, keep listening
                except Exception as e:
                    if self.running:  # Only emit error if still supposed to be running
                        self.error_occurred.emit(f"Socket error: {e}")
                        break
                        
        except Exception as e:
            self.error_occurred.emit(f"Failed to start UDP listener: {e}")
        finally:
            self._cleanup()
    
    def stop_listening(self):
        """Stop listening for UDP broadcasts"""
        self.running = False
        if self.socket:
            try:
                self.socket.close()
            except:
                pass
    
    def _process_broadcast(self, data: bytes, addr: tuple):
        """Process received UDP broadcast data"""
        try:
            # Decode JSON data
            message = json.loads(data.decode('utf-8'))
            
            # Validate message structure
            if (isinstance(message, dict) and 
                message.get('service') == 'MBetterClient' and
                'url' in message):
                
                # Add source address info
                message['source_ip'] = addr[0]
                message['discovered_at'] = datetime.now().isoformat()
                
                print(f"Discovered MBetterClient: {message['url']} from {addr[0]}")
                self.server_found.emit(message)
                
        except (json.JSONDecodeError, UnicodeDecodeError) as e:
            print(f"Invalid broadcast data from {addr[0]}: {e}")
        except Exception as e:
            print(f"Error processing broadcast from {addr[0]}: {e}")
    
    def _cleanup(self):
        """Cleanup resources"""
        if self.socket:
            try:
                self.socket.close()
            except:
                pass
            self.socket = None


class MBetterDiscoveryApp(QMainWindow):
    """Main application window"""
    
    def __init__(self):
        super().__init__()
        self.setWindowTitle("MBetter Client Discovery")
        self.setGeometry(100, 100, 600, 500)
        
        # Discovery worker and thread
        self.discovery_worker = None
        self.discovery_thread = None
        
        # Settings
        self.auto_open_browser = True
        self.exit_after_open = True  # Exit after opening browser
        self.listen_port = 45123
        self.discovered_servers = {}  # IP -> server_info
        
        # Setup UI
        self.setup_ui()
        self.setup_system_tray()
        
        # Start discovery
        self.start_discovery()
        
        # Status update timer
        self.status_timer = QTimer()
        self.status_timer.timeout.connect(self.update_status)
        self.status_timer.start(1000)  # Update every second
    
    def setup_ui(self):
        """Setup the user interface"""
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        
        # Title
        title_label = QLabel("MBetter Client Discovery")
        title_font = QFont()
        title_font.setPointSize(16)
        title_font.setBold(True)
        title_label.setFont(title_font)
        title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(title_label)
        
        # Description
        desc_label = QLabel("Automatically discovers MBetterClient servers on your local network")
        desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        desc_label.setStyleSheet("color: gray; margin-bottom: 10px;")
        layout.addWidget(desc_label)
        
        # Settings group
        settings_group = QGroupBox("Settings")
        settings_layout = QVBoxLayout(settings_group)
        
        # Auto-open browser checkbox
        self.auto_open_checkbox = QCheckBox("Automatically open browser when server found")
        self.auto_open_checkbox.setChecked(self.auto_open_browser)
        self.auto_open_checkbox.stateChanged.connect(self.on_auto_open_changed)
        settings_layout.addWidget(self.auto_open_checkbox)
        
        # Exit after open checkbox
        self.exit_after_open_checkbox = QCheckBox("Exit application after opening browser")
        self.exit_after_open_checkbox.setChecked(self.exit_after_open)
        self.exit_after_open_checkbox.stateChanged.connect(self.on_exit_after_open_changed)
        settings_layout.addWidget(self.exit_after_open_checkbox)
        
        # Listen port setting
        port_layout = QHBoxLayout()
        port_layout.addWidget(QLabel("Listen Port:"))
        self.port_spinbox = QSpinBox()
        self.port_spinbox.setRange(1024, 65535)
        self.port_spinbox.setValue(self.listen_port)
        self.port_spinbox.valueChanged.connect(self.on_port_changed)
        port_layout.addWidget(self.port_spinbox)
        port_layout.addStretch()
        settings_layout.addLayout(port_layout)
        
        layout.addWidget(settings_group)
        
        # Discovered servers group
        servers_group = QGroupBox("Discovered Servers")
        servers_layout = QVBoxLayout(servers_group)
        
        self.servers_list = QListWidget()
        self.servers_list.itemDoubleClicked.connect(self.on_server_double_clicked)
        servers_layout.addWidget(self.servers_list)
        
        # Manual connection
        manual_layout = QHBoxLayout()
        manual_layout.addWidget(QLabel("Manual URL:"))
        self.manual_url_input = QLineEdit()
        self.manual_url_input.setPlaceholderText("http://192.168.1.100:5001")
        manual_layout.addWidget(self.manual_url_input)
        
        self.connect_button = QPushButton("Connect")
        self.connect_button.clicked.connect(self.on_manual_connect)
        manual_layout.addWidget(self.connect_button)
        
        servers_layout.addLayout(manual_layout)
        layout.addWidget(servers_group)
        
        # Status and log group
        status_group = QGroupBox("Status & Log")
        status_layout = QVBoxLayout(status_group)
        
        self.status_label = QLabel("Starting discovery...")
        status_layout.addWidget(self.status_label)
        
        self.log_text = QTextEdit()
        self.log_text.setMaximumHeight(150)
        self.log_text.setReadOnly(True)
        status_layout.addWidget(self.log_text)
        
        layout.addWidget(status_group)
        
        # Control buttons
        button_layout = QHBoxLayout()
        
        self.start_stop_button = QPushButton("Stop Discovery")
        self.start_stop_button.clicked.connect(self.toggle_discovery)
        button_layout.addWidget(self.start_stop_button)
        
        self.clear_log_button = QPushButton("Clear Log")
        self.clear_log_button.clicked.connect(self.clear_log)
        button_layout.addWidget(self.clear_log_button)
        
        button_layout.addStretch()
        
        self.minimize_button = QPushButton("Minimize to Tray")
        self.minimize_button.clicked.connect(self.hide)
        button_layout.addWidget(self.minimize_button)
        
        layout.addLayout(button_layout)
    
    def setup_system_tray(self):
        """Setup system tray icon"""
        if not QSystemTrayIcon.isSystemTrayAvailable():
            self.log("System tray not available on this system")
            return
        
        # Create tray icon
        self.tray_icon = QSystemTrayIcon(self)
        
        # Create a simple icon (you can replace with a proper icon file)
        pixmap = QPixmap(16, 16)
        pixmap.fill(Qt.GlobalColor.blue)
        self.tray_icon.setIcon(QIcon(pixmap))
        
        # Create tray menu
        tray_menu = QMenu()
        
        show_action = QAction("Show Discovery Window", self)
        show_action.triggered.connect(self.show)
        tray_menu.addAction(show_action)
        
        tray_menu.addSeparator()
        
        quit_action = QAction("Exit", self)
        quit_action.triggered.connect(self.close_application)
        tray_menu.addAction(quit_action)
        
        self.tray_icon.setContextMenu(tray_menu)
        self.tray_icon.activated.connect(self.on_tray_activated)
        self.tray_icon.show()
    
    def start_discovery(self):
        """Start UDP discovery"""
        try:
            # Stop existing discovery if running
            self.stop_discovery()
            
            # Create worker and thread
            self.discovery_worker = UDPDiscoveryWorker(self.listen_port)
            self.discovery_thread = QThread()
            
            # Move worker to thread
            self.discovery_worker.moveToThread(self.discovery_thread)
            
            # Connect signals
            self.discovery_worker.server_found.connect(self.on_server_found)
            self.discovery_worker.error_occurred.connect(self.on_discovery_error)
            
            # Connect thread signals
            self.discovery_thread.started.connect(self.discovery_worker.start_listening)
            self.discovery_thread.finished.connect(self.discovery_worker.deleteLater)
            
            # Start thread
            self.discovery_thread.start()
            
            self.log("Discovery started successfully")
            self.start_stop_button.setText("Stop Discovery")
            
        except Exception as e:
            self.log(f"Failed to start discovery: {e}")
    
    def stop_discovery(self):
        """Stop UDP discovery"""
        if self.discovery_worker:
            self.discovery_worker.stop_listening()
        
        if self.discovery_thread and self.discovery_thread.isRunning():
            self.discovery_thread.quit()
            self.discovery_thread.wait(3000)  # Wait up to 3 seconds
        
        self.discovery_worker = None
        self.discovery_thread = None
        
        self.log("Discovery stopped")
        self.start_stop_button.setText("Start Discovery")
    
    def on_server_found(self, server_info: Dict[str, Any]):
        """Handle discovered server"""
        try:
            server_ip = server_info.get('source_ip', 'unknown')
            server_url = server_info.get('url', '')
            ssl_enabled = server_info.get('ssl', False)
            
            # Store server info
            self.discovered_servers[server_ip] = server_info
            
            # Update servers list
            self.update_servers_list()
            
            # Log discovery
            ssl_text = " (SSL)" if ssl_enabled else ""
            self.log(f"Discovered: {server_url}{ssl_text} from {server_ip}")
            
            # Auto-open browser if enabled
            if self.auto_open_browser:
                self.open_browser(server_url)
                self.log(f"Opened browser: {server_url}")
                
                # Exit after opening browser if enabled
                if self.exit_after_open:
                    self.log("Exiting application after opening browser")
                    # Give a moment for the browser to start
                    QTimer.singleShot(1000, self.close_application)
            
            # Show tray notification
            if hasattr(self, 'tray_icon') and self.tray_icon.isVisible():
                self.tray_icon.showMessage(
                    "MBetter Server Found",
                    f"Found server at {server_url}",
                    QSystemTrayIcon.MessageIcon.Information,
                    3000
                )
                
        except Exception as e:
            self.log(f"Error handling discovered server: {e}")
    
    def on_discovery_error(self, error_msg: str):
        """Handle discovery error"""
        self.log(f"Discovery error: {error_msg}")
    
    def update_servers_list(self):
        """Update the list of discovered servers"""
        self.servers_list.clear()
        
        for ip, server_info in self.discovered_servers.items():
            url = server_info.get('url', 'Unknown URL')
            ssl_text = " [SSL]" if server_info.get('ssl', False) else ""
            discovered_time = server_info.get('discovered_at', 'Unknown time')
            
            # Parse time for display
            try:
                dt = datetime.fromisoformat(discovered_time)
                time_str = dt.strftime("%H:%M:%S")
            except:
                time_str = "Unknown"
            
            item_text = f"{url}{ssl_text} - {ip} (discovered at {time_str})"
            
            item = QListWidgetItem(item_text)
            item.setData(Qt.ItemDataRole.UserRole, server_info)
            self.servers_list.addItem(item)
    
    def on_server_double_clicked(self, item: QListWidgetItem):
        """Handle double-click on server item"""
        server_info = item.data(Qt.ItemDataRole.UserRole)
        if server_info:
            url = server_info.get('url', '')
            if url:
                self.open_browser(url)
                self.log(f"Opened browser: {url}")
                
                # Exit after opening browser if enabled
                if self.exit_after_open:
                    self.log("Exiting application after manual browser open")
                    # Give a moment for the browser to start
                    QTimer.singleShot(1000, self.close_application)
    
    def open_browser(self, url: str):
        """Open URL in default web browser"""
        try:
            webbrowser.open(url)
        except Exception as e:
            self.log(f"Failed to open browser for {url}: {e}")
    
    def on_auto_open_changed(self, state):
        """Handle auto-open browser setting change"""
        self.auto_open_browser = state == Qt.CheckState.Checked.value
        self.log(f"Auto-open browser: {'enabled' if self.auto_open_browser else 'disabled'}")
    
    def on_exit_after_open_changed(self, state):
        """Handle exit after open setting change"""
        self.exit_after_open = state == Qt.CheckState.Checked.value
        self.log(f"Exit after open: {'enabled' if self.exit_after_open else 'disabled'}")
    
    def on_port_changed(self, port):
        """Handle port change"""
        if port != self.listen_port:
            self.listen_port = port
            self.log(f"Listen port changed to {port}. Restart discovery to apply.")
    
    def on_manual_connect(self):
        """Handle manual connection"""
        url = self.manual_url_input.text().strip()
        if url:
            if not url.startswith(('http://', 'https://')):
                url = 'http://' + url
            self.open_browser(url)
            self.log(f"Manual connection: {url}")
            
            # Exit after opening browser if enabled
            if self.exit_after_open:
                self.log("Exiting application after manual connection")
                # Give a moment for the browser to start
                QTimer.singleShot(1000, self.close_application)
        else:
            QMessageBox.warning(self, "Warning", "Please enter a URL")
    
    def toggle_discovery(self):
        """Toggle discovery on/off"""
        if self.discovery_thread and self.discovery_thread.isRunning():
            self.stop_discovery()
        else:
            self.start_discovery()
    
    def clear_log(self):
        """Clear the log text"""
        self.log_text.clear()
    
    def update_status(self):
        """Update status display"""
        if self.discovery_thread and self.discovery_thread.isRunning():
            server_count = len(self.discovered_servers)
            status = f"Discovery running on port {self.listen_port} - {server_count} server(s) found"
        else:
            status = "Discovery stopped"
        
        self.status_label.setText(status)
    
    def log(self, message: str):
        """Add message to log"""
        timestamp = datetime.now().strftime("%H:%M:%S")
        log_message = f"[{timestamp}] {message}"
        self.log_text.append(log_message)
        print(log_message)  # Also print to console
    
    def on_tray_activated(self, reason):
        """Handle tray icon activation"""
        if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
            if self.isVisible():
                self.hide()
            else:
                self.show()
                self.raise_()
                self.activateWindow()
    
    def closeEvent(self, event):
        """Handle window close event"""
        if hasattr(self, 'tray_icon') and self.tray_icon.isVisible():
            # Hide to tray instead of closing
            event.ignore()
            self.hide()
            self.tray_icon.showMessage(
                "MBetter Discovery",
                "Application was minimized to tray",
                QSystemTrayIcon.MessageIcon.Information,
                2000
            )
        else:
            self.close_application()
    
    def close_application(self):
        """Close the application completely"""
        self.stop_discovery()
        
        if hasattr(self, 'tray_icon'):
            self.tray_icon.hide()
        
        QApplication.quit()


def main():
    """Main entry point"""
    app = QApplication(sys.argv)
    app.setApplicationName("MBetter Discovery")
    app.setApplicationVersion("1.0.10")
    app.setQuitOnLastWindowClosed(False)  # Keep running in tray
    
    # Create and show main window
    window = MBetterDiscoveryApp()
    window.show()
    
    # Run application
    sys.exit(app.exec())


if __name__ == "__main__":
    main()