"""
Component manager for ESP32 Arduino framework builds in PlatformIO.

This module provides the ComponentManager class for handling IDF component
addition/removal, library ignore processing, and build script modifications.
It supports managing ESP-IDF components within Arduino framework projects,
allowing developers to add or remove specific components and handle library
dependencies efficiently.
"""

import os
import shutil
import re
import yaml
from yaml import SafeLoader
from os.path import join
from typing import Set, Optional, Dict, Any, List, Tuple


class ComponentManagerConfig:
    """
    Handles configuration and environment setup for component management.
    
    This class centralizes all configuration-related operations and provides
    a unified interface for accessing PlatformIO environment settings,
    board configurations, and framework paths.
    """
    
    def __init__(self, env):
        """
        Initialize the configuration manager with PlatformIO environment.
        
        Extracts and stores essential configuration parameters from the PlatformIO
        environment including platform details, board configuration, MCU type,
        and framework paths. This initialization ensures all dependent classes
        have consistent access to configuration data.
        
        Args:
            env: PlatformIO environment object containing project configuration,
                 board settings, and platform information
        """
        self.env = env
        self.platform = env.PioPlatform()
        self.config = env.GetProjectConfig()
        self.board = env.BoardConfig()
        # Extract MCU type from board configuration, defaulting to esp32
        self.mcu = self.board.get("build.mcu", "esp32").lower()
        # Get project source directory path
        self.project_src_dir = env.subst("$PROJECT_SRC_DIR")
        # Get Arduino framework installation directory
        self.arduino_framework_dir = self.platform.get_package_dir("framework-arduinoespressif32")
        # Get MCU-specific Arduino libraries directory
        self.arduino_libs_mcu = join(self.platform.get_package_dir("framework-arduinoespressif32-libs"), self.mcu)


class ComponentLogger:
    """
    Simple logging functionality for component operations.
    
    Provides centralized logging for all component management operations,
    tracking changes made during the build process and offering summary
    reporting capabilities.
    """
    
    def __init__(self):
        """
        Initialize the logger with empty change tracking.
        
        Sets up internal data structures for tracking component changes
        and modifications made during the build process.
        """
        # List to store all change messages for summary reporting
        self.component_changes: List[str] = []
    
    def log_change(self, message: str) -> None:
        """
        Log a change message with immediate console output.
        
        Records the change message internally for summary reporting and
        immediately prints it to the console with a component manager prefix
        for real-time feedback during build operations.
        
        Args:
            message: Descriptive message about the change or operation performed
        """
        self.component_changes.append(message)
        print(f"[ComponentManager] {message}")
    
    def get_changes_summary(self) -> List[str]:
        """
        Get a copy of all changes made during the session.
        
        Returns a defensive copy of the change log to prevent external
        modification while allowing access to the complete change history.
        
        Returns:
            List of change messages in chronological order
        """
        return self.component_changes.copy()
    
    def print_changes_summary(self) -> None:
        """
        Print a formatted summary of all changes made.
        
        Outputs a nicely formatted summary of all component changes if any
        were made, or a simple message indicating no changes occurred.
        Useful for end-of-build reporting and debugging.
        """
        if self.component_changes:
            print("\n=== Component Manager Changes ===")
            for change in self.component_changes:
                print(f"  {change}")
            print("=" * 35)
        else:
            print("[ComponentManager] No changes made")


