#!/usr/bin/env python3
"""
MegaCLI SAS Status Tool - Modernized Python implementation

Original by Adam Cecile <gandalf@le-vert.net>
Modified by Vincent S. Cojot <vincent@cojot.name>
Modernized Python implementation

A tool to monitor RAID controller status using MegaCLI, perccli, or storcli.
"""

# $Id: megaclisas-status,v 2.21 2025/11/14 20:00:00 formatting-consistency-fix Exp $
__version__ = "megaclisas-status,v 2.21 2025/11/14 20:00:00 formatting-consistency-fix Exp"

#
# VERSION HISTORY:
# ================
#
# v2.21 (2025-11-14): Output formatting consistency improvements
#   - Fixed drive model formatting in legacy MegaCli parser to match perccli order
#   - Drive model now displays: "Manufacturer DriveType Firmware Serial" (less to more specific)
#   - Previous order was: "Serial Manufacturer DriveType Firmware" (inconsistent with native perccli)
#   - Fixed array size formatting to use TB for large arrays (>= 1000 GB)
#   - Array sizes now display as "1.818 TB" instead of "1818G" for consistency with perccli
#   - Fixed disk size formatting to use TB for large disks (>= 1000 GB)
#   - Disk sizes now run through _parse_size() for consistent TB/GB formatting
#   - Sizes >= 1 TB use TB format with 3 decimal places, smaller sizes use GB/MB
#   - Eliminates minor rounding differences between MegaCli and perccli output
#
# v2.20 (2025-11-14): Fixed storcli command syntax selection bug
#   - Fixed critical bug where storcli was detected correctly but commands used wrong syntax
#   - Changed all command generation methods to check supports_legacy_syntax/supports_native_syntax
#   - Previous logic checked only for PERCCLI types, causing storcli to fall through to legacy syntax
#   - Now properly uses native syntax (/c0 show all) for storcli instead of legacy (-AdpAllInfo -a0)
#   - Affected methods: get_controller_info, get_array_info, get_all_arrays_info, get_physical_drives_info,
#     get_pci_info, get_bbu_status, get_foreign_disks
#   - Added legacy syntax testing for storcli (previously hardcoded as native-only)
#   - Older storcli versions may support legacy syntax; now dynamically detected like perccli
#   - Tool detection improved: actually tests legacy command instead of assuming capabilities
#   - Reported by user: https://github.com/eLvErDe/hwraid/issues/148#issuecomment-3524076649
#
# v2.19 (2025-09-18): Fixed disk ID formatting during RAID conversions
#   - Fixed disk ID display issue when DG (Drive Group) field shows "-" during RAID conversions
#   - Disks in transitional state (e.g., being added to arrays) now show as "c0uXpY" instead of "c0u-pY"
#   - Uses "X" marker like unconfigured drives since drive group parenthood cannot be determined
#   - Safe for multi-drive-group systems - avoids assumptions about target drive group assignment
#   - Discovered during live RAID-0 to RAID-1 conversion where second disk showed DG="-" in perccli
#
# v2.18 (2025-09-08): Fixed v2.16 regressions reported by community users
#   - Fixed BBU status detection regression: controllers now properly show "Good" instead of "N/A"
#   - Enhanced BBU detection logic to recognize any valid BBU status (not just "Present")
#   - Fixed hotspare disk classification: hotspares now appear in Unconfigured section again
#   - Restored original disk ordering by logical ID (c0u0p0, c0u0p1, c0u1p0...) for better readability
#   - Improved array-based disk grouping instead of slot-based sorting
#   - Community feedback: https://github.com/eLvErDe/hwraid/issues/148#issuecomment-3267036072
#
# v2.17 (2025-09-06): Foreign disk detection feature from community contribution
#   - Added foreign disk detection capability for unconfigured drives
#   - Implemented get_foreign_disks() method with support for both MegaCLI and perccli syntax
#   - Added parse_foreign_disks() parser to extract foreign disk slot numbers from tool output
#   - Foreign drives now display as "State (Foreign)" in unconfigured disk section
#   - Based on community contribution: https://github.com/eLvErDe/hwraid/pull/132/files
#   - Enhanced debugging output shows detected foreign disk slots per controller
#   - Gracefully handles systems without foreign configurations (no errors)
#
# v2.16 (2025-09-06): Fixed --notemp regression bug
#   - Fixed regression where --notemp argument was parsed but not applied to temperature display
#   - Added temperature override logic in check_system_status() for both controller and disk temperatures
#   - Now properly shows "N/A" for all temperatures when --notemp flag is used
#   - Matches legacy script behavior: temperatures suppressed at display time, not parsing time
#   - Applied fix to both configured and unconfigured disk temperature display
#
# v2.15 (2025-09-02): Drive model whitespace normalization and cache property parsing fix
#   - Fixed excessive whitespace in Drive Model column reported by community users
#   - Added comprehensive whitespace normalization using re.sub(r'\s{2,}', ' ') across all parsing paths
#   - Normalizes whitespace in inquiry data, model numbers, and combined model fields
#   - Handles firmware-reported fields with inconsistent padding and spacing
#   - Ensures consistent Drive Model display formatting across all controller types
#   - Fixed cache property parsing bug: reordered ReadAheadNone before ReadAhead to avoid substring matching
#   - Code cleanup: replaced whitespace-only lines with proper blank lines
#   - Tested and validated on multiple systems (daltigoth, palanthas)
#
# v2.14 (2025-08-28): Flexible tool path arguments and improved help text
#   - Added support for --perccli-path and --tool-path as aliases for --megacli-path
#   - All three argument names are completely interchangeable (auto-detection still works)
#   - Cleaned up help text formatting with generic PATH metavar for clarity
#   - Enhanced user experience: choose the argument name that matches your tool preference
#
# v2.12 (2025-08-28): Performance optimization and enhanced diagnostics
#   - Optimized syntax detection: Once-only detection during initialization vs every command
#   - Enhanced --version flag: Shows all discovered tools, active tool, and capabilities
#   - Fixed critical tool type detection bug: MegaCli64 now correctly identified as 'megacli'
#   - Improved user experience: Better formatting and clearer diagnostic messages
#
# v2.11 (2025-08-28): Hardware detection accuracy fix
#   - Fixed critical bug where /proc/scsi/scsi LUNs were incorrectly counted as controllers
#   - Removed /proc/scsi/scsi from controller detection (it shows LUNs, not controllers)
#   - Made lspci the sole authoritative source for physical controller counting
#   - This correctly detects single controllers on both daltigoth and palanthas
#
# v2.10 (2025-08-28): Multi-tool auto-detection and hardware validation
#   - Added comprehensive tool discovery (MegaCli64, perccli legacy/native, storcli)
#   - Implemented hardware validation using lspci, /proc/scsi/scsi, kernel modules
#   - Added intelligent fallback when tools fail to detect known hardware
#   - Enhanced error reporting with detailed diagnostics and recommendations
#   - Automatic tool capability detection (legacy vs native syntax support)
#   - Cross-validation between tool reports and hardware detection
#   - Robust handling of tool version incompatibilities and regressions
#
# v2.05 (2025-08-25): Progress reporting and consistency check enhancements
#   - Enhanced progress reporting to handle both legacy and native formats
#   - Improved consistency check detection across different tool versions
#   - Added comprehensive version history documentation
#
# v2.05 (2025-08-25): OS Path fallback fix for system compatibility
#   - Fixed OS Path display to fall back to target_id when device path unavailable
#   - Resolves regression on systems like daltigoth where /dev/disk/by-path mapping fails
#   - Maintains compatibility with original script behavior
#   - Ensures consistent output across different system configurations
#
# v2.04 (2025-08-25): Enhanced progress reporting for perccli64
#   - Added "Active Operations" parsing for native perccli format
#   - Both MegaCli64 and perccli64 paths now show consistency check progress
#   - Complete feature parity across all tool combinations
#
# v2.03 (2025-08-25): Fixed progress reporting regression
#   - Added "Ongoing Progresses" parsing for MegaCli64 format
#   - Restored consistency check progress display like legacy script
#   - Handles Check Consistency, Background Initialization, Reconstruction
#
# v2.02 (2025-08-25): Defensive programming enhancements
#   - Enhanced array/disk state handling with maintenance state recognition
#   - Added graceful handling for unknown states with warning logging
#   - Improved robustness for production environments
#   - Added data validation framework with safe defaults
#
# v2.01 (2025-08-25): Major parsing and display improvements
#   - Fixed unconfigured drive ID format (c0uXpY instead of c0uXpUnknown)
#   - Improved drive model display format (model firmware serial)
#   - Enhanced parsing logic for both MegaCli64 and perccli64
#   - Better unconfigured drive detection and separate section display
#   - Fixed serial numbers and firmware versions in Drive Model column
#
# v2.00 (2025-08-24): Initial modernization and versioning
#   - Added --version flag support
#   - Renamed from megaclisas-status-v2 to main megaclisas-status
#   - Established version string format compatible with original $Id$ field
#   - Maintained megaclisas-status-v2 copy for community link compatibility
#
# PRIOR VERSIONS: Legacy Python script with various community enhancements
#   - Original concept and implementation
#   - Basic MegaCli64 support and output formatting
#   - Nagios and InfluxDB output format support
#

import os
import re
import sys
import time
import logging
import subprocess
import shutil
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from functools import lru_cache
from pathlib import Path
from typing import Dict, List, Optional, Pattern, Tuple, Union
import argparse

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)


class OutputMode(Enum):
    """Output format modes."""
    NORMAL = "normal"
    NAGIOS = "nagios"
    INFLUXDB = "influxdb"


class DiskState(Enum):
    """Disk states."""
    ONLINE = "Online"
    REBUILDING = "Rebuilding"
    FAILED = "Failed"
    HOTSPARE = "Hotspare"
    UNCONFIGURED = "Unconfigured"
    JBOD = "JBOD"
    OFFLINE = "Offline"


class ArrayState(Enum):
    """Array states."""
    OPTIMAL = "Optimal"
    DEGRADED = "Degraded"
    FAILED = "Failed"
    REBUILDING = "Rebuilding"


@dataclass
class Config:
    """Configuration settings."""
    output_mode: OutputMode = OutputMode.NORMAL
    debug: bool = False
    no_temp: bool = False
    print_controller: bool = True
    print_array: bool = True
    print_disk: bool = True
    megacli_path: Optional[str] = None
    has_admin_privileges: bool = False


@dataclass
class RAIDStats:
    """RAID system statistics."""
    good_arrays: int = 0
    bad_arrays: int = 0
    good_disks: int = 0
    bad_disks: int = 0
    total_drives: int = 0
    total_configured_drives: int = 0
    total_unconfigured_drives: int = 0

    @property
    def has_errors(self) -> bool:
        """Check if there are any errors in the system."""
        return self.bad_arrays > 0 or self.bad_disks > 0


@dataclass
class DiskInfo:
    """Information about a physical disk."""
    controller_id: int
    array_id: str
    disk_id: str
    enclosure_id: str
    slot_id: str
    lsi_id: str
    media_type: str  # HDD, SSD
    model: str
    size: str
    state: str
    speed: str
    temperature: str
    manufacturer: str = ""
    serial: str = ""


@dataclass
class ArrayInfo:
    """Information about a RAID array."""
    controller_id: int
    array_id: str
    raid_type: str
    size: str
    strip_size: str
    properties: str
    disk_cache: str
    state: str
    target_id: str
    cache_cade_info: str
    in_progress: str
    os_path: str = "N/A"


@dataclass
class ControllerInfo:
    """Information about a RAID controller."""
    controller_id: int
    model: str
    memory: str
    temperature: str
    bbu_status: str
    firmware: str
    pci_path: str = ""


class MegaCLIError(Exception):
    """Custom exception for MegaCLI related errors."""
    pass


class ToolType(Enum):
    """Types of RAID management tools."""
    MEGACLI = "megacli"
    PERCCLI_LEGACY = "perccli_legacy"  # Old perccli with MegaCli compatibility
    PERCCLI_NATIVE = "perccli_native"  # New perccli with native syntax only
    STORCLI = "storcli"


@dataclass
class ToolInfo:
    """Information about detected RAID management tool."""
    tool_type: ToolType
    binary_path: str
    version: str
    supports_legacy_syntax: bool
    supports_native_syntax: bool
    priority: int  # Lower number = higher priority


@dataclass
class HardwareInfo:
    """System hardware detection information."""
    controllers_in_lspci: List[str]
    megaraid_driver_loaded: bool
    expected_controller_count: int


