"""
Configuration service for the multi-agent maze runner workflow.

This module provides a centralized singleton service for accessing configuration values
loaded from the YAML configuration file. Import only the ConfigService class to get
full access to all configuration values.

Example usage:
    from orchestrator_maze_implementation.config.config_service import ConfigService
    
    config = ConfigService()
    config.load_config('config.yaml')
    
    # Access any config value
    maze_uuid = config.get_maze_uuid()
    num_agents = config.get_num_execution_agents()
    recursion_limit = config.get_recursion_limit()
"""

import yaml
import os
import platform
from typing import Dict, Any, Optional, Tuple


class ConfigService:
    """
    Singleton service for managing configuration throughout the application.
    
    This class provides centralized access to all configuration values loaded from
    a YAML file. It ensures only one instance exists and provides convenient
    methods for accessing common configuration values.
    """
    
    _instance: Optional['ConfigService'] = None
    _config: Optional[Dict[str, Any]] = None
    _config_path: Optional[str] = None
    
    def __new__(cls) -> 'ConfigService':
        """Ensure only one instance of ConfigService exists (singleton pattern)."""
        if cls._instance is None:
            cls._instance = super(ConfigService, cls).__new__(cls)
        return cls._instance
    
    def load_config(self, config_path: str) -> None:
        """
        Load configuration from YAML file.
        
        Args:
            config_path: Path to the YAML configuration file
            
        Raises:
            FileNotFoundError: If config file doesn't exist
            yaml.YAMLError: If YAML file is malformed
        """
        if not os.path.exists(config_path):
            raise FileNotFoundError(f"Configuration file not found: {config_path}")
        
        try:
            with open(config_path, 'r', encoding='utf-8') as file:
                self._config = yaml.safe_load(file)
                self._config_path = config_path
        except yaml.YAMLError as e:
            raise yaml.YAMLError(f"Error parsing YAML configuration: {e}")
    
    def get_config(self) -> Dict[str, Any]:
        """
        Get the full configuration dictionary.
        
        Returns:
            dict: Complete configuration dictionary
            
        Raises:
            RuntimeError: If configuration hasn't been loaded
        """
        if self._config is None:
            raise RuntimeError("Configuration not loaded. Call load_config() first.")
        return self._config
    
    def get(self, key_path: str, default: Any = None) -> Any:
        """
        Get a configuration value using dot notation.
        
        Args:
            key_path: Dot-separated path to the configuration value (e.g., 'agents.global.num_execution_agents')
            default: Default value if key not found
            
        Returns:
            Configuration value or default if not found
            
        Examples:
            config.get('agents.global.num_execution_agents')
            config.get('setup.recursion_limit')
            config.get('maze.uuid')
        """
        if self._config is None:
            raise RuntimeError("Configuration not loaded. Call load_config() first.")
        
        keys = key_path.split('.')
        current = self._config
        
        try:
            for key in keys:
                current = current[key]
            return current
        except (KeyError, TypeError):
            return default
    
    def is_loaded(self) -> bool:
        """Check if configuration has been loaded."""
        return self._config is not None
    
    @property
    def config_path(self) -> Optional[str]:
        """Get the path of the loaded configuration file."""
        return self._config_path
    
    # =============================================================================
    # Configuration Getter Methods
    # =============================================================================
    
    # Setup Configuration
    def get_recursion_limit(self) -> int:
        """Get the recursion limit setting."""
        return self.get('setup.recursion_limit', 50000)
    
    def get_num_turn_iterations(self) -> int:
        """Get the number of turn iterations setting.""" 
        return self.get('setup.num_turn_iterations', 5)
    
    def env_input(self) -> int:
        """Get the number of turn iterations setting.""" 
        return self.get('setup.env_input', False)
    
    def get_max_total_steps(self) -> Optional[int]:
        """Return the maximum total number of steps across all agents before failing.

        This value limits the sum of steps taken by all agents combined. If the
        limit is reached, the run should be marked as failed and results should
        still be saved by the caller.

        Configuration keys (checked in order):
        - setup.max_step   (preferred)
        - setup.max_steps  (backward/alias)

        Returns:
            Optional[int]: Maximum total steps, or None if unlimited.
        """
        # Prefer singular key but accept plural for compatibility
        default_value = 1000
        value = self.get('setup.max_step', None)
        if value is None:
            value = self.get('setup.max_steps', default_value)
        return value
    
    # Maze Configuration
    def get_maze_uuid(self) -> str:
        """Get the maze UUID setting."""
        return self.get('maze.uuid', '1fbad51aa9164fa7')
    
    # Agent Configuration
    def get_num_execution_agents(self) -> int:
        """Get the number of execution agents setting."""
        return self.get('agents.global.num_execution_agents', 2)
    
    def get_steps_per_agent(self) -> int:
        """Get the steps per agent setting."""
        return self.get('agents.global.steps_per_agent', 1)
    
    def get_execution_model(self) -> str:
        """Get the execution model setting."""
        return self.get('agents.execution.model', 'gpt-4.1-nano')
    
    def get_execution_temperature(self) -> float:
        """Get the execution temperature setting."""
        return self.get('agents.execution.temperature', 0.1)
    
    def get_reasoning_temperature(self) -> float:
        """Get the execution temperature setting."""
        return self.get('agents.reasoning.temperature', 1)
    
    def get_planning_model(self) -> str:
        """Get the planning model setting."""
        return self.get('agents.planning.model', 'gpt-4.1-nano')
    
    def get_planning_temperature(self) -> float:
        """Get the planning temperature setting."""
        return self.get('agents.planning.temperature', 0.1)
    
    def get_orchestration_model(self) -> str:
        """Get the orchestration model setting."""
        return self.get('agents.orchestration.model', 'gpt-4.1')
    
    def get_orchestration_temperature(self) -> float:
        """Get the orchestration temperature setting."""
        return self.get('agents.orchestration.temperature', 0.1)
    
    # Visualization Configuration
    def get_figure_size(self) -> Tuple[int, int]:
        """Get the figure size setting."""
        size = self.get('visualization.figure_size', [16, 8])
        return tuple(size)
    
    def get_final_display_duration(self) -> int:
        """Get the final display duration setting."""
        return self.get('visualization.final_display_duration', 5)
    
    def get_message_history_limit(self) -> int:
        """Get the message history limit setting."""
        return self.get('visualization.message_history_limit', 8)
    
    def get_enable_maze_visualization(self) -> bool:
        """Get the enable maze visualization setting."""
        return self.get('visualization.enable_maze_visualization', True)
    
    def get_visualization_safe_mode(self) -> bool:
        """Return whether to use safe visualization mode.

        Precedence:
        1) Environment variable MAZE_VIS_SAFE_MODE (1/true/yes/on or 0/false/no/off)
        2) YAML config key visualization.safe_mode (bool)
        3) Auto-detect: enabled on macOS (Darwin), disabled otherwise
        """
        # Default: auto-detect macOS
        default_safe = platform.system() == 'Darwin'

        # YAML override if explicitly set and config is loaded
        yaml_value = None
        if self.is_loaded():
            yaml_value = self.get('visualization.safe_mode', None)
            if isinstance(yaml_value, bool):
                default_safe = yaml_value

        # Env override highest precedence
        env_val = os.getenv('MAZE_VIS_SAFE_MODE')
        if env_val is not None:
            v = env_val.strip().lower()
            if v in ('1', 'true', 'yes', 'on'):
                return True
            if v in ('0', 'false', 'no', 'off'):
                return False
        return default_safe
    
    # Output Configuration
    def get_save_images(self) -> bool:
        """Get the save images setting."""
        return self.get('output.save_images', True)
    
    def get_final_maze_filename(self) -> str:
        """Get the final maze filename setting."""
        return self.get('output.final_maze_filename', 'final_maze_state.png')
    
    # Display Configuration
    def get_show_path(self) -> bool:
        """Get the show path setting."""
        return self.get('display.show_path', True)
    
    def get_show_agent(self) -> bool:
        """Get the show agent setting."""
        return self.get('display.show_agent', True)
    
    def get_show_start(self) -> bool:
        """Get the show start setting."""
        return self.get('display.show_start', True)
    
    def get_show_failed_moves(self) -> bool:
        """Get the show failed moves setting."""
        return self.get('display.show_failed_moves', True)
    
    def get_show_marked_dead_ends(self) -> bool:
        """Get the show marked dead ends setting."""
        return self.get('display.show_marked_dead_ends', True)

# Create a global instance for easy access - this ensures singleton behavior
# and provides a convenient way to access the service throughout the application
config = ConfigService() 