class ComponentHandler:
    """
    Handles IDF component addition and removal operations.
    
    Manages the core functionality for adding and removing ESP-IDF components
    from Arduino framework projects, including YAML file manipulation,
    component validation, and cleanup operations.
    """
    
    def __init__(self, config: ComponentManagerConfig, logger: ComponentLogger):
        """
        Initialize the component handler with configuration and logging.
        
        Sets up the component handler with necessary dependencies for
        configuration access and change logging. Initializes tracking
        for removed components to enable proper cleanup operations.
        
        Args:
            config: Configuration manager instance providing access to paths and settings
            logger: Logger instance for recording component operations
        """
        self.config = config
        self.logger = logger
        # Track removed components for cleanup operations
        self.removed_components: Set[str] = set()
    
    def handle_component_settings(self, add_components: bool = False, remove_components: bool = False) -> None:
        """
        Handle adding and removing IDF components based on project configuration.
        
        Main entry point for component management operations. Processes both
        component additions and removals based on project configuration options,
        manages backup creation, and handles cleanup of removed components.
        
        Args:
            add_components: Whether to process component additions from custom_component_add
            remove_components: Whether to process component removals from custom_component_remove
        """
        # Create backup before first component removal and on every add of a component
        if remove_components and not self.removed_components or add_components:
            self._backup_pioarduino_build_py()
            self.logger.log_change("Created backup of build file")
        
        # Check if env and GetProjectOption are available
        if hasattr(self.config, 'env') and hasattr(self.config.env, 'GetProjectOption'):
            component_yml_path = self._get_or_create_component_yml()
            component_data = self._load_component_yml(component_yml_path)
            
            if remove_components:
                self._process_component_removals(component_data)
            
            if add_components:
                self._process_component_additions(component_data)
            
            self._save_component_yml(component_yml_path, component_data)
            
            # Clean up removed components
            if self.removed_components:
                self._cleanup_removed_components()
    
    def _process_component_removals(self, component_data: Dict[str, Any]) -> None:
        """
        Process component removal requests from project configuration.
        
        Reads the custom_component_remove option from platformio.ini and
        processes each component for removal from the dependency list.
        Handles errors gracefully and logs all operations.
        
        Args:
            component_data: Component configuration data dictionary containing dependencies
        """
        try:
            remove_option = self.config.env.GetProjectOption("custom_component_remove", None)
            if remove_option:
                # Split multiline option into individual components
                components_to_remove = remove_option.splitlines()
                self._remove_components(component_data, components_to_remove)
        except Exception as e:
            self.logger.log_change(f"Error removing components: {str(e)}")
    
    def _process_component_additions(self, component_data: Dict[str, Any]) -> None:
        """
        Process component addition requests from project configuration.
        
        Reads the custom_component_add option from platformio.ini and
        processes each component for addition to the dependency list.
        Handles errors gracefully and logs all operations.
        
        Args:
            component_data: Component configuration data dictionary containing dependencies
        """
        try:
            add_option = self.config.env.GetProjectOption("custom_component_add", None)
            if add_option:
                # Split multiline option into individual components
                components_to_add = add_option.splitlines()
                self._add_components(component_data, components_to_add)
        except Exception as e:
            self.logger.log_change(f"Error adding components: {str(e)}")
    
    def _get_or_create_component_yml(self) -> str:
        """
        Get path to idf_component.yml, creating it if necessary.
        
        Searches for existing idf_component.yml files in the Arduino framework
        directory first, then in the project source directory. If no file
        exists, creates a new one in the project source directory with
        default content.
        
        Returns:
            Absolute path to the component YAML file
        """
        # Try Arduino framework first
        framework_yml = join(self.config.arduino_framework_dir, "idf_component.yml")
        if os.path.exists(framework_yml):
            self._create_backup(framework_yml)
            return framework_yml
        
        # Try project source directory
        project_yml = join(self.config.project_src_dir, "idf_component.yml")
        if os.path.exists(project_yml):
            self._create_backup(project_yml)
            return project_yml
        
        # Create new file in project source
        self._create_default_component_yml(project_yml)
        return project_yml
    
    def _create_backup(self, file_path: str) -> None:
        """
        Create backup of a file with .orig extension.
        
        Creates a backup copy of the specified file by appending .orig
        to the filename. Only creates the backup if it doesn't already
        exist to preserve the original state.
        
        Args:
            file_path: Absolute path to the file to backup
        """
        backup_path = f"{file_path}.orig"
        if not os.path.exists(backup_path):
            shutil.copy(file_path, backup_path)
    
    def _create_default_component_yml(self, file_path: str) -> None:
        """
        Create a default idf_component.yml file with basic ESP-IDF dependency.
        
        Creates a new component YAML file with minimal default content
        specifying ESP-IDF version 5.1 or higher as the base dependency.
        This ensures compatibility with modern ESP-IDF features.
        
        Args:
            file_path: Absolute path where to create the new YAML file
        """
        default_content = {
            "dependencies": {
                "idf": ">=5.1"
            }
        }
        
        with open(file_path, 'w', encoding='utf-8') as f:
            yaml.dump(default_content, f)
    
    def _load_component_yml(self, file_path: str) -> Dict[str, Any]:
        """
        Load and parse idf_component.yml file safely.
        
        Attempts to load and parse the YAML file using SafeLoader for
        security. Returns a default structure with empty dependencies
        if the file cannot be read or parsed.
        
        Args:
            file_path: Absolute path to the YAML file to load
            
        Returns:
            Parsed YAML data as dictionary, or default structure on failure
        """
        try:
            with open(file_path, "r", encoding='utf-8') as f:
                return yaml.load(f, Loader=SafeLoader) or {"dependencies": {}}
        except Exception:
            return {"dependencies": {}}
    
    def _save_component_yml(self, file_path: str, data: Dict[str, Any]) -> None:
        """
        Save component data to YAML file safely.
        
        Attempts to write the component data dictionary to the specified
        YAML file. Handles errors gracefully by silently failing to
        prevent build interruption.
        
        Args:
            file_path: Absolute path to the YAML file to write
            data: Component data dictionary to serialize
        """
        try:
            with open(file_path, "w", encoding='utf-8') as f:
                yaml.dump(data, f)
        except Exception:
            pass
    
    def _remove_components(self, component_data: Dict[str, Any], components_to_remove: list) -> None:
        """
        Remove specified components from the configuration.
        
        Iterates through the list of components to remove, checking if each
        exists in the dependencies and removing it if found. Tracks removed
        components for later cleanup operations and logs all actions.
        
        Args:
            component_data: Component configuration data dictionary
            components_to_remove: List of component names to remove
        """
        dependencies = component_data.setdefault("dependencies", {})
        
        for component in components_to_remove:
            component = component.strip()
            if not component:
                continue
                
            if component in dependencies:
                self.logger.log_change(f"Removed component: {component}")
                del dependencies[component]
                
                # Track for cleanup - convert to filesystem-safe name
                filesystem_name = self._convert_component_name_to_filesystem(component)
                self.removed_components.add(filesystem_name)
            else:
                self.logger.log_change(f"Component not found: {component}")
    
    def _add_components(self, component_data: Dict[str, Any], components_to_add: list) -> None:
        """
        Add specified components to the configuration.
        
        Processes each component entry, parsing name and version information,
        and adds new components to the dependencies. Skips components that
        already exist and filters out entries that are too short to be valid.
        
        Args:
            component_data: Component configuration data dictionary
            components_to_add: List of component entries to add (format: name@version or name)
        """
        dependencies = component_data.setdefault("dependencies", {})
        
        for component in components_to_add:
            component = component.strip()
            if len(component) <= 4:  # Skip too short entries
                continue
            
            component_name, version = self._parse_component_entry(component)
            
            if component_name not in dependencies:
                dependencies[component_name] = {"version": version}
                self.logger.log_change(f"Added component: {component_name} ({version})")
            else:
                self.logger.log_change(f"Component already exists: {component_name}")
    
    def _parse_component_entry(self, entry: str) -> Tuple[str, str]:
        """
        Parse component entry into name and version components.
        
        Splits component entries that contain version information (format: name@version)
        and returns both parts. If no version is specified, defaults to "*" for
        latest version.
        
        Args:
            entry: Component entry string (e.g., "espressif/esp_timer@1.0.0" or "espressif/esp_timer")
            
        Returns:
            Tuple containing (component_name, version)
        """
        if "@" in entry:
            name, version = entry.split("@", 1)
            return (name.strip(), version.strip())
        return (entry.strip(), "*")
    
    def _convert_component_name_to_filesystem(self, component_name: str) -> str:
        """
        Convert component name from registry format to filesystem format.
        
        Converts component names from ESP Component Registry format (using forward slashes)
        to filesystem-safe format (using double underscores) for directory operations.
        
        Args:
            component_name: Component name in registry format (e.g., "espressif/esp_timer")
            
        Returns:
            Filesystem-safe component name (e.g., "espressif__esp_timer")
        """
        return component_name.replace("/", "__")
    
    def _backup_pioarduino_build_py(self) -> None:
        """
        Create backup of the original pioarduino-build.py file.
        
        Creates a backup of the Arduino framework's build script before
        making modifications. Only operates when Arduino framework is active
        and creates MCU-specific backup names to avoid conflicts.
        """
        if "arduino" not in self.config.env.subst("$PIOFRAMEWORK"):
            return
        
        build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
        backup_path = join(self.config.arduino_libs_mcu, f"pioarduino-build.py.{self.config.mcu}")
        
        if os.path.exists(build_py_path) and not os.path.exists(backup_path):
            shutil.copy2(build_py_path, backup_path)
    
    def _cleanup_removed_components(self) -> None:
        """
        Clean up removed components and restore original build file.
        
        Performs cleanup operations for all components that were removed,
        including removing include directories and cleaning up CPPPATH
        entries from the build script.
        """
        for component in self.removed_components:
            self._remove_include_directory(component)
        
        self._remove_cpppath_entries()
    
    def _remove_include_directory(self, component: str) -> None:
        """
        Remove include directory for a specific component.
        
        Removes the component's include directory from the Arduino framework
        libraries to prevent compilation errors and reduce build overhead.
        
        Args:
            component: Component name in filesystem format
        """
        include_path = join(self.config.arduino_libs_mcu, "include", component)
        
        if os.path.exists(include_path):
            shutil.rmtree(include_path)
    
    def _remove_cpppath_entries(self) -> None:
        """
        Remove CPPPATH entries for removed components from pioarduino-build.py.
        
        Scans the Arduino build script and removes include path entries
        for all components that were removed from the project. Uses
        multiple regex patterns to catch different include path formats.
        """
        build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
        
        if not os.path.exists(build_py_path):
            return
        
        try:
            with open(build_py_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            original_content = content
            
            # Remove CPPPATH entries for each removed component
            for component in self.removed_components:
                patterns = [
                    rf'.*join\([^,]*,\s*"include",\s*"{re.escape(component)}"[^)]*\),?\n',
                    rf'.*"include/{re.escape(component)}"[^,\n]*,?\n',
                    rf'.*"[^"]*include[^"]*{re.escape(component)}[^"]*"[^,\n]*,?\n'
                ]
                
                for pattern in patterns:
                    content = re.sub(pattern, '', content)
            
            if content != original_content:
                with open(build_py_path, 'w', encoding='utf-8') as f:
                    f.write(content)
                
        except Exception:
            pass


class LibraryIgnoreHandler:
    """
    Handles lib_ignore processing and include removal.
    
    Manages the processing of lib_ignore entries from platformio.ini,
    converting library names to include paths and removing corresponding
    entries from the build script while protecting critical components.
    """
    
    def __init__(self, config: ComponentManagerConfig, logger: ComponentLogger):
        """
        Initialize the library ignore handler.
        
        Sets up the handler with configuration and logging dependencies,
        initializes tracking for ignored libraries, and prepares caching
        for Arduino library mappings.
        
        Args:
            config: Configuration manager instance for accessing paths and settings
            logger: Logger instance for recording library operations
        """
        self.config = config
        self.logger = logger
        # Track ignored libraries for processing
        self.ignored_libs: Set[str] = set()
        # Cache for Arduino library mappings (lazy loaded)
        self._arduino_libraries_cache = None
    
    def handle_lib_ignore(self) -> None:
        """
        Handle lib_ignore entries from platformio.ini and remove corresponding includes.
        
        Main entry point for library ignore processing. Creates backup if needed,
        processes lib_ignore entries from the current environment, and removes
        corresponding include paths from the build script.
        """
        # Create backup before processing lib_ignore
        if not self.ignored_libs:
            self._backup_pioarduino_build_py()
        
        # Get lib_ignore entries from current environment only
        lib_ignore_entries = self._get_lib_ignore_entries()
        
        if lib_ignore_entries:
            self.ignored_libs.update(lib_ignore_entries)
            self._remove_ignored_lib_includes()
            self.logger.log_change(f"Processed {len(lib_ignore_entries)} ignored libraries")
    
    def _get_lib_ignore_entries(self) -> List[str]:
        """
        Get lib_ignore entries from current environment configuration only.
        
        Extracts and processes lib_ignore entries from the platformio.ini
        configuration, converting library names to include directory names
        and filtering out critical ESP32 components that should never be ignored.
        
        Returns:
            List of processed library names ready for include path removal
        """
        try:
            # Get lib_ignore from current environment only
            lib_ignore = self.config.env.GetProjectOption("lib_ignore", [])
            
            if isinstance(lib_ignore, str):
                lib_ignore = [lib_ignore]
            elif lib_ignore is None:
                lib_ignore = []
            
            # Clean and normalize entries
            cleaned_entries = []
            for entry in lib_ignore:
                entry = str(entry).strip()
                if entry:
                    # Convert library names to potential include directory names
                    include_name = self._convert_lib_name_to_include(entry)
                    cleaned_entries.append(include_name)
            
            # Filter out critical ESP32 components that should never be ignored
            critical_components = [
                'lwip',           # Network stack
                'freertos',       # Real-time OS
                'esp_system',     # System functions
                'esp_common',     # Common ESP functions
                'driver',         # Hardware drivers
                'nvs_flash',      # Non-volatile storage
                'spi_flash',      # Flash memory access
                'esp_timer',      # Timer functions
                'esp_event',      # Event system
                'log'             # Logging system
            ]
            
            filtered_entries = []
            for entry in cleaned_entries:
                if entry not in critical_components:
                    filtered_entries.append(entry)
            
            return filtered_entries
            
        except Exception:
            return []
    
    def _has_bt_ble_dependencies(self) -> bool:
        """
        Check if lib_deps contains any BT/BLE related dependencies.
        
        Scans the lib_deps configuration option for Bluetooth or BLE
        related keywords to determine if BT components should be protected
        from removal even if they appear in lib_ignore.
        
        Returns:
            True if BT/BLE dependencies are found in lib_deps
        """
        try:
            # Get lib_deps from current environment
            lib_deps = self.config.env.GetProjectOption("lib_deps", [])
            
            if isinstance(lib_deps, str):
                lib_deps = [lib_deps]
            elif lib_deps is None:
                lib_deps = []
            
            # Convert to string and check for BT/BLE keywords
            lib_deps_str = ' '.join(str(dep) for dep in lib_deps).upper()
            
            bt_ble_keywords = ['BLE', 'BT', 'NIMBLE', 'BLUETOOTH']

            return any(keyword in lib_deps_str for keyword in bt_ble_keywords)
            
        except Exception:
            return False
    
    def _is_bt_related_library(self, lib_name: str) -> bool:
        """
        Check if a library name is related to Bluetooth/BLE functionality.
        
        Examines library names for Bluetooth and BLE related keywords
        to determine if the library should be protected when BT dependencies
        are present in the project.
        
        Args:
            lib_name: Library name to check for BT/BLE relation
            
        Returns:
            True if library name contains BT/BLE related keywords
        """
        lib_name_upper = lib_name.upper()
        
        bt_related_names = [
            'BT',
            'BLE', 
            'BLUETOOTH',
            'NIMBLE',
            'ESP32_BLE',
            'ESP32BLE',
            'BLUETOOTHSERIAL',
            'BLE_ARDUINO',
            'ESP_BLE',
            'ESP_BT'
        ]

        return any(bt_name in lib_name_upper for bt_name in bt_related_names)
    
    def _get_arduino_core_libraries(self) -> Dict[str, str]:
        """
        Get all Arduino core libraries and their corresponding include paths.
        
        Scans the Arduino framework libraries directory to build a mapping
        of library names to their corresponding include paths. Reads
        library.properties files to get official library names.
        
        Returns:
            Dictionary mapping library names to include directory names
        """
        libraries_mapping = {}
        
        # Path to Arduino Core Libraries
        arduino_libs_dir = join(self.config.arduino_framework_dir, "libraries")
        
        if not os.path.exists(arduino_libs_dir):
            return libraries_mapping
        
        try:
            for entry in os.listdir(arduino_libs_dir):
                lib_path = join(arduino_libs_dir, entry)
                if os.path.isdir(lib_path):
                    lib_name = self._get_library_name_from_properties(lib_path)
                    if lib_name:
                        include_path = self._map_library_to_include_path(lib_name, entry)
                        libraries_mapping[lib_name.lower()] = include_path
                        libraries_mapping[entry.lower()] = include_path  # Also use directory name as key
        except Exception:
            pass
        
        return libraries_mapping
    
    def _get_library_name_from_properties(self, lib_dir: str) -> Optional[str]:
        """
        Extract library name from library.properties file.
        
        Reads the library.properties file in the given directory and
        extracts the official library name from the 'name=' field.
        
        Args:
            lib_dir: Path to library directory containing library.properties
            
        Returns:
            Official library name or None if not found or readable
        """
        prop_path = join(lib_dir, "library.properties")
        if not os.path.isfile(prop_path):
            return None
        
        try:
            with open(prop_path, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if line.startswith('name='):
                        return line.split('=', 1)[1].strip()
        except Exception:
            pass
        
        return None
    
    def _map_library_to_include_path(self, lib_name: str, dir_name: str) -> str:
        """
        Map library name to corresponding include path.
        
        Converts Arduino library names to their corresponding ESP-IDF
        component include paths using an extensive mapping table.
        Handles common Arduino libraries and their ESP-IDF equivalents.
        
        Args:
            lib_name: Official library name from library.properties
            dir_name: Directory name of the library
            
        Returns:
            Corresponding ESP-IDF component include path name
        """
        lib_name_lower = lib_name.lower().replace(' ', '').replace('-', '_')
        dir_name_lower = dir_name.lower()
        
        # Extended mapping list with Arduino Core Libraries
        extended_mapping = {
            # Core ESP32 mappings
            'wifi': 'esp_wifi',
            'bluetooth': 'bt',
            'bluetoothserial': 'bt',
            'ble': 'bt',
            'bt': 'bt',
            'ethernet': 'esp_eth',
            'websocket': 'esp_websocket_client',
            'http': 'esp_http_client',
            'https': 'esp_https_ota',
            'ota': 'esp_https_ota',
            'spiffs': 'spiffs',
            'fatfs': 'fatfs',
            'mesh': 'esp_wifi_mesh',
            'smartconfig': 'esp_smartconfig',
            'mdns': 'mdns',
            'coap': 'coap',
            'mqtt': 'mqtt',
            'json': 'cjson',
            'mbedtls': 'mbedtls',
            'openssl': 'openssl',
            
            # Arduino Core specific mappings (safe mappings that don't conflict with critical components)
            'esp32blearduino': 'bt',
            'esp32_ble_arduino': 'bt',
            'esp32': 'esp32',
            'wire': 'driver',
            'spi': 'driver',
            'i2c': 'driver',
            'uart': 'driver',
            'serial': 'driver',
            'analogwrite': 'driver',
            'ledc': 'driver',
            'pwm': 'driver',
            'dac': 'driver',
            'adc': 'driver',
            'touch': 'driver',
            'hall': 'driver',
            'rtc': 'driver',
            'timer': 'esp_timer',
            'preferences': 'arduino_preferences',
            'eeprom': 'arduino_eeprom',
            'update': 'esp_https_ota',
            'httpupdate': 'esp_https_ota',
            'httpclient': 'esp_http_client',
            'httpsclient': 'esp_https_ota',
            'wifimanager': 'esp_wifi',
            'wificlientsecure': 'esp_wifi',
            'wifiserver': 'esp_wifi',
            'wifiudp': 'esp_wifi',
            'wificlient': 'esp_wifi',
            'wifiap': 'esp_wifi',
            'wifimulti': 'esp_wifi',
            'esp32webserver': 'esp_http_server',
            'webserver': 'esp_http_server',
            'asyncwebserver': 'esp_http_server',
            'dnsserver': 'lwip',
            'netbios': 'netbios',
            'simpletime': 'lwip',
            'fs': 'vfs',
            'sd': 'fatfs',
            'sd_mmc': 'fatfs',
            'littlefs': 'esp_littlefs',
            'ffat': 'fatfs',
            'camera': 'esp32_camera',
            'esp_camera': 'esp32_camera',
            'arducam': 'esp32_camera',
            'rainmaker': 'esp_rainmaker',
            'esp_rainmaker': 'esp_rainmaker',
            'provisioning': 'wifi_provisioning',
            'wifiprovisioning': 'wifi_provisioning',
            'espnow': 'esp_now',
            'esp_now': 'esp_now',
            'esptouch': 'esp_smartconfig',
            'ping': 'lwip',
            'netif': 'lwip',
            'tcpip': 'lwip'
        }
        
        # Check extended mapping first
        if lib_name_lower in extended_mapping:
            return extended_mapping[lib_name_lower]
        
        # Check directory name
        if dir_name_lower in extended_mapping:
            return extended_mapping[dir_name_lower]
        
        # Fallback: Use directory name as include path
        return dir_name_lower
    
    def _convert_lib_name_to_include(self, lib_name: str) -> str:
        """
        Convert library name to potential include directory name.
        
        Converts library names from platformio.ini lib_ignore entries
        to their corresponding include directory names. Uses Arduino
        core library mappings and common naming conventions.
        
        Args:
            lib_name: Library name from lib_ignore configuration
            
        Returns:
            Converted include directory name for path removal
        """
        # Load Arduino Core Libraries on first call
        if not hasattr(self, '_arduino_libraries_cache'):
            self._arduino_libraries_cache = self._get_arduino_core_libraries()
        
        lib_name_lower = lib_name.lower()
        
        # Check Arduino Core Libraries first
        if lib_name_lower in self._arduino_libraries_cache:
            return self._arduino_libraries_cache[lib_name_lower]
        
        # Remove common prefixes and suffixes
        cleaned_name = lib_name_lower
        
        # Remove common prefixes
        prefixes_to_remove = ['lib', 'arduino-', 'esp32-', 'esp-']
        for prefix in prefixes_to_remove:
            if cleaned_name.startswith(prefix):
                cleaned_name = cleaned_name[len(prefix):]
        
        # Remove common suffixes
        suffixes_to_remove = ['-lib', '-library', '.h']
        for suffix in suffixes_to_remove:
            if cleaned_name.endswith(suffix):
                cleaned_name = cleaned_name[:-len(suffix)]
        
        # Check again with cleaned name
        if cleaned_name in self._arduino_libraries_cache:
            return self._arduino_libraries_cache[cleaned_name]
        
        # Direct mapping for common cases not in Arduino libraries
        direct_mapping = {
            'ble': 'bt',
            'bluetooth': 'bt',
            'bluetoothserial': 'bt'
        }
        
        if cleaned_name in direct_mapping:
            return direct_mapping[cleaned_name]
        
        return cleaned_name
    
    def _remove_ignored_lib_includes(self) -> None:
        """
        Remove include entries for ignored libraries from pioarduino-build.py.
        
        Processes the Arduino build script to remove CPPPATH entries for
        all ignored libraries. Implements protection for BT/BLE and DSP
        components when dependencies are detected. Uses multiple regex
        patterns to catch different include path formats.
        """
        build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
        
        if not os.path.exists(build_py_path):
            self.logger.log_change("Build file not found")
            return
        
        # Check if BT/BLE dependencies exist in lib_deps
        bt_ble_protected = self._has_bt_ble_dependencies()
        if bt_ble_protected:
            self.logger.log_change("BT/BLE protection enabled")
        
        try:
            with open(build_py_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            original_content = content
            total_removed = 0
            
            # Remove CPPPATH entries for each ignored library
            for lib_name in self.ignored_libs:
                # Skip BT-related libraries if BT/BLE dependencies are present
                if bt_ble_protected and self._is_bt_related_library(lib_name):
                    self.logger.log_change(f"Protected BT library: {lib_name}")
                    continue
                
                # Hard protection for DSP components
                if lib_name.lower() in ['dsp', 'esp_dsp', 'dsps', 'fft2r', 'dsps_fft2r']:
                    self.logger.log_change(f"Protected DSP component: {lib_name}")
                    continue
                    
                # Multiple patterns to catch different include formats
                patterns = [
                    rf'.*join\([^,]*,\s*"include",\s*"{re.escape(lib_name)}"[^)]*\),?\n',
                    rf'.*"include/{re.escape(lib_name)}"[^,\n]*,?\n',
                    rf'.*"[^"]*include[^"]*{re.escape(lib_name)}[^"]*"[^,\n]*,?\n',
                    rf'.*"[^"]*/{re.escape(lib_name)}/include[^"]*"[^,\n]*,?\n',
                    rf'.*"[^"]*{re.escape(lib_name)}[^"]*include[^"]*"[^,\n]*,?\n',
                    rf'.*join\([^)]*"include"[^)]*"{re.escape(lib_name)}"[^)]*\),?\n',
                    rf'.*"{re.escape(lib_name)}/include"[^,\n]*,?\n',
                    rf'\s*"[^"]*/{re.escape(lib_name)}/[^"]*",?\n'
                ]
                
                removed_count = 0
                for pattern in patterns:
                    matches = re.findall(pattern, content)
                    if matches:
                        content = re.sub(pattern, '', content)
                        removed_count += len(matches)
                
                if removed_count > 0:
                    self.logger.log_change(f"Ignored library: {lib_name} ({removed_count} entries)")
                    total_removed += removed_count
            
            # Clean up empty lines and trailing commas
            content = re.sub(r'\n\s*\n', '\n', content)
            content = re.sub(r',\s*\n\s*\]', '\n]', content)
            
            # Validate and write changes
            if self._validate_changes(original_content, content) and content != original_content:
                with open(build_py_path, 'w', encoding='utf-8') as f:
                    f.write(content)
                self.logger.log_change(f"Updated build file ({total_removed} total removals)")
                
        except (IOError, OSError) as e:
            self.logger.log_change(f"Error processing libraries: {str(e)}")
        except Exception as e:
            self.logger.log_change(f"Unexpected error processing libraries: {str(e)}")
    
    def _validate_changes(self, original_content: str, new_content: str) -> bool:
        """
        Validate that the changes are reasonable and safe.
        
        Performs sanity checks on the modified content to ensure that
        the changes don't remove too much content or create invalid
        modifications that could break the build process.
        
        Args:
            original_content: Original file content before modifications
            new_content: Modified file content after processing
            
        Returns:
            True if changes are within acceptable limits and safe to apply
        """
        original_lines = len(original_content.splitlines())
        new_lines = len(new_content.splitlines())
        removed_lines = original_lines - new_lines
        
        # Don't allow removing more than 50% of the file or negative changes
        return not (removed_lines > original_lines * 0.5 or removed_lines < 0)
    
    def _backup_pioarduino_build_py(self) -> None:
        """
        Create backup of the original pioarduino-build.py file.
        
        Creates a backup copy of the Arduino build script before making
        modifications. Only operates when Arduino framework is active
        and uses MCU-specific backup naming to avoid conflicts.
        """
        if "arduino" not in self.config.env.subst("$PIOFRAMEWORK"):
            return
        
        build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
        backup_path = join(self.config.arduino_libs_mcu, f"pioarduino-build.py.{self.config.mcu}")
        
        if os.path.exists(build_py_path) and not os.path.exists(backup_path):
            shutil.copy2(build_py_path, backup_path)


class BackupManager:
    """
    Handles backup and restore operations for build files.
    
    Manages the creation and restoration of backup files for the Arduino
    framework build scripts, ensuring that original files can be restored
    when needed or when builds are cleaned.
    """
    
    def __init__(self, config: ComponentManagerConfig):
        """
        Initialize the backup manager with configuration access.
        
        Sets up the backup manager with access to configuration paths
        and settings needed for backup and restore operations.
        
        Args:
            config: Configuration manager instance providing access to paths
        """
        self.config = config
    
    def backup_pioarduino_build_py(self) -> None:
        """
        Create backup of the original pioarduino-build.py file.
        
        Creates a backup copy of the Arduino framework's build script
        with MCU-specific naming to prevent conflicts between different
        ESP32 variants. Only creates backup if it doesn't already exist.
        """
        if "arduino" not in self.config.env.subst("$PIOFRAMEWORK"):
            return
        
        build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
        backup_path = join(self.config.arduino_libs_mcu, f"pioarduino-build.py.{self.config.mcu}")
        
        if os.path.exists(build_py_path) and not os.path.exists(backup_path):
            shutil.copy2(build_py_path, backup_path)
    
    def restore_pioarduino_build_py(self, target=None, source=None, env=None) -> None:
        """
        Restore the original pioarduino-build.py from backup.
        
        Restores the original Arduino build script from the backup copy
        and removes the backup file. This is typically called during
        clean operations or when resetting the build environment.
        
        Args:
            target: Build target (unused, for PlatformIO compatibility)
            source: Build source (unused, for PlatformIO compatibility)
            env: Environment (unused, for PlatformIO compatibility)
        """
        build_py_path = join(self.config.arduino_libs_mcu, "pioarduino-build.py")
        backup_path = join(self.config.arduino_libs_mcu, f"pioarduino-build.py.{self.config.mcu}")
        
        if os.path.exists(backup_path):
            shutil.copy2(backup_path, build_py_path)
            os.remove(backup_path)


class ComponentManager:
    """
    Main component manager that orchestrates all operations.
    
    Primary interface for component management operations, coordinating
    between specialized handlers for components, libraries, and backups.
    Uses composition pattern to organize functionality into focused classes.
    """
    
    def __init__(self, env):
        """
        Initialize the ComponentManager with composition pattern.
        
        Creates and configures all specialized handler instances using
        the composition pattern for better separation of concerns and
        maintainability. Each handler focuses on a specific aspect
        of component management.
        
        Args:
            env: PlatformIO environment object containing project configuration
        """
        self.config = ComponentManagerConfig(env)
        self.logger = ComponentLogger()
        self.component_handler = ComponentHandler(self.config, self.logger)
        self.library_handler = LibraryIgnoreHandler(self.config, self.logger)
        self.backup_manager = BackupManager(self.config)
    
    def handle_component_settings(self, add_components: bool = False, remove_components: bool = False) -> None:
        """
        Handle component operations by delegating to specialized handlers.
        
        Main entry point for component management operations. Coordinates
        component addition/removal and library ignore processing, then
        provides a summary of all changes made during the session.
        
        Args:
            add_components: Whether to process component additions from configuration
            remove_components: Whether to process component removals from configuration
        """
        self.component_handler.handle_component_settings(add_components, remove_components)
        self.library_handler.handle_lib_ignore()
        
        # Print summary
        changes = self.logger.get_changes_summary()
        if changes:
            self.logger.log_change(f"Session completed with {len(changes)} changes")
    
    def handle_lib_ignore(self) -> None:
        """
        Delegate lib_ignore handling to specialized handler.
        
        Provides direct access to library ignore processing for cases
        where only library handling is needed without component operations.
        """
        self.library_handler.handle_lib_ignore()
    
    def restore_pioarduino_build_py(self, target=None, source=None, env=None) -> None:
        """
        Delegate backup restoration to backup manager.
        
        Provides access to backup restoration functionality, typically
        used during clean operations or build environment resets.
        
        Args:
            target: Build target (unused, for PlatformIO compatibility)
            source: Build source (unused, for PlatformIO compatibility)
            env: Environment (unused, for PlatformIO compatibility)
        """
        self.backup_manager.restore_pioarduino_build_py(target, source, env)
    
    def get_changes_summary(self) -> List[str]:
        """
        Get summary of changes from logger.
        
        Provides access to the complete list of changes made during
        the current session for reporting or debugging purposes.
        
        Returns:
            List of change messages in chronological order
        """
        return self.logger.get_changes_summary()
    
    def print_changes_summary(self) -> None:
        """
        Print changes summary via logger.
        
        Outputs a formatted summary of all changes made during the
        session, useful for build reporting and debugging.
        """
        self.logger.print_changes_summary()
