#!/usr/bin/env python3
"""
Batch Configuration Runner for Multi-Agent Maze Runner

This script runs multiple configuration files in parallel with configurable
parallelism and timeout settings. It's designed to efficiently execute
large-scale experiments across different maze and parameter combinations.

Usage:
    # Batch mode (default) - run multiple configs in parallel subprocesses
    python batch_runner.py --config_dir generated_configs/ --parallel 4 --timeout 300
    
    # Debug mode - run single config in same process (for debugging)
    python batch_runner.py --debug --debug_config config_maze_05441a190ea44838_temp_0_1.yaml
    python batch_runner.py --debug  # runs first available config

Arguments:
    --config_dir: Directory containing configuration files (default: generated_configs/)
    --parallel: Number of parallel executions (default: 2)
    --timeout: Timeout in seconds for each execution (default: 600)
    --output_dir: Directory to save results (default: batch_results/)
    --resume: Resume from where previous run left off (default: False)
    --debug: Enable debug mode (single config, same process, no subprocess isolation)
    --debug_config: Specific config file to run in debug mode (optional)
"""

import argparse
import os
import sys
import subprocess
import time
import json
import shutil
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor, as_completed, TimeoutError
from datetime import datetime
from typing import List, Dict, Any, Tuple, Optional
import logging
import importlib.util
import traceback
import re


# ================== CONFIGURABLE PARAMETERS ==================

# Default values
DEFAULT_CONFIG_DIR = "generated_configs/1_N2_Tests"
DEFAULT_PARALLEL = 2
DEFAULT_TIMEOUT = 7200  # 120 minutes per execution
DEFAULT_OUTPUT_DIR = "batch_results"
DEFAULT_LOG_LEVEL = "INFO"

# Main script to execute (relative to parent directory)
MAIN_SCRIPT = "orchestrator_maze_implementation/main.py"

# ================== SCRIPT IMPLEMENTATION ==================

