"""
Builder module for ESPal

This module provides functionality to:
- Execute build commands
- Capture command output
- Log build results
"""

import os
import sys
import time
import subprocess
import threading
import datetime
import select
import shlex


class CommandRunner:
    """
    Execute commands and capture their output
    """
    
    def __init__(
        self, 
        log_dir="logs/build", 
        exit_callback=None
    ):
        """
        Initialize command runner
        
        Args:
            log_dir: Directory to store command output logs
            exit_callback: Function to call when command exits
        """
        # 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.process = None
        self.output_callback = None
        self.log_file = None
        self.log_file_path = None
        self.running = False
        self.exit_code = None
        
        # Create log directory if it doesn't exist
        os.makedirs(self.log_dir, exist_ok=True)
    
    def _generate_log_filename(self, command_name="build"):
        """
        Generate timestamped log filename
        
        Args:
            command_name: Name of the command for the log filename
        
        Returns:
            str: Log filename
        """
        timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
        return f"{command_name}_{timestamp}.log"
    
    def _update_symlink(self, command_name="build"):
        """
        Update the latest.log symlink to point to the current log file
        
        Args:
            command_name: Name of the command for the symlink
        """
        symlink_path = os.path.join(self.log_dir, f"{command_name}.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 run(
        self, 
        command, 
        command_name="build", 
        output_callback=None, 
        env=None, 
        cwd=None,
        shell=False
    ):
        """
        Run a command and capture output
        
        Args:
            command: Command to run (string or list)
            command_name: Name of the command for logging
            output_callback: Function to call with output
            env: Environment variables dict
            cwd: Working directory
            shell: Whether to run in shell

        Returns:
            bool: True if command started successfully
        """
        if self.running:
            return False
        
        self.running = True
        self.exit_code = None
        self.output_callback = output_callback
        
        # Convert string to list if needed
        if isinstance(command, str) and not shell:
            command = shlex.split(command)
        
        # Open log file
        self.log_file_path = os.path.join(self.log_dir, self._generate_log_filename(command_name))
        try:
            self.log_file = open(self.log_file_path, 'wb')
            self._update_symlink(command_name)
        except IOError as e:
            print(f"Error opening log file: {e}")
            self.running = False
            return False
        
        # Log command
        if isinstance(command, list):
            cmd_str = " ".join(command)
        else:
            cmd_str = command
            
        cmd_log = f"Running command: {cmd_str}\n"
        cmd_log_bytes = cmd_log.encode('utf-8')
        self.log_file.write(cmd_log_bytes)
        self.log_file.flush()
        
        if self.output_callback:
            self.output_callback(cmd_log)
        
        # Start the process
        try:
            self.process = subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                env=env,
                cwd=cwd,
                shell=shell,
                bufsize=1  # Line buffered
            )
        except (subprocess.SubprocessError, OSError) as e:
            error_msg = f"Error starting command: {e}\n"
            self.log_file.write(error_msg.encode('utf-8'))
            self.log_file.flush()
            
            if self.output_callback:
                self.output_callback(error_msg)
            
            self.running = False
            return False
        
        # Start output reader thread
        thread = threading.Thread(target=self._read_output)
        thread.daemon = True
        thread.start()
        
        return True
    
    def _read_output(self):
        """Read output from process and handle it"""
        try:
            # Track process while it's running
            stdout = self.process.stdout
            stderr = self.process.stderr
            
            # Set up select for non-blocking I/O
            poll = select.poll()
            poll.register(stdout, select.POLLIN)
            poll.register(stderr, select.POLLIN)
            
            # Continue polling until process exits and all output is read
            while self.running:
                # Check if process has exited
                if self.process.poll() is not None:
                    # Check for remaining output
                    if not stdout.readable() and not stderr.readable():
                        break
                
                # Use a timeout to avoid busy waiting
                for fd, event in poll.poll(100):  # 100ms timeout
                    if fd == stdout.fileno() and event & select.POLLIN:
                        line = stdout.readline()
                        if line:
                            self._handle_output(line, is_stderr=False)
                    
                    if fd == stderr.fileno() and event & select.POLLIN:
                        line = stderr.readline()
                        if line:
                            self._handle_output(line, is_stderr=True)
                
                # Short sleep to prevent CPU hogging
                time.sleep(0.01)
            
            # Get exit code
            self.exit_code = self.process.returncode
            
            # Add exit message to log
            timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            exit_msg = f"\n[{timestamp}] Command exited with code {self.exit_code}\n"
            self.log_file.write(exit_msg.encode('utf-8'))
            self.log_file.flush()
            
            if self.output_callback:
                self.output_callback(exit_msg)
        
        finally:
            # Cleanup
            self.running = False
            
            if self.log_file:
                self.log_file.close()
                self.log_file = None
            
            if self.exit_callback:
                self.exit_callback(self.exit_code)
    
    def _handle_output(self, data, is_stderr=False):
        """
        Handle a chunk of output data
        
        Args:
            data: Output data (bytes)
            is_stderr: Whether this is from stderr
        """
        # Write to log file
        if self.log_file:
            self.log_file.write(data)
            self.log_file.flush()
        
        # Pass to callback
        if self.output_callback:
            # Convert bytes to string for callback
            try:
                text = data.decode('utf-8')
                self.output_callback(text, is_stderr)
            except UnicodeDecodeError:
                # If we can't decode, just pass the raw bytes
                self.output_callback(str(data), is_stderr)
    
    def terminate(self):
        """Terminate the running process"""
        if self.process and self.running:
            self.process.terminate()
            # Give it a moment to terminate gracefully
            time.sleep(0.5)
            # If still running, kill it
            if self.process.poll() is None:
                self.process.kill()
            
            self.running = False
    
    def is_running(self):
        """
        Check if command is still running
        
        Returns:
            bool: True if running, False otherwise
        """
        return self.running and (self.process and self.process.poll() is None)
    
    def get_exit_code(self):
        """
        Get exit code of completed command
        
        Returns:
            int: Exit code or None if not completed
        """
        return self.exit_code


