"""
Serial monitor module for ESPal

This module provides functionality to:
- Monitor ESP32 serial output
- Log data to versioned log files
- Support interactive commands during monitoring
"""

import os
import sys
import time
import threading
import datetime
import serial


class SerialMonitor:
    """Handles serial port monitoring and interaction"""
    
    def __init__(
        self, 
        device_path, 
        baudrate=115200, 
        log_dir="logs/monitor", 
        exit_callback=None
    ):
        """
        Initialize serial monitor
        
        Args:
            device_path: Serial device path
            baudrate: Serial baudrate
            log_dir: Directory to store log files
            exit_callback: Function to call when monitor exits
        """
        self.device_path = device_path
        self.baudrate = baudrate
        
        # Use absolute path for logs directory
        if not os.path.isabs(log_dir):
            base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
            self.log_dir = os.path.join(base_dir, log_dir)
        else:
            self.log_dir = log_dir
            
        self.exit_callback = exit_callback
        
        self.serial_conn = None
        self.monitoring = False
        self.monitor_thread = None
        self.log_file = None
        self.log_file_path = None
        
        # Create log directory if it doesn't exist
        os.makedirs(self.log_dir, exist_ok=True)
    
    def _generate_log_filename(self):
        """
        Generate timestamped log filename
        
        Returns:
            str: Log filename
        """
        timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
        return f"monitor_{timestamp}.log"
    
    def _update_symlink(self):
        """Update the latest.log symlink to point to the current log file"""
        symlink_path = os.path.join(self.log_dir, "monitor.log")
        
        # Remove existing symlink if it exists
        if os.path.exists(symlink_path):
            try:
                os.remove(symlink_path)
            except OSError:
                # If we can't remove it, just continue
                pass
        
        # Create new symlink
        try:
            os.symlink(os.path.basename(self.log_file_path), symlink_path)
        except OSError:
            # If we can't create the symlink, just continue
            pass
    
    def start(self):
        """
        Start serial monitoring
        
        Returns:
            bool: True if successfully started, False otherwise
        """
        if self.monitoring:
            return True
        
        try:
            self.serial_conn = serial.Serial(
                self.device_path, 
                baudrate=self.baudrate,
                timeout=0.1  # Short timeout for responsive reading
            )
        except serial.SerialException as e:
            print(f"Error opening serial port: {e}")
            return False
        
        # Open log file
        self.log_file_path = os.path.join(self.log_dir, self._generate_log_filename())
        try:
            self.log_file = open(self.log_file_path, 'wb')
            self._update_symlink()
        except IOError as e:
            print(f"Error opening log file: {e}")
            self.serial_conn.close()
            self.serial_conn = None
            return False
        
        # Start monitoring thread
        self.monitoring = True
        self.monitor_thread = threading.Thread(target=self._monitor_thread)
        self.monitor_thread.daemon = True
        self.monitor_thread.start()
        
        return True
    
    def stop(self):
        """Stop serial monitoring"""
        self.monitoring = False
        
        if self.monitor_thread:
            self.monitor_thread.join(timeout=2.0)
            self.monitor_thread = None
        
        if self.serial_conn:
            try:
                self.serial_conn.close()
            except Exception as e:
                print(f"Error closing serial connection: {e}")
            self.serial_conn = None
        
        if self.log_file:
            try:
                self.log_file.close()
            except Exception as e:
                print(f"Error closing log file: {e}")
            self.log_file = None
        
        print("Monitor stopped")
    
    def _monitor_thread(self):
        """Main monitoring thread function"""
        reconnect_delay = 1.0  # Start with 1 second delay for reconnect attempts
        max_reconnect_delay = 5.0  # Maximum delay between reconnect attempts
        
        try:
            while self.monitoring:
                # Check if serial connection is valid
                if not self.serial_conn:
                    try:
                        # Try to reconnect
                        print(f"\nAttempting to reconnect to {self.device_path}...")
                        self.serial_conn = serial.Serial(
                            self.device_path, 
                            baudrate=self.baudrate,
                            timeout=0.1
                        )
                        print(f"Successfully reconnected to {self.device_path}")
                        
                        # Reset reconnect delay on successful connection
                        reconnect_delay = 1.0
                    except serial.SerialException as e:
                        # Failed to reconnect
                        print(f"Reconnect failed: {e}")
                        
                        # Sleep before next reconnect attempt with exponential backoff
                        time.sleep(reconnect_delay)
                        
                        # Increase delay for next attempt, up to max_reconnect_delay
                        reconnect_delay = min(reconnect_delay * 1.5, max_reconnect_delay)
                        continue
                
                # Attempt to read data from the serial port
                try:
                    if self.serial_conn and self.serial_conn.in_waiting > 0:
                        data = self.serial_conn.read(self.serial_conn.in_waiting)
                        
                        # Write to log file
                        if self.log_file:
                            self.log_file.write(data)
                            self.log_file.flush()
                        
                        # Write to console
                        sys.stdout.buffer.write(data)
                        sys.stdout.buffer.flush()
                except (serial.SerialException, OSError, IOError) as e:
                    # Serial port error or device disconnected
                    print(f"\nSerial connection lost: {e}")
                    
                    # Close the current connection
                    if self.serial_conn:
                        try:
                            self.serial_conn.close()
                        except Exception:
                            pass
                        self.serial_conn = None
                    
                    # Wait before attempting to reconnect
                    time.sleep(reconnect_delay)
                    continue
                
                # Small sleep to prevent CPU hogging
                time.sleep(0.01)
        
        finally:
            # Clean up
            if self.monitoring:
                self.stop()
            
            if self.exit_callback:
                self.exit_callback()
    
    def send(self, data):
        """
        Send data to the serial port
        
        Args:
            data: Data to send, can be string or bytes
            
        Returns:
            bool: True if data was sent, False otherwise
        """
        if not self.serial_conn:
            return False
        
        # Convert string to bytes if needed
        if isinstance(data, str):
            data = data.encode('utf-8')
        
        try:
            self.serial_conn.write(data)
            return True
        except serial.SerialException:
            return False
    
    def is_active(self):
        """
        Check if the monitor is active
        
        Returns:
            bool: True if active, False otherwise
        """
        return self.monitoring and self.monitor_thread and self.monitor_thread.is_alive()