class RegexPatterns:
    """Compiled regex patterns for parsing."""

    VIRTUAL_DRIVE: Pattern = re.compile(r"^(CacheCade )?Virtual Drive:.*(Target Id: ([0-9]+)).*$")
    RAID_LEVEL: Pattern = re.compile(r"^RAID Level.*?:.*$")
    ARRAY_SIZE: Pattern = re.compile(r"^Size.*?:.*$")
    ARRAY_STATE: Pattern = re.compile(r"^State.*?:.*$")
    STRIP_SIZE: Pattern = re.compile(r"^Strip Size.*?:.*$")
    SPAN_DEPTH: Pattern = re.compile(r"^Span Depth.*?:.*$")
    CACHE_POLICY: Pattern = re.compile(r"^Current Cache Policy.*?:.*$")
    DISK_CACHE_POLICY: Pattern = re.compile(r"^Disk Cache Policy.*?:.*$")

    ENCLOSURE_ID: Pattern = re.compile(r"Enclosure Device ID: (.*)$")
    SLOT_NUMBER: Pattern = re.compile(r"Slot Number: (.*)$")
    DEVICE_ID: Pattern = re.compile(r"^Device Id: (.*)$")
    FIRMWARE_STATE: Pattern = re.compile(r"Firmware state: (.*)$")
    INQUIRY_DATA: Pattern = re.compile(r"Inquiry Data: (.*)$")
    MEDIA_TYPE: Pattern = re.compile(r"^Media Type: (.*)$")
    DEVICE_SPEED: Pattern = re.compile(r"Device Speed: (.*)$")
    DRIVE_TEMP: Pattern = re.compile(r"Drive Temperature :(.*)$")
    COERCED_SIZE: Pattern = re.compile(r"^Coerced Size: (.*)$")

    CONTROLLER_COUNT: Pattern = re.compile(r"^Controller Count.*$")
    PRODUCT_NAME: Pattern = re.compile(r"^Product Name.*$")
    MEMORY_SIZE: Pattern = re.compile(r"^Memory Size.*$")
    FW_PACKAGE: Pattern = re.compile(r"^FW Package Build.*$")
    ROC_TEMP: Pattern = re.compile(r"^ROC temperature :.*$")
    BBU_PRESENT: Pattern = re.compile(r"^BBU +:.*$")
    BBU_REPLACEMENT: Pattern = re.compile(r"^ *Battery Replacement required +:.*$")

    PCI_BUS: Pattern = re.compile(r"^Bus Number.*:.*$")
    PCI_DEVICE: Pattern = re.compile(r"^Device Number.*:.*$")
    PCI_FUNCTION: Pattern = re.compile(r"^Function Number.*:.*$")

    PD_INFORMATION: Pattern = re.compile(r"PD: ([0-9]+) Information.*$")
    DRIVES_POSITION: Pattern = re.compile(r"^Drive.s posi*tion: DiskGroup: [0-9]+,.*$")