class BuildRunner(CommandRunner):
    """
    Specialized command runner for build commands
    """
    
    def __init__(self, log_dir="logs/build", exit_callback=None):
        super().__init__(log_dir, exit_callback)
    
    def build(self, command, output_callback=None, env=None, cwd=None):
        """
        Run a build command
        
        Args:
            command: Build command
            output_callback: Function to call with output
            env: Environment variables dict
            cwd: Working directory
            
        Returns:
            bool: True if build started successfully
        """
        return self.run(command, "build", output_callback, env, cwd)
    
    def upload(self, command, output_callback=None, env=None, cwd=None):
        """
        Run an upload command
        
        Args:
            command: Upload command
            output_callback: Function to call with output
            env: Environment variables dict
            cwd: Working directory
            
        Returns:
            bool: True if upload started successfully
        """
        return self.run(command, "upload", output_callback, env, cwd)
    
    def clean(self, command, output_callback=None, env=None, cwd=None):
        """
        Run a clean command
        
        Args:
            command: Clean command
            output_callback: Function to call with output
            env: Environment variables dict
            cwd: Working directory
            
        Returns:
            bool: True if clean started successfully
        """
        return self.run(command, "clean", output_callback, env, cwd)


# Simple test function for module
if __name__ == "__main__":
    import argparse
    
    parser = argparse.ArgumentParser(description='Test command runner')
    parser.add_argument('command', nargs='+', help='Command to run')
    args = parser.parse_args()
    
    runner = CommandRunner()
    
    # Register a callback to print output to console
    def output_callback(data, is_stderr=False):
        if is_stderr:
            sys.stderr.write(data)
        else:
            sys.stdout.write(data)
    
    print(f"Running command: {' '.join(args.command)}")
    
    # Run the command
    success = runner.run(args.command, output_callback=output_callback)
    if not success:
        print("Failed to run command")
        sys.exit(1)
    
    try:
        # Wait for the command to complete
        while runner.is_running():
            time.sleep(0.1)
        
        print(f"Command exited with code {runner.get_exit_code()}")
    except KeyboardInterrupt:
        print("\nTerminating command...")
        runner.terminate()
        sys.exit(1)
