# src/pycemrg/system/carp_runner.py

import subprocess
import logging
from pathlib import Path
from typing import Optional, Sequence, Union, Dict

from .runner import CommandRunner


class CarpEnvironmentError(RuntimeError):
    """Raised when CARPentry environment cannot be loaded or is invalid."""
    pass


class CarpRunner:
    """
    A specialized runner for executing CARPentry/openCARP commands.
    
    This class handles the CARPentry-specific environment configuration by 
    sourcing the config.sh file generated during CARPentry installation. This
    file sets up all necessary environment variables including PATH, PYTHONPATH,
    LD_LIBRARY_PATH, and CARPentry-specific variables.
    
    The config.sh file is typically located in the root of your CARPentry 
    installation directory (e.g., /path/to/carpentry_bundle/config.sh).
    
    Example:
        >>> import logging
        >>> from pycemrg.system import CommandRunner, CarpRunner
        >>> 
        >>> logger = logging.getLogger(__name__)
        >>> runner = CommandRunner(logger=logger)
        >>> carp = CarpRunner(
        ...     runner=runner,
        ...     carp_config_path="/home/user/carpentry_bundle/config.sh"
        ... )
        >>> 
        >>> # Run CARPentry commands
        >>> output = carp.run(['openCARP', '+F', 'experiment.par'])
        >>> output = carp.run(['meshtool', '-convert=vtk', 'mesh.pts'])
        >>> 
        >>> # Validate commands before using
        >>> if carp.validate_command_exists('cusummary'):
        ...     carp.run(['cusummary'])
    """
    
    def __init__(
        self,
        runner: CommandRunner,
        carp_config_path: Union[str, Path],
        logger: Optional[logging.Logger] = None
    ):
        """
        Initialize the CarpRunner.
        
        Args:
            runner: A CommandRunner instance to delegate command execution to
            carp_config_path: Path to the CARPentry config.sh file, typically
                located in the root of your CARPentry installation directory.
                This file is generated by the deploy.carpentry_bundle.sh script.
            logger: Optional logger instance. If None, creates a new logger.
        
        Raises:
            FileNotFoundError: If carp_config_path doesn't exist
            
        Note:
            The config.sh file should be the one generated during CARPentry
            installation, not manually created. It contains environment setup
            including virtual environment paths, library paths, and CARPentry
            license information.
        """
        self.runner = runner
        self.carp_config_path = Path(carp_config_path)
        self.logger = logger or logging.getLogger(__name__)
        
        if not self.carp_config_path.exists():
            raise FileNotFoundError(
                f"CARPentry config file not found: {self.carp_config_path}\n"
                f"This file should be generated during CARPentry installation.\n"
                f"Expected location: <carpentry_installation_dir>/config.sh"
            )
        
        self.logger.info(f"CarpRunner initialized with config: {self.carp_config_path}")
        self._carp_env: Optional[Dict[str, str]] = None
        self._installation_root: Optional[Path] = None
    
    def _load_carp_environment(self) -> Dict[str, str]:
        """
        Load CARPentry environment by sourcing config.sh and capturing variables.
        
        The config.sh file sets up the complete CARPentry environment including:
        - PATH (binaries for openCARP, carputils, meshtool, meshalyzer, etc.)
        - PYTHONPATH (carputils and related Python modules)
        - LD_LIBRARY_PATH (PETSc and other shared libraries)
        - CARPENTRY_LICENSE (license file location)
        - CARPUTILS_SETTINGS (carputils configuration file)
        - MPI settings (OPAL_PREFIX, OPAL_BINDIR, OPAL_LIBDIR)
        - Virtual environment paths (if venv was created during installation)
        
        Returns:
            Dictionary of environment variables with CARPentry configuration
            
        Raises:
            CarpEnvironmentError: If environment loading fails or required
                variables are missing
        """
        self.logger.debug("Loading CARPentry environment from config.sh")
        
        # Command to source config.sh and print all environment variables
        env_cmd = f'source "{self.carp_config_path}" && env'
        
        try:
            result = subprocess.run(
                env_cmd,
                shell=True,
                executable='/bin/bash',
                capture_output=True,
                text=True,
                check=True
            )
        except subprocess.CalledProcessError as e:
            self.logger.error(
                f"Failed to source CARPentry config.sh: {e.stderr}",
                exc_info=True
            )
            raise CarpEnvironmentError(
                f"Could not load CARPentry environment from {self.carp_config_path}.\n"
                f"Stderr: {e.stderr}"
            ) from e
        
        # Parse environment variables from output
        carp_env = {}
        for line in result.stdout.splitlines():
            if '=' in line:
                # Split only on first '=' to handle values containing '='
                key, value = line.split('=', 1)
                carp_env[key] = value
        
        # Validate that expected CARPentry variables are present
        expected_vars = [
            'PATH',
            'LD_LIBRARY_PATH',
            'CARPENTRY_LICENSE',
            'CARPUTILS_SETTINGS'
        ]
        missing_vars = [var for var in expected_vars if var not in carp_env]
        
        if missing_vars:
            self.logger.error(
                f"CARPentry environment is missing required variables: {missing_vars}"
            )
            raise CarpEnvironmentError(
                f"CARPentry environment incomplete. Missing variables: {missing_vars}\n"
                f"The config.sh file may be corrupted or incomplete."
            )
        
        # Log optional variables that might be present
        optional_vars = ['PYTHONPATH', 'VIRTUAL_ENV', 'OPAL_PREFIX']
        present_optional = [var for var in optional_vars if var in carp_env]
        if present_optional:
            self.logger.debug(
                f"Optional CARPentry variables found: {present_optional}"
            )
        
        self.logger.info(
            f"CARPentry environment loaded successfully with {len(carp_env)} variables"
        )
        return carp_env
    
    @property
    def carp_env(self) -> Dict[str, str]:
        """
        Get the CARPentry environment, loading it if not already cached.
        
        The environment is loaded once and cached for efficiency. Use
        reload_environment() to force a reload if the config.sh file changes.
        
        Returns:
            Dictionary of CARPentry environment variables
        """
        if self._carp_env is None:
            self._carp_env = self._load_carp_environment()
        return self._carp_env
    
    @property
    def installation_root(self) -> Path:
        """
        Get the root directory of the CARPentry installation.
        
        Returns:
            Path to the CARPentry installation root directory
        """
        if self._installation_root is None:
            # config.sh is in the installation root
            self._installation_root = self.carp_config_path.parent
        return self._installation_root
    
    def reload_environment(self) -> None:
        """
        Force reload of the CARPentry environment.
        
        Useful if config.sh has been modified or if you want to ensure
        you have the latest environment state. The environment will be
        reloaded on the next command execution or property access.
        """
        self.logger.info("Forcing reload of CARPentry environment")
        self._carp_env = None
    
    def run(
        self,
        cmd: Sequence[Union[str, Path]],
        expected_outputs: Optional[Sequence[Path]] = None,
        cwd: Optional[Path] = None,
        ignore_errors: Optional[Sequence[str]] = None
    ) -> str:
        """
        Execute a CARPentry command with the proper environment configuration.
        
        This method delegates to the underlying CommandRunner but ensures the
        CARPentry environment is properly set. All CARPentry tools (openCARP,
        meshtool, carputils, meshalyzer, etc.) should be available.
        
        Args:
            cmd: Command and arguments to execute. Examples:
                - ['openCARP', '+F', 'experiment.par']
                - ['meshtool', '-convert=vtk', 'mesh.pts']
                - ['cusummary']
            expected_outputs: Optional list of files expected after command runs
            cwd: Working directory for command execution
            ignore_errors: Error strings to treat as warnings rather than failures
            
        Returns:
            Command stdout as string
            
        Raises:
            CommandExecutionError: If command fails
            FileNotFoundError: If expected outputs are missing
            
        Example:
            >>> # Run openCARP simulation
            >>> carp.run(
            ...     ['openCARP', '+F', 'experiment.par'],
            ...     expected_outputs=[Path('experiment_vm.igb')],
            ...     cwd=Path('/path/to/simulation')
            ... )
        """
        self.logger.debug(f"Running CARPentry command: {' '.join(str(c) for c in cmd)}")
        
        # Delegate to CommandRunner with CARPentry environment
        return self.runner.run(
            cmd=cmd,
            expected_outputs=expected_outputs,
            cwd=cwd,
            ignore_errors=ignore_errors,
            env=self.carp_env
        )
    
    def get_carp_path(self, relative_path: str = "") -> Path:
        """
        Get a path relative to the CARPentry installation directory.
        
        Useful for locating CARPentry resources like meshes, examples,
        or specific tool directories.
        
        Args:
            relative_path: Path relative to CARPentry installation root
            
        Returns:
            Absolute path to the requested location
            
        Example:
            >>> # Get paths to CARPentry resources
            >>> bin_dir = carp.get_carp_path("bin")
            >>> meshes_dir = carp.get_carp_path("meshes")
            >>> examples_dir = carp.get_carp_path("carp-examples")
            >>> petsc_lib = carp.get_carp_path("petsc/lib")
        """
        return self.installation_root / relative_path
    
    def validate_command_exists(self, command: str) -> bool:
        """
        Check if a CARPentry command is available in the environment.
        
        This validates that a specific CARPentry tool can be found and
        executed. Useful for checking optional components or verifying
        installation completeness.
        
        Args:
            command: Name of the command to check. Common commands include:
                - 'openCARP': Main cardiac simulation solver
                - 'meshtool': Mesh manipulation tool
                - 'cusummary': CARPutils summary tool
                - 'meshalyzer': Visualization tool
                - 'bench': Benchmarking tool
                
        Returns:
            True if command is found in PATH, False otherwise
            
        Example:
            >>> # Check for optional tools
            >>> if carp.validate_command_exists('meshalyzer'):
            ...     print("Visualization available")
            >>> if not carp.validate_command_exists('openCARP'):
            ...     raise RuntimeError("openCARP not found!")
        """
        try:
            result = subprocess.run(
                ['which', command],
                env=self.carp_env,
                capture_output=True,
                text=True,
                check=False
            )
            exists = result.returncode == 0
            if exists:
                self.logger.debug(
                    f"CARPentry command '{command}' found at: {result.stdout.strip()}"
                )
            else:
                self.logger.warning(
                    f"CARPentry command '{command}' not found in environment"
                )
            return exists
        except Exception as e:
            self.logger.error(f"Error checking for command '{command}': {e}")
            return False
    
    def get_carputils_settings_path(self) -> Optional[Path]:
        """
        Get the path to the carputils settings.yaml file.
        
        Returns:
            Path to settings.yaml if CARPUTILS_SETTINGS is set, None otherwise
            
        Example:
            >>> settings_path = carp.get_carputils_settings_path()
            >>> if settings_path and settings_path.exists():
            ...     with open(settings_path) as f:
            ...         print(f.read())
        """
        settings_var = self.carp_env.get('CARPUTILS_SETTINGS')
        if settings_var:
            return Path(settings_var)
        return None
    
    def get_license_path(self) -> Optional[Path]:
        """
        Get the path to the CARPentry license file.
        
        Returns:
            Path to license.bin if CARPENTRY_LICENSE is set, None otherwise
            
        Example:
            >>> license_path = carp.get_license_path()
            >>> if license_path and license_path.exists():
            ...     print(f"License found at: {license_path}")
        """
        license_var = self.carp_env.get('CARPENTRY_LICENSE')
        if license_var:
            return Path(license_var)
        return None
    
    @classmethod
    def find_installation(
        cls,
        search_paths: Optional[Sequence[Path]] = None
    ) -> Optional[Path]:
        """
        Attempt to locate a CARPentry installation by searching for config.sh.
        
        This is a convenience method for discovering CARPentry installations
        when the exact path is not known.
        
        Args:
            search_paths: Optional list of paths to search. If None, searches
                common installation locations.
                
        Returns:
            Path to config.sh if found, None otherwise
            
        Example:
            >>> # Try to find CARPentry automatically
            >>> config_path = CarpRunner.find_installation()
            >>> if config_path:
            ...     carp = CarpRunner(runner, config_path)
            ... else:
            ...     print("CARPentry not found in common locations")
            >>>
            >>> # Search specific locations
            >>> custom_paths = [
            ...     Path("/opt/carpentry"),
            ...     Path("/data/software/carpentry_bundle")
            ... ]
            >>> config_path = CarpRunner.find_installation(custom_paths)
        """
        if search_paths is None:
            search_paths = [
                Path.home() / "carpentry_bundle",
                Path.home() / "CARPentry",
                Path.home() / "opencarp",
                Path("/opt/carpentry_bundle"),
                Path("/opt/CARPentry"),
                Path("/usr/local/carpentry_bundle"),
            ]
        
        for location in search_paths:
            config_file = location / "config.sh"
            if config_file.exists():
                return config_file
        
        return None