class MegaCLIInterface:
    """Interface to MegaCLI/perccli/storcli commands."""

    def __init__(self, config: Config):
        """Initialize MegaCLI interface with enhanced tool detection."""
        self.config = config
        self.available_tools: List[ToolInfo] = []
        self.hardware_info: HardwareInfo = self._detect_hardware()
        self.active_tool: Optional[ToolInfo] = None
        self.megacli_path = self._find_best_tool()
        self._command_cache: Dict[str, List[str]] = {}

    def _find_megacli_binary(self) -> str:
        """Find MegaCLI binary in PATH."""
        if self.config.megacli_path:
            if Path(self.config.megacli_path).is_file():
                return self.config.megacli_path
            else:
                # User explicitly specified a path, but it doesn't exist - fail immediately
                raise MegaCLIError(f"Cannot find specified MegaCLI binary: {self.config.megacli_path}")

        # Add common paths to search
        search_paths = [
            "/opt/MegaRAID/MegaCli",
            "/ms/dist/hwmgmt/bin",
            "/opt/MegaRAID/perccli",
            "/opt/MegaRAID/storcli",
            "/opt/lsi/storcli",
        ]

        for path in search_paths:
            if self.config.debug:
                self._debug_print(f"Looking in PATH {path}")
            os.environ["PATH"] += os.pathsep + path

        binary_names = [
            "MegaCli64", "MegaCli", "megacli", "MegaCli.exe",
            "perccli64", "perccli", "storcli64", "storcli"
        ]

        for binary in binary_names:
            if self.config.debug:
                self._debug_print(f"Looking for {binary} in PATH...")
            if path := shutil.which(binary):
                if self.config.debug:
                    self._debug_print(f"Will use this executable: {path}")
                return path

        raise MegaCLIError("Cannot find MegaCLI, perccli, or storcli binary in PATH")

    def _detect_hardware(self) -> HardwareInfo:
        """Detect RAID hardware using system information."""
        controllers_in_lspci = []
        megaraid_driver_loaded = False

        try:
            # Check lspci for RAID controllers (this is the authoritative method)
            lspci_output = subprocess.run(['lspci'], capture_output=True, text=True, timeout=10)
            if lspci_output.returncode == 0:
                for line in lspci_output.stdout.splitlines():
                    if any(keyword in line.lower() for keyword in ['raid', 'perc', 'megaraid', 'lsi']):
                        controllers_in_lspci.append(line.strip())
                        if self.config.debug:
                            self._debug_print(f"Found RAID controller in lspci: {line.strip()}")
        except (subprocess.TimeoutExpired, FileNotFoundError):
            if self.config.debug:
                self._debug_print("Could not run lspci command")

        try:
            # Check if megaraid_sas driver is loaded (confirmation that hardware is working)
            lsmod_output = subprocess.run(['lsmod'], capture_output=True, text=True, timeout=10)
            if lsmod_output.returncode == 0:
                megaraid_driver_loaded = 'megaraid_sas' in lsmod_output.stdout
                if self.config.debug:
                    self._debug_print(f"megaraid_sas driver loaded: {megaraid_driver_loaded}")
        except (subprocess.TimeoutExpired, FileNotFoundError):
            if self.config.debug:
                self._debug_print("Could not run lsmod command")

        # Controller count is based solely on lspci results
        expected_count = len(controllers_in_lspci)
        if self.config.debug:
            self._debug_print(f"Hardware controller count from lspci: {expected_count}")

        hardware_info = HardwareInfo(
            controllers_in_lspci=controllers_in_lspci,
            megaraid_driver_loaded=megaraid_driver_loaded,
            expected_controller_count=expected_count
        )

        if self.config.debug:
            self._debug_print(f"Hardware detection results: {hardware_info}")

        return hardware_info

    def _discover_all_tools(self) -> List[ToolInfo]:
        """Discover all available RAID management tools and their capabilities."""
        tools = []

        # Search paths for tools
        search_paths = [
            "/opt/MegaRAID/MegaCli",
            "/ms/dist/hwmgmt/bin",
            "/opt/MegaRAID/perccli",
            "/opt/MegaRAID/storcli",
            "/opt/lsi/storcli",
        ]

        # Add paths to environment
        for path in search_paths:
            if os.path.isdir(path):
                os.environ["PATH"] += os.pathsep + path

        # Tool discovery configuration: (binary_name, tool_type, priority)
        tool_configs = [
            ("MegaCli64", ToolType.MEGACLI, 1),
            ("MegaCli", ToolType.MEGACLI, 2),
            ("perccli64", ToolType.PERCCLI_LEGACY, 3),  # Will be reclassified after version check
            ("perccli", ToolType.PERCCLI_LEGACY, 4),
            ("storcli64", ToolType.STORCLI, 5),
            ("storcli", ToolType.STORCLI, 6),
        ]

        for binary_name, tool_type, priority in tool_configs:
            binary_path = shutil.which(binary_name)
            if binary_path:
                try:
                    version, capabilities = self._analyze_tool_version(binary_path, tool_type)
                    tool_info = ToolInfo(
                        tool_type=capabilities['tool_type'],  # Use the refined type
                        binary_path=binary_path,
                        version=version,
                        supports_legacy_syntax=capabilities['legacy_syntax'],
                        supports_native_syntax=capabilities['native_syntax'],
                        priority=priority
                    )
                    tools.append(tool_info)
                    if self.config.debug:
                        self._debug_print(f"Discovered tool: {tool_info}")
                except Exception as e:
                    if self.config.debug:
                        self._debug_print(f"Error analyzing {binary_name}: {e}")

        # Sort by priority (lower number = higher priority)
        tools.sort(key=lambda t: t.priority)
        return tools

    def _determine_initial_tool_type(self, binary_path: str) -> ToolType:
        """Determine initial tool type based on binary name."""
        binary_name = Path(binary_path).name.lower()

        if "megacli" in binary_name:
            return ToolType.MEGACLI
        elif "perccli" in binary_name:
            return ToolType.PERCCLI_LEGACY  # Will be refined in _analyze_tool_version
        elif "storcli" in binary_name:
            return ToolType.STORCLI
        else:
            # Default assumption for unknown binaries
            return ToolType.PERCCLI_LEGACY

    def _analyze_tool_version(self, binary_path: str, initial_type: ToolType) -> Tuple[str, Dict]:
        """Analyze tool version and determine its capabilities."""
        version = "Unknown"
        capabilities = {
            'tool_type': initial_type,
            'legacy_syntax': False,
            'native_syntax': False
        }

        try:
            # Get version information
            version_cmd = f"{binary_path} -v"
            if initial_type in [ToolType.PERCCLI_LEGACY, ToolType.STORCLI]:
                # Try native syntax first for perccli/storcli
                version_result = subprocess.run(
                    [binary_path, "-v"],
                    capture_output=True,
                    text=True,
                    timeout=15
                )
                if version_result.returncode == 0:
                    version = self._extract_version_from_output(version_result.stdout)

                    # For perccli and storcli, determine if they support legacy syntax
                    if "perccli" in binary_path.lower():
                        legacy_test = subprocess.run(
                            [binary_path, "-adpCount", "-NoLog"],
                            capture_output=True,
                            text=True,
                            timeout=10
                        )

                        if legacy_test.returncode == 0 and "syntax error" not in legacy_test.stdout.lower():
                            capabilities['tool_type'] = ToolType.PERCCLI_LEGACY
                            capabilities['legacy_syntax'] = True
                            capabilities['native_syntax'] = True
                        else:
                            capabilities['tool_type'] = ToolType.PERCCLI_NATIVE
                            capabilities['legacy_syntax'] = False
                            capabilities['native_syntax'] = True
                    elif "storcli" in binary_path.lower():
                        # Test storcli for legacy syntax support (older versions might support it)
                        legacy_test = subprocess.run(
                            [binary_path, "-adpCount", "-NoLog"],
                            capture_output=True,
                            text=True,
                            timeout=10
                        )

                        if legacy_test.returncode == 0 and "syntax error" not in legacy_test.stdout.lower():
                            # Older storcli that supports legacy syntax
                            capabilities['legacy_syntax'] = True
                            capabilities['native_syntax'] = True
                        else:
                            # Newer storcli that only supports native syntax
                            capabilities['legacy_syntax'] = False
                            capabilities['native_syntax'] = True

            elif initial_type == ToolType.MEGACLI:
                # MegaCli only supports legacy syntax
                capabilities['legacy_syntax'] = True
                capabilities['native_syntax'] = False
                version_result = subprocess.run(
                    [binary_path, "-v"],
                    capture_output=True,
                    text=True,
                    timeout=15
                )
                if version_result.returncode == 0:
                    version = self._extract_version_from_output(version_result.stdout)

        except subprocess.TimeoutExpired:
            if self.config.debug:
                self._debug_print(f"Timeout analyzing {binary_path}")
        except Exception as e:
            if self.config.debug:
                self._debug_print(f"Error analyzing {binary_path}: {e}")

        return version, capabilities

    def _extract_version_from_output(self, output: str) -> str:
        """Extract version string from tool output."""
        # Look for common version patterns
        patterns = [
            r"Ver\s+(\d+\.\d+\.\d+\.\d+)",       # "Ver 007.3208.0000.0000"
            r"Version\s+(\d+\.\d+\.\d+)",        # "Version 8.07.14"
            r"MegaCli\s+SAS\s+RAID\s+Management\s+Tool\s+Ver\s+(\S+)",
            r"PercCli\s+.*Ver\s+(\S+)",
            r"StorCli\s+.*Ver\s+(\S+)"
        ]

        for pattern in patterns:
            match = re.search(pattern, output, re.IGNORECASE)
            if match:
                return match.group(1)

        return "Unknown"

    def _find_best_tool(self) -> str:
        """Find the best available tool, with intelligent fallback and validation."""
        # If user specified a path, validate and use it
        if self.config.megacli_path:
            if Path(self.config.megacli_path).is_file():
                # Analyze the specified tool
                try:
                    initial_type = self._determine_initial_tool_type(self.config.megacli_path)
                    version, capabilities = self._analyze_tool_version(
                        self.config.megacli_path,
                        initial_type
                    )
                    self.active_tool = ToolInfo(
                        tool_type=capabilities['tool_type'],
                        binary_path=self.config.megacli_path,
                        version=version,
                        supports_legacy_syntax=capabilities['legacy_syntax'],
                        supports_native_syntax=capabilities['native_syntax'],
                        priority=0  # User-specified gets highest priority
                    )
                    if self.config.debug:
                        syntax_mode = "legacy syntax" if self.active_tool.supports_legacy_syntax else "native syntax"
                        self._debug_print(f"Selected user-specified tool {self.active_tool.binary_path} (type: {self.active_tool.tool_type.value}, supports: {syntax_mode})")
                    return self.config.megacli_path
                except Exception as e:
                    if self.config.debug:
                        self._debug_print(f"Error analyzing user-specified tool: {e}")
            else:
                raise MegaCLIError(f"Cannot find specified MegaCLI binary: {self.config.megacli_path}")

        # Discover all available tools
        self.available_tools = self._discover_all_tools()

        if not self.available_tools:
            raise MegaCLIError("Cannot find MegaCLI, perccli, or storcli binary in PATH")

        # If hardware is detected but no tools can see it, try alternative approaches
        if self.hardware_info.expected_controller_count > 0:
            if self.config.debug:
                self._debug_print(f"Hardware detected: {self.hardware_info.expected_controller_count} controllers")

            # Try each tool and validate it can see the hardware
            for tool in self.available_tools:
                if self._validate_tool_with_hardware(tool):
                    self.active_tool = tool
                    if self.config.debug:
                        syntax_mode = "legacy syntax" if tool.supports_legacy_syntax else "native syntax"
                        self._debug_print(f"Selected validated tool {tool.binary_path} (type: {tool.tool_type.value}, supports: {syntax_mode})")
                    return tool.binary_path

        # Fallback: use the highest priority available tool
        self.active_tool = self.available_tools[0]
        if self.config.debug:
            syntax_mode = "legacy syntax" if self.active_tool.supports_legacy_syntax else "native syntax"
            self._debug_print(f"Selected fallback tool {self.active_tool.binary_path} (type: {self.active_tool.tool_type.value}, supports: {syntax_mode})")
        return self.active_tool.binary_path

    def _validate_tool_with_hardware(self, tool: ToolInfo) -> bool:
        """Test if a tool can actually detect the hardware we know exists."""
        try:
            # Test controller count detection
            if tool.supports_legacy_syntax:
                test_cmd = f"{tool.binary_path} -adpCount -NoLog"
            else:
                test_cmd = f"{tool.binary_path} show ctrlcount"

            result = subprocess.run(
                test_cmd.split(),
                capture_output=True,
                text=True,
                timeout=15
            )

            if result.returncode == 0:
                detected_count = self._parse_controller_count_from_output(result.stdout, tool)
                if detected_count > 0:
                    if self.config.debug:
                        self._debug_print(f"Tool {tool.binary_path} detected {detected_count} controllers")
                    return True

        except Exception as e:
            if self.config.debug:
                self._debug_print(f"Tool validation failed for {tool.binary_path}: {e}")

        return False

    def _parse_controller_count_from_output(self, output: str, tool: ToolInfo) -> int:
        """Parse controller count from tool output."""
        # Try various patterns to extract controller count
        patterns = [
            r"Controller Count\s*[=:]\s*(\d+)",
            r"Number of Controllers\s*[=:]\s*(\d+)",
            r"Controllers Found\s*[=:]\s*(\d+)",
            r"Adapter Count\s*[=:]\s*(\d+)",
            r"Controllers\s*[=:]\s*(\d+)",
        ]

        for line in output.splitlines():
            line_stripped = line.strip()
            for pattern in patterns:
                match = re.search(pattern, line_stripped, re.IGNORECASE)
                if match:
                    return int(match.group(1))

        return 0

    def execute_command(self, cmd: str) -> List[str]:
        """Execute MegaCLI command with caching and error handling."""
        if cmd in self._command_cache:
            if self.config.debug:
                self._debug_print(f"Got Cached value: {cmd}")
            return self._command_cache[cmd]

        if self.config.debug:
            self._debug_print(f"Not a Cached value: {cmd}")

        try:
            result = subprocess.run(
                cmd.split(),
                capture_output=True,
                text=True,
                check=False,  # Don't raise on non-zero exit
                timeout=30
            )

            lines = [line.strip() for line in result.stdout.splitlines() if line.strip()]
            self._command_cache[cmd] = lines

            if self.config.debug:
                self._debug_print(f"Command executed: {cmd}")
                self._debug_print(f"Command output ({len(lines)} lines):")
                for line in lines:
                    sys.stderr.write(f"    {line}\n")
                sys.stderr.write("\n")

            return lines

        except subprocess.TimeoutExpired:
            raise MegaCLIError(f"Command timed out: {cmd}")
        except Exception as e:
            raise MegaCLIError(f"Command execution failed: {cmd}\nError: {str(e)}")

    def _debug_print(self, msg: str):
        """Print debug message to stderr like the original script."""
        if self.config.debug:
            import inspect
            frame = inspect.currentframe().f_back
            line_num = frame.f_lineno if frame else "?"
            sys.stderr.write(f"# DEBUG ({line_num}) : {msg}\n")

    def get_controller_count(self) -> int:
        """Get the number of controllers using progressive tool testing."""
        if not self.active_tool:
            if self.config.debug:
                self._debug_print("No active tool available")
            return 0

        # For perccli tools, try both legacy and native syntax
        if self.active_tool.tool_type in [ToolType.PERCCLI_LEGACY, ToolType.PERCCLI_NATIVE]:
            return self._try_perccli_progressive_detection()
        else:
            # For MegaCli64 and storcli, use their native approach
            return self._try_controller_detection()

    def _try_perccli_progressive_detection(self) -> int:
        """Try perccli with legacy syntax first, then native syntax."""
        if self.config.debug:
            self._debug_print(f"Testing perccli progressive detection with {self.active_tool.binary_path}")

        # Step 1: Try legacy syntax first (works with older perccli versions)
        try:
            cmd = f"{self.active_tool.binary_path} -adpCount -NoLog"
            output = self.execute_command(cmd)

            # Check if legacy command worked (no syntax errors)
            has_syntax_error = any("syntax error" in line.lower() or "deprecated command" in line.lower()
                                 for line in output)

            if not has_syntax_error:
                count = self._parse_controller_count_from_output("\n".join(output), self.active_tool)
                if count > 0:
                    if self.config.debug:
                        self._debug_print(f"Legacy syntax succeeded: found {count} controllers")
                    return count
                elif self.config.debug:
                    self._debug_print(f"Legacy syntax worked but found 0 controllers")
            else:
                if self.config.debug:
                    self._debug_print("Legacy syntax not supported, trying native syntax")

        except Exception as e:
            if self.config.debug:
                self._debug_print(f"Legacy syntax failed: {e}")

        # Step 2: Try native syntax
        try:
            cmd = f"{self.active_tool.binary_path} show ctrlcount"
            output = self.execute_command(cmd)
            count = self._parse_controller_count_from_output("\n".join(output), self.active_tool)

            if count > 0:
                if self.config.debug:
                    self._debug_print(f"Native syntax succeeded: found {count} controllers")
                return count
            elif self.config.debug:
                self._debug_print(f"Native syntax worked but found 0 controllers")

        except Exception as e:
            if self.config.debug:
                self._debug_print(f"Native syntax failed: {e}")

        # Step 3: Both approaches failed or returned 0
        if self.config.debug:
            self._debug_print("Both legacy and native syntax failed to detect controllers")

        return 0

    def _try_controller_detection(self) -> int:
        """Try controller detection using the active tool."""
        if not self.active_tool:
            return 0

        try:
            # Choose command based on tool capabilities
            if self.active_tool.supports_legacy_syntax:
                cmd = f"{self.active_tool.binary_path} -adpCount -NoLog"
            else:
                cmd = f"{self.active_tool.binary_path} show ctrlcount"

            output = self.execute_command(cmd)
            count = self._parse_controller_count_from_output("\n".join(output), self.active_tool)

            if self.config.debug:
                self._debug_print(f"Tool {self.active_tool.tool_type.value} detected {count} controllers")

            return count

        except Exception as e:
            if self.config.debug:
                self._debug_print(f"Controller detection failed with active tool: {e}")
            return 0

    def _try_fallback_tools(self) -> int:
        """Try fallback tools when primary tool fails to detect controllers."""
        for tool in self.available_tools:
            if tool == self.active_tool:
                continue  # Skip the tool that already failed

            try:
                if self.config.debug:
                    self._debug_print(f"Trying fallback tool: {tool.binary_path}")

                # Switch to this tool and try progressive detection if it's perccli
                old_tool = self.active_tool
                self.active_tool = tool
                self.megacli_path = tool.binary_path

                if tool.tool_type in [ToolType.PERCCLI_LEGACY, ToolType.PERCCLI_NATIVE]:
                    count = self._try_perccli_progressive_detection()
                else:
                    count = self._try_controller_detection()

                if count > 0:
                    if self.config.debug:
                        self._debug_print(f"Fallback tool {tool.tool_type.value} successfully detected {count} controllers")
                    return count
                else:
                    # Restore original tool if fallback also failed
                    self.active_tool = old_tool
                    self.megacli_path = old_tool.binary_path if old_tool else ""

            except Exception as e:
                if self.config.debug:
                    self._debug_print(f"Fallback tool {tool.binary_path} failed: {e}")

        # If no tools can detect controllers, there are no controllers accessible to management tools
        if self.config.debug:
            self._debug_print("No management tools could detect controllers")

        return 0

    def get_controller_info(self, controller_id: int) -> List[str]:
        """Get controller information."""
        if self.active_tool:
            if self.active_tool.supports_legacy_syntax:
                # Use legacy syntax for MegaCli64 and legacy perccli
                cmd = f"{self.megacli_path} -AdpAllInfo -a{controller_id} -NoLog"
            elif self.active_tool.supports_native_syntax:
                # Use native syntax for native perccli and storcli
                cmd = f"{self.megacli_path} /c{controller_id} show all"
            else:
                # Fallback to legacy syntax if no capabilities detected
                cmd = f"{self.megacli_path} -AdpAllInfo -a{controller_id} -NoLog"
            return self.execute_command(cmd)
        else:
            # No active tool - use legacy syntax as fallback
            cmd = f"{self.megacli_path} -AdpAllInfo -a{controller_id} -NoLog"
            return self.execute_command(cmd)













    def get_array_info(self, controller_id: int, array_id: int) -> List[str]:
        """Get array information."""
        if self.active_tool:
            if self.active_tool.supports_legacy_syntax:
                # Use legacy syntax for MegaCli64 and legacy perccli
                cmd = f"{self.megacli_path} -LDInfo -l{array_id} -a{controller_id} -NoLog"
            elif self.active_tool.supports_native_syntax:
                # Use native syntax for native perccli and storcli
                cmd = f"{self.megacli_path} /c{controller_id}/v{array_id} show all"
            else:
                # Fallback to legacy syntax if no capabilities detected
                cmd = f"{self.megacli_path} -LDInfo -l{array_id} -a{controller_id} -NoLog"
            return self.execute_command(cmd)
        else:
            # No active tool - use legacy syntax as fallback
            cmd = f"{self.megacli_path} -LDInfo -l{array_id} -a{controller_id} -NoLog"
            return self.execute_command(cmd)

    def get_all_arrays_info(self, controller_id: int) -> List[str]:
        """Get information for all arrays on controller."""
        if self.active_tool:
            if self.active_tool.supports_legacy_syntax:
                # Use legacy syntax for MegaCli64 and legacy perccli
                cmd = f"{self.megacli_path} -LDInfo -lall -a{controller_id} -NoLog"
            elif self.active_tool.supports_native_syntax:
                # Use native syntax for native perccli and storcli
                cmd = f"{self.megacli_path} /c{controller_id}/vall show all"
            else:
                # Fallback to legacy syntax if no capabilities detected
                cmd = f"{self.megacli_path} -LDInfo -lall -a{controller_id} -NoLog"
            return self.execute_command(cmd)
        else:
            # No active tool - use legacy syntax as fallback
            cmd = f"{self.megacli_path} -LDInfo -lall -a{controller_id} -NoLog"
            return self.execute_command(cmd)

    def get_physical_drives_info(self, controller_id: int) -> List[str]:
        """Get physical drives information."""
        if self.active_tool:
            if self.active_tool.supports_legacy_syntax:
                # Use legacy syntax for MegaCli64 and legacy perccli
                cmd = f"{self.megacli_path} -PDList -a{controller_id} -NoLog"
            elif self.active_tool.supports_native_syntax:
                # Use native syntax for native perccli and storcli
                cmd = f"{self.megacli_path} /c{controller_id}/eall/sall show all"
            else:
                # Fallback to legacy syntax if no capabilities detected
                cmd = f"{self.megacli_path} -PDList -a{controller_id} -NoLog"
            return self.execute_command(cmd)
        else:
            # No active tool - use legacy syntax as fallback
            cmd = f"{self.megacli_path} -PDList -a{controller_id} -NoLog"
            return self.execute_command(cmd)

    def get_unconfigured_drives_info(self, controller_id: int) -> List[str]:
        """Get unconfigured drives information."""
        # Same as physical drives - we filter unconfigured ones in the parser
        return self.get_physical_drives_info(controller_id)

    def get_pci_info(self, controller_id: int) -> List[str]:
        """Get PCI information for controller."""
        if self.active_tool:
            if self.active_tool.supports_legacy_syntax:
                # Use legacy syntax for MegaCli64 and legacy perccli
                cmd = f"{self.megacli_path} -AdpGetPciInfo -a{controller_id} -NoLog"
            elif self.active_tool.supports_native_syntax:
                # PCI info is embedded in controller info for native perccli and storcli
                cmd = f"{self.megacli_path} /c{controller_id} show all"
            else:
                # Fallback to legacy syntax if no capabilities detected
                cmd = f"{self.megacli_path} -AdpGetPciInfo -a{controller_id} -NoLog"
            return self.execute_command(cmd)
        else:
            # No active tool - use legacy syntax as fallback
            cmd = f"{self.megacli_path} -AdpGetPciInfo -a{controller_id} -NoLog"
            return self.execute_command(cmd)

    def get_bbu_status(self, controller_id: int) -> List[str]:
        """Get BBU status information."""
        if self.active_tool:
            if self.active_tool.supports_legacy_syntax:
                # Use legacy syntax for MegaCli64 and legacy perccli
                cmd = f"{self.megacli_path} -AdpBbuCmd -GetBbuStatus -a{controller_id} -NoLog"
            elif self.active_tool.supports_native_syntax:
                # Use native syntax for native perccli and storcli
                cmd = f"{self.megacli_path} /c{controller_id}/bbu show all"
            else:
                # Fallback to legacy syntax if no capabilities detected
                cmd = f"{self.megacli_path} -AdpBbuCmd -GetBbuStatus -a{controller_id} -NoLog"
            return self.execute_command(cmd)
        else:
            # No active tool - use legacy syntax as fallback
            cmd = f"{self.megacli_path} -AdpBbuCmd -GetBbuStatus -a{controller_id} -NoLog"
            return self.execute_command(cmd)

    def get_foreign_disks(self, controller_id: int) -> List[str]:
        """Get foreign disk slot numbers for controller."""
        if self.active_tool:
            if self.active_tool.supports_legacy_syntax:
                # Use legacy syntax for MegaCli64 and legacy perccli
                cmd = f"{self.megacli_path} -CfgForeign -Dsply -a{controller_id} -NoLog"
            elif self.active_tool.supports_native_syntax:
                # Use native syntax for native perccli and storcli
                cmd = f"{self.megacli_path} /c{controller_id}/fall show"
            else:
                # Fallback to legacy syntax if no capabilities detected
                cmd = f"{self.megacli_path} -CfgForeign -Dsply -a{controller_id} -NoLog"
            return self.execute_command(cmd)
        else:
            # No active tool - use legacy syntax as fallback
            cmd = f"{self.megacli_path} -CfgForeign -Dsply -a{controller_id} -NoLog"
            return self.execute_command(cmd)


