"""
ESPal fixed implementation with client-server architecture
"""

import os
import sys
import time
import signal
import threading
import socket
import json
import tempfile
import atexit

# Define constants for IPC
SOCKET_PATH = os.path.join(tempfile.gettempdir(), "esppal_socket")
LOCK_FILE = os.path.join(tempfile.gettempdir(), "esppal.lock")
CMD_TIMEOUT = 30  # Seconds to wait for a command to complete

# Import required modules
from .device import DeviceDetector, list_esp_devices
from .monitor import InteractiveMonitor
from .builder import BuildRunner
from .configparser import detect_build_system

# Forward declarations
class ESPalServer:
    """
    Server component for handling IPC between ESPal instances
    """
    def __init__(self, esppal_instance):
        self.esppal = esppal_instance
        self.socket = None
        self.server_thread = None
        self.running = False
        
    def start(self):
        """Start the server thread"""
        # Remove socket file if it exists
        if os.path.exists(SOCKET_PATH):
            try:
                os.unlink(SOCKET_PATH)
            except OSError:
                pass
            
        # Create, bind and listen
        self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.socket.bind(SOCKET_PATH)
        self.socket.listen(5)
        self.socket.settimeout(1)  # 1 second timeout for accept()
        
        # Set file permissions to allow all users to connect
        os.chmod(SOCKET_PATH, 0o777)
        
        # Create lock file with PID
        with open(LOCK_FILE, 'w') as f:
            f.write(str(os.getpid()))
        
        # Register cleanup on exit
        atexit.register(self.cleanup)
        
        # Start server thread
        self.running = True
        self.server_thread = threading.Thread(target=self._server_loop)
        self.server_thread.daemon = True
        self.server_thread.start()
        
        print(f"ESPal server started (PID: {os.getpid()})")
        return True
        
    def _server_loop(self):
        """Main server loop"""
        while self.running:
            try:
                # Accept connection with timeout
                client, _ = self.socket.accept()
                client.settimeout(5)  # 5 second timeout for receive/send
                
                # Handle client
                try:
                    # Receive command
                    data = client.recv(4096).decode('utf-8')
                    if not data:
                        continue
                    
                    # Parse command
                    try:
                        command = json.loads(data)
                        result = self._handle_command(command)
                        
                        # Send response
                        client.send(json.dumps(result).encode('utf-8'))
                    except json.JSONDecodeError:
                        client.send(json.dumps({
                            'success': False,
                            'error': 'Invalid command format'
                        }).encode('utf-8'))
                except Exception as e:
                    # Send error response
                    try:
                        client.send(json.dumps({
                            'success': False,
                            'error': str(e)
                        }).encode('utf-8'))
                    except:
                        pass
                finally:
                    # Close client connection
                    try:
                        client.close()
                    except:
                        pass
            except socket.timeout:
                # This is normal, just continue
                pass
            except Exception as e:
                if self.running:
                    print(f"Server error: {e}")
                    time.sleep(1)  # Add delay to prevent CPU spinning
    
    def _handle_command(self, command):
        """Handle a command from a client"""
        cmd_type = command.get('type')
        
        if cmd_type == 'ping':
            # Simple ping to check if server is running
            return {'success': True, 'message': 'ESPal server is running'}
        
        elif cmd_type == 'build':
            # Build the project
            if self.esppal.is_building:
                return {'success': False, 'error': 'A build is already in progress'}
            
            build_args = command.get('args', None)
            result = self.esppal.run_build_command('build', build_args)
            
            if result:
                return {'success': True, 'message': 'Build started', 'cmd_id': 'build'}
            else:
                return {'success': False, 'error': 'Failed to start build'}
        
        elif cmd_type == 'upload':
            # Upload firmware
            if self.esppal.is_building:
                return {'success': False, 'error': 'A build is already in progress'}
            
            upload_args = command.get('args', None)
            result = self.esppal.run_build_command('upload', upload_args)
            
            if result:
                return {'success': True, 'message': 'Upload started', 'cmd_id': 'upload'}
            else:
                return {'success': False, 'error': 'Failed to start upload'}
        
        elif cmd_type == 'status':
            # Get status information
            status = {
                'is_building': self.esppal.is_building,
                'is_monitoring': self.esppal.is_monitoring,
                'current_device': self.esppal.current_device or '',
                'reset_pending': self.esppal.reset_pending,
                'project_dir': self.esppal.project_dir,
                'build_system': self.esppal.build_system or '',
                'pid': os.getpid()
            }
            return {'success': True, 'status': status}
        
        elif cmd_type == 'quit':
            # Quit the server
            threading.Thread(target=self._delayed_shutdown).start()
            return {'success': True, 'message': 'ESPal server shutting down'}
        
        else:
            return {'success': False, 'error': f'Unknown command type: {cmd_type}'}
    
    def _delayed_shutdown(self):
        """Shut down the server after a short delay to allow the response to be sent"""
        time.sleep(0.5)
        self.stop()
        os._exit(0)  # Force exit
    
    def stop(self):
        """Stop the server"""
        if not self.running:
            return
            
        self.running = False
        
        # Close socket
        if self.socket:
            try:
                self.socket.close()
            except:
                pass
        
        # Wait for server thread to exit
        if self.server_thread and self.server_thread.is_alive():
            self.server_thread.join(2)  # Wait up to 2 seconds
        
        # Clean up
        self.cleanup()
    
    def cleanup(self):
        """Clean up resources"""
        # Remove socket file
        if os.path.exists(SOCKET_PATH):
            try:
                os.unlink(SOCKET_PATH)
            except:
                pass
        
        # Remove lock file
        if os.path.exists(LOCK_FILE):
            try:
                os.unlink(LOCK_FILE)
            except:
                pass