class BatchRunner:
    """
    Manages batch execution of maze runner configurations with parallel processing.
    """
    
    def __init__(self, config_dir: str, parallel: int, timeout: int, output_dir: str, resume: bool = False):
        """
        Initialize the batch runner.
        
        Args:
            config_dir: Directory containing configuration files
            parallel: Number of parallel executions
            timeout: Timeout in seconds for each execution
            output_dir: Directory to save results
            resume: Whether to resume from previous run
        """
        self.config_dir = Path(config_dir)
        self.parallel = parallel
        self.timeout = timeout
        # Create a unique batch id for this run for grouping results together
        # Format: batch_YYYYmmdd_HHMMSS
        self.batch_id = datetime.now().strftime("batch_%Y%m%d_%H%M%S")
        self.output_dir_root = Path(output_dir)
        # The root for this batch run
        self.output_dir = self.output_dir_root / self.batch_id
        self.resume = resume
        
        # Ensure output directory exists before logging
        self.output_dir.mkdir(exist_ok=True, parents=True)

        # Setup logging
        self.setup_logging()
        
        # Track execution state
        self.results_file = self.output_dir / "execution_results.json"
        self.completed_configs = self.load_completed_configs() if resume else set()
        
        self.logger.info(f"Batch Runner initialized:")
        self.logger.info(f"  Config directory: {self.config_dir}")
        self.logger.info(f"  Parallel executions: {self.parallel}")
        self.logger.info(f"  Timeout per execution: {self.timeout}s")
        self.logger.info(f"  Output directory root: {self.output_dir_root}")
        self.logger.info(f"  Batch id: {self.batch_id}")
        self.logger.info(f"  Output directory: {self.output_dir}")
        self.logger.info(f"  Resume mode: {self.resume}")
    
    def setup_logging(self) -> None:
        """Setup logging configuration."""
        log_file = self.output_dir / "batch_runner.log"
        self.output_dir.mkdir(exist_ok=True, parents=True)
        
        logging.basicConfig(
            level=getattr(logging, DEFAULT_LOG_LEVEL),
            format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler(log_file),
                logging.StreamHandler(sys.stdout)
            ]
        )
        self.logger = logging.getLogger('BatchRunner')
    
    def load_completed_configs(self) -> set:
        """Load list of completed configurations from previous runs."""
        if not self.results_file.exists():
            return set()
        
        try:
            with open(self.results_file, 'r') as f:
                results = json.load(f)
                completed = {result['config_file'] for result in results if result['status'] == 'completed'}
                self.logger.info(f"Loaded {len(completed)} completed configurations from previous run")
                return completed
        except Exception as e:
            self.logger.warning(f"Could not load previous results: {e}")
            return set()
    
    def get_config_files(self) -> List[Path]:
        """
        Get list of configuration files to process.
        
        Returns:
            List of configuration file paths
        """
        if not self.config_dir.exists():
            raise FileNotFoundError(f"Configuration directory not found: {self.config_dir}")
        
        # Find all YAML configuration files
        config_files = list(self.config_dir.glob("*.yaml")) + list(self.config_dir.glob("*.yml"))
        
        if not config_files:
            raise ValueError(f"No configuration files found in: {self.config_dir}")
        
        # Filter out completed configurations if resuming
        if self.resume:
            config_files = [f for f in config_files if f.name not in self.completed_configs]
            self.logger.info(f"Resuming: {len(config_files)} configurations remaining")
        
        return sorted(config_files)
    
    def run_single_config(self, config_file: Path) -> Dict[str, Any]:
        """
        Run a single configuration file with progress monitoring.
        
        Args:
            config_file: Path to the configuration file
            
        Returns:
            Dictionary containing execution results
        """
        start_time = time.time()
        config_name = config_file.name
        
        # Create individual output directory for this configuration
        config_output_dir = self.output_dir / config_name.replace('.yaml', '').replace('.yml', '')
        config_output_dir.mkdir(exist_ok=True, parents=True)
        
        # Copy config file to output directory for reference
        shutil.copy2(config_file, config_output_dir / "config.yaml")
        
        result = {
            'config_file': config_name,
            'config_path': str(config_file),
            'output_dir': str(config_output_dir),
            'start_time': datetime.fromtimestamp(start_time).isoformat(),
            'status': 'unknown',
            'duration': 0,
            'error': None,
            'stdout': None,
            'stderr': None
        }
        
        try:
            self.logger.info(f"🚀 Starting execution: {config_name}")
            
            # Prepare command to run main.py with the specific config
            config_file_abs = config_file.resolve()
            cmd = [
                sys.executable, MAIN_SCRIPT,
                "--config", str(config_file_abs),
                "--output_dir", str(config_output_dir)
            ]
            
            # Set up environment with proper PYTHONPATH
            env = os.environ.copy()
            project_root = Path(__file__).parent.parent
            env['PYTHONPATH'] = str(project_root)
            
            # Start process with streaming to files for monitoring
            stdout_file = config_output_dir / "stdout.txt"
            stderr_file = config_output_dir / "stderr.txt"
            
            with open(stdout_file, 'w') as stdout_f, open(stderr_file, 'w') as stderr_f:
                process = subprocess.Popen(
                    cmd,
                    cwd=project_root,
                    stdout=stdout_f,
                    stderr=stderr_f,
                    env=env,
                    text=True
                )
                
                # Monitor the process with periodic updates
                last_update_time = time.time()
                last_stdout_size = 0
                last_stderr_size = 0
                
                while process.poll() is None:
                    current_time = time.time()
                    elapsed = current_time - start_time
                    
                    # Check for timeout
                    if elapsed > self.timeout:
                        self.logger.warning(f"⏰ Timeout: {config_name} after {elapsed:.1f}s - initiating graceful termination")
                        
                        # Graceful termination sequence for reproducibility:
                        # 1. Send SIGTERM to allow process to save final state
                        # 2. Wait for process to finish saving (up to 30 seconds)
                        # 3. If still running, send SIGKILL as last resort
                        
                        # Step 1: Send SIGTERM (graceful shutdown signal)
                        self.logger.info(f"   Step 1: Sending SIGTERM to {config_name} for graceful shutdown...")
                        process.terminate()  # Sends SIGTERM
                        
                        # Step 2: Wait for graceful shutdown with monitoring
                        graceful_timeout = 30  # seconds
                        graceful_start = time.time()
                        
                        while process.poll() is None and (time.time() - graceful_start) < graceful_timeout:
                            time.sleep(1)
                            
                            # Monitor output growth during graceful shutdown
                            try:
                                current_stdout_size = stdout_file.stat().st_size if stdout_file.exists() else 0
                                if current_stdout_size > last_stdout_size:
                                    self.logger.info(f"   📝 {config_name}: Process saving state... ({current_stdout_size - last_stdout_size} bytes output)")
                                    last_stdout_size = current_stdout_size
                            except Exception:
                                pass
                        
                        # Step 3: Check if graceful shutdown succeeded
                        if process.poll() is None:
                            # Process still running - force termination
                            self.logger.warning(f"   ⚡ Step 2: Graceful shutdown timeout for {config_name} - sending SIGKILL")
                            process.kill()  # Sends SIGKILL
                            try:
                                process.wait(timeout=5)
                                self.logger.warning(f"   💀 Process {config_name} force-terminated")
                            except subprocess.TimeoutExpired:
                                self.logger.error(f"   🚨 Process {config_name} could not be killed!")
                        else:
                            # Graceful shutdown succeeded
                            graceful_time = time.time() - graceful_start
                            self.logger.info(f"   ✅ Process {config_name} shut down gracefully in {graceful_time:.1f}s")
                        
                        break
                    
                    # Periodic progress update every 30 seconds
                    if current_time - last_update_time > 30:
                        try:
                            # Check if output files are growing
                            current_stdout_size = stdout_file.stat().st_size if stdout_file.exists() else 0
                            current_stderr_size = stderr_file.stat().st_size if stderr_file.exists() else 0
                            
                            stdout_growth = current_stdout_size - last_stdout_size
                            stderr_growth = current_stderr_size - last_stderr_size
                            
                            # Look for turn information in recent output
                            turn_info = ""
                            if stdout_file.exists() and current_stdout_size > 0:
                                try:
                                    with open(stdout_file, 'r') as f:
                                        # Read last 2KB for turn information
                                        f.seek(max(0, current_stdout_size - 2048))
                                        recent_output = f.read()
                                        
                                        # Find turn information
                                        turn_matches = re.findall(r'Turn (\d+)', recent_output)
                                        if turn_matches:
                                            latest_turn = max(int(t) for t in turn_matches)
                                            turn_info = f"Turn {latest_turn}"
                                        
                                        # Check for completion indicators
                                        if "at exit" in recent_output.lower():
                                            turn_info += " [Near exit]"
                                        elif "completed" in recent_output.lower():
                                            turn_info += " [Completed]"
                                            
                                except Exception:
                                    pass
                            
                            status_msg = f"⏳ {config_name}: {elapsed:.1f}s"
                            if turn_info:
                                status_msg += f" - {turn_info}"
                            if stdout_growth > 0 or stderr_growth > 0:
                                status_msg += f" (stdout +{stdout_growth}, stderr +{stderr_growth} bytes)"
                            else:
                                status_msg += " [No output growth]"
                                
                            self.logger.info(status_msg)
                            
                            last_update_time = current_time
                            last_stdout_size = current_stdout_size
                            last_stderr_size = current_stderr_size
                            
                        except Exception as e:
                            self.logger.debug(f"Error monitoring {config_name}: {e}")
                    
                    time.sleep(1)  # Check every second
                
                # Wait for process to complete
                return_code = process.wait()
            
            end_time = time.time()
            duration = end_time - start_time
            
            # Read the output files
            stdout_content = ""
            stderr_content = ""
            
            if stdout_file.exists():
                with open(stdout_file, 'r') as f:
                    stdout_content = f.read()
            
            if stderr_file.exists():
                with open(stderr_file, 'r') as f:
                    stderr_content = f.read()
            
            # Count turns completed
            turn_matches = re.findall(r'Turn (\d+)', stdout_content)
            turns_completed = max(int(t) for t in turn_matches) if turn_matches else 0
            
            result.update({
                'status': 'completed' if return_code == 0 else 'failed',
                'duration': duration,
                'return_code': return_code,
                'stdout': stdout_content,
                'stderr': stderr_content,
                'turns_completed': turns_completed
            })
            
            if return_code == 0:
                self.logger.info(f"✅ Completed: {config_name} ({duration:.1f}s, {turns_completed} turns)")
            else:
                self.logger.error(f"❌ Failed: {config_name} (code: {return_code}, {duration:.1f}s)")
                # Log recent stderr for debugging
                if stderr_content:
                    stderr_lines = stderr_content.strip().split('\n')
                    recent_errors = stderr_lines[-3:] if len(stderr_lines) > 3 else stderr_lines
                    for line in recent_errors:
                        if line.strip():
                            self.logger.error(f"❌ {config_name}: {line.strip()}")
            
        except subprocess.TimeoutExpired:
            end_time = time.time()
            duration = end_time - start_time
            
            result.update({
                'status': 'timeout',
                'duration': duration,
                'error': f'Execution timed out after {self.timeout} seconds'
            })
            
            # Check for any final outputs or pickle files created despite timeout
            final_outputs_found = []
            try:
                pickle_files = list(config_output_dir.glob("*.pickle"))
                if pickle_files:
                    final_outputs_found.extend([f"pickle: {f.name}" for f in pickle_files])
                    
                viz_files = list(config_output_dir.glob("*maze*.png"))
                if viz_files:
                    final_outputs_found.extend([f"visualization: {f.name}" for f in viz_files])
                    
                if final_outputs_found:
                    result['final_outputs_preserved'] = True
                    result['preserved_files'] = final_outputs_found
                    self.logger.info(f"   📁 Final outputs preserved despite timeout: {', '.join(final_outputs_found)}")
                else:
                    result['final_outputs_preserved'] = False
                    
            except Exception as check_error:
                self.logger.debug(f"Error checking for preserved outputs: {check_error}")
            
            self.logger.warning(f"⏰ Timeout: {config_name} ({duration:.1f}s)")
            if final_outputs_found:
                self.logger.info(f"   ✅ Scientific reproducibility preserved: {len(final_outputs_found)} final outputs saved")
            
        except Exception as e:
            end_time = time.time()
            duration = end_time - start_time
            
            result.update({
                'status': 'error',
                'duration': duration,
                'error': str(e)
            })
            
            self.logger.error(f"💥 Error: {config_name} - {e}")
        
        # Save individual result
        with open(config_output_dir / "result.json", 'w') as f:
            json.dump(result, f, indent=2)
        
        return result
    
    def run_single_config_debug(self, config_file: Path) -> Dict[str, Any]:
        """
        Run a single configuration file in debug mode (same process).
        
        Args:
            config_file: Path to the configuration file
            
        Returns:
            Dictionary containing execution results
        """
        start_time = time.time()
        config_name = config_file.name
        
        # Create individual output directory for this configuration
        config_output_dir = self.output_dir / config_name.replace('.yaml', '').replace('.yml', '')
        config_output_dir.mkdir(exist_ok=True, parents=True)
        
        # Copy config file to output directory for reference
        shutil.copy2(config_file, config_output_dir / "config.yaml")
        
        result = {
            'config_file': config_name,
            'config_path': str(config_file),
            'output_dir': str(config_output_dir),
            'start_time': datetime.fromtimestamp(start_time).isoformat(),
            'status': 'unknown',
            'duration': 0,
            'error': None,
            'mode': 'debug'
        }
        
        try:
            self.logger.info(f"🐛 Starting DEBUG execution: {config_name}")
            
            # Set up environment similar to subprocess execution
            original_cwd = os.getcwd()
            project_root = Path(__file__).parent.parent
            config_file_abs = config_file.resolve()
            
            # Change to project root directory
            os.chdir(project_root)
            
            # Add project root to Python path if not already there
            if str(project_root) not in sys.path:
                sys.path.insert(0, str(project_root))
            
            try:
                # Import and run the main module directly
                main_module_path = project_root / "orchestrator_maze_implementation" / "main.py"
                
                # Load the main module
                spec = importlib.util.spec_from_file_location("main_module", main_module_path)
                if spec is None or spec.loader is None:
                    raise ValueError(f"Could not load module spec from {main_module_path}")
                
                main_module = importlib.util.module_from_spec(spec)
                
                # Execute the module (this loads all the functions)
                spec.loader.exec_module(main_module)
                
                # Call the main function with the config file
                self.logger.info(f"🐛 Calling main function with config: {config_file_abs}")
                main_module.main(config_path=str(config_file_abs), output_dir=str(config_output_dir))
                
                end_time = time.time()
                duration = end_time - start_time
                
                result.update({
                    'status': 'completed',
                    'duration': duration,
                    'return_code': 0
                })
                
                self.logger.info(f"✅ DEBUG Completed: {config_name} ({duration:.1f}s)")
                
            except Exception as e:
                end_time = time.time()
                duration = end_time - start_time
                
                # Capture full traceback for debugging
                error_traceback = traceback.format_exc()
                
                result.update({
                    'status': 'failed',
                    'duration': duration,
                    'error': str(e),
                    'traceback': error_traceback,
                    'return_code': 1
                })
                
                # Save traceback to file for debugging
                config_output_dir.mkdir(exist_ok=True, parents=True)
                with open(config_output_dir / "traceback.txt", 'w') as f:
                    f.write(error_traceback)
                
                self.logger.error(f"❌ DEBUG Failed: {config_name} - {e}")
                self.logger.debug(f"Traceback: {error_traceback}")
                
            finally:
                # Restore original working directory
                os.chdir(original_cwd)
                
                # Remove project root from sys.path if we added it
                if str(project_root) in sys.path and sys.path[0] == str(project_root):
                    sys.path.pop(0)
            
        except Exception as e:
            end_time = time.time()
            duration = end_time - start_time
            
            result.update({
                'status': 'error',
                'duration': duration,
                'error': f'Debug setup error: {str(e)}',
                'traceback': traceback.format_exc()
            })
            
            self.logger.error(f"💥 DEBUG Error: {config_name} - {e}")
        
        # Save individual result
        with open(config_output_dir / "result.json", 'w') as f:
            json.dump(result, f, indent=2)
        
        return result
    
    def save_results(self, results: List[Dict[str, Any]]) -> None:
        """
        Save execution results to JSON file.
        
        Args:
            results: List of execution results
        """
        # Load existing results if resuming
        all_results = []
        if self.resume and self.results_file.exists():
            try:
                with open(self.results_file, 'r') as f:
                    all_results = json.load(f)
            except Exception as e:
                self.logger.warning(f"Could not load existing results: {e}")
        
        # Add new results
        all_results.extend(results)
        
        # Save updated results
        with open(self.results_file, 'w') as f:
            json.dump(all_results, f, indent=2)
        
        self.logger.info(f"Results saved to: {self.results_file}")
    
    def print_summary(self, results: List[Dict[str, Any]]) -> None:
        """
        Print execution summary.
        
        Args:
            results: List of execution results
        """
        total = len(results)
        completed = sum(1 for r in results if r['status'] == 'completed')
        failed = sum(1 for r in results if r['status'] == 'failed')
        timeout = sum(1 for r in results if r['status'] == 'timeout')
        error = sum(1 for r in results if r['status'] == 'error')
        
        # Count state preservation for scientific reproducibility
        timeout_with_outputs = sum(1 for r in results 
                                   if r['status'] == 'timeout' and r.get('final_outputs_preserved', False))
        total_with_final_outputs = completed + timeout_with_outputs
        
        total_duration = sum(r['duration'] for r in results)
        avg_duration = total_duration / total if total > 0 else 0
        
        print("\n" + "="*60)
        print("BATCH EXECUTION SUMMARY")
        print("="*60)
        print(f"Total configurations: {total}")
        print(f"✅ Completed successfully: {completed}")
        print(f"❌ Failed: {failed}")
        print(f"⏰ Timed out: {timeout}")
        if timeout_with_outputs > 0:
            print(f"   └─ Timeouts with preserved state: {timeout_with_outputs} (🔬 Scientific reproducibility maintained)")
        print(f"💥 Errors: {error}")
        print(f"Total execution time: {total_duration:.1f}s")
        print(f"Average time per config: {avg_duration:.1f}s")
        print("")
        print(f"   Final outputs preserved: {total_with_final_outputs}/{total} ({(total_with_final_outputs/total)*100:.1f}%)")
        if timeout_with_outputs > 0:
            print(f"   Graceful termination success: {timeout_with_outputs}/{timeout} timeouts preserved state")
        print("")
        print(f"Results directory: {self.output_dir}")
        print("="*60)
    
    def run_batch(self) -> None:
        """Run batch execution of all configurations."""
        try:
            config_files = self.get_config_files()
            total_configs = len(config_files)
            
            if total_configs == 0:
                self.logger.info("No configurations to process")
                return
            
            self.logger.info(f"Starting batch execution of {total_configs} configurations")
            
            results = []
            
            # Use ProcessPoolExecutor for parallel execution
            with ProcessPoolExecutor(max_workers=self.parallel) as executor:
                # Submit all tasks
                future_to_config = {
                    executor.submit(self.run_single_config, config_file): config_file
                    for config_file in config_files
                }
                
                # Process completed tasks
                for future in as_completed(future_to_config):
                    config_file = future_to_config[future]
                    try:
                        result = future.result()
                        results.append(result)
                        
                        # Save results incrementally
                        self.save_results([result])
                        
                        progress = len(results)
                        self.logger.info(f"Progress: {progress}/{total_configs} configurations processed")
                        
                    except Exception as e:
                        self.logger.error(f"Unexpected error processing {config_file}: {e}")
                        # Create error result
                        error_result = {
                            'config_file': config_file.name,
                            'config_path': str(config_file),
                            'status': 'error',
                            'error': str(e),
                            'duration': 0
                        }
                        results.append(error_result)
            
            # Print final summary
            self.print_summary(results)
            
        except Exception as e:
            self.logger.error(f"Batch execution failed: {e}")
            raise
    
    def run_debug(self, debug_config: Optional[str] = None) -> None:
        """
        Run debug execution of a single configuration.
        
        Args:
            debug_config: Specific config file name to run, or None to run first available
        """
        try:
            config_files = self.get_config_files()
            
            if not config_files:
                self.logger.error("No configuration files found for debug mode")
                return
            
            # Select config file to debug
            if debug_config:
                # Find the specified config file
                config_file = None
                for cf in config_files:
                    if cf.name == debug_config:
                        config_file = cf
                        break
                
                if config_file is None:
                    self.logger.error(f"Debug config file '{debug_config}' not found in {self.config_dir}")
                    self.logger.info(f"Available configs: {[cf.name for cf in config_files[:5]]}...")
                    return
            else:
                # Use first available config file
                config_file = config_files[0]
                self.logger.info(f"No specific debug config specified, using: {config_file.name}")
            
            self.logger.info(f"🐛 Starting DEBUG mode for: {config_file.name}")
            
            # Run the single config in debug mode
            result = self.run_single_config_debug(config_file)
            
            # Save results
            self.save_results([result])
            
            # Print summary
            self.print_debug_summary(result)
            
        except Exception as e:
            self.logger.error(f"Debug execution failed: {e}")
            raise
    
    def print_debug_summary(self, result: Dict[str, Any]) -> None:
        """
        Print debug execution summary.
        
        Args:
            result: Debug execution result
        """
        print("\n" + "="*60)
        print("🐛 DEBUG EXECUTION SUMMARY")
        print("="*60)
        print(f"Configuration: {result['config_file']}")
        print(f"Status: {result['status']}")
        print(f"Duration: {result['duration']:.1f}s")
        
        if result['status'] == 'completed':
            print("✅ Execution completed successfully")
        elif result['status'] == 'failed':
            print(f"❌ Execution failed: {result.get('error', 'Unknown error')}")
            if 'traceback' in result:
                print("💡 Full traceback saved to traceback.txt in output directory")
        elif result['status'] == 'error':
            print(f"💥 Setup error: {result.get('error', 'Unknown error')}")
        
        print(f"Output directory: {result['output_dir']}")
        print("="*60)