class RAIDParser:
    """Parser for MegaCLI output."""

    @staticmethod
    def parse_controller_info(output: List[str], controller_id: int) -> ControllerInfo:
        """Parse controller information from MegaCLI or native perccli output."""
        model = "Unknown"
        memory = "Unknown"
        firmware = "Unknown"
        temperature = "N/A"
        bbu_status = "N/A"

        # Detect if this is native perccli output (contains "=" instead of ":")
        is_native_perccli = False
        for line in output:
            if "Model =" in line or "Controller =" in line:
                is_native_perccli = True
                break

        for line in output:
            line_stripped = line.strip()

            if is_native_perccli:
                # Native perccli format: "Field = Value"
                if line_stripped.startswith("Model ="):
                    model = line_stripped.split("=", 1)[1].strip()
                elif line_stripped.startswith("On Board Memory Size ="):
                    memory = line_stripped.split("=", 1)[1].strip()
                elif line_stripped.startswith("Firmware Package Build ="):
                    firmware = line_stripped.split("=", 1)[1].strip()
                elif line_stripped.startswith("ROC temperature(Degree Celsius) ="):
                    temp_value = line_stripped.split("=", 1)[1].strip()
                    temperature = f"{temp_value}C"
                elif line_stripped.startswith("BBU ="):
                    bbu_present = line_stripped.split("=", 1)[1].strip()
                    # If any BBU value is found (not just "Present"), consider BBU present for further checking
                    bbu_status = "Present" if bbu_present and bbu_present.lower() not in ["no", "absent", "n/a", "none"] else "N/A"
            else:
                # Legacy MegaCLI format: "Field : Value"
                if match := RegexPatterns.PRODUCT_NAME.match(line_stripped):
                    model = line_stripped.split(":")[1].strip()
                elif match := RegexPatterns.MEMORY_SIZE.match(line_stripped):
                    memory = line_stripped.split(":")[1].strip()
                elif match := RegexPatterns.FW_PACKAGE.match(line_stripped):
                    firmware = line_stripped.split(":")[1].strip()
                elif match := RegexPatterns.ROC_TEMP.match(line_stripped):
                    temp_str = line_stripped.split(":")[1].strip()
                    temperature = re.sub(r" +.*$", "", temp_str) + "C"
                elif match := RegexPatterns.BBU_PRESENT.match(line_stripped):
                    bbu_present = line_stripped.split(":")[1].strip()
                    # If any BBU value is found (not just "Present"), consider BBU present for further checking
                    bbu_status = "Present" if bbu_present and bbu_present.lower() not in ["no", "absent", "n/a", "none"] else "N/A"

        return ControllerInfo(
            controller_id=controller_id,
            model=model,
            memory=memory,
            temperature=temperature,
            bbu_status=bbu_status,
            firmware=firmware
        )

    @staticmethod
    def parse_bbu_status(output: List[str]) -> str:
        """Parse BBU status from MegaCLI BBU output."""
        for line in output:
            if match := re.match(r"^ *Battery Replacement required +:.*$", line.strip()):
                replacement_required = line.split(":")[1].strip()
                replacement_required = re.sub(r" +.*$", "", replacement_required)
                if replacement_required == "Yes":
                    return "REPL"
                else:
                    return "Good"
        return "Good"

    @staticmethod
    def parse_pci_info(output: List[str]) -> str:
        """Parse PCI path information."""
        bus_id = ""
        dev_id = ""
        func_id = ""

        for line in output:
            if match := RegexPatterns.PCI_BUS.match(line.strip()):
                bus_id = str(line.strip().split(":")[1].strip()).zfill(2)
            elif match := RegexPatterns.PCI_DEVICE.match(line.strip()):
                dev_id = str(line.strip().split(":")[1].strip()).zfill(2)
            elif match := RegexPatterns.PCI_FUNCTION.match(line.strip()):
                func_id = str(line.strip().split(":")[1].strip()).zfill(1)

        if bus_id:
            pci_path = f"0000:{bus_id}:{dev_id}.{func_id}"
            if hasattr(RAIDParser, '_megacli_ref') and RAIDParser._megacli_ref and RAIDParser._megacli_ref.config.debug:
                RAIDParser._megacli_ref._debug_print(f"Array PCI path : {pci_path}")
            return pci_path
        return ""

    @staticmethod
    def parse_array_info(output: List[str], controller_id: int, array_id: int) -> ArrayInfo:
        """Parse array information from MegaCLI or native perccli output."""
        target_id = ""
        raid_level = 0
        raid_type = "Unknown"  # Initialize for native perccli format
        size = ""
        state = "N/A"
        strip_size = ""
        disk_cache = "N/A"
        properties = ""
        span_depth = 0
        cache_cade_info = "None"
        in_progress = "None"

        # Check if this is native perccli output (look for VD table format)
        is_native_perccli = False
        for line in output:
            if "DG/VD TYPE" in line and "State" in line:
                is_native_perccli = True
                break

        if is_native_perccli:
            # Parse native perccli VD table format and extract OS path
            # Format: "0/0   RAID0 Optl  RW     Yes     RWBD  -   OFF  1.818 TB boot"

            in_vd_table = False
            os_path = "N/A"

            for line in output:
                line_stripped = line.strip()

                # Extract OS Drive Name if present
                if "OS Drive Name" in line_stripped and "=" in line_stripped:
                    os_path = line_stripped.split("=", 1)[1].strip()

                # Extract Active Operations (progress) if present
                elif "Active Operations" in line_stripped and "=" in line_stripped:
                    operations = line_stripped.split("=", 1)[1].strip()
                    if operations and operations != "None":
                        in_progress = operations

                if "DG/VD TYPE" in line_stripped and "State" in line_stripped:
                    in_vd_table = True
                    continue
                elif line_stripped.startswith("---") or line_stripped.startswith("==="):
                    continue
                elif in_vd_table and line_stripped:
                    # Look for our specific array_id in the table
                    parts = line_stripped.split()
                    if len(parts) >= 8:
                        # Parse: "0/239 RAID0 Optl  RW     Yes     RWBD  -   ON  1.818 TB Virtual Disk0"
                        dg_vd = parts[0]  # "0/239" format: DG/VD
                        # Extract VD number (second part after "/")
                        if "/" in dg_vd:
                            vd_number = int(dg_vd.split("/")[1])
                            if vd_number == array_id:
                                # Normalize RAID type format to include hyphen (RAID0 -> RAID-0)
                                raw_raid_type = parts[1]  # "RAID0", "RAID6", etc.
                                raid_type = raw_raid_type.replace("RAID", "RAID-") if raw_raid_type.startswith("RAID") else raw_raid_type
                                state = "Optimal" if parts[2] == "Optl" else parts[2]
                                # Size parsing: look for pattern like "1.818 TB" in the line
                                # Format can be: "...OFF  1.818 TB boot" or "...OFF 43.656 TB"
                                size = "Unknown"
                                for i in range(len(parts) - 1):
                                    if parts[i+1] == "TB" or parts[i+1] == "GB":
                                        try:
                                            # Verify the previous part is a number
                                            float(parts[i])
                                            size = f"{parts[i]} {parts[i+1]}"
                                            break
                                        except ValueError:
                                            continue

                                target_id = str(array_id)
                                properties = "ADRA,WB"  # Default based on cache column
                                disk_cache = "Enabled"   # Default assumption
                                strip_size = "512KB"     # Default, not shown in table
                                in_vd_table = False  # Continue processing to find OS Drive Name
                    elif line_stripped and not line_stripped.startswith("VD="):
                        # End of table
                        break

        # Return ArrayInfo for native perccli after processing all lines
        if is_native_perccli:
            return ArrayInfo(
                controller_id=controller_id,
                array_id=f"c{controller_id}u{array_id}",
                raid_type=raid_type,
                size=size,
                strip_size=strip_size,
                properties=properties,
                disk_cache=disk_cache,
                state=state,
                target_id=target_id,
                cache_cade_info=cache_cade_info,
                in_progress=in_progress,
                os_path=os_path
            )
        else:
            # Legacy MegaCLI format parsing
            for i, line in enumerate(output):
                if match := RegexPatterns.VIRTUAL_DRIVE.match(line.strip()):
                    target_id = line.strip().split(":")[2].split(")")[0].strip()
                elif match := RegexPatterns.RAID_LEVEL.match(line.strip()):
                    raid_level = int(line.strip().split(":")[1].split(",")[0].split("-")[1].strip())
                elif match := RegexPatterns.ARRAY_SIZE.match(line.strip()):
                    size = RAIDParser._parse_size(line.strip().split(":")[1])
                elif match := RegexPatterns.SPAN_DEPTH.match(line.strip()):
                    span_depth = int(line.strip().split(":")[1].strip())
                elif match := RegexPatterns.ARRAY_STATE.match(line.strip()):
                    state = line.strip().split(":")[1].strip()
                elif match := RegexPatterns.STRIP_SIZE.match(line.strip()):
                    strip_size = line.strip().split(":")[1].strip().replace(" ", "")
                elif match := RegexPatterns.CACHE_POLICY.match(line.strip()):
                    props = line.strip().split(":")[1].strip()
                    properties = RAIDParser._parse_cache_properties(props)
                elif match := RegexPatterns.DISK_CACHE_POLICY.match(line.strip()):
                    props = line.strip().split(":")[1].strip()
                    if "Disabled" in props:
                        disk_cache = "Disabled"
                    elif "Default" in props:
                        disk_cache = "Default"
                    elif "Enabled" in props:
                        disk_cache = "Enabled"
                elif "Ongoing Progresses:" in line:
                    # Look ahead for progress details
                    for j in range(i + 1, min(i + 10, len(output))):  # Look at next 10 lines max
                        progress_line = output[j].strip()
                        if not progress_line:
                            continue
                        if "Check Consistency" in progress_line and ":" in progress_line:
                            # Extract: "Check Consistency        : Completed 0%, Taken 0 min."
                            progress_info = progress_line.split(":", 1)[1].strip()
                            in_progress = f"Check Consistency : {progress_info}"
                            break
                        elif "Background Initialization" in progress_line and ":" in progress_line:
                            progress_info = progress_line.split(":", 1)[1].strip()
                            in_progress = f"Background Initialization : {progress_info}"
                            break
                        elif "Reconstruction" in progress_line and ":" in progress_line:
                            progress_info = progress_line.split(":", 1)[1].strip()
                            in_progress = f"Reconstruction : {progress_info}"
                            break
                        elif not progress_line.startswith(" ") and progress_line != "":
                            # Stop looking if we hit a line that doesn't start with space (new section)
                            break

            # Determine RAID type for legacy format
            raid_type = RAIDParser._determine_raid_type(raid_level, span_depth)

        # If we reach here, return default/fallback array info
        return ArrayInfo(
            controller_id=controller_id,
            array_id=f"c{controller_id}u{array_id}",
            raid_type=raid_type if 'raid_type' in locals() else f"RAID-{raid_level}" if raid_level else "Unknown",
            size=size,
            strip_size=strip_size,
            properties=properties,
            disk_cache=disk_cache,
            state=state,
            target_id=target_id,
            cache_cade_info=cache_cade_info,
            in_progress=in_progress
        )

    @staticmethod
    def _parse_size(size_str: str) -> str:
        """Parse and normalize size string to TB or GB format."""
        size_str = size_str.strip()

        if size_str.endswith("MB"):
            size_mb = float(size_str.rstrip("MB").strip())
            size_gb = size_mb / 1000
            if size_gb >= 1000:
                # Convert to TB for very large sizes
                return f"{size_gb / 1000:.3f} TB"
            elif size_gb >= 1:
                return f"{int(round(size_gb))}G"
            else:
                return f"{int(round(size_mb))}M"
        elif size_str.endswith("TB"):
            # Keep TB format with 3 decimal places for consistency
            size_tb = float(size_str.rstrip("TB").strip())
            return f"{size_tb:.3f} TB"
        else:  # GB or no unit
            size_gb = float(size_str.rstrip("GB").strip())
            if size_gb >= 1000:
                # Convert to TB for large sizes
                return f"{size_gb / 1000:.3f} TB"
            else:
                return f"{int(round(size_gb))}G"

    @staticmethod
    def _parse_cache_properties(props: str) -> str:
        """Parse cache properties."""
        properties = ""

        # Check most specific patterns first to avoid substring matching issues
        if "ReadAheadNone" in props:
            properties += "NORA"
        elif "ReadAdaptive" in props:
            properties += "ADRA"
        elif "ReadAhead" in props:
            properties += "RA"

        if "WriteBack" in props:
            properties += ",WB"
        elif "WriteThrough" in props:
            properties += ",WT"

        return properties

    @staticmethod
    def _determine_raid_type(raid_level: int, span_depth: int) -> str:
        """Determine RAID type from level and span depth."""
        if span_depth >= 2:
            return f"RAID-{raid_level}0"
        else:
            return f"RAID-{raid_level}"

    @staticmethod
    def parse_disk_info(output: List[str], controller_id: int) -> List[DiskInfo]:
        """Parse disk information from MegaCLI or native perccli output."""
        disks = []
        current_disk = {}
        array_id = "Unknown"
        array_index = -1
        disk_id = "Unknown"

        # Check if this is native perccli output
        is_native_perccli = False
        for line in output:
            if "Drive /c" in line and "/e" in line and "/s" in line and line.endswith(" :"):
                is_native_perccli = True
                break

        if is_native_perccli:
            return RAIDParser._parse_native_perccli_disks(output, controller_id)

        # Check if this is -PDList format (lists all drives individually)
        is_pdlist_format = False
        for line in output:
            if "Enclosure Device ID:" in line and "Virtual Drive:" not in ''.join(output[:10]):
                is_pdlist_format = True
                break

        if is_pdlist_format:
            return RAIDParser._parse_megacli_pdlist(output, controller_id)

        for line in output:
            line = line.strip()

            if match := re.match(r"^(CacheCade )?Virtual (Disk|Drive): ([0-9]+).*$", line):
                array_index += 1
                array_id = match.group(3)
            elif match := RegexPatterns.PD_INFORMATION.match(line):
                disk_id = match.group(1)
            elif match := RegexPatterns.ENCLOSURE_ID.match(line):
                if current_disk and "model" in current_disk:
                    # Save previous disk
                    disks.append(RAIDParser._create_disk_info(current_disk, controller_id, array_id, disk_id))
                current_disk = {"enclosure_id": match.group(1).strip().replace("N/A", "")}
            elif match := RegexPatterns.SLOT_NUMBER.match(line):
                current_disk["slot_id"] = match.group(1).strip()
            elif match := RegexPatterns.DEVICE_ID.match(line):
                current_disk["lsi_id"] = match.group(1).strip()
            elif match := RegexPatterns.FIRMWARE_STATE.match(line):
                current_disk["state"] = match.group(1).strip()
            elif match := RegexPatterns.INQUIRY_DATA.match(line):
                # Normalize whitespace in inquiry data immediately
                raw_model = match.group(1).strip()
                normalized_model = re.sub(r'\s{2,}', ' ', raw_model)
                current_disk["model"] = normalized_model
                current_disk.update(RAIDParser._parse_drive_model(current_disk["model"]))
            elif match := RegexPatterns.MEDIA_TYPE.match(line):
                media_type = match.group(1).strip()
                if media_type == "Hard Disk Device":
                    current_disk["media_type"] = "HDD"
                elif media_type == "Solid State Device":
                    current_disk["media_type"] = "SSD"
                else:
                    current_disk["media_type"] = "N/A"
            elif match := RegexPatterns.COERCED_SIZE.match(line):
                size_str = match.group(1).strip()
                # Remove sector information in brackets
                size_str = re.sub(r" \[.*\].*$", "", size_str).strip()
                # Normalize size format through _parse_size() for consistency
                current_disk["size"] = RAIDParser._parse_size(size_str)
            elif match := RegexPatterns.DEVICE_SPEED.match(line):
                current_disk["speed"] = match.group(1).strip()
            elif match := RegexPatterns.DRIVE_TEMP.match(line):
                temp_str = match.group(1).strip()
                current_disk["temperature"] = re.sub(r" \(.*\)", "", temp_str)

        # Don't forget the last disk
        if current_disk and "model" in current_disk:
            disks.append(RAIDParser._create_disk_info(current_disk, controller_id, array_id, disk_id))

        return disks

    @staticmethod
    def _parse_native_perccli_disks(output: List[str], controller_id: int) -> List[DiskInfo]:
        """Parse disk information from native perccli output."""
        disks = []
        current_disk = {}
        current_drive = None
        in_device_table = False

        for line in output:
            line_stripped = line.strip()

            # Start of new drive section: "Drive /c0/e64/s0 :"
            if match := re.match(r"^Drive /c(\d+)/e(\d+)/s(\d+) :$", line_stripped):
                # Save previous disk if we have one
                if current_disk and current_drive:
                    disks.append(RAIDParser._create_native_disk_info(current_disk, controller_id, current_drive))

                # Start new disk
                current_drive = {
                    "controller": match.group(1),
                    "enclosure": match.group(2),
                    "slot": match.group(3)
                }
                current_disk = {}
                in_device_table = False

            elif current_drive:  # Only parse if we're in a drive section
                # Device table header: "EID:Slt DID State DG     Size Intf Med SED PI SeSz Model                   Sp Type"
                if line_stripped.startswith("EID:Slt DID State"):
                    in_device_table = True
                elif line_stripped.startswith("---"):
                    continue  # Skip separator lines
                elif in_device_table and line_stripped and not line_stripped.startswith("EID="):
                    # Parse device table row: "64:0      0 Onln   0 1.818 TB SATA SSD Y   N  512B Samsung SSD 870 EVO 2TB U  -"
                    parts = line_stripped.split()
                    if len(parts) >= 11:  # Ensure we have enough parts
                        try:
                            # Parse: EID:Slt, DID, State, DG, Size, Intf, Med, SED, PI, SeSz, Model...
                            size_idx = 4  # Size is at index 4
                            if parts[size_idx+1] in ["TB", "GB", "MB"]:  # Size + unit
                                size_str = f"{parts[size_idx]} {parts[size_idx+1]}"
                                # Normalize size format through _parse_size() for consistency
                                current_disk["size"] = RAIDParser._parse_size(size_str)

                            intf_idx = size_idx + 2  # Interface after size
                            current_disk["interface"] = parts[intf_idx]

                            med_idx = intf_idx + 1  # Media type after interface
                            current_disk["media_type"] = parts[med_idx]

                            # Model starts after SeSz (sector size) - usually around index 10+
                            model_start = 10
                            if len(parts) > model_start:
                                # Combine remaining parts as model, excluding last 2 (Sp Type)
                                model_parts = parts[model_start:-2] if len(parts) > model_start+2 else parts[model_start:]
                                raw_model = " ".join(model_parts)
                                # Normalize whitespace in model
                                current_disk["model"] = re.sub(r'\s{2,}', ' ', raw_model.strip())

                            # State - convert perccli abbreviations to full status
                            state_raw = parts[2]
                            if state_raw == "Onln":
                                current_disk["state"] = "Online, Spun Up"
                            elif state_raw == "UGood":
                                current_disk["state"] = "Unconfigured(good), Spun Up"
                            else:
                                current_disk["state"] = state_raw

                            # Array ID (DG) - handle "-" case for drives in transition
                            dg_value = parts[3]
                            if dg_value == "-":
                                # Drive not assigned to specific group yet (e.g., during RAID conversion)
                                # Use "X" like unconfigured drives since parenthood cannot be determined
                                current_disk["array_id"] = "X"
                            else:
                                current_disk["array_id"] = dg_value

                            in_device_table = False  # We've processed the table row
                        except (IndexError, ValueError):
                            pass  # Ignore parsing errors

                # Drive position = DriveGroup:0, Span:0, Row:0 (backup method)
                elif line_stripped.startswith("Drive position =") and "array_id" not in current_disk:
                    if "DriveGroup:" in line_stripped:
                        try:
                            dg_part = line_stripped.split("DriveGroup:")[1].split(",")[0].strip()
                            current_disk["array_id"] = dg_part
                        except:
                            pass

                # Drive Temperature = 26C (78.80 F)
                elif line_stripped.startswith("Drive Temperature ="):
                    temp_match = re.search(r"(\d+)C", line_stripped)
                    current_disk["temperature"] = f"{temp_match.group(1)}C" if temp_match else "Unknown"

                # Device attributes section
                elif line_stripped.startswith("Model Number ="):
                    raw_model = line_stripped.split("=")[1].strip()
                    # Normalize whitespace in model number
                    current_disk["model"] = re.sub(r'\s{2,}', ' ', raw_model)
                elif line_stripped.startswith("Coerced size ="):
                    # "Coerced size = 1.818 TB [0xe8d00000 Sectors]"
                    size_part = line_stripped.split("=")[1].split("[")[0].strip()
                    # Normalize size format through _parse_size() for consistency
                    current_disk["size"] = RAIDParser._parse_size(size_part)
                elif line_stripped.startswith("Device Speed ="):
                    current_disk["speed"] = line_stripped.split("=")[1].strip()
                elif line_stripped.startswith("SN ="):
                    current_disk["serial"] = line_stripped.split("=")[1].strip()
                elif line_stripped.startswith("Firmware Revision ="):
                    current_disk["firmware"] = line_stripped.split("=")[1].strip()

        # Don't forget the last disk
        if current_disk and current_drive:
            disks.append(RAIDParser._create_native_disk_info(current_disk, controller_id, current_drive))

        return disks

    @staticmethod
    def _create_native_disk_info(disk_data: dict, controller_id: int, drive_info: dict) -> DiskInfo:
        """Create DiskInfo object from native perccli parsed data."""
        # Create disk ID based on slot
        disk_id = drive_info["slot"]
        array_id = disk_data.get("array_id", "Unknown")

        # Use media type from parsed table data first, fall back to model-based detection
        media_type = disk_data.get("media_type", "HDD")  # Use parsed "Med" column

        # Combine model, firmware, and serial into one field
        base_model = disk_data.get("model", "Unknown")
        firmware = disk_data.get("firmware", "").strip()
        serial = disk_data.get("serial", "").strip()

        # Format: "model firmware serial" (more readable)
        model_parts = []
        if base_model and base_model != "Unknown":
            # Normalize whitespace in base model
            base_model = re.sub(r'\s{2,}', ' ', base_model.strip())
            model_parts.append(base_model)
        if firmware:
            model_parts.append(firmware)
        if serial:
            model_parts.append(serial)

        model = " ".join(model_parts) if model_parts else "Unknown"
        # Final normalization to ensure no excessive whitespace
        model = re.sub(r'\s{2,}', ' ', model.strip())

        # If media_type is still generic, try to detect from model name
        if media_type == "HDD":  # Only override if it's the default
            if "SSD" in model.upper() or "NVME" in model.upper():
                media_type = "SSD"

        return DiskInfo(
            controller_id=controller_id,
            array_id=array_id,
            disk_id=disk_id,
            enclosure_id=drive_info["enclosure"],
            slot_id=drive_info["slot"],
            lsi_id=disk_data.get("lsi_id", drive_info["slot"]),  # Use slot as fallback
            media_type=media_type,
            model=model,
            size=disk_data.get("size", "Unknown"),
            state=disk_data.get("state", "Unknown"),
            speed=disk_data.get("speed", "Unknown"),
            temperature=disk_data.get("temperature", "Unknown"),
            manufacturer=disk_data.get("manufacturer", ""),
            serial=disk_data.get("serial", "")
        )

    @staticmethod
    def _parse_inquiry_hex(hex_line: str, disk_data: dict):
        """Try to extract model info from inquiry hex data."""
        try:
            # Convert hex to ASCII and look for readable strings
            hex_bytes = bytes.fromhex(hex_line.replace(' ', ''))
            ascii_str = ''.join(chr(b) if 32 <= b <= 126 else ' ' for b in hex_bytes)

            # Look for common patterns in inquiry data
            if "Samsung" in ascii_str and "model" not in disk_data:
                normalized_model = re.sub(r'\s{2,}', ' ', ascii_str.strip())
                disk_data["model"] = normalized_model
                disk_data["manufacturer"] = "Samsung"
            elif "WDC" in ascii_str and "model" not in disk_data:
                normalized_model = re.sub(r'\s{2,}', ' ', ascii_str.strip())
                disk_data["model"] = normalized_model
                disk_data["manufacturer"] = "Western Digital"
            elif len(ascii_str.strip()) > 10 and "model" not in disk_data:
                normalized_model = re.sub(r'\s{2,}', ' ', ascii_str.strip())
                disk_data["model"] = normalized_model
        except:
            pass  # Ignore parsing errors

    @staticmethod
    def _parse_megacli_pdlist(output: List[str], controller_id: int) -> List[DiskInfo]:
        """Parse disk information from MegaCli -PDList output format."""
        disks = []
        current_disk = {}

        for line in output:
            line = line.strip()

            # Start of new drive entry
            if line.startswith("Enclosure Device ID:"):
                # Save previous disk if we have one
                if current_disk and "model" in current_disk:
                    # If no "Drive's position:" line was encountered, this is an unconfigured drive
                    if "array_id" not in current_disk:
                        current_disk["array_id"] = "X"  # Unconfigured marker
                        current_disk["disk_id"] = current_disk.get("slot_id", "Unknown")
                    disks.append(RAIDParser._create_pdlist_disk_info(current_disk, controller_id))

                # Start new disk
                current_disk = {"enclosure_id": line.split(":")[1].strip()}

            elif line.startswith("Slot Number:"):
                current_disk["slot_id"] = line.split(":")[1].strip()

            elif line.startswith("Device Id:"):
                current_disk["lsi_id"] = line.split(":")[1].strip()

            elif line.startswith("Drive's position:"):
                # Extract array info: "DiskGroup: 0, Span: 0, Arm: 0" or empty for unconfigured
                position_info = line.split(":", 1)[1].strip()
                if "DiskGroup:" in position_info:
                    # Configured drive - extract DiskGroup (array ID)
                    try:
                        dg_part = position_info.split("DiskGroup:")[1].split(",")[0].strip()
                        current_disk["array_id"] = dg_part
                        # For configured drives, disk_id is the slot position within the array
                        current_disk["disk_id"] = current_disk.get("slot_id", "Unknown")
                    except:
                        current_disk["array_id"] = "Unknown"
                        current_disk["disk_id"] = current_disk.get("slot_id", "Unknown")
                else:
                    # Unconfigured drive
                    current_disk["array_id"] = "X"  # Unconfigured marker like legacy
                    slot_id = current_disk.get("slot_id", "Unknown")
                    current_disk["disk_id"] = slot_id
                    # Debug: print what we got for unconfigured drive
                    print(f"DEBUG: Unconfigured drive - slot_id='{slot_id}', current_disk keys={list(current_disk.keys())}")

            elif line.startswith("Firmware state:"):
                current_disk["state"] = line.split(":", 1)[1].strip()

            elif line.startswith("Inquiry Data:"):
                # Parse: "S753NS0W812677Y     Samsung SSD 870 EVO 2TB                 SVT03B6Q"
                inquiry_data = line.split(":", 1)[1].strip()
                # Normalize whitespace in inquiry data immediately
                normalized_inquiry = re.sub(r'\s{2,}', ' ', inquiry_data)
                current_disk["model"] = normalized_inquiry
                current_disk.update(RAIDParser._parse_drive_model(normalized_inquiry))

            elif line.startswith("Media Type:"):
                media_type = line.split(":", 1)[1].strip()
                if media_type == "Hard Disk Device":
                    current_disk["media_type"] = "HDD"
                elif media_type == "Solid State Device":
                    current_disk["media_type"] = "SSD"
                else:
                    current_disk["media_type"] = "N/A"

            elif line.startswith("Coerced Size:"):
                # Parse size like "1.818 TB [0xe8d00000 Sectors]" or "14551 GB [0x...]"
                size_str = line.split(":", 1)[1].strip()
                if "[" in size_str:
                    size_str = size_str.split("[")[0].strip()
                # Normalize size format through _parse_size() for consistency
                current_disk["size"] = RAIDParser._parse_size(size_str)

            elif line.startswith("Device Speed:"):
                current_disk["speed"] = line.split(":", 1)[1].strip()

            elif line.startswith("Drive Temperature"):
                # Parse: "Drive Temperature :27C (80.60 F)"
                temp_part = line.split(":")[1].strip()
                temp_match = re.search(r"(\d+)C", temp_part)
                current_disk["temperature"] = f"{temp_match.group(1)}C" if temp_match else "Unknown"

        # Don't forget the last disk
        if current_disk and "model" in current_disk:
            # If no "Drive's position:" line was encountered, this is an unconfigured drive
            if "array_id" not in current_disk:
                current_disk["array_id"] = "X"  # Unconfigured marker
                current_disk["disk_id"] = current_disk.get("slot_id", "Unknown")
            disks.append(RAIDParser._create_pdlist_disk_info(current_disk, controller_id))

        return disks

    @staticmethod
    def _create_disk_info(disk_data: dict, controller_id: int, array_id: str, disk_id: str) -> DiskInfo:
        """Create DiskInfo object from parsed data."""
        # Normalize model whitespace
        model = disk_data.get("model", "Unknown")
        if model != "Unknown":
            model = re.sub(r'\s{2,}', ' ', model.strip())

        return DiskInfo(
            controller_id=controller_id,
            array_id=array_id,
            disk_id=disk_id,
            enclosure_id=disk_data.get("enclosure_id", ""),
            slot_id=disk_data.get("slot_id", ""),
            lsi_id=disk_data.get("lsi_id", "Unknown"),
            media_type=disk_data.get("media_type", "Unknown"),
            model=model,
            size=disk_data.get("size", "Unknown"),
            state=disk_data.get("state", "Unknown"),
            speed=disk_data.get("speed", "Unknown"),
            temperature=disk_data.get("temperature", "Unknown"),
            manufacturer=disk_data.get("manufacturer", ""),
            serial=disk_data.get("serial", "")
        )

    @staticmethod
    def _create_pdlist_disk_info(disk_data: dict, controller_id: int) -> DiskInfo:
        """Create DiskInfo object from -PDList parsed data."""
        # Normalize model whitespace
        model = disk_data.get("model", "Unknown")
        if model != "Unknown":
            model = re.sub(r'\s{2,}', ' ', model.strip())

        return DiskInfo(
            controller_id=controller_id,
            array_id=disk_data.get("array_id", "Unknown"),
            disk_id=disk_data.get("disk_id", "Unknown"),
            enclosure_id=disk_data.get("enclosure_id", "Unknown"),
            slot_id=disk_data.get("slot_id", "Unknown"),
            lsi_id=disk_data.get("lsi_id", "Unknown"),
            media_type=disk_data.get("media_type", "Unknown"),
            model=model,
            size=disk_data.get("size", "Unknown"),
            state=disk_data.get("state", "Unknown"),
            speed=disk_data.get("speed", "Unknown"),
            temperature=disk_data.get("temperature", "Unknown"),
            manufacturer=disk_data.get("manufacturer", ""),
            serial=disk_data.get("serial", "")
        )

    @staticmethod
    def parse_foreign_disks(output: List[str]) -> List[str]:
        """Parse foreign disk slot numbers from MegaCLI/perccli foreign config output."""
        foreign_slots = []

        for line in output:
            line = line.strip()

            # MegaCLI and legacy perccli format: "Slot Number: X"
            slot_match = re.search(r"Slot Number:\s*(\d+)", line)
            if slot_match:
                foreign_slots.append(slot_match.group(1))
                continue

            # Native perccli might use different format - look for slot patterns
            # This is a fallback for any other slot identification patterns
            # Format might be like "EID:Slt" in foreign config tables
            if "foreign" in line.lower() or "cfg" in line.lower():
                # Look for EID:Slot patterns like "64:0"
                eid_slot_match = re.search(r"(\d+):(\d+)", line)
                if eid_slot_match:
                    # Use the slot part (second number)
                    slot_number = eid_slot_match.group(2)
                    if slot_number not in foreign_slots:
                        foreign_slots.append(slot_number)

        return foreign_slots

    @staticmethod
    def _parse_drive_model(model: str) -> dict:
        """Parse manufacturer and serial from drive model string."""
        original_model = model
        # Normalize all whitespace (not just spaces)
        model = re.sub(r'\s{2,}', ' ', model.strip())

        # Handle Seagate drives specially
        seagate_match = re.match(r"(\w{8})(ST\w+)(?:-(\w{6}))?(?:\s+(\w+))", model)
        if seagate_match:
            if seagate_match.group(3):
                formatted_model = f"{seagate_match.group(2)}-{seagate_match.group(3)} {seagate_match.group(4)} {seagate_match.group(1)}"
            else:
                formatted_model = f"{seagate_match.group(2)} {seagate_match.group(4):>10} {seagate_match.group(1)}"

            return {
                "model": formatted_model,
                "manufacturer": "Seagate",
                "serial": seagate_match.group(1)
            }

        # Generic parsing for MegaCli inquiry data format
        # Format: "SERIAL MANUFACTURER_MODEL FIRMWARE" (with variable spacing)
        # Example: "S753NS0W812677Y Samsung SSD 870 EVO 2TB SVT03B6Q"
        # Example: "4BJZ594H WDC WD161KFGX-68AFPN0 83.00A83"
        parts = model.split()
        if len(parts) >= 3:
            # parts[0] = serial, parts[1:-1] = manufacturer+model, parts[-1] = firmware
            serial = parts[0]
            manufacturer = parts[1]  # First word after serial is manufacturer
            firmware = parts[-1]

            # Reconstruct as: "MANUFACTURER MODEL FIRMWARE SERIAL" (less to more specific)
            manufacturer_model = " ".join(parts[1:-1])  # Everything between serial and firmware
            formatted_model = f"{manufacturer_model} {firmware} {serial}"
        elif len(parts) == 2:
            # Minimal format: "SERIAL SOMETHING"
            serial = parts[0]
            manufacturer = parts[1]
            formatted_model = f"{parts[1]} {parts[0]}"
        else:
            # Single part or empty - use as-is
            manufacturer = model.split()[0] if model.split() else "Unknown"
            formatted_model = original_model
            serial = ""

        return {
            "model": formatted_model,
            "manufacturer": manufacturer,
            "serial": serial
        }