class ESPalClient:
    """
    Client component for sending commands to an existing ESPal instance
    """
    @staticmethod
    def is_server_running():
        """Check if an ESPal server is running"""
        # Check if lock file exists
        if not os.path.exists(LOCK_FILE):
            return False
    
    # Command handlers for interactive mode
    def _cmd_build(self, args):
        """Build command handler"""
        if self.is_building:
            print("A build is already in progress.")
            return
        
        command = args.strip() if args.strip() else None
        self.run_build_command("build", command)
    
    def _cmd_upload(self, args):
        """Upload command handler"""
        if self.is_building:
            print("A build is already in progress.")
            return
        
        command = args.strip() if args.strip() else None
        self.run_build_command("upload", command)
    
    def _cmd_clean(self, args):
        """Clean command handler"""
        if self.is_building:
            print("A build is already in progress.")
            return
        
        command = args.strip() if args.strip() else None
        self.run_build_command("clean", command)
    
    def _cmd_reset(self, args):
        """Reset device command handler"""
        if self.is_monitoring and self.monitor:
            print("Resetting device...")
            self.stop_monitor()
            self.reset_pending = True
            
            # Toggle DTR/RTS lines to reset ESP32
            import serial
            try:
                with serial.Serial(self.current_device, 115200, timeout=0.1) as ser:
                    ser.dtr = False
                    ser.rts = False
                    time.sleep(0.1)
                    ser.dtr = True
                    ser.rts = True
                    time.sleep(0.1)
                    ser.dtr = False
                    ser.rts = False
            except serial.SerialException:
                print("Failed to reset device via serial.")
    
    @staticmethod
    def send_command(command):
        """Send a command to the ESPal server"""
        if not os.path.exists(SOCKET_PATH):
            raise ConnectionError("ESPal server not running or socket not available")
        
        # Create socket and connect
        client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        client.settimeout(CMD_TIMEOUT)
        
        try:
            client.connect(SOCKET_PATH)
            
            # Send command
            client.send(json.dumps(command).encode('utf-8'))
            
            # Receive response
            response = client.recv(4096).decode('utf-8')
            if not response:
                raise ConnectionError("No response from ESPal server")
            
            # Parse response
            return json.loads(response)
        finally:
            # Close connection
            client.close()
    
    @staticmethod
    def wait_for_command(cmd_id, timeout=CMD_TIMEOUT):
        """Wait for a command to complete"""
        start_time = time.time()
        while time.time() - start_time < timeout:
            try:
                status = ESPalClient.send_command({'type': 'status'})
                if status.get('success', False):
                    if not status.get('status', {}).get('is_building', False):
                        return True
            except:
                pass
            time.sleep(1)
        
        return False
    
    @staticmethod
    def quit_server():
        """Tell the server to quit"""
        try:
            return ESPalClient.send_command({'type': 'quit'})
        except:
            return {'success': False, 'error': 'Failed to communicate with server'}
        
        # Check if process is still running
        try:
            with open(LOCK_FILE, 'r') as f:
                pid = int(f.read().strip())
                
            # Check if PID exists
            try:
                # Check if the process exists by sending signal 0
                os.kill(pid, 0)
                
                # Try to connect to the socket to verify it's responsive
                try:
                    result = ESPalClient.send_command({'type': 'ping'})
                    return result.get('success', False)
                except:
                    # Clean up stale lock file and socket
                    if os.path.exists(SOCKET_PATH):
                        try:
                            os.unlink(SOCKET_PATH)
                        except:
                            pass
                    if os.path.exists(LOCK_FILE):
                        try:
                            os.unlink(LOCK_FILE)
                        except:
                            pass
                    return False
            except OSError:
                # Process doesn't exist
                # Clean up stale lock file
                try:
                    os.unlink(LOCK_FILE)
                except:
                    pass
                return False
        except:
            # Error reading lock file
            return False
        
        return False