class InteractiveMonitor(SerialMonitor):
    """Extends SerialMonitor with interactive commands and keyboard shortcuts"""
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.input_thread = None
        self.command_handlers = {}
        self.shortcuts = {}
        
        # Register default commands
        self.register_command("help", self._cmd_help, "Show available commands")
        self.register_command("quit", self._cmd_quit, "Exit the monitor")
        self.register_command("send", self._cmd_send, "Send data to device")
        self.register_command("reconnect", self._cmd_reconnect, "Force reconnection to the device")
        
        # Register default shortcuts
        self.register_shortcut("^C", self._shortcut_ctrl_c, "Exit the monitor")
    
    def register_command(self, command, handler, help_text=""):
        """
        Register a command handler
        
        Args:
            command: Command string
            handler: Function to call when command is entered
            help_text: Help text for the command
        """
        self.command_handlers[command] = {
            'handler': handler,
            'help': help_text
        }
    
    def register_shortcut(self, key_sequence, handler, help_text=""):
        """
        Register a keyboard shortcut handler
        
        Args:
            key_sequence: Key sequence string (e.g., "^C" for Ctrl+C)
            handler: Function to call when shortcut is pressed
            help_text: Help text for the shortcut
        """
        self.shortcuts[key_sequence] = {
            'handler': handler,
            'help': help_text
        }
    
    def start(self):
        """Start monitoring with interactive input"""
        if not super().start():
            return False
        
        # Start input thread
        self.input_thread = threading.Thread(target=self._input_thread)
        self.input_thread.daemon = True
        self.input_thread.start()
        
        return True
    
    def stop(self):
        """Stop monitoring"""
        super().stop()
        
        if self.input_thread:
            # Just let it terminate naturally as it checks self.monitoring
            self.input_thread = None
    
    def _input_thread(self):
        """Input thread to handle commands"""
        import re
        
        # Print initial message
        print("\nESPal Interactive Monitor")
        print("Type 'help' for available commands\n")
        
        while self.monitoring:
            try:
                user_input = input(">> ")
                
                # Parse command
                match = re.match(r'^(\w+)(?:\s+(.*))?$', user_input)
                if match:
                    cmd = match.group(1)
                    args = match.group(2) or ""
                    
                    if cmd in self.command_handlers:
                        self.command_handlers[cmd]['handler'](args)
                    else:
                        print(f"Unknown command: {cmd}")
                        print("Type 'help' for available commands")
            
            except EOFError:
                # Ctrl+D pressed
                break
            
            except KeyboardInterrupt:
                # Ctrl+C pressed
                if "^C" in self.shortcuts:
                    self.shortcuts["^C"]['handler']()
                break
            
            except Exception as e:
                print(f"Error: {e}")
    
    # Default command handlers
    def _cmd_help(self, args):
        """Show help for available commands"""
        print("\nAvailable commands:")
        for cmd, info in sorted(self.command_handlers.items()):
            print(f"  {cmd:<10} - {info['help']}")
        
        if self.shortcuts:
            print("\nKeyboard shortcuts:")
            for key, info in sorted(self.shortcuts.items()):
                print(f"  {key:<10} - {info['help']}")
        print()
    
    def _cmd_quit(self, args):
        """Exit the monitor"""
        print("Exiting monitor...")
        self.stop()
    
    def _cmd_send(self, args):
        """Send data to the device"""
        if args:
            self.send(args + '\r\n')
        else:
            print("Usage: send <data>")
    
    def _cmd_reconnect(self, args):
        """Force reconnection to the device"""
        print(f"Attempting to reconnect to {self.device_path}...")
        
        # Close any existing connection
        if self.serial_conn:
            try:
                self.serial_conn.close()
            except Exception:
                pass
            self.serial_conn = None
        
        # Try to open a new connection
        try:
            self.serial_conn = serial.Serial(
                self.device_path, 
                baudrate=self.baudrate,
                timeout=0.1
            )
            print(f"Successfully reconnected to {self.device_path}")
        except serial.SerialException as e:
            print(f"Failed to reconnect: {e}")
    
    # Default shortcut handlers
    def _shortcut_ctrl_c(self):
        """Handle Ctrl+C"""
        print("\nCtrl+C pressed, exiting...")
        self.stop()


# Simple test function for module
if __name__ == "__main__":
    import argparse
    
    parser = argparse.ArgumentParser(description='Test serial monitor')
    parser.add_argument('--port', '-p', help='Serial port device')
    parser.add_argument('--baud', '-b', type=int, default=115200, help='Baudrate')
    args = parser.parse_args()
    
    if not args.port:
        # Try to find a serial port
        from esppal.device import DeviceDetector
        detector = DeviceDetector()
        port = detector.find_device()
        if not port:
            print("No serial port found. Specify with --port.")
            sys.exit(1)
    else:
        port = args.port
    
    print(f"Starting monitor on {port} at {args.baud} baud")
    
    monitor = InteractiveMonitor(port, baudrate=args.baud)
    if not monitor.start():
        print("Failed to start monitor")
        sys.exit(1)
    
    try:
        # This will keep the main thread alive until monitor stops
        while monitor.is_active():
            time.sleep(0.1)
    except KeyboardInterrupt:
        print("\nExiting...")
    finally:
        monitor.stop()