class OutputFormatter(ABC):
    """Abstract base class for output formatters."""

    @abstractmethod
    def format_controllers(self, controllers: List[ControllerInfo]) -> str:
        pass

    @abstractmethod
    def format_arrays(self, arrays: List[ArrayInfo]) -> str:
        pass

    @abstractmethod
    def format_disks(self, disks: List[DiskInfo]) -> str:
        pass

    @abstractmethod
    def format_unconfigured_disks(self, disks: List[DiskInfo]) -> str:
        pass

    @abstractmethod
    def format_summary(self, stats: RAIDStats) -> str:
        pass


class NormalFormatter(OutputFormatter):
    """Formatter for normal human-readable output."""

    def format_controllers(self, controllers: List[ControllerInfo]) -> str:
        if not controllers:
            return ""

        lines = ["-- Controller information --"]

        # Calculate column widths
        model_width = max(len("H/W Model"), max(len(c.model) for c in controllers))

        # Header
        header_fmt = f"{'-- ID':<5} | {f'H/W Model':<{model_width}} | {'RAM':<6} | {'Temp':<4} | {'BBU':<6} | {'Firmware':<12}"
        lines.append(header_fmt)

        # Controllers
        for controller in controllers:
            line_fmt = f"{f'c{controller.controller_id}':<5} | {controller.model:<{model_width}} | {controller.memory:<6} | {controller.temperature:<4} | {controller.bbu_status:<6} | {controller.firmware:<12}"
            lines.append(line_fmt)

        lines.append("")
        return "\n".join(lines)

    def format_arrays(self, arrays: List[ArrayInfo]) -> str:
        if not arrays:
            return ""

        lines = ["-- Array information --"]

        # Calculate column widths
        id_width = max(len("-- ID"), max(len(a.array_id) for a in arrays))
        type_width = max(len("Type"), max(len(a.raid_type) for a in arrays))
        size_width = max(len("Size"), max(len(a.size) for a in arrays))
        flags_width = max(len("Flags"), max(len(a.properties) for a in arrays))
        cache_width = max(len("CacheCade"), max(len(a.cache_cade_info) for a in arrays))

        # Header
        header_fmt = f"{'-- ID':<{id_width}} | {f'Type':<{type_width}} | {'Size':<{size_width}} | {'Strpsz':<7} | {f'Flags':<{flags_width}} | {'DskCache':<8} | {'Status':<8} | {'OS Path':<8} | {f'CacheCade':<{cache_width}} | {'InProgress':<12}"
        lines.append(header_fmt)

        # Arrays
        for array in arrays:
            line_fmt = f"{array.array_id:<{id_width}} | {array.raid_type:<{type_width}} | {array.size:<{size_width}} | {array.strip_size:<7} | {array.properties:<{flags_width}} | {array.disk_cache:<8} | {array.state:<8} | {array.os_path:<8} | {array.cache_cade_info:<{cache_width}} | {array.in_progress:<12}"
            lines.append(line_fmt)

        lines.append("")
        return "\n".join(lines)

    def format_disks(self, disks: List[DiskInfo]) -> str:
        if not disks:
            return ""

        lines = ["-- Disk information --"]

        # Calculate column widths
        disk_ids = [f"c{d.controller_id}u{d.array_id}p{d.disk_id}" for d in disks]
        id_width = max(len("-- ID"), max(len(disk_id) for disk_id in disk_ids)) if disk_ids else len("-- ID")
        model_width = max(len("Drive Model"), max(len(d.model) for d in disks))
        size_width = max(len("Size"), max(len(d.size) for d in disks))
        status_width = max(len("Status"), max(len(d.state) for d in disks))

        # Header
        header_fmt = f"{'-- ID':<{id_width}} | {'Type':<4} | {f'Drive Model':<{model_width}} | {'Size':<{size_width}} | {f'Status':<{status_width}} | {'Speed':<8} | {'Temp':<4} | {'Slot ID':<8} | {'LSI ID':<8}"
        lines.append(header_fmt)

        # Disks
        for disk in disks:
            disk_id = f"c{disk.controller_id}u{disk.array_id}p{disk.disk_id}"
            slot_info = f"[{disk.enclosure_id}:{disk.slot_id}]"

            line_fmt = f"{disk_id:<{id_width}} | {disk.media_type:<4} | {disk.model:<{model_width}} | {disk.size:<{size_width}} | {disk.state:<{status_width}} | {disk.speed:<8} | {disk.temperature:<4} | {slot_info:<8} | {disk.lsi_id:<8}"
            lines.append(line_fmt)

        lines.append("")
        return "\n".join(lines)

    def format_unconfigured_disks(self, disks: List[DiskInfo]) -> str:
        if not disks:
            return ""

        lines = ["-- Unconfigured Disk information --"]

        # Calculate column widths - unconfigured drives use c0uXpY format like legacy
        disk_ids = [f"c{d.controller_id}uXp{d.disk_id}" for d in disks]
        id_width = max(len("-- ID"), max(len(disk_id) for disk_id in disk_ids)) if disk_ids else len("-- ID")
        model_width = max(len("Drive Model"), max(len(d.model) for d in disks))
        size_width = max(len("Size"), max(len(d.size) for d in disks))
        status_width = max(len("Status"), max(len(d.state) for d in disks))

        # Header (same as regular disks but with Path column like legacy)
        header_fmt = f"{'-- ID':<{id_width}} | {'Type':<4} | {f'Drive Model':<{model_width}} | {'Size':<{size_width}} | {f'Status':<{status_width}} | {'Speed':<8} | {'Temp':<4} | {'Slot ID':<8} | {'LSI ID':<8} | {'Path':<8}"
        lines.append(header_fmt)

        # Unconfigured disks
        for disk in disks:
            disk_id = f"c{disk.controller_id}uXp{disk.disk_id}"  # Legacy format for unconfigured
            slot_info = f"[{disk.enclosure_id}:{disk.slot_id}]"

            line_fmt = f"{disk_id:<{id_width}} | {disk.media_type:<4} | {disk.model:<{model_width}} | {disk.size:<{size_width}} | {disk.state:<{status_width}} | {disk.speed:<8} | {disk.temperature:<4} | {slot_info:<8} | {disk.lsi_id:<8} | {'N/A':<8}"
            lines.append(line_fmt)

        lines.append("")
        return "\n".join(lines)

    def format_summary(self, stats: RAIDStats) -> str:
        if stats.has_errors:
            return f"""
There is at least one disk/array in a NOT OPTIMAL state.
RAID ERROR - Arrays: OK:{stats.good_arrays} Bad:{stats.bad_arrays} - Disks: OK:{stats.good_disks} Bad:{stats.bad_disks}
"""
        return ""