class ESPal:
    """
    Main ESPal class that integrates all components
    """
    
    def __init__(self, project_dir=None, device=None):
        """
        Initialize ESPal
        
        Args:
            project_dir: Project directory path, defaults to current directory
            device: Specific device path to use instead of auto-detection
        """
        self.project_dir = project_dir or os.getcwd()
        self.specific_device = device
        
        # Detect build system
        self.build_system, self.config = detect_build_system(self.project_dir)
        
        # Initialize components
        self.device_detector = DeviceDetector(
            callback=self._device_state_changed,
            specific_device=device
        )
        
        self.current_device = None
        self.monitor = None
        self.builder = BuildRunner(exit_callback=self._build_completed)
        
        # State tracking
        self.is_monitoring = False
        self.is_building = False
        self.reset_pending = False
        self.exit_requested = False
        
        # Initialize server
        self.server = ESPalServer(self)
        
        # Signal handlers
        signal.signal(signal.SIGINT, self._signal_handler)
        signal.signal(signal.SIGTERM, self._signal_handler)
    
    def _signal_handler(self, sig, frame):
        """
        Handle signals (e.g., Ctrl+C)
        """
        if sig in (signal.SIGINT, signal.SIGTERM):
            print("\nExiting...")
            self.exit_requested = True
            self.cleanup()
            sys.exit(0)
    
    def _device_state_changed(self, connected, device):
        """
        Handle device connection state changes
        
        Args:
            connected: Whether device is connected
            device: Device path
        """
        if connected:
            print(f"Device connected: {device}")
            
            # Check if this is a new device path for the same physical device
            if self.current_device != device:
                old_device = self.current_device
                self.current_device = device
                
                # If we were monitoring the old device, switch to the new one
                if self.is_monitoring and old_device:
                    print(f"Device path changed from {old_device} to {device}")
                    print("Reconnecting monitor to new device path...")
                    
                    # Remember current baudrate
                    current_baudrate = self.get_baudrate()
                    
                    # Stop current monitor
                    self.stop_monitor()
                    
                    # Start new monitor with the same baudrate
                    self.start_monitor(current_baudrate)
            else:
                self.current_device = device
            
            # Auto-restart monitoring if needed
            if self.reset_pending and not self.is_monitoring:
                print("Restarting monitor after reset...")
                self.reset_pending = False
                self.start_monitor()
        else:
            print(f"Device disconnected: {device}")
            if device == self.current_device:
                # Try to find if the device reconnected with a different path
                new_device = self.device_detector.find_device()
                
                if new_device and new_device != device:
                    # Device path changed, handle it
                    print(f"Device appears to have reconnected as {new_device}")
                    self.current_device = new_device
                    
                    # If we were monitoring, reconnect to the new path
                    if self.is_monitoring:
                        print("Reconnecting monitor to new device path...")
                        current_baudrate = self.get_baudrate()
                        self.stop_monitor()
                        self.start_monitor(current_baudrate)
                else:
                    # Device truly disconnected
                    self.current_device = None
                    
                    # Stop monitoring if running
                    if self.is_monitoring:
                        print("Stopping monitor due to device disconnect...")
                        self.stop_monitor()
                        # Mark as reset pending to restart later
                        self.reset_pending = True
    
    def _build_completed(self, exit_code):
        """
        Handle build completion
        
        Args:
            exit_code: Build exit code
        """
        self.is_building = False
        
        # If we stopped monitoring for a build/upload, restart it
        if self.reset_pending and not self.is_monitoring:
            # Wait a moment for device to reconnect if necessary
            time.sleep(1)
            self.reset_pending = False
            
            # Find device again
            if not self.current_device:
                self.current_device = self.device_detector.find_device()
            
            if self.current_device:
                self.start_monitor()
            else:
                print("Waiting for device to reconnect...")
                self.device_detector.start_monitoring()
    
    def detect_device(self):
        """
        Detect ESP32 device
        
        Returns:
            str: Device path if found, None otherwise
        """
        # First try specified device if any
        if self.specific_device and os.path.exists(self.specific_device):
            self.current_device = self.specific_device
            return self.specific_device
        
        # Then try to get from config if available
        if self.config:
            env = self.config.get_default_environment()
            if env:
                device_from_config = self.config.get_upload_port(env)
                if device_from_config and os.path.exists(device_from_config):
                    self.current_device = device_from_config
                    return device_from_config
        
        # Finally try auto-detection
        self.current_device = self.device_detector.find_device()
        return self.current_device
    
    def get_baudrate(self):
        """
        Get baudrate for the current device
        
        Returns:
            int: Baudrate value
        """
        if self.config:
            env = self.config.get_default_environment()
            if env:
                return self.config.get_baudrate(env)
        
        # Default baudrate
        return 115200
    
    def start_monitor(self, baudrate=None):
        """
        Start serial monitoring
        
        Args:
            baudrate: Serial baudrate, if None, auto-detect from config
            
        Returns:
            bool: True if successfully started, False otherwise
        """
        if self.is_monitoring:
            print("Already monitoring.")
            return True
        
        # Make sure we have a device
        if not self.current_device:
            if not self.detect_device():
                print("No device found to monitor.")
                return False
        
        # Use provided baudrate or get from config
        if baudrate is None:
            baudrate = self.get_baudrate()
        
        print(f"Starting monitor on {self.current_device} at {baudrate} baud")
        
        # Create and start the monitor
        self.monitor = InteractiveMonitor(
            self.current_device, 
            baudrate=baudrate,
            exit_callback=self._monitor_exited
        )
        
        # Register ESPal-specific commands
        self.monitor.register_command("build", self._cmd_build, "Build the project")
        self.monitor.register_command("upload", self._cmd_upload, "Upload firmware to the device")
        self.monitor.register_command("clean", self._cmd_clean, "Clean the project")
        self.monitor.register_command("reset", self._cmd_reset, "Reset the device")
        self.monitor.register_command("baudrate", self._cmd_baudrate, "Change monitor baudrate")
        self.monitor.register_command("quit", self._cmd_quit, "Cleanly exit ESPal")
        
        # Register shortcut for build (Ctrl+B)
        self.monitor.register_shortcut("^B", self._shortcut_ctrl_b, "Build the project")
        
        # Start monitoring
        if self.monitor.start():
            self.is_monitoring = True
            return True
        else:
            print("Failed to start monitor.")
            self.monitor = None
            return False
    
    def stop_monitor(self):
        """
        Stop serial monitoring
        
        Returns:
            bool: True if successfully stopped, False otherwise
        """
        if not self.is_monitoring or not self.monitor:
            return False
        
        self.monitor.stop()
        self.monitor = None
        self.is_monitoring = False
    
    def run_build_command(self, command_type, command=None):
        """
        Run a build-related command
        
        Args:
            command_type: Type of command (build, upload, clean)
            command: Command to run, if None, generate from build system
            
        Returns:
            bool: True if command started successfully
        """
        # Stop monitoring if running (will restart after build)
        was_monitoring = self.is_monitoring
        if was_monitoring:
            print("Pausing monitor for build...")
            self.stop_monitor()
            self.reset_pending = True
        
        if not command and self.build_system:
            # Generate command based on build system
            if self.build_system == "platformio":
                env = self.config.get_default_environment()
                env_arg = f"-e {env}" if env else ""
                
                if command_type == "build":
                    command = f"platformio run {env_arg}"
                elif command_type == "upload":
                    command = f"platformio run {env_arg} --target upload"
                elif command_type == "clean":
                    command = f"platformio run {env_arg} --target clean"
            
            elif self.build_system == "arduino":
                if self.config:
                    board = self.config.get_default_environment()
                    if not board:
                        print("No board specified in Arduino config")
                        return False
                    
                    if command_type == "build":
                        command = f"arduino-cli compile --fqbn {board} ."
                    elif command_type == "upload":
                        port = self.current_device or ""
                        port_arg = f"--port {port}" if port else ""
                        command = f"arduino-cli upload --fqbn {board} {port_arg} ."
                    elif command_type == "clean":
                        command = f"arduino-cli compile --clean --fqbn {board} ."
            
            elif self.build_system == "espidf":
                if command_type == "build":
                    command = "idf.py build"
                elif command_type == "upload":
                    port = f"-p {self.current_device}" if self.current_device else ""
                    command = f"idf.py {port} flash"
                elif command_type == "clean":
                    command = "idf.py clean"
        
        if not command:
            print(f"No {command_type} command specified or build system detected")
            return False
        
        # Define output callback to display in console
        def output_callback(data, is_stderr=False):
            if is_stderr:
                sys.stderr.write(data)
            else:
                sys.stdout.write(data)
        
        # Run the command
        print(f"Running {command_type} command: {command}")
        
        if command_type == "build":
            result = self.builder.build(command, output_callback, cwd=self.project_dir)
        elif command_type == "upload":
            result = self.builder.upload(command, output_callback, cwd=self.project_dir)
        elif command_type == "clean":
            result = self.builder.clean(command, output_callback, cwd=self.project_dir)
        else:
            print(f"Unknown command type: {command_type}")
            return False
        
        if result:
            self.is_building = True
            return True
        else:
            print(f"Failed to start {command_type} command")
            
            # If we stopped monitoring, but failed to start build, restart it
            if was_monitoring and self.reset_pending:
                print("Restarting monitor...")
                self.reset_pending = False
                self.start_monitor()
            
            return False
        return True
    
    def _monitor_exited(self):
        """
        Handle monitor exit
        """
        self.monitor = None
        self.is_monitoring = False

# Main function stub
def main():
    pass

if __name__ == "__main__":
    sys.exit(main())