def main():
    """Main function to run batch configuration execution."""
    parser = argparse.ArgumentParser(
        description="Batch runner for multi-agent maze runner configurations",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    
    parser.add_argument(
        "--config_dir", "-c",
        type=str,
        default=DEFAULT_CONFIG_DIR,
        help="Directory containing configuration files"
    )
    
    parser.add_argument(
        "--parallel", "-p",
        type=int,
        default=DEFAULT_PARALLEL,
        help="Number of parallel executions"
    )
    
    parser.add_argument(
        "--timeout", "-t",
        type=int,
        default=DEFAULT_TIMEOUT,
        help="Timeout in seconds for each execution"
    )
    
    parser.add_argument(
        "--output_dir", "-o",
        type=str,
        default=DEFAULT_OUTPUT_DIR,
        help="Directory to save results"
    )
    
    parser.add_argument(
        "--resume", "-r",
        action="store_true",
        help="Resume from where previous run left off"
    )
    
    parser.add_argument(
        "--debug", "-d",
        action="store_true",
        help="Debug mode: run single config in same process (no subprocess isolation)"
    )
    
    parser.add_argument(
        "--debug_config",
        type=str,
        default=None,
        help="Specific config file to run in debug mode (relative to config_dir)"
    )
    
    args = parser.parse_args()
    
    # Validate debug mode arguments
    if args.debug and args.parallel > 1:
        print("⚠️  Warning: Debug mode ignores parallel setting (runs in single process)")
    
    mode = "DEBUG" if args.debug else "BATCH"
    print(f"Multi-Agent Maze Runner - {mode} Configuration Runner")
    print("=" * 60)
    print(f"Mode: {mode}")
    print(f"Configuration directory: {args.config_dir}")
    
    if args.debug:
        print(f"Debug config: {args.debug_config or 'First available'}")
    else:
        print(f"Parallel executions: {args.parallel}")
        print(f"Timeout per execution: {args.timeout}s")
        print(f"Resume mode: {args.resume}")
    
    print(f"Output directory: {args.output_dir}")
    print("=" * 60)
    
    try:
        runner = BatchRunner(
            config_dir=args.config_dir,
            parallel=args.parallel,
            timeout=args.timeout,
            output_dir=args.output_dir,
            resume=args.resume
        )
        
        if args.debug:
            # Run in debug mode
            runner.run_debug(debug_config=args.debug_config)
        else:
            # Run in batch mode
            runner.run_batch()
        
    except KeyboardInterrupt:
        print(f"\n🛑 {mode} execution interrupted by user")
        sys.exit(1)
    except Exception as e:
        print(f"\n💥 {mode} execution failed: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()