class NagiosFormatter(OutputFormatter):
    """Formatter for Nagios monitoring output."""

    def format_controllers(self, controllers: List[ControllerInfo]) -> str:
        return ""  # Nagios doesn't need detailed controller info

    def format_arrays(self, arrays: List[ArrayInfo]) -> str:
        return ""  # Handled in summary

    def format_disks(self, disks: List[DiskInfo]) -> str:
        return ""  # Handled in summary

    def format_unconfigured_disks(self, disks: List[DiskInfo]) -> str:
        return ""  # Handled in summary

    def format_summary(self, stats: RAIDStats) -> str:
        if stats.has_errors:
            return f"RAID ERROR - Arrays: OK:{stats.good_arrays} Bad:{stats.bad_arrays} - Disks: OK:{stats.good_disks} Bad:{stats.bad_disks}"
        else:
            return f"RAID OK - Arrays: OK:{stats.good_arrays} Bad:{stats.bad_arrays} - Disks: OK:{stats.good_disks} Bad:{stats.bad_disks}"


class InfluxDBFormatter(OutputFormatter):
    """Formatter for InfluxDB metrics output."""

    def __init__(self):
        self.hostname = os.uname().nodename
        self.timestamp = self._get_timestamp()

    def _get_timestamp(self) -> str:
        """Get timestamp for InfluxDB."""
        if sys.version_info >= (3, 7):
            return str(time.time_ns())
        else:
            return str(int(time.time() * 1000000000))

    def format_controllers(self, controllers: List[ControllerInfo]) -> str:
        lines = []
        for controller in controllers:
            line = (f'megaraid_controller,host={self.hostname},'
                   f'model="{controller.model}",firmware="{controller.firmware}" '
                   f'memory={controller.memory},temp={controller.temperature},'
                   f'bbu={controller.bbu_status},controller={controller.controller_id}i {self.timestamp}')
            lines.append(line)
        return "\n".join(lines)

    def format_arrays(self, arrays: List[ArrayInfo]) -> str:
        lines = []
        for array in arrays:
            line = (f'megaraid_array,host={self.hostname},status={array.state} '
                   f'array_id="{array.array_id}",raid_type="{array.raid_type}",'
                   f'size="{array.size}",stripsz="{array.strip_size}",'
                   f'flags="{array.properties.replace(",", "+")}",cache="{array.disk_cache}",'
                   f'task="{array.in_progress}" {self.timestamp}')
            lines.append(line)
        return "\n".join(lines)

    def format_disks(self, disks: List[DiskInfo]) -> str:
        lines = []
        for disk in disks:
            disk_id = f"c{disk.controller_id}u{disk.array_id}p{disk.disk_id}"
            temp = disk.temperature.rstrip('C') if disk.temperature.endswith('C') else disk.temperature

            line = (f'megaraid_disk,host={self.hostname},'
                   f'status={disk.state.replace(" ", "").replace(",", "+")},'
                   f'serial={disk.serial},disk_id={disk_id} '
                   f'model="{disk.manufacturer}",disk_type="{disk.media_type}",'
                   f'size="{disk.size}",speed="{disk.speed}",temp={temp}i {self.timestamp}')
            lines.append(line)
        return "\n".join(lines)

    def format_unconfigured_disks(self, disks: List[DiskInfo]) -> str:
        lines = []
        for disk in disks:
            disk_id = f"c{disk.controller_id}uXp{disk.disk_id}"  # Unconfigured format
            temp = disk.temperature.rstrip('C') if disk.temperature.endswith('C') else disk.temperature

            line = (f'megaraid_unconfigured_disk,host={self.hostname},'
                   f'status={disk.state.replace(" ", "").replace(",", "+")},'
                   f'serial={disk.serial},disk_id={disk_id} '
                   f'model="{disk.manufacturer}",disk_type="{disk.media_type}",'
                   f'size="{disk.size}",speed="{disk.speed}",temp={temp}i {self.timestamp}')
            lines.append(line)
        return "\n".join(lines)

    def format_summary(self, stats: RAIDStats) -> str:
        status = "ERROR" if stats.has_errors else "OK"
        return (f'megaraid_global,host={self.hostname},raid_global={status} '
               f'raid_ok={stats.good_arrays}i,raid_bad={stats.bad_arrays}i,'
               f'disks_ok={stats.good_disks}i,disks_bad={stats.bad_disks}i {self.timestamp}')


class RAIDStatusChecker:
    """Main RAID status checking class."""

    def __init__(self, config: Config):
        self.config = config
        self.megacli = MegaCLIInterface(config)
        self.parser = RAIDParser()
        self.formatter = self._create_formatter()

        # Check privileges
        self._check_privileges()

    def _check_privileges(self):
        """Check if running with required privileges, but allow diagnostics mode."""
        try:
            if os.name == 'nt':  # Windows
                import ctypes
                is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
            else:  # Unix-like
                is_admin = os.geteuid() == 0

            if not is_admin:
                # Store privilege status but don't exit immediately
                # Allow the script to show diagnostics when tools fail
                self.config.has_admin_privileges = False
                if self.config.debug:
                    self.megacli._debug_print("Running without administrator privileges - functionality may be limited")
            else:
                self.config.has_admin_privileges = True

        except Exception as e:
            logger.warning(f"Could not check privileges: {e}")
            self.config.has_admin_privileges = False

    def _create_formatter(self) -> OutputFormatter:
        """Create appropriate formatter based on config."""
        if self.config.output_mode == OutputMode.NAGIOS:
            return NagiosFormatter()
        elif self.config.output_mode == OutputMode.INFLUXDB:
            return InfluxDBFormatter()
        else:
            return NormalFormatter()

    def check_system_status(self) -> Tuple[List[ControllerInfo], List[ArrayInfo], List[DiskInfo], List[DiskInfo], RAIDStats]:
        """Check the status of the entire RAID system."""
        controllers = []
        arrays = []
        disks = []
        unconfigured_disks = []
        stats = RAIDStats()

        # Set reference for debug output in parser
        RAIDParser._megacli_ref = self.megacli

        try:
            controller_count = self.megacli.get_controller_count()
            if controller_count == 0:
                self._handle_no_controllers_detected()
                # If no controllers and no admin privileges, suggest running as admin
                if not self.config.has_admin_privileges:
                    print("\nTry running with administrator/root privileges for complete functionality.")
                    sys.exit(5)  # Use exit code 5 for privilege issues
                else:
                    sys.exit(1)  # Use exit code 1 for no controllers detected

            for controller_id in range(controller_count):
                # Get controller info
                if self.config.print_controller:
                    controller_output = self.megacli.get_controller_info(controller_id)
                    controller_info = self.parser.parse_controller_info(controller_output, controller_id)

                    # Check BBU status if present
                    if controller_info.bbu_status == "Present":
                        try:
                            bbu_output = self.megacli.get_bbu_status(controller_id)
                            controller_info.bbu_status = self.parser.parse_bbu_status(bbu_output)
                        except Exception as e:
                            logger.warning(f"Could not get BBU status for controller {controller_id}: {e}")
                            controller_info.bbu_status = "Unknown"

                    # Override temperature if --notemp flag is set
                    if self.config.no_temp:
                        from dataclasses import replace
                        controller_info = replace(controller_info, temperature="N/A")

                    controllers.append(controller_info)

                # Get array info
                if self.config.print_array:
                    # Build LD table to map array indices to actual LD IDs
                    ld_table = self._build_ld_table(controller_id)

                    # Get PCI info for OS path detection
                    pci_output = self.megacli.get_pci_info(controller_id)
                    pci_path = self.parser.parse_pci_info(pci_output)

                    for array_index, ld_id in enumerate(ld_table):
                        try:
                            single_array_output = self.megacli.get_array_info(controller_id, ld_id)
                            array_info = self.parser.parse_array_info(single_array_output, controller_id, ld_id)

                            # Try to find OS device path
                            if pci_path:
                                array_info.os_path = self._find_os_device_path(pci_path, array_info.target_id)

                            arrays.append(array_info)

                            # Debug array state
                            if self.config.debug:
                                self.megacli._debug_print(f"Array state : LD {array_info.array_id}, status : {array_info.state}")

                            # Update stats with defensive handling
                            if self._is_array_healthy(array_info.state):
                                stats.good_arrays += 1
                            else:
                                stats.bad_arrays += 1

                        except Exception as e:
                            logger.warning(f"Could not parse array {ld_id} (index {array_index}) on controller {controller_id}: {e}")

                # Get disk info
                if self.config.print_disk:
                    try:
                        disk_output = self.megacli.get_physical_drives_info(controller_id)
                        disk_list = self.parser.parse_disk_info(disk_output, controller_id)

                        # Sort disks by logical ID (c0u0p0, c0u0p1, c0u1p0, etc.) for better readability
                        disk_list.sort(key=lambda d: (
                            d.controller_id,
                            999 if d.array_id == "X" else int(d.array_id) if d.array_id.isdigit() else 0,
                            int(d.disk_id) if d.disk_id.isdigit() else 0
                        ))

                        # Get foreign disk slot numbers
                        try:
                            foreign_output = self.megacli.get_foreign_disks(controller_id)
                            foreign_slots = self.parser.parse_foreign_disks(foreign_output)
                            if self.config.debug and foreign_slots:
                                self.megacli._debug_print(f"Controller {controller_id} foreign disk slots: {foreign_slots}")
                        except Exception as e:
                            if self.config.debug:
                                self.megacli._debug_print(f"Could not get foreign disk info for controller {controller_id}: {e}")
                            foreign_slots = []

                        # Separate configured and unconfigured drives
                        for disk in disk_list:
                            # Override temperature if --notemp flag is set
                            if self.config.no_temp:
                                from dataclasses import replace
                                disk = replace(disk, temperature="N/A")

                            # Mark foreign disks for unconfigured drives
                            if (self._is_disk_unconfigured(disk.state) and 
                                disk.slot_id in foreign_slots):
                                from dataclasses import replace
                                disk = replace(disk, state=f"{disk.state} (Foreign)")

                            disk_name = f"{controller_id}{disk.enclosure_id}{disk.slot_id}"
                            if self.config.debug:
                                self.megacli._debug_print(f"Disk c{disk_name} status : {disk.state}")

                            if self._is_disk_unconfigured(disk.state):
                                # Unconfigured drives go to separate list
                                unconfigured_disks.append(disk)
                            else:
                                # Configured drives go to main list and count toward stats
                                disks.append(disk)
                                if self._is_disk_good(disk.state):
                                    stats.good_disks += 1
                                else:
                                    stats.bad_disks += 1

                    except Exception as e:
                        logger.warning(f"Could not parse disks for controller {controller_id}: {e}")

        except MegaCLIError as e:
            logger.error(f"MegaCLI error: {e}")
            sys.exit(3)
        except Exception as e:
            logger.error(f"Unexpected error: {e}")
            sys.exit(1)

        return controllers, arrays, disks, unconfigured_disks, stats

    def _count_arrays(self, output: List[str]) -> int:
        """Count the number of arrays from output (legacy and native formats)."""
        count = 0

        # Check if this is native perccli output - look for VD sections
        is_native_perccli = False
        for line in output:
            if "/c0/v" in line and " :" in line:
                is_native_perccli = True
                break

        if is_native_perccli:
            # Count native perccli VD sections: "/c0/v0 :", "/c0/v1 :", etc.
            for line in output:
                if re.match(r"^/c\d+/v\d+ :$", line.strip()):
                    count += 1

            if self.config.debug:
                self.megacli._debug_print(f"Native perccli array count: {count}")
            return count
        else:
            # Check if this is controller overview with "Virtual Drives = X"
            for line in output:
                if "Virtual Drives =" in line:
                    try:
                        count = int(line.split("=")[1].strip())
                        if self.config.debug:
                            self.megacli._debug_print(f"Native perccli array count from overview: {count}")
                        return count
                    except (IndexError, ValueError):
                        continue

            # Legacy format parsing
            for line in output:
                if re.match(r"^(CacheCade )?Virtual Drive:.*$", line.strip()):
                    count += 1

        if self.config.debug and count > 0:
            self.megacli._debug_print(f"Array count detected: {count}")

        return count

    def _build_ld_table(self, controller_id: int) -> List[int]:
        """Build mapping of array indices to actual LD IDs, like original script."""
        ld_table = []

        try:
            # First get array count
            all_arrays_output = self.megacli.get_all_arrays_info(controller_id)
            array_count = self._count_arrays(all_arrays_output)

            # Check if we're using native perccli format
            is_native_perccli = False
            for line in all_arrays_output:
                if "/c0/v" in line and " :" in line:
                    is_native_perccli = True
                    break

            if is_native_perccli:
                # For native perccli, extract actual VD numbers from /c0/vX : lines
                ld_table = []
                import re
                for line in all_arrays_output:
                    # Look for lines like "/c0/v239 :"
                    match = re.match(r"^/c\d+/v(\d+)\s*:$", line.strip())
                    if match:
                        vd_number = int(match.group(1))
                        ld_table.append(vd_number)

                if self.config.debug:
                    self.megacli._debug_print(f"Native perccli LD table (actual VD numbers): {ld_table}")
                return ld_table

            # Legacy approach: extract LD IDs directly from -LDInfo -lall output
            # This is much more efficient than probing every possible ID!
            import re
            for line in all_arrays_output:
                # Look for "Virtual Drive: N (Target Id: X)" lines
                match = re.match(r"^(CacheCade )?Virtual Drive:\s*(\d+)", line.strip())
                if match:
                    ld_id = int(match.group(2))  # group(1) is optional CacheCade, group(2) is the ID
                    ld_table.append(ld_id)

            if self.config.debug:
                self.megacli._debug_print(f"Legacy LD table (from -lall): {ld_table}")

            return ld_table

        except Exception as e:
            logger.warning(f"Could not build LD table for controller {controller_id}: {e}")
            # Fall back to simple sequential mapping
            array_count = self._count_arrays(self.megacli.get_all_arrays_info(controller_id))
            return list(range(array_count))

    def _is_array_healthy(self, state: str) -> bool:
        """Check if array state indicates healthy status (good or maintenance)."""
        good_states = ["Optimal", "Optl", "N/A"]
        # Known maintenance states (not failures)
        maintenance_states = [
            "CC", "Check Consistency", "BGI", "Background Initialization",
            "Reconstruction", "Rebuild", "Rebuilding", "Migrating", "Migration",
            "Initializing", "INIT", "Synchronizing", "SYNC"
        ]

        if state in good_states:
            return True
        elif state in maintenance_states:
            if self.config.debug:
                print(f"INFO: Array in maintenance state: {state}")
            return True  # Maintenance is not a failure
        else:
            # Log unknown states for future enhancement
            print(f"WARNING: Unknown array state encountered: '{state}' - treating as potentially bad")
            return False

    def _is_disk_good(self, state: str) -> bool:
        """Check if disk state is considered good."""
        good_states = [
            "Online", "Online, Spun Up", "Rebuilding",
            "Unconfigured(good), Spun Up", "Unconfigured(good), Spun down",
            "UGood",  # perccli format for unconfigured good drives
            "Hotspare, Spun Up", "Hotspare, Spun down", "JBOD"
        ]

        # Known maintenance states for disks
        maintenance_states = ["Copyback", "Patrol Read", "Smart Error"]

        # Handle rebuilding states with percentages
        if state.startswith("Rebuilding"):
            return True

        # Handle maintenance states
        if any(maintenance in state for maintenance in maintenance_states):
            if self.config.debug:
                print(f"INFO: Disk in maintenance state: {state}")
            return True

        if state in good_states:
            return True
        else:
            # Log unknown disk states for future enhancement
            if self.config.debug:
                print(f"WARNING: Unknown disk state encountered: '{state}' - treating as potentially bad")
            return False

    def _is_disk_unconfigured(self, state: str) -> bool:
        """Check if disk is unconfigured (good but not part of array)."""
        unconfigured_states = [
            "Unconfigured(good), Spun Up", "Unconfigured(good), Spun down",
            "UGood",  # perccli format for unconfigured good drives
            "Hotspare, Spun Up", "Hotspare, Spun down"  # hotspares should be in unconfigured section
        ]
        return state in unconfigured_states

    def _validate_parsed_data(self, data_type: str, data: dict) -> dict:
        """Validate parsed data and provide safe defaults for missing/invalid fields."""
        validated = {}

        if data_type == "array":
            validated["array_id"] = str(data.get("array_id", "Unknown"))
            validated["raid_type"] = data.get("raid_type", "Unknown")
            validated["state"] = data.get("state", "Unknown")
            validated["size"] = data.get("size", "Unknown")
            validated["properties"] = data.get("properties", "Unknown")
            validated["disk_cache"] = data.get("disk_cache", "Unknown")
            validated["os_path"] = data.get("os_path", "N/A")

        elif data_type == "disk":
            validated["array_id"] = str(data.get("array_id", "Unknown"))
            validated["disk_id"] = str(data.get("disk_id", "Unknown"))
            validated["enclosure_id"] = str(data.get("enclosure_id", ""))
            validated["slot_id"] = str(data.get("slot_id", "Unknown"))
            validated["model"] = data.get("model", "Unknown")
            validated["state"] = data.get("state", "Unknown")
            validated["media_type"] = data.get("media_type", "Unknown")
            validated["size"] = data.get("size", "Unknown")
            validated["speed"] = data.get("speed", "Unknown")
            validated["temperature"] = data.get("temperature", "N/A")
            validated["lsi_id"] = str(data.get("lsi_id", "Unknown"))

        # Log if we had to use defaults
        missing_fields = [k for k, v in validated.items() if v in ["Unknown", "N/A", ""]]
        if missing_fields and self.config.debug:
            print(f"INFO: Used defaults for {data_type} fields: {missing_fields}")

        return validated

    def _find_os_device_path(self, pci_path: str, target_id: str) -> str:
        """Find OS device path from PCI path and target ID."""
        if not pci_path or not target_id:
            return "N/A"

        disk_prefix = f"/dev/disk/by-path/pci-{pci_path}-scsi-0:"

        if self.config.debug:
            self.megacli._debug_print(f"Will look for DISKprefix : {disk_prefix}")

        # First attempt: Try the reported target ID with various channels (original logic)
        for channel in range(1, 8):
            disk_path = f"{disk_prefix}{channel}:{target_id}:0"
            if self.config.debug:
                self.megacli._debug_print(f"Looking for DISKpath : {disk_path}")

            if os.path.exists(disk_path):
                try:
                    real_path = os.path.realpath(disk_path)
                    if self.config.debug:
                        self.megacli._debug_print(f"Found DISK match: {disk_path} -> {real_path}")
                    return real_path
                except OSError:
                    continue

        # Second attempt: Fallback search through all SCSI paths for this controller
        # This handles cases where Virtual Drive Target ID != actual SCSI Target ID
        if self.config.debug:
            self.megacli._debug_print(f"Target ID {target_id} not found, searching all SCSI paths for controller")

        try:
            import glob
            # Search for all SCSI paths matching this PCI controller
            search_pattern = f"/dev/disk/by-path/pci-{pci_path}-scsi-*"
            scsi_paths = glob.glob(search_pattern)

            # Filter out partition entries (we want base devices only)
            base_devices = [path for path in scsi_paths if not re.search(r'-part\d+$', path)]

            if base_devices:
                # Sort by device name to get consistent results
                base_devices.sort()
                found_path = base_devices[0]  # Take the first (usually /dev/sda)

                try:
                    real_path = os.path.realpath(found_path)
                    if self.config.debug:
                        self.megacli._debug_print(f"Fallback found DISK match: {found_path} -> {real_path}")
                        self.megacli._debug_print(f"Available SCSI paths: {base_devices}")
                    return real_path
                except OSError:
                    pass

        except Exception as e:
            if self.config.debug:
                self.megacli._debug_print(f"Fallback search failed: {str(e)}")

        # Final fallback: return target_id like the original script
        if self.config.debug:
            self.megacli._debug_print(f"No real path found, falling back to target_id: {target_id}")
        return target_id

    def _print_debug_dump(self, controllers: List[ControllerInfo], arrays: List[ArrayInfo], disks: List[DiskInfo]):
        """Print comprehensive debug dump like the original script."""
        self.megacli._debug_print("Printing Command Cache")
        for cmd, output in self.megacli._command_cache.items():
            self.megacli._debug_print(f"Command: {cmd}")
            sys.stderr.write("\n".join(output) + "\n\n")

        if controllers:
            self.megacli._debug_print("Printing Controllers")
            for controller in controllers:
                sys.stderr.write(f"Controller {controller.controller_id}: {controller.model} | {controller.memory} | {controller.temperature} | {controller.bbu_status} | {controller.firmware}\n")

        if arrays:
            self.megacli._debug_print("Printing Arrays")
            for array in arrays:
                sys.stderr.write(f"{array.array_id} | {array.raid_type} | {array.size} | {array.state} | {array.os_path}\n")

        if disks:
            self.megacli._debug_print("Printing Disks")
            for disk in disks:
                disk_id = f"c{disk.controller_id}u{disk.array_id}p{disk.disk_id}"
                sys.stderr.write(f"{disk_id} | {disk.media_type} | {disk.model} | {disk.size} | {disk.state} | {disk.temperature}\n")

    def _handle_no_controllers_detected(self):
        """Provide detailed diagnostics when no controllers are detected."""
        print("No MegaRAID or PERC adapter detected by management tools!")
        print()

        # Show hardware detection results
        if self.megacli.hardware_info.controllers_in_lspci:
            print("However, hardware detection found:")
            for controller in self.megacli.hardware_info.controllers_in_lspci:
                print(f"  lspci: {controller}")
            print()

        if self.megacli.hardware_info.megaraid_driver_loaded:
            print("The megaraid_sas kernel driver is loaded.")
            print()

        # Show tool information
        if self.megacli.active_tool:
            print(f"Active tool: {self.megacli.active_tool.binary_path}")
            print(f"Tool type: {self.megacli.active_tool.tool_type.value}")
            print(f"Version: {self.megacli.active_tool.version}")
            print(f"Legacy syntax support: {self.megacli.active_tool.supports_legacy_syntax}")
            print(f"Native syntax support: {self.megacli.active_tool.supports_native_syntax}")
            print()

        if self.megacli.available_tools:
            print("Available tools discovered:")
            for tool in self.megacli.available_tools:
                print(f"  {tool.binary_path} ({tool.tool_type.value}, v{tool.version})")
            print()

        # Show privilege status
        if not self.config.has_admin_privileges:
            print("PRIVILEGE STATUS:")
            print("- Running without administrator/root privileges")
            print("- This may prevent tools from accessing RAID hardware")
            print()

        # Provide recommendations
        if self.megacli.hardware_info.controllers_in_lspci:
            print("RECOMMENDATIONS:")
            print("- RAID hardware is present but management tools cannot detect it")
            print("- This may be due to:")
            if not self.config.has_admin_privileges:
                print("  * Insufficient privileges (try running as root/administrator)")
            print("  * Tool version incompatibility with newer hardware")
            print("  * Missing or outdated management software")
            print("  * Driver or firmware issues")
            print("- Try updating to the latest perccli/storcli from Dell/Broadcom")
            print("- Check system logs (dmesg) for controller initialization errors")
            if not self.config.has_admin_privileges:
                print("- Run as root/administrator for full functionality")
        else:
            print("No RAID hardware detected on this system.")
            if not self.config.has_admin_privileges:
                print("Note: Limited detection capabilities without administrator privileges.")

    def run(self):
        """Main execution method."""
        controllers, arrays, disks, unconfigured_disks, stats = self.check_system_status()

        # Format and print output
        if self.config.print_controller:
            output = self.formatter.format_controllers(controllers)
            if output:
                print(output)

        if self.config.print_array:
            output = self.formatter.format_arrays(arrays)
            if output:
                print(output)

        if self.config.print_disk:
            output = self.formatter.format_disks(disks)
            if output:
                print(output)

            # Print unconfigured disks section (like legacy version)
            if unconfigured_disks:
                output = self.formatter.format_unconfigured_disks(unconfigured_disks)
                if output:
                    print(output)

        # Print summary
        summary = self.formatter.format_summary(stats)
        if summary:
            print(summary)

        # Debug output dump (like original script)
        if self.config.debug:
            self._print_debug_dump(controllers, arrays, disks)

        # Exit with appropriate code
        if stats.has_errors:
            if self.config.output_mode == OutputMode.NAGIOS:
                sys.exit(2)
            else:
                sys.exit(1)
        else:
            sys.exit(0)


def create_config_from_args() -> Config:
    """Create configuration from command line arguments."""
    parser = argparse.ArgumentParser(
        description="MegaCLI/perccli/storcli RAID status checker",
        epilog="Modernized Python implementation with improved error handling and structure."
    )

    parser.add_argument(
        "--version",
        action="store_true",
        help="Show version and auto-detected tool information"
    )
    parser.add_argument(
        "--nagios",
        action="store_true",
        help="Enable Nagios monitoring output format"
    )
    parser.add_argument(
        "--influxdb",
        action="store_true",
        help="Enable InfluxDB metrics output format"
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        help="Enable debug logging output"
    )
    parser.add_argument(
        "--notemp",
        action="store_true",
        help="Disable temperature reporting"
    )
    parser.add_argument(
        "--nocontroller",
        action="store_false",
        dest="controller",
        help="Omit controller information from output"
    )
    parser.add_argument(
        "--noarray",
        action="store_false",
        dest="array",
        help="Omit array information from output"
    )
    parser.add_argument(
        "--nodisk",
        action="store_false",
        dest="disk",
        help="Omit disk information from output"
    )
    parser.add_argument(
        "--megacli-path", "--perccli-path", "--tool-path",
        dest="megacli_path",
        metavar="PATH",
        help="Explicit path to MegaCLI/perccli/storcli binary (all argument names are interchangeable)"
    )

    args = parser.parse_args()

    # Handle --version flag with tool detection info
    if args.version:
        print(__version__)
        print()

        # If no specific megacli-path provided, show auto-detection results
        if not args.megacli_path:
            try:
                # Create a temporary config for tool detection
                temp_config = Config(
                    output_mode=OutputMode.NORMAL,
                    debug=False,
                    no_temp=False,
                    print_controller=True,
                    print_array=True,
                    print_disk=True,
                    megacli_path=None
                )

                # Initialize interface to trigger auto-detection
                megacli = MegaCLIInterface(temp_config)

                if megacli.available_tools:
                    print("Available tools discovered:")
                    for i, tool in enumerate(megacli.available_tools):
                        active_marker = " (active)" if tool == megacli.active_tool else ""
                        print(f"  {tool.binary_path} ({tool.tool_type.value}, v{tool.version}){active_marker}")

                    if megacli.active_tool:
                        print(f"\nActive tool ({megacli.active_tool.binary_path}) capabilities:")
                        print(f"  Legacy syntax support: {megacli.active_tool.supports_legacy_syntax}")
                        print(f"  Native syntax support: {megacli.active_tool.supports_native_syntax}")
                else:
                    print("No RAID management tools detected.")

            except Exception as e:
                print(f"Tool detection failed: {e}")
        else:
            print(f"\nUsing specified tool: {args.megacli_path}")

        sys.exit(0)

    # Determine output mode
    output_mode = OutputMode.NORMAL
    if args.nagios:
        output_mode = OutputMode.NAGIOS
    elif args.influxdb:
        output_mode = OutputMode.INFLUXDB

    # Configure logging level
    if args.debug:
        logging.getLogger().setLevel(logging.DEBUG)
        # Also configure stderr output to be unbuffered like original
        sys.stderr.reconfigure(line_buffering=True)

    return Config(
        output_mode=output_mode,
        debug=args.debug,
        no_temp=args.notemp,
        print_controller=args.controller,
        print_array=args.array,
        print_disk=args.disk,
        megacli_path=args.megacli_path
    )


def main():
    """Main entry point."""
    try:
        config = create_config_from_args()
        checker = RAIDStatusChecker(config)
        checker.run()
    except KeyboardInterrupt:
        print("\nInterrupted by user")
        sys.exit(130)
    except Exception as e:
        logger.error(f"Fatal error: {e}")
        if config.debug:
            import traceback
            traